pmtool / src /services /assetApi.ts
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
12.6 kB
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();