pmtool / src /services /assetRequestApi.ts
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
28.1 kB
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 <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(`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<AssetRequest> {
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<AssetRequest | null> {
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<AssetRequest[]> {
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<AssetRequest[]> {
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<AssetRequest[]> {
return this.getByEmployeeId(employeeId);
}
/**
* Get all pending asset requests with availability information
* @returns List of pending requests with availability status
*/
async getPending(): Promise<AssetRequestWithAvailability[]> {
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<AssetRequest> {
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<AssetRequest> {
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<AssetRequest> {
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<string, number>();
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<AssetRequest> {
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<void> {
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<AvailabilityCheck> {
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();