import apiClient from '@/lib/api/axios-instance'; import { AxiosError } from 'axios'; import { Asset, AssetCreateRequest, AssetUpdateRequest, BulkImportResponse, validateAssetApiResponse } from '@/types/asset-management'; // Helper to create a safe asset object with default values for null/undefined fields const createSafeAsset = (data: any): Asset => { return { assetID: data.assetID || 0, assetType: data.assetType || '', brandModel: data.brandModel || '', serialNumber: data.serialNumber || '', assetCondition: data.assetCondition || 'Good', }; }; // Helper to handle API errors with user-friendly messages const handleApiError = (error: unknown, operation: string): never => { console.error(`AssetApi: 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 was not found.'); case 409: throw new Error('This asset already exists or conflicts with existing data.'); 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(`AssetApi: Retry attempt ${attempt}/${maxRetries} in ${waitTime}ms`); await new Promise(resolve => setTimeout(resolve, waitTime)); } } throw lastError; }; class AssetApi { private baseUrl = `/api/Assets`; async getAll(): Promise { return retryOperation(async () => { try { console.log('AssetApi: Fetching all assets from', this.baseUrl); const response = await apiClient.get(this.baseUrl); console.log('AssetApi: Response status', response.status); if (!response.data) { console.warn('AssetApi: No data in response'); return []; } if (!Array.isArray(response.data)) { console.warn('AssetApi: Response is not an array', response.data); return []; } console.log(`AssetApi: Found ${response.data.length} assets`); // Map each item to ensure it has all required fields and validate return response.data.map(item => { try { return validateAssetApiResponse(item); } catch (error) { console.warn('AssetApi: Invalid asset data, using safe fallback:', error); return createSafeAsset(item); } }); } catch (error) { handleApiError(error, 'fetch assets'); } }); } async getById(id: number): Promise { return retryOperation(async () => { try { console.log(`AssetApi: Fetching asset ${id}`); const response = await apiClient.get(`${this.baseUrl}/${id}`); console.log(`AssetApi: Response status for asset ${id}`, response.status); if (response.data) { try { return validateAssetApiResponse(response.data); } catch (error) { console.warn(`AssetApi: Invalid asset data for ID ${id}, using safe fallback:`, error); return createSafeAsset(response.data); } } return null; } catch (error) { if (error instanceof AxiosError && error.response?.status === 404) { return null; // Asset not found is a valid case } handleApiError(error, `fetch asset ${id}`); } }); } async create(asset: AssetCreateRequest): Promise { return retryOperation(async () => { try { // Validate and normalize the input data structure const assetData = { ...asset, // Ensure serialNumber is always a string serialNumber: String(asset.serialNumber || '').trim() }; console.log('AssetApi: Creating asset with data', assetData); const response = await apiClient.post(this.baseUrl, assetData); console.log('AssetApi: Create response status', response.status); try { return validateAssetApiResponse(response.data); } catch (error) { console.warn('AssetApi: Invalid response data from create, using safe fallback:', error); return createSafeAsset(response.data); } } catch (error) { handleApiError(error, 'create asset'); } }, 2); // Fewer retries for create operations } async update(id: number, asset: AssetUpdateRequest): Promise { return retryOperation(async () => { try { const assetData = { ...asset, // Ensure serialNumber is always a string serialNumber: String(asset.serialNumber || '').trim() }; console.log(`AssetApi: Updating asset ${id} with data`, assetData); const response = await apiClient.put(`${this.baseUrl}/${id}`, assetData); console.log(`AssetApi: Update response status for asset ${id}`, response.status); try { return validateAssetApiResponse(response.data); } catch (error) { console.warn(`AssetApi: Invalid response data from update for ID ${id}, using safe fallback:`, error); return createSafeAsset(response.data); } } catch (error) { handleApiError(error, `update asset ${id}`); } }, 2); // Fewer retries for update operations } async delete(id: number): Promise { return retryOperation(async () => { try { console.log(`AssetApi: Deleting asset ${id}`); // Check for referential integrity - prevent deletion if asset has active assignments try { const response = await apiClient.get(`/api/AssetAssignments/by-asset/${id}`); const assignments = response.data || []; // Check if there are any active assignments (no returned date) const activeAssignments = assignments.filter((assignment: any) => !assignment.returnedDate); if (activeAssignments.length > 0) { throw new Error( `Cannot delete asset: Asset is currently assigned to ${activeAssignments.length} employee(s). ` + 'Please return all active assignments before deleting this asset.' ); } // Check if there are any assignments at all (for user confirmation) if (assignments.length > 0) { console.warn(`AssetApi: Asset ${id} has ${assignments.length} historical assignment(s) that will be deleted`); } } catch (checkError) { // If the check fails due to network issues, we still allow deletion // but if it's a referential integrity error, we throw it if (checkError instanceof Error && checkError.message.includes('Cannot delete asset')) { throw checkError; } console.warn(`AssetApi: Could not check assignments for asset ${id}, proceeding with deletion:`, checkError); } await apiClient.delete(`${this.baseUrl}/${id}`); console.log(`AssetApi: Asset ${id} deleted successfully`); } catch (error) { handleApiError(error, `delete asset ${id}`); } }, 2); // Fewer retries for delete operations } async bulkImport(assets: AssetCreateRequest[]): Promise { return retryOperation(async () => { try { console.log(`AssetApi: Bulk importing ${assets.length} assets`); // Validate input if (!Array.isArray(assets) || assets.length === 0) { throw new Error('Assets array is required and cannot be empty'); } if (assets.length > 1000) { throw new Error('Cannot import more than 1000 assets at once'); } // Normalize all assets to ensure serialNumber is a string const normalizedAssets = assets.map(asset => ({ ...asset, // Ensure serialNumber is always a string, handle numbers, nulls, etc. serialNumber: String(asset.serialNumber || '').trim(), // Also normalize other string fields that might come as numbers assetType: String(asset.assetType || '').trim(), brandModel: String(asset.brandModel || '').trim(), assetCondition: String(asset.assetCondition || '').trim(), location: asset.location ? String(asset.location).trim() : undefined, notes: asset.notes ? String(asset.notes).trim() : undefined, purchaseDate: asset.purchaseDate ? String(asset.purchaseDate).trim() : undefined, warrantyExpiry: asset.warrantyExpiry ? String(asset.warrantyExpiry).trim() : undefined })); const requestData = { assets: normalizedAssets }; console.log('AssetApi: Bulk import request data', requestData); const response = await apiClient.post(`${this.baseUrl}/bulk-import`, requestData); console.log('AssetApi: Bulk import response status', response.status); console.log('AssetApi: Bulk import response data', response.data); // Validate response structure if (!response.data || typeof response.data !== 'object') { throw new Error('Invalid response format from bulk import API'); } const result = response.data as BulkImportResponse; // Ensure required fields exist if (typeof result.success !== 'boolean') { throw new Error('Invalid response: missing success field'); } if (!result.results || typeof result.results !== 'object') { throw new Error('Invalid response: missing results field'); } // Validate and sanitize created assets if (result.results.createdAssets && Array.isArray(result.results.createdAssets)) { result.results.createdAssets = result.results.createdAssets.map(asset => { try { return validateAssetApiResponse(asset); } catch (error) { console.warn('AssetApi: Invalid asset in bulk import response, using safe fallback:', error); return createSafeAsset(asset); } }); } console.log(`AssetApi: Bulk import completed - ${result.results.successful} successful, ${result.results.failed} failed`); return result; } catch (error) { handleApiError(error, 'bulk import assets'); } }, 1); // No retries for bulk operations to avoid duplicate imports } } export const assetApi = new AssetApi();