Spaces:
Sleeping
Sleeping
| 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 <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(`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<Asset[]> { | |
| 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<Asset | null> { | |
| 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<Asset> { | |
| 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<Asset> { | |
| 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<void> { | |
| 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<BulkImportResponse> { | |
| 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(); |