import apiClient from '@/lib/api/axios-instance'; import { AxiosError } from 'axios'; import { AssetRequest, AssetRequestCreateRequest, AssetRequestApprovalRequest, AssetRequestRejectionRequest, AssetRequestAssignmentRequest, AssetRequestCommentRequest, AssetRequestWithAvailability, AvailabilityCheck, RequestFilters, RequestedAssetItem, validateAssetRequestApiResponse, isValidStatusTransition } from '@/types/asset-request'; import { assetAssignmentApi } from './assetAssignmentApi'; import { assetApi } from './assetApi'; // Helper to create a safe asset request object with default values for null/undefined fields const createSafeAssetRequest = (data: any): AssetRequest => { return { requestID: data.requestID || 0, employeeID: data.employeeID || 0, employeeName: data.employeeName || '', requestedAssets: data.requestedAssets || [], justification: data.justification || '', status: data.status || 'pending', submittedDate: data.submittedDate || new Date().toISOString(), reviewedDate: data.reviewedDate || null, reviewedBy: data.reviewedBy || null, reviewerName: data.reviewerName || null, adminComments: data.adminComments || null, assignedDate: data.assignedDate || null, assignedBy: data.assignedBy || null, assignerName: data.assignerName || null, acknowledgedDate: data.acknowledgedDate || null, acknowledgedBy: data.acknowledgedBy || null, acknowledgementComments: data.acknowledgementComments || null, }; }; // Helper to handle API errors with user-friendly messages const handleApiError = (error: unknown, operation: string): never => { console.error(`AssetRequestApi: 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 asset request was not found.'); case 409: throw new Error('This request conflicts with existing data or cannot be processed in its current state.'); 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 ( operation: () => Promise, maxRetries: number = 3, delay: number = 1000 ): Promise => { 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(`AssetRequestApi: Retry attempt ${attempt}/${maxRetries} in ${waitTime}ms`); await new Promise(resolve => setTimeout(resolve, waitTime)); } } throw lastError; }; class AssetRequestApi { private baseUrl = `/api/AssetRequests`; /** * Create a new asset request * @param request - Asset request creation data * @returns Created asset request */ async create(request: AssetRequestCreateRequest): Promise { return retryOperation(async () => { try { console.log('AssetRequestApi: Creating asset request with data', request); const response = await apiClient.post(this.baseUrl, request); console.log('AssetRequestApi: Create response status', response.status); // Handle wrapped response format: { success, message, data } const responseData = response.data.data || response.data; try { return validateAssetRequestApiResponse(responseData); } catch (error) { console.warn('AssetRequestApi: Invalid response data from create, using safe fallback:', error); return createSafeAssetRequest(responseData); } } catch (error) { handleApiError(error, 'create asset request'); } }, 2); // Fewer retries for create operations } /** * Get asset request by ID * @param id - Request ID * @returns Asset request or null if not found */ async getById(id: number): Promise { return retryOperation(async () => { try { console.log(`AssetRequestApi: Fetching asset request ${id}`); const response = await apiClient.get(`${this.baseUrl}/${id}`); console.log(`AssetRequestApi: Response status for asset request ${id}`, response.status); if (response.data) { // Handle wrapped response format: { success, message, data } const responseData = response.data.data || response.data; try { return validateAssetRequestApiResponse(responseData); } catch (error) { console.warn(`AssetRequestApi: Invalid asset request data for ID ${id}, using safe fallback:`, error); return createSafeAssetRequest(responseData); } } return null; } catch (error) { if (error instanceof AxiosError && error.response?.status === 404) { return null; // Request not found is a valid case } handleApiError(error, `fetch asset request ${id}`); } }); } /** * Get asset requests by employee ID * @param employeeId - Employee ID * @returns List of asset requests for the employee */ async getByEmployeeId(employeeId: number): Promise { return retryOperation(async () => { try { console.log(`AssetRequestApi: Fetching asset requests for employee ${employeeId}`); const response = await apiClient.get(`${this.baseUrl}/employee/${employeeId}`); console.log(`AssetRequestApi: Response status for employee ${employeeId} requests`, response.status); if (!response.data) { console.warn(`AssetRequestApi: No asset requests found for employee ${employeeId}`); return []; } // Handle wrapped response format: { success, message, data } const dataArray = response.data.data || response.data; if (!Array.isArray(dataArray)) { console.warn(`AssetRequestApi: Response is not an array for employee ${employeeId}`, response.data); return []; } console.log(`AssetRequestApi: Found ${dataArray.length} asset requests for employee ${employeeId}`); // Map each item to ensure it has all required fields and validate return dataArray.map(item => { try { return validateAssetRequestApiResponse(item); } catch (error) { console.warn(`AssetRequestApi: Invalid asset request data for employee ${employeeId}, using safe fallback:`, error); return createSafeAssetRequest(item); } }); } catch (error) { if (error instanceof AxiosError && error.response?.status === 404) { return []; // No requests found is a valid case } handleApiError(error, `fetch asset requests for employee ${employeeId}`); } }); } /** * Get all asset requests with optional filtering * @param filters - Optional filters for status, employee, date range * @returns List of asset requests */ async getAll(filters?: RequestFilters): Promise { return retryOperation(async () => { try { console.log('AssetRequestApi: Fetching all asset requests with filters', filters); // Build query parameters const params: any = {}; if (filters?.status) params.status = filters.status; if (filters?.employeeId) params.employeeId = filters.employeeId; if (filters?.dateFrom) params.dateFrom = filters.dateFrom; if (filters?.dateTo) params.dateTo = filters.dateTo; const response = await apiClient.get(this.baseUrl, { params }); console.log('AssetRequestApi: Response status', response.status); if (!response.data) { console.warn('AssetRequestApi: No data in response'); return []; } // Handle wrapped response format: { success, message, data } const dataArray = response.data.data || response.data; if (!Array.isArray(dataArray)) { console.warn('AssetRequestApi: Response is not an array', response.data); return []; } console.log(`AssetRequestApi: Found ${dataArray.length} asset requests`); // Map each item to ensure it has all required fields and validate return dataArray.map(item => { try { return validateAssetRequestApiResponse(item); } catch (error) { console.warn('AssetRequestApi: Invalid asset request data, using safe fallback:', error); return createSafeAssetRequest(item); } }); } catch (error) { console.error('AssetRequestApi: Error fetching asset requests:', error); // Return empty array instead of throwing to prevent UI crashes return []; } }); } /** * Get request history for an employee * @param employeeId - Employee ID * @returns List of asset requests for the employee (alias for getByEmployeeId) */ async getRequestHistory(employeeId: number): Promise { return this.getByEmployeeId(employeeId); } /** * Get all pending asset requests with availability information * @returns List of pending requests with availability status */ async getPending(): Promise { return retryOperation(async () => { try { console.log('AssetRequestApi: Fetching pending asset requests with availability'); const response = await apiClient.get(`${this.baseUrl}/pending`); console.log('AssetRequestApi: Response status for pending requests', response.status); if (!response.data) { console.warn('AssetRequestApi: No pending requests found'); return []; } // Handle wrapped response format: { success, message, data } const dataArray = response.data.data || response.data; if (!Array.isArray(dataArray)) { console.warn('AssetRequestApi: Response is not an array', response.data); return []; } console.log(`AssetRequestApi: Found ${dataArray.length} pending asset requests`); // Map each item and ensure availability status is present return dataArray.map(item => { try { const baseRequest = validateAssetRequestApiResponse(item); return { ...baseRequest, availabilityStatus: item.availabilityStatus || [] } as AssetRequestWithAvailability; } catch (error) { console.warn('AssetRequestApi: Invalid pending request data, using safe fallback:', error); const safeRequest = createSafeAssetRequest(item); return { ...safeRequest, availabilityStatus: item.availabilityStatus || [] } as AssetRequestWithAvailability; } }); } catch (error) { console.error('AssetRequestApi: Error fetching pending requests:', error); // Return empty array instead of throwing to prevent UI crashes return []; } }); } /** * Approve an asset request * @param id - Request ID * @param data - Approval data with admin comments and reviewer ID * @returns Updated asset request */ async approve(id: number, data: AssetRequestApprovalRequest): Promise { return retryOperation(async () => { try { // First, get the current request to validate status transition const currentRequest = await this.getById(id); if (!currentRequest) { throw new Error(`Asset request with ID ${id} not found`); } // Validate status transition if (!isValidStatusTransition(currentRequest.status, 'approved')) { throw new Error( `Cannot approve request: Request is currently ${currentRequest.status}. ` + 'Only pending requests can be approved.' ); } console.log(`AssetRequestApi: Approving asset request ${id} with data`, data); const response = await apiClient.put(`${this.baseUrl}/${id}/approve`, data); console.log(`AssetRequestApi: Approve response status for request ${id}`, response.status); // Handle wrapped response format: { success, message, data } const responseData = response.data.data || response.data; try { return validateAssetRequestApiResponse(responseData); } catch (error) { console.warn(`AssetRequestApi: Invalid response data from approve for ID ${id}, using safe fallback:`, error); return createSafeAssetRequest(responseData); } } catch (error) { handleApiError(error, `approve asset request ${id}`); } }, 2); // Fewer retries for state-changing operations } /** * Reject an asset request * @param id - Request ID * @param data - Rejection data with admin comments and reviewer ID * @returns Updated asset request */ async reject(id: number, data: AssetRequestRejectionRequest): Promise { return retryOperation(async () => { try { // First, get the current request to validate status transition const currentRequest = await this.getById(id); if (!currentRequest) { throw new Error(`Asset request with ID ${id} not found`); } // Validate status transition if (!isValidStatusTransition(currentRequest.status, 'rejected')) { throw new Error( `Cannot reject request: Request is currently ${currentRequest.status}. ` + 'Only pending requests can be rejected.' ); } console.log(`AssetRequestApi: Rejecting asset request ${id} with data`, data); const response = await apiClient.put(`${this.baseUrl}/${id}/reject`, data); console.log(`AssetRequestApi: Reject response status for request ${id}`, response.status); // Handle wrapped response format: { success, message, data } const responseData = response.data.data || response.data; try { return validateAssetRequestApiResponse(responseData); } catch (error) { console.warn(`AssetRequestApi: Invalid response data from reject for ID ${id}, using safe fallback:`, error); return createSafeAssetRequest(responseData); } } catch (error) { handleApiError(error, `reject asset request ${id}`); } }, 2); // Fewer retries for state-changing operations } /** * Assign assets to an approved request * @param id - Request ID * @param data - Assignment data with asset details and assigner ID * @returns Updated asset request */ async assign(id: number, data: AssetRequestAssignmentRequest): Promise { return retryOperation(async () => { const createdAssignments: number[] = []; // Track created assignments for rollback try { // First, get the current request to validate status transition const currentRequest = await this.getById(id); if (!currentRequest) { throw new Error(`Asset request with ID ${id} not found`); } // Validate status transition if (!isValidStatusTransition(currentRequest.status, 'assigned')) { throw new Error( `Cannot assign assets: Request is currently ${currentRequest.status}. ` + 'Only approved requests can have assets assigned.' ); } // Validate that we have assets to assign if (!data.assignedAssets || data.assignedAssets.length === 0) { throw new Error('At least one asset must be assigned'); } // Check availability for all requested assets before proceeding const availabilityCheck = await this.checkAvailability(currentRequest.requestedAssets); if (!availabilityCheck.canFulfill) { const shortfalls = availabilityCheck.details .filter(d => d.shortfall > 0) .map(d => `${d.assetType}: need ${d.requested}, only ${d.available} available`) .join('; '); throw new Error(`Insufficient inventory to fulfill request: ${shortfalls}`); } // Validate that assigned assets match requested quantities const assignedByType = new Map(); data.assignedAssets.forEach(asset => { const count = assignedByType.get(asset.assetType) || 0; assignedByType.set(asset.assetType, count + 1); }); for (const requestedItem of currentRequest.requestedAssets) { const assignedCount = assignedByType.get(requestedItem.assetType) || 0; if (assignedCount !== requestedItem.quantity) { throw new Error( `Asset quantity mismatch for ${requestedItem.assetType}: ` + `requested ${requestedItem.quantity}, assigning ${assignedCount}` ); } } // Validate that all assets are currently available (not assigned) const allAssignments = await assetAssignmentApi.getAll(); const activeAssignments = allAssignments.filter(a => !a.returnedDate); const assignedAssetIds = new Set(activeAssignments.map(a => a.assetID)); for (const assignedAsset of data.assignedAssets) { if (assignedAssetIds.has(assignedAsset.assetID)) { throw new Error( `Asset ${assignedAsset.assetCode} is already assigned to another employee. ` + 'Please select a different asset.' ); } } console.log(`AssetRequestApi: Assigning assets to request ${id} with data`, data); console.log(`AssetRequestApi: Validation passed - proceeding with ${data.assignedAssets.length} assignments`); // Create asset assignments for each assigned asset for (const assignedAsset of data.assignedAssets) { try { const assignmentData = { assetID: assignedAsset.assetID, employeeID: currentRequest.employeeID, employeeName: currentRequest.employeeName, assetCode: assignedAsset.assetCode, assignedDate: new Date().toISOString(), returnedDate: null, remarks: `Assigned via asset request #${id}: ${currentRequest.justification.substring(0, 100)}` }; console.log(`AssetRequestApi: Creating assignment for asset ${assignedAsset.assetID}`, assignmentData); const assignment = await assetAssignmentApi.create(assignmentData); createdAssignments.push(assignment.assignmentID); console.log(`AssetRequestApi: Successfully created assignment ${assignment.assignmentID} for asset ${assignedAsset.assetID}`); } catch (error) { console.error(`AssetRequestApi: Failed to create assignment for asset ${assignedAsset.assetID}:`, error); throw new Error(`Failed to assign asset ${assignedAsset.assetCode}: ${error instanceof Error ? error.message : 'Unknown error'}`); } } console.log(`AssetRequestApi: Successfully created ${data.assignedAssets.length} asset assignments`); // Update the request status to assigned try { const response = await apiClient.put(`${this.baseUrl}/${id}/assign`, data); console.log(`AssetRequestApi: Assign response status for request ${id}`, response.status); // Log audit trail console.log(`AssetRequestApi: Audit log - Request ${id} assigned by user ${data.assignedBy} at ${new Date().toISOString()}`); console.log(`AssetRequestApi: Audit log - Assets assigned: ${data.assignedAssets.map(a => a.assetCode).join(', ')}`); console.log(`AssetRequestApi: Audit log - Employee: ${currentRequest.employeeName} (ID: ${currentRequest.employeeID})`); // Handle wrapped response format: { success, message, data } const responseData = response.data.data || response.data; try { return validateAssetRequestApiResponse(responseData); } catch (error) { console.warn(`AssetRequestApi: Invalid response data from assign for ID ${id}, using safe fallback:`, error); return createSafeAssetRequest(responseData); } } catch (error) { // If updating the request status fails, rollback all created assignments console.error(`AssetRequestApi: Failed to update request status, rolling back ${createdAssignments.length} assignments`); await this.rollbackAssignments(createdAssignments); throw error; } } catch (error) { // If any error occurs, rollback all created assignments if (createdAssignments.length > 0) { console.error(`AssetRequestApi: Error during assignment, rolling back ${createdAssignments.length} assignments`); await this.rollbackAssignments(createdAssignments); } handleApiError(error, `assign assets to request ${id}`); } }, 1); // No retries for assignment operations to avoid duplicate assignments } /** * Add admin comments to a request without changing status * @param id - Request ID * @param data - Comment data with admin comments and commenter ID * @returns Updated asset request */ async addComment(id: number, data: AssetRequestCommentRequest): Promise { return retryOperation(async () => { try { console.log(`AssetRequestApi: Adding comment to asset request ${id} with data`, data); const response = await apiClient.put(`${this.baseUrl}/${id}/comment`, data); console.log(`AssetRequestApi: Add comment response status for request ${id}`, response.status); // Handle wrapped response format: { success, message, data } const responseData = response.data.data || response.data; try { return validateAssetRequestApiResponse(responseData); } catch (error) { console.warn(`AssetRequestApi: Invalid response data from addComment for ID ${id}, using safe fallback:`, error); return createSafeAssetRequest(responseData); } } catch (error) { handleApiError(error, `add comment to asset request ${id}`); } }, 2); // Allow retries for comment operations } /** * Rollback created assignments in case of failure * @param assignmentIds - Array of assignment IDs to delete */ private async rollbackAssignments(assignmentIds: number[]): Promise { console.log(`AssetRequestApi: Starting rollback for ${assignmentIds.length} assignments`); for (const assignmentId of assignmentIds) { try { await assetAssignmentApi.delete(assignmentId); console.log(`AssetRequestApi: Rolled back assignment ${assignmentId}`); } catch (error) { console.error(`AssetRequestApi: Failed to rollback assignment ${assignmentId}:`, error); // Continue with other rollbacks even if one fails } } console.log(`AssetRequestApi: Rollback completed`); } /** * Check availability of requested assets * @param requestedAssets - List of requested asset items * @returns Availability check result with details for each asset type */ async checkAvailability(requestedAssets: RequestedAssetItem[]): Promise { return retryOperation(async () => { try { console.log('AssetRequestApi: Checking availability for requested assets', requestedAssets); // Get all assets to calculate availability const allAssets = await assetApi.getAll(); // Get all active assignments to calculate what's currently assigned const allAssignments = await assetAssignmentApi.getAll(); const activeAssignments = allAssignments.filter(assignment => !assignment.returnedDate); // Calculate availability for each requested asset type const details = requestedAssets.map(requestedItem => { // Count total assets of this type const totalAssets = allAssets.filter(asset => asset.assetType === requestedItem.assetType).length; // Count how many are currently assigned const assignedCount = activeAssignments.filter(assignment => { const asset = allAssets.find(a => a.assetID === assignment.assetID); return asset && asset.assetType === requestedItem.assetType; }).length; // Calculate available const available = totalAssets - assignedCount; const shortfall = Math.max(0, requestedItem.quantity - available); return { assetType: requestedItem.assetType, requested: requestedItem.quantity, available, shortfall }; }); // Check if all requested items can be fulfilled const canFulfill = details.every(detail => detail.shortfall === 0); console.log('AssetRequestApi: Availability check result', { canFulfill, details }); return { canFulfill, details }; } catch (error) { console.error('AssetRequestApi: Error checking availability:', error); // Return a conservative result on error return { canFulfill: false, details: requestedAssets.map(item => ({ assetType: item.assetType, requested: item.quantity, available: 0, shortfall: item.quantity })) }; } }); } } export const assetRequestApi = new AssetRequestApi();