import API_CONFIG from '../config/api'; // Define interfaces locally to avoid import issues interface ApiResponse { message?: T; error?: string; status_code?: number; } interface UserDetails { user_id: string; full_name: string; email: string; user_image?: string; roles: string[]; permissions: Record; last_login?: string; enabled: boolean; creation: string; modified: string; language: string; custom_site_name?: string; custom_phcc_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; } 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; body?: any; } // USER PERMISSION INTERFACES interface RestrictionInfo { field: string; values: string[]; count: number; } interface PermissionFiltersResponse { is_admin: boolean; filters: Record; restrictions: Record; 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; } class ApiService { private baseURL: string; // private token: string | null = null; private endpoints: Record; private defaultHeaders: Record; 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 { try { // First, try to get CSRF token from window (injected by Frappe in HTML) if (typeof window !== 'undefined' && (window as any).csrf_token) { return (window as any).csrf_token; } // If not in window, try to fetch from API (but only if user is authenticated) // Check if user is logged in by checking localStorage const user = localStorage.getItem('user'); if (!user) { // User not logged in, skip CSRF token fetch return null; } const response = await fetch(`${this.baseURL}${this.endpoints.CSRF_TOKEN}`, { method: 'GET', headers: { 'Accept': 'application/json' }, credentials: 'include' // Include cookies for session }); if (response.ok) { const data: ApiResponse = await response.json(); return data.message || null; } else { // Silently fail - CSRF token not required for GET requests return null; } } catch (error) { // Silently fail - CSRF token not required for GET requests return null; } } // Generic API call method async apiCall(endpoint: string, options: RequestOptions = {}): Promise { const url = `${this.baseURL}${endpoint}`; const defaultOptions: RequestInit = { method: 'GET', headers: { ...this.defaultHeaders, ...options.headers, // 'Authorization': `token ${this.token}` }, ...options }; // Add CSRF token for non-GET requests // if (defaultOptions.method !== 'GET') { const csrfToken = await this.getCSRFToken(); if (csrfToken) { (defaultOptions.headers as Record)['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 = 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 { // 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 login(credentials: LoginCredentials): Promise { // const formData = new FormData(); // formData.append('usr', credentials.email); // formData.append('pwd', credentials.password); // const response = await fetch(`${this.baseURL}/api/method/login`, { // method: 'POST', // body: formData, // }); // const data = await response.json(); // if (data.message === 'Logged In') { // // Now get API keys or generate token // await this.fetchApiKeys(credentials); // return { message: data } as LoginResponse; // } // throw new ApiError('Login failed'); // } // private async fetchApiKeys(credentials: LoginCredentials): Promise { // // Use Basic Auth to get API keys // const basicAuth = btoa(`${credentials.email}:${credentials.password}`); // // First, check if user has API keys, if not generate them // const response = await fetch( // `${this.baseURL}/api/method/frappe.core.doctype.user.user.generate_keys?user=${encodeURIComponent(credentials.email)}`, // { // method: 'POST', // headers: { // 'Authorization': `Basic ${basicAuth}`, // 'Accept': 'application/json', // }, // } // ); // const data = await response.json(); // if (data.message?.api_secret) { // // Fetch the api_key from user doc // const userResponse = await fetch( // `${this.baseURL}/api/resource/User/${encodeURIComponent(credentials.email)}`, // { // method: 'GET', // headers: { // 'Authorization': `Basic ${basicAuth}`, // 'Accept': 'application/json', // }, // } // ); // const userData = await userResponse.json(); // const apiKey = userData.data.api_key; // const apiSecret = data.message.api_secret; // // Store the token // this.token = `${apiKey}:${apiSecret}`; // localStorage.setItem('auth_token', this.token); // localStorage.setItem('user_email', credentials.email); // } // } async logout(): Promise { await this.apiCall(this.endpoints.LOGOUT, { method: 'POST' }); } // User Management async getUserDetails(userId?: string): Promise { const params = userId ? `?user_id=${userId}` : ''; return this.apiCall(`${this.endpoints.USER_DETAILS}${params}`); } // Data Management async getDoctypeRecords( doctype: string, filters?: Record, fields?: string[], limit: number = 20, offset: number = 0 ): Promise { 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(`${this.endpoints.DOCTYPE_RECORDS}?${params}`); } // Dashboard async getDashboardStats(): Promise { return this.apiCall(this.endpoints.DASHBOARD_STATS); } async getNumberCards(): Promise { return this.apiCall(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): Promise { const params = new URLSearchParams({ chart_name: chartName }); if (filters) params.append('report_filters', JSON.stringify(filters)); return this.apiCall(`${this.endpoints.DASHBOARD_CHART_DATA}?${params}`); } // KYC Management async getKycDetails(): Promise { return this.apiCall(this.endpoints.KYC_DETAILS); } // File Upload async uploadFile(options: FileUploadOptions): Promise { 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 { const params = userId ? `?user=${encodeURIComponent(userId)}` : ''; return this.apiCall(`${this.endpoints.GET_USER_PERMISSIONS}${params}`); } async getPermissionFilters(targetDoctype: string, userId?: string): Promise { const params = new URLSearchParams({ target_doctype: targetDoctype }); if (userId) params.append('user', userId); return this.apiCall(`${this.endpoints.GET_PERMISSION_FILTERS}?${params}`); } async getAllowedValues(allowDoctype: string, userId?: string): Promise { const params = new URLSearchParams({ allow_doctype: allowDoctype }); if (userId) params.append('user', userId); return this.apiCall(`${this.endpoints.GET_ALLOWED_VALUES}?${params}`); } async checkDocumentAccess(doctype: string, docname: string, userId?: string): Promise { const params = new URLSearchParams({ doctype, docname }); if (userId) params.append('user', userId); return this.apiCall(`${this.endpoints.CHECK_DOCUMENT_ACCESS}?${params}`); } async getConfiguredDoctypes(): Promise { return this.apiCall(this.endpoints.GET_CONFIGURED_DOCTYPES); } async getUserDefaults(userId?: string): Promise { const params = userId ? `?user=${encodeURIComponent(userId)}` : ''; return this.apiCall(`${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 };