Seera/src/services/apiService.ts
2025-11-26 20:11:28 +05:30

490 lines
13 KiB
TypeScript

import API_CONFIG from '../config/api';
// Define interfaces locally to avoid import issues
interface ApiResponse<T = any> {
message?: T;
error?: string;
status_code?: number;
}
interface UserDetails {
user_id: string;
full_name: string;
email: string;
user_image?: string;
roles: string[];
permissions: Record<string, {
read: boolean;
write: boolean;
create: boolean;
delete: boolean;
}>;
last_login?: string;
enabled: boolean;
creation: string;
modified: string;
language: string;
custom_site_name:string;
}
interface DocTypeRecord {
name: string;
creation: string;
modified: string;
modified_by: string;
owner: string;
docstatus: number;
[key: string]: any;
}
interface DocTypeRecordsResponse {
records: DocTypeRecord[];
total_count: number;
limit: number;
offset: number;
has_more: boolean;
doctype: string;
}
interface DashboardStats {
total_users: number;
total_customers: number;
total_items: number;
total_orders: number;
recent_activities: RecentActivity[];
}
// Dashboard number cards
interface NumberCards {
total_assets: number;
work_orders_open: number;
work_orders_in_progress: number;
work_orders_completed: number;
}
// Dashboard chart payload
interface ChartDataset { name: string; values: number[]; color?: string }
interface ChartResponse {
labels: string[];
datasets: ChartDataset[];
type: 'Bar' | 'Pie' | 'Line' | string;
options?: Record<string, any>;
}
interface RecentActivity {
type: string;
name: string;
title: string;
creation: string;
}
interface KycRecord {
name: string;
kyc_status: string;
kyc_type: string;
creation: string;
modified: string;
}
interface KycDetailsResponse {
records: KycRecord[];
summary: {
total: number;
pending: number;
approved: number;
};
}
interface LoginResponse {
message: {
full_name: string;
user_id: string;
sid: string;
};
}
interface LoginCredentials {
email: string;
password: string;
}
interface FileUploadOptions {
file: File;
doctype: string;
docname: string;
fieldname: string;
}
interface RequestOptions {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
headers?: Record<string, string>;
body?: any;
}
// USER PERMISSION INTERFACES
interface RestrictionInfo {
field: string;
values: string[];
count: number;
}
interface PermissionFiltersResponse {
is_admin: boolean;
filters: Record<string, any>;
restrictions: Record<string, RestrictionInfo>;
target_doctype: string;
user?: string;
total_restrictions?: number;
warning?: string;
}
interface AllowedValuesResponse {
is_admin: boolean;
allowed_values: string[];
default_value?: string | null;
has_restriction: boolean;
allow_doctype: string;
}
interface DocumentAccessResponse {
has_access: boolean;
is_admin?: boolean;
no_restrictions?: boolean;
error?: string;
denied_by?: string;
field?: string;
document_value?: string;
allowed_values?: string[];
}
interface UserDefaultsResponse {
is_admin: boolean;
defaults: Record<string, string>;
}
class ApiService {
private baseURL: string;
private endpoints: Record<string, string>;
private defaultHeaders: Record<string, string>;
private timeout: number;
constructor() {
this.baseURL = API_CONFIG.BASE_URL;
this.endpoints = API_CONFIG.ENDPOINTS;
this.defaultHeaders = API_CONFIG.DEFAULT_HEADERS;
this.timeout = API_CONFIG.TIMEOUT;
}
// Get CSRF Token for authenticated requests
async getCSRFToken(): Promise<string | null> {
try {
// Try to get CSRF token, but don't fail if it's not available
const response = await fetch(`${this.baseURL}${this.endpoints.CSRF_TOKEN}`, {
method: 'GET',
headers: {
'Accept': 'application/json'
}
});
if (response.ok) {
const data: ApiResponse<string> = await response.json();
return data.message || null;
} else {
console.warn('CSRF token not available, continuing without it');
return null;
}
} catch (error) {
console.warn('Error getting CSRF token, continuing without it:', error);
return null;
}
}
// Generic API call method
async apiCall<T = any>(endpoint: string, options: RequestOptions = {}): Promise<T> {
const url = `${this.baseURL}${endpoint}`;
const defaultOptions: RequestInit = {
method: 'GET',
headers: {
...this.defaultHeaders,
...options.headers
},
...options
};
// Add CSRF token for non-GET requests
if (defaultOptions.method !== 'GET') {
const csrfToken = await this.getCSRFToken();
if (csrfToken) {
(defaultOptions.headers as Record<string, string>)['X-Frappe-CSRF-Token'] = csrfToken;
}
}
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
const response = await fetch(url, {
...defaultOptions,
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorData: ApiResponse = await response.json().catch(() => ({}));
throw new ApiError(
errorData.error || `HTTP error! status: ${response.status}`,
response.status
);
}
const data: ApiResponse<T> = await response.json();
// Handle Frappe API response format
if (data.message !== undefined) {
return data.message;
}
return data as T;
} catch (error) {
if (error instanceof Error) {
console.error('API call failed:', error);
throw new ApiError(error.message);
}
throw error;
}
}
// Authentication Methods
async login(credentials: LoginCredentials): Promise<LoginResponse> {
// Only log in development mode (hide sensitive data in production)
if (import.meta.env.DEV) {
console.log('[API Service] Login attempt for:', credentials.email);
}
const formData = new FormData();
formData.append('usr', credentials.email);
formData.append('pwd', credentials.password);
const url = `${this.baseURL}${this.endpoints.LOGIN}`;
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
const response = await fetch(url, {
method: 'POST',
headers: {
'Accept': 'application/json'
},
body: formData,
credentials: 'include', // Important: Include cookies
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
const errorData: ApiResponse = await response.json().catch(() => ({}));
// Hide detailed error messages in production
const errorMessage = import.meta.env.DEV
? (errorData.error || `HTTP error! status: ${response.status}`)
: 'Invalid credentials. Please try again.';
throw new ApiError(errorMessage, response.status);
}
const data: any = await response.json();
// Handle Frappe API response format
// Check if message is a string (like "Logged In")
if (typeof data.message === 'string' && data.message === 'Logged In') {
const userData = {
full_name: data.full_name,
user_id: data.user || data.email,
home_page: data.home_page,
sid: data.sid
};
if (import.meta.env.DEV) {
console.log('[API Service] Login successful');
}
return { message: userData } as LoginResponse;
}
// If message contains user object
if (data.message && typeof data.message === 'object') {
if (import.meta.env.DEV) {
console.log('[API Service] Login successful');
}
return { message: data.message } as LoginResponse;
}
// Sometimes Frappe returns full_name, user directly
if (data.full_name || data.user) {
if (import.meta.env.DEV) {
console.log('[API Service] Login successful');
}
return { message: data } as LoginResponse;
}
return { message: data } as LoginResponse;
} catch (error) {
if (error instanceof Error) {
// Only log detailed errors in development
if (import.meta.env.DEV) {
console.error('[API Service] Login error:', error.message);
}
throw new ApiError(
import.meta.env.DEV ? error.message : 'Login failed. Please try again.'
);
}
throw error;
}
}
async logout(): Promise<void> {
await this.apiCall(this.endpoints.LOGOUT, {
method: 'POST'
});
}
// User Management
async getUserDetails(userId?: string): Promise<UserDetails> {
const params = userId ? `?user_id=${userId}` : '';
return this.apiCall<UserDetails>(`${this.endpoints.USER_DETAILS}${params}`);
}
// Data Management
async getDoctypeRecords(
doctype: string,
filters?: Record<string, any>,
fields?: string[],
limit: number = 20,
offset: number = 0
): Promise<DocTypeRecordsResponse> {
const params = new URLSearchParams({
doctype,
limit: limit.toString(),
offset: offset.toString()
});
if (filters) {
params.append('filters', JSON.stringify(filters));
}
if (fields) {
params.append('fields', JSON.stringify(fields));
}
return this.apiCall<DocTypeRecordsResponse>(`${this.endpoints.DOCTYPE_RECORDS}?${params}`);
}
// Dashboard
async getDashboardStats(): Promise<DashboardStats> {
return this.apiCall<DashboardStats>(this.endpoints.DASHBOARD_STATS);
}
async getNumberCards(): Promise<NumberCards> {
return this.apiCall<NumberCards>(this.endpoints.DASHBOARD_NUMBER_CARDS);
}
async listDashboardCharts(publicOnly: boolean = true): Promise<{ charts: any[] }> {
const params = new URLSearchParams({ public_only: publicOnly ? '1' : '0' });
return this.apiCall<{ charts: any[] }>(`${this.endpoints.DASHBOARD_LIST_CHARTS}?${params}`);
}
async getDashboardChartData(chartName: string, filters?: Record<string, any>): Promise<ChartResponse> {
const params = new URLSearchParams({ chart_name: chartName });
if (filters) params.append('report_filters', JSON.stringify(filters));
return this.apiCall<ChartResponse>(`${this.endpoints.DASHBOARD_CHART_DATA}?${params}`);
}
// KYC Management
async getKycDetails(): Promise<KycDetailsResponse> {
return this.apiCall<KycDetailsResponse>(this.endpoints.KYC_DETAILS);
}
// File Upload
async uploadFile(options: FileUploadOptions): Promise<any> {
const formData = new FormData();
formData.append('file', options.file);
formData.append('doctype', options.doctype);
formData.append('docname', options.docname);
formData.append('fieldname', options.fieldname);
return this.apiCall(this.endpoints.UPLOAD_FILE, {
method: 'POST',
headers: {}, // Don't set Content-Type for FormData
body: formData
});
}
// USER PERMISSION METHODS
async getUserPermissions(userId?: string): Promise<any> {
const params = userId ? `?user=${encodeURIComponent(userId)}` : '';
return this.apiCall(`${this.endpoints.GET_USER_PERMISSIONS}${params}`);
}
async getPermissionFilters(targetDoctype: string, userId?: string): Promise<PermissionFiltersResponse> {
const params = new URLSearchParams({ target_doctype: targetDoctype });
if (userId) params.append('user', userId);
return this.apiCall<PermissionFiltersResponse>(`${this.endpoints.GET_PERMISSION_FILTERS}?${params}`);
}
async getAllowedValues(allowDoctype: string, userId?: string): Promise<AllowedValuesResponse> {
const params = new URLSearchParams({ allow_doctype: allowDoctype });
if (userId) params.append('user', userId);
return this.apiCall<AllowedValuesResponse>(`${this.endpoints.GET_ALLOWED_VALUES}?${params}`);
}
async checkDocumentAccess(doctype: string, docname: string, userId?: string): Promise<DocumentAccessResponse> {
const params = new URLSearchParams({ doctype, docname });
if (userId) params.append('user', userId);
return this.apiCall<DocumentAccessResponse>(`${this.endpoints.CHECK_DOCUMENT_ACCESS}?${params}`);
}
async getConfiguredDoctypes(): Promise<any> {
return this.apiCall(this.endpoints.GET_CONFIGURED_DOCTYPES);
}
async getUserDefaults(userId?: string): Promise<UserDefaultsResponse> {
const params = userId ? `?user=${encodeURIComponent(userId)}` : '';
return this.apiCall<UserDefaultsResponse>(`${this.endpoints.GET_USER_DEFAULTS}${params}`);
}
// Utility Methods
isAuthenticated(): boolean {
// Check if user is authenticated (implement based on your auth strategy)
return !!localStorage.getItem('frappe_session_id');
}
getSessionId(): string | null {
return localStorage.getItem('frappe_session_id');
}
setSessionId(sessionId: string): void {
localStorage.setItem('frappe_session_id', sessionId);
}
}
// Custom Error Class
class ApiError extends Error {
public status?: number;
public code?: string;
constructor(message: string, status?: number, code?: string) {
super(message);
this.name = 'ApiError';
this.status = status;
this.code = code;
}
}
// Create and export singleton instance
const apiService = new ApiService();
export default apiService;
export { ApiError };