490 lines
13 KiB
TypeScript
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 };
|