Spaces:
Sleeping
Sleeping
| import apiClient from '@/lib/api/axios-instance'; | |
| import { AxiosError } from 'axios'; | |
| import { | |
| AssetAssignment, | |
| AssetAssignmentCreateRequest, | |
| AssetAssignmentUpdateRequest, | |
| validateAssetAssignmentApiResponse | |
| } from '@/types/asset-management'; | |
| import { assetApi } from './assetApi'; | |
| import { employeeApi } from './employeeApi'; | |
| // Helper to create a safe asset assignment object with default values for null/undefined fields | |
| const createSafeAssetAssignment = (data: any): AssetAssignment => { | |
| return { | |
| assignmentID: data.assignmentID || 0, | |
| assetID: data.assetID || 0, | |
| employeeID: data.employeeID || 0, | |
| employeeName: data.employeeName || '', | |
| assetCode: data.assetCode || '', | |
| assignedDate: data.assignedDate || new Date().toISOString(), | |
| returnedDate: data.returnedDate || null, | |
| remarks: data.remarks || '', | |
| }; | |
| }; | |
| // Helper to ensure dates are in ISO 8601 format | |
| const formatDateToISO = (dateString: string): string => { | |
| if (!dateString) return new Date().toISOString(); | |
| // If already in ISO format, return as is | |
| if (dateString.includes('T') && dateString.includes('Z')) { | |
| return dateString; | |
| } | |
| // If it's a date-only string, add time component | |
| if (dateString.match(/^\d{4}-\d{2}-\d{2}$/)) { | |
| return `${dateString}T12:00:00.000Z`; | |
| } | |
| // Try to parse and convert to ISO | |
| try { | |
| return new Date(dateString).toISOString(); | |
| } catch (error) { | |
| console.warn('AssetAssignmentApi: Invalid date format, using current date:', dateString); | |
| return new Date().toISOString(); | |
| } | |
| }; | |
| // Helper to handle API errors with user-friendly messages | |
| const handleApiError = (error: unknown, operation: string): never => { | |
| console.error(`AssetAssignmentApi: Error during ${operation}:`, error); | |
| if (error instanceof AxiosError) { | |
| const status = error.response?.status; | |
| const message = error.response?.data?.message || error.message; | |
| switch (status) { | |
| case 400: | |
| throw new Error(`Invalid request: ${message}`); | |
| case 401: | |
| throw new Error('You are not authorized to perform this action. Please log in again.'); | |
| case 403: | |
| throw new Error('You do not have permission to perform this action.'); | |
| case 404: | |
| throw new Error('The requested assignment was not found.'); | |
| case 409: | |
| throw new Error('This assignment conflicts with existing data or the asset is already assigned.'); | |
| case 422: | |
| throw new Error(`Validation failed: ${message}`); | |
| case 500: | |
| throw new Error('Server error occurred. Please try again later.'); | |
| case 503: | |
| throw new Error('Service is temporarily unavailable. Please try again later.'); | |
| default: | |
| throw new Error(`${operation} failed: ${message}`); | |
| } | |
| } | |
| if (error instanceof Error) { | |
| throw new Error(`${operation} failed: ${error.message}`); | |
| } | |
| throw new Error(`${operation} failed: Unknown error occurred`); | |
| }; | |
| // Helper for retry logic | |
| const retryOperation = async <T>( | |
| operation: () => Promise<T>, | |
| maxRetries: number = 3, | |
| delay: number = 1000 | |
| ): Promise<T> => { | |
| let lastError: unknown; | |
| for (let attempt = 1; attempt <= maxRetries; attempt++) { | |
| try { | |
| return await operation(); | |
| } catch (error) { | |
| lastError = error; | |
| // Don't retry on client errors (4xx) except for 408 (timeout) and 429 (rate limit) | |
| if (error instanceof AxiosError) { | |
| const status = error.response?.status; | |
| if (status && status >= 400 && status < 500 && status !== 408 && status !== 429) { | |
| throw error; | |
| } | |
| } | |
| if (attempt === maxRetries) { | |
| throw lastError; | |
| } | |
| // Exponential backoff | |
| const waitTime = delay * Math.pow(2, attempt - 1); | |
| console.log(`AssetAssignmentApi: Retry attempt ${attempt}/${maxRetries} in ${waitTime}ms`); | |
| await new Promise(resolve => setTimeout(resolve, waitTime)); | |
| } | |
| } | |
| throw lastError; | |
| }; | |
| class AssetAssignmentApi { | |
| private baseUrl = `/api/AssetAssignments`; | |
| async getAll(): Promise<AssetAssignment[]> { | |
| try { | |
| console.log('AssetAssignmentApi: Fetching all asset assignments from', this.baseUrl); | |
| const response = await apiClient.get(this.baseUrl); | |
| console.log('AssetAssignmentApi: Response status', response.status); | |
| if (!response.data) { | |
| console.warn('AssetAssignmentApi: No data in response'); | |
| return []; | |
| } | |
| if (!Array.isArray(response.data)) { | |
| console.warn('AssetAssignmentApi: Response is not an array', response.data); | |
| return []; | |
| } | |
| console.log(`AssetAssignmentApi: Found ${response.data.length} asset assignments`); | |
| // Map each item to ensure it has all required fields and validate | |
| return response.data.map(item => { | |
| try { | |
| return validateAssetAssignmentApiResponse(item); | |
| } catch (error) { | |
| console.warn('AssetAssignmentApi: Invalid asset assignment data, using safe fallback:', error); | |
| return createSafeAssetAssignment(item); | |
| } | |
| }); | |
| } catch (error) { | |
| console.error('AssetAssignmentApi: Error fetching asset assignments:', error); | |
| // Return empty array instead of throwing to prevent UI crashes | |
| return []; | |
| } | |
| } | |
| async getById(id: number): Promise<AssetAssignment | null> { | |
| try { | |
| console.log(`AssetAssignmentApi: Fetching asset assignment ${id}`); | |
| const response = await apiClient.get(`${this.baseUrl}/${id}`); | |
| console.log(`AssetAssignmentApi: Response status for asset assignment ${id}`, response.status); | |
| if (response.data) { | |
| try { | |
| return validateAssetAssignmentApiResponse(response.data); | |
| } catch (error) { | |
| console.warn(`AssetAssignmentApi: Invalid asset assignment data for ID ${id}, using safe fallback:`, error); | |
| return createSafeAssetAssignment(response.data); | |
| } | |
| } | |
| return null; | |
| } catch (error) { | |
| console.error(`AssetAssignmentApi: Error fetching asset assignment ${id}:`, error); | |
| return null; | |
| } | |
| } | |
| async getByAssetId(assetId: number): Promise<AssetAssignment[]> { | |
| try { | |
| console.log(`AssetAssignmentApi: Fetching asset assignments for asset ${assetId}`); | |
| const response = await apiClient.get(`${this.baseUrl}/by-asset/${assetId}`); | |
| console.log(`AssetAssignmentApi: Response status for asset ${assetId} assignments`, response.status); | |
| if (!response.data) { | |
| console.warn(`AssetAssignmentApi: No asset assignments found for asset ${assetId}`); | |
| return []; | |
| } | |
| if (!Array.isArray(response.data)) { | |
| console.warn(`AssetAssignmentApi: Response is not an array for asset ${assetId}`, response.data); | |
| return []; | |
| } | |
| console.log(`AssetAssignmentApi: Found ${response.data.length} asset assignments for asset ${assetId}`); | |
| // Map each item to ensure it has all required fields and validate | |
| return response.data.map(item => { | |
| try { | |
| return validateAssetAssignmentApiResponse(item); | |
| } catch (error) { | |
| console.warn(`AssetAssignmentApi: Invalid asset assignment data for asset ${assetId}, using safe fallback:`, error); | |
| return createSafeAssetAssignment(item); | |
| } | |
| }); | |
| } catch (error) { | |
| console.error(`AssetAssignmentApi: Error fetching asset assignments for asset ${assetId}:`, error); | |
| // Return empty array instead of throwing to prevent UI crashes | |
| return []; | |
| } | |
| } | |
| async getByEmployeeId(employeeId: number): Promise<AssetAssignment[]> { | |
| try { | |
| console.log(`AssetAssignmentApi: Fetching asset assignments for employee ${employeeId}`); | |
| const response = await apiClient.get(`${this.baseUrl}/by-employee/${employeeId}`); | |
| console.log(`AssetAssignmentApi: Response status for employee ${employeeId} assignments`, response.status); | |
| if (!response.data) { | |
| console.warn(`AssetAssignmentApi: No asset assignments found for employee ${employeeId}`); | |
| return []; | |
| } | |
| if (!Array.isArray(response.data)) { | |
| console.warn(`AssetAssignmentApi: Response is not an array for employee ${employeeId}`, response.data); | |
| return []; | |
| } | |
| console.log(`AssetAssignmentApi: Found ${response.data.length} asset assignments for employee ${employeeId}`); | |
| // Map each item to ensure it has all required fields and validate | |
| return response.data.map(item => { | |
| try { | |
| return validateAssetAssignmentApiResponse(item); | |
| } catch (error) { | |
| console.warn(`AssetAssignmentApi: Invalid asset assignment data for employee ${employeeId}, using safe fallback:`, error); | |
| return createSafeAssetAssignment(item); | |
| } | |
| }); | |
| } catch (error) { | |
| console.error(`AssetAssignmentApi: Error fetching asset assignments for employee ${employeeId}:`, error); | |
| // Return empty array instead of throwing to prevent UI crashes | |
| return []; | |
| } | |
| } | |
| async create(assetAssignment: AssetAssignmentCreateRequest): Promise<AssetAssignment> { | |
| return retryOperation(async () => { | |
| try { | |
| // Validate that the referenced asset exists | |
| const asset = await assetApi.getById(assetAssignment.assetID); | |
| if (!asset) { | |
| throw new Error(`Asset with ID ${assetAssignment.assetID} does not exist. Please select a valid asset.`); | |
| } | |
| // Validate that the referenced employee exists | |
| const employeeResponse = await employeeApi.getById(assetAssignment.employeeID); | |
| if (!employeeResponse.success || !employeeResponse.data) { | |
| throw new Error(`Employee with ID ${assetAssignment.employeeID} does not exist. Please select a valid employee.`); | |
| } | |
| // Format dates to ISO 8601 | |
| const assetAssignmentData = { | |
| ...assetAssignment, | |
| assignedDate: formatDateToISO(assetAssignment.assignedDate), | |
| returnedDate: assetAssignment.returnedDate ? formatDateToISO(assetAssignment.returnedDate) : null, | |
| }; | |
| console.log('AssetAssignmentApi: Creating asset assignment with data', assetAssignmentData); | |
| const response = await apiClient.post(this.baseUrl, assetAssignmentData); | |
| console.log('AssetAssignmentApi: Create response status', response.status); | |
| try { | |
| return validateAssetAssignmentApiResponse(response.data); | |
| } catch (error) { | |
| console.warn('AssetAssignmentApi: Invalid response data from create, using safe fallback:', error); | |
| return createSafeAssetAssignment(response.data); | |
| } | |
| } catch (error) { | |
| handleApiError(error, 'create asset assignment'); | |
| } | |
| }, 2); // Fewer retries for create operations | |
| } | |
| async update(id: number, assetAssignment: AssetAssignmentUpdateRequest): Promise<AssetAssignment> { | |
| try { | |
| // Validate that the referenced asset exists | |
| const asset = await assetApi.getById(assetAssignment.assetID); | |
| if (!asset) { | |
| const errorMessage = `Asset with ID ${assetAssignment.assetID} does not exist`; | |
| console.error('AssetAssignmentApi:', errorMessage); | |
| throw new Error(errorMessage); | |
| } | |
| // Validate that the referenced employee exists | |
| const employeeResponse = await employeeApi.getById(assetAssignment.employeeID); | |
| if (!employeeResponse.success || !employeeResponse.data) { | |
| const errorMessage = `Employee with ID ${assetAssignment.employeeID} does not exist`; | |
| console.error('AssetAssignmentApi:', errorMessage); | |
| throw new Error(errorMessage); | |
| } | |
| // Format dates to ISO 8601 | |
| const assetAssignmentData = { | |
| ...assetAssignment, | |
| assignedDate: formatDateToISO(assetAssignment.assignedDate), | |
| returnedDate: assetAssignment.returnedDate ? formatDateToISO(assetAssignment.returnedDate) : null, | |
| }; | |
| console.log(`AssetAssignmentApi: Updating asset assignment ${id} with data`, assetAssignmentData); | |
| const response = await apiClient.put(`${this.baseUrl}/${id}`, assetAssignmentData); | |
| console.log(`AssetAssignmentApi: Update response status for asset assignment ${id}`, response.status); | |
| try { | |
| return validateAssetAssignmentApiResponse(response.data); | |
| } catch (error) { | |
| console.warn(`AssetAssignmentApi: Invalid response data from update for ID ${id}, using safe fallback:`, error); | |
| return createSafeAssetAssignment(response.data); | |
| } | |
| } catch (error) { | |
| console.error(`AssetAssignmentApi: Error updating asset assignment ${id}:`, error); | |
| throw error; | |
| } | |
| } | |
| async delete(id: number): Promise<void> { | |
| try { | |
| console.log(`AssetAssignmentApi: Deleting asset assignment ${id}`); | |
| await apiClient.delete(`${this.baseUrl}/${id}`); | |
| console.log(`AssetAssignmentApi: Asset assignment ${id} deleted successfully`); | |
| } catch (error) { | |
| console.error(`AssetAssignmentApi: Error deleting asset assignment ${id}:`, error); | |
| throw error; | |
| } | |
| } | |
| async getAssignmentHistory(assetId: number): Promise<{ | |
| success: boolean; | |
| data: Array<AssetAssignment & { | |
| assignmentStatus: string; | |
| assignmentDuration: number | null; | |
| returnReason: string | null; | |
| employee: { | |
| id: number; | |
| empCode: string; | |
| firstName: string; | |
| middleName: string; | |
| lastName: string; | |
| email: string; | |
| mobile: string; | |
| type: string; | |
| academics: string; | |
| financialData: string; | |
| status: string; | |
| description: string; | |
| userId: number; | |
| passwordHash: string | null; | |
| roleId: number; | |
| createdBy: number; | |
| createdAt: string; | |
| createdByName: string | null; | |
| updatedBy: number; | |
| updatedAt: string; | |
| updatedByName: string; | |
| }; | |
| }>; | |
| totalAssignments: number; | |
| activeAssignments: number; | |
| completedAssignments: number; | |
| totalDaysAssigned: number; | |
| message: string; | |
| }> { | |
| return retryOperation(async () => { | |
| try { | |
| console.log(`AssetAssignmentApi: Fetching assignment history for asset ${assetId}`); | |
| // Use the history by-asset endpoint | |
| const response = await apiClient.get(`${this.baseUrl}/history/by-asset/${assetId}`); | |
| console.log(`AssetAssignmentApi: Response status for asset ${assetId} assignment history`, response.status); | |
| if (!response.data) { | |
| console.warn(`AssetAssignmentApi: No assignment history found for asset ${assetId}`); | |
| return { | |
| success: false, | |
| data: [], | |
| totalAssignments: 0, | |
| activeAssignments: 0, | |
| completedAssignments: 0, | |
| totalDaysAssigned: 0, | |
| message: `No assignment history found for asset ${assetId}` | |
| }; | |
| } | |
| // Check if response already has the expected structure (from your curl example) | |
| if (response.data.success !== undefined && response.data.data !== undefined) { | |
| console.log(`AssetAssignmentApi: Found assignment history for asset ${assetId}:`, response.data); | |
| return response.data; | |
| } | |
| // If it's just an array, transform it to match the expected structure | |
| const assignments = Array.isArray(response.data) ? response.data : []; | |
| const activeAssignments = assignments.filter((assignment: any) => !assignment.returnedDate).length; | |
| const completedAssignments = assignments.filter((assignment: any) => assignment.returnedDate).length; | |
| // Calculate total days assigned for completed assignments | |
| const totalDaysAssigned = assignments.reduce((total: number, assignment: any) => { | |
| if (assignment.returnedDate && assignment.assignedDate) { | |
| const assignedDate = new Date(assignment.assignedDate); | |
| const returnedDate = new Date(assignment.returnedDate); | |
| const diffTime = Math.abs(returnedDate.getTime() - assignedDate.getTime()); | |
| const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); | |
| return total + diffDays; | |
| } | |
| return total; | |
| }, 0); | |
| console.log(`AssetAssignmentApi: Found ${assignments.length} assignments for asset ${assetId}`); | |
| return { | |
| success: true, | |
| data: assignments.map((assignment: any) => ({ | |
| ...assignment, | |
| assignmentStatus: assignment.returnedDate ? 'Returned' : 'Active', | |
| assignmentDuration: assignment.returnedDate && assignment.assignedDate | |
| ? Math.ceil((new Date(assignment.returnedDate).getTime() - new Date(assignment.assignedDate).getTime()) / (1000 * 60 * 60 * 24)) | |
| : null, | |
| returnReason: assignment.returnReason || null | |
| })), | |
| totalAssignments: assignments.length, | |
| activeAssignments, | |
| completedAssignments, | |
| totalDaysAssigned, | |
| message: 'Assignment history retrieved successfully' | |
| }; | |
| } catch (error) { | |
| if (error instanceof AxiosError && error.response?.status === 404) { | |
| console.log(`AssetAssignmentApi: No assignment history found for asset ${assetId}`); | |
| return { | |
| success: false, | |
| data: [], | |
| totalAssignments: 0, | |
| activeAssignments: 0, | |
| completedAssignments: 0, | |
| totalDaysAssigned: 0, | |
| message: `No assignment history found for asset ${assetId}` | |
| }; | |
| } | |
| handleApiError(error, `fetch assignment history for asset ${assetId}`); | |
| } | |
| }); | |
| } | |
| async getActiveAssignments(): Promise<{ | |
| success: boolean; | |
| data: Array<AssetAssignment & { | |
| assignmentStatus: string; | |
| daysSinceAssigned: number; | |
| employee: { | |
| id: number; | |
| empCode: string; | |
| firstName: string; | |
| middleName: string; | |
| lastName: string; | |
| email: string; | |
| mobile: string; | |
| type: string; | |
| academics: string; | |
| financialData: string; | |
| status: string; | |
| description: string; | |
| userId: number; | |
| passwordHash: string | null; | |
| roleId: number; | |
| createdBy: number; | |
| createdAt: string; | |
| createdByName: string | null; | |
| updatedBy: number; | |
| updatedAt: string; | |
| updatedByName: string; | |
| }; | |
| asset?: { | |
| assetID: number; | |
| assetCode: string; | |
| assetType: string; | |
| brandModel: string; | |
| serialNumber: string; | |
| assetCondition: string; | |
| }; | |
| }>; | |
| totalActiveAssignments: number; | |
| overdueAssignments: number; | |
| assignmentsByDepartment: Record<string, number>; | |
| message: string; | |
| }> { | |
| return retryOperation(async () => { | |
| try { | |
| console.log('AssetAssignmentApi: Fetching all active assignments'); | |
| const response = await apiClient.get(`${this.baseUrl}/active`); | |
| console.log('AssetAssignmentApi: Response status for active assignments', response.status); | |
| if (!response.data) { | |
| console.warn('AssetAssignmentApi: No active assignments found'); | |
| return { | |
| success: false, | |
| data: [], | |
| totalActiveAssignments: 0, | |
| overdueAssignments: 0, | |
| assignmentsByDepartment: {}, | |
| message: 'No active assignments found' | |
| }; | |
| } | |
| console.log('AssetAssignmentApi: Found active assignments:', response.data); | |
| // Return the response as-is since it should match the expected structure | |
| return response.data; | |
| } catch (error) { | |
| if (error instanceof AxiosError && error.response?.status === 404) { | |
| console.log('AssetAssignmentApi: No active assignments found'); | |
| return { | |
| success: false, | |
| data: [], | |
| totalActiveAssignments: 0, | |
| overdueAssignments: 0, | |
| assignmentsByDepartment: {}, | |
| message: 'No active assignments found' | |
| }; | |
| } | |
| handleApiError(error, 'fetch active assignments'); | |
| } | |
| }); | |
| } | |
| async bulkReturn(request: { | |
| assignmentIds: number[]; | |
| returnedDate: string; | |
| returnReason?: string; | |
| remarks?: string; | |
| performedBy?: { | |
| employeeID: number; | |
| name: string; | |
| }; | |
| }): Promise<{ | |
| success: boolean; | |
| data: { | |
| processedAssignments: number; | |
| successfulReturns: number; | |
| failedReturns: number; | |
| results: Array<{ | |
| assignmentID: number; | |
| status: 'success' | 'failed'; | |
| message: string; | |
| returnedAssignment?: AssetAssignment; | |
| error?: string; | |
| }>; | |
| }; | |
| summary: { | |
| totalRequested: number; | |
| successful: number; | |
| failed: number; | |
| assetsReturned: number; | |
| employeesAffected: number; | |
| }; | |
| message: string; | |
| }> { | |
| return retryOperation(async () => { | |
| try { | |
| // Validate input | |
| if (!Array.isArray(request.assignmentIds) || request.assignmentIds.length === 0) { | |
| throw new Error('Assignment IDs array is required and cannot be empty'); | |
| } | |
| if (request.assignmentIds.length > 100) { | |
| throw new Error('Cannot return more than 100 assignments at once'); | |
| } | |
| // Format the request data | |
| const requestData = { | |
| assignmentIds: request.assignmentIds, | |
| returnedDate: formatDateToISO(request.returnedDate), | |
| returnReason: request.returnReason || 'Bulk return operation', | |
| remarks: request.remarks || 'Bulk returned via system', | |
| performedBy: request.performedBy | |
| }; | |
| console.log(`AssetAssignmentApi: Bulk returning ${request.assignmentIds.length} assignments`, requestData); | |
| const response = await apiClient.post(`${this.baseUrl}/bulk-return`, requestData); | |
| console.log('AssetAssignmentApi: Bulk return response status', response.status); | |
| console.log('AssetAssignmentApi: Bulk return response data', response.data); | |
| // Validate response structure | |
| if (!response.data || typeof response.data !== 'object') { | |
| throw new Error('Invalid response format from bulk return API'); | |
| } | |
| const result = response.data; | |
| // Ensure required fields exist | |
| if (typeof result.success !== 'boolean') { | |
| throw new Error('Invalid response: missing success field'); | |
| } | |
| console.log(`AssetAssignmentApi: Bulk return completed - ${result.data?.successfulReturns || 0} successful, ${result.data?.failedReturns || 0} failed`); | |
| return result; | |
| } catch (error) { | |
| handleApiError(error, 'bulk return assignments'); | |
| } | |
| }, 1); // No retries for bulk operations to avoid duplicate operations | |
| } | |
| } | |
| export const assetAssignmentApi = new AssetAssignmentApi(); |