247 lines
7.4 KiB
TypeScript
247 lines
7.4 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { uploadDocument } from '@/services/documentApi';
|
|
import { getPublicConfigurations, AdminConfiguration } from '@/services/adminApi';
|
|
import { toast } from 'sonner';
|
|
|
|
/**
|
|
* Custom Hook: useDocumentUpload
|
|
*
|
|
* Purpose: Manages document upload functionality with loading states
|
|
*
|
|
* Responsibilities:
|
|
* - Handles file input change events
|
|
* - Validates file selection
|
|
* - Uploads document to backend
|
|
* - Triggers refresh after successful upload
|
|
* - Manages upload loading state
|
|
* - Handles errors with user-friendly messages
|
|
*
|
|
* @param apiRequest - Current request object (contains requestId for upload)
|
|
* @param refreshDetails - Function to refresh request data after upload
|
|
* @returns Object with upload handler, trigger function, and loading state
|
|
*/
|
|
export function useDocumentUpload(
|
|
apiRequest: any,
|
|
refreshDetails: () => Promise<void>
|
|
) {
|
|
// State: Indicates if document is currently being uploaded
|
|
const [uploadingDocument, setUploadingDocument] = useState(false);
|
|
|
|
// State: Stores document for preview modal
|
|
const [previewDocument, setPreviewDocument] = useState<{
|
|
fileName: string;
|
|
fileType: string;
|
|
documentId: string;
|
|
fileSize?: number;
|
|
} | null>(null);
|
|
|
|
// Document policy state
|
|
const [documentPolicy, setDocumentPolicy] = useState<{
|
|
maxFileSizeMB: number;
|
|
allowedFileTypes: string[];
|
|
}>({
|
|
maxFileSizeMB: 10,
|
|
allowedFileTypes: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'jpg', 'jpeg', 'png', 'gif']
|
|
});
|
|
|
|
// Document validation error state
|
|
const [documentError, setDocumentError] = useState<{
|
|
show: boolean;
|
|
errors: Array<{ fileName: string; reason: string }>;
|
|
}>({
|
|
show: false,
|
|
errors: []
|
|
});
|
|
|
|
// Fetch document policy on mount
|
|
useEffect(() => {
|
|
const loadDocumentPolicy = async () => {
|
|
try {
|
|
const configs = await getPublicConfigurations('DOCUMENT_POLICY');
|
|
const configMap: Record<string, string> = {};
|
|
configs.forEach((c: AdminConfiguration) => {
|
|
configMap[c.configKey] = c.configValue;
|
|
});
|
|
|
|
const maxFileSizeMB = parseInt(configMap['MAX_FILE_SIZE_MB'] || '10');
|
|
const allowedFileTypesStr = configMap['ALLOWED_FILE_TYPES'] || 'pdf,doc,docx,xls,xlsx,ppt,pptx,jpg,jpeg,png,gif';
|
|
const allowedFileTypes = allowedFileTypesStr.split(',').map(ext => ext.trim().toLowerCase());
|
|
|
|
setDocumentPolicy({
|
|
maxFileSizeMB,
|
|
allowedFileTypes
|
|
});
|
|
} catch (error) {
|
|
console.error('Failed to load document policy:', error);
|
|
// Use defaults if loading fails
|
|
}
|
|
};
|
|
|
|
loadDocumentPolicy();
|
|
}, []);
|
|
|
|
/**
|
|
* Function: validateFile
|
|
*
|
|
* Purpose: Validate file against document policy
|
|
*
|
|
* @param file - File to validate
|
|
* @returns Validation result with reason if invalid
|
|
*/
|
|
const validateFile = (file: File): { valid: boolean; reason?: string } => {
|
|
// Check file size
|
|
const maxSizeBytes = documentPolicy.maxFileSizeMB * 1024 * 1024;
|
|
if (file.size > maxSizeBytes) {
|
|
return {
|
|
valid: false,
|
|
reason: `File size exceeds the maximum allowed size of ${documentPolicy.maxFileSizeMB}MB. Current size: ${(file.size / (1024 * 1024)).toFixed(2)}MB`
|
|
};
|
|
}
|
|
|
|
// Check file extension
|
|
const fileName = file.name.toLowerCase();
|
|
const fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1);
|
|
|
|
if (!documentPolicy.allowedFileTypes.includes(fileExtension)) {
|
|
return {
|
|
valid: false,
|
|
reason: `File type "${fileExtension}" is not allowed. Allowed types: ${documentPolicy.allowedFileTypes.join(', ')}`
|
|
};
|
|
}
|
|
|
|
return { valid: true };
|
|
};
|
|
|
|
/**
|
|
* Function: handleDocumentUpload
|
|
*
|
|
* Purpose: Process file upload when user selects a file
|
|
*
|
|
* Process:
|
|
* 1. Validate file selection
|
|
* 2. Validate against document policy
|
|
* 3. Get request UUID (required for backend API)
|
|
* 4. Upload file to backend
|
|
* 5. Refresh request details to show new document
|
|
* 6. Clear file input for next upload
|
|
* 7. Show success/error messages
|
|
*
|
|
* @param event - File input change event
|
|
*/
|
|
const handleDocumentUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
const files = event.target.files;
|
|
|
|
// Validate: Check if file is selected
|
|
if (!files || files.length === 0) return;
|
|
|
|
const fileArray = Array.from(files);
|
|
|
|
// Validate all files against document policy
|
|
const validationErrors: Array<{ fileName: string; reason: string }> = [];
|
|
const validFiles: File[] = [];
|
|
|
|
fileArray.forEach(file => {
|
|
const validation = validateFile(file);
|
|
if (!validation.valid) {
|
|
validationErrors.push({
|
|
fileName: file.name,
|
|
reason: validation.reason || 'Unknown validation error'
|
|
});
|
|
} else {
|
|
validFiles.push(file);
|
|
}
|
|
});
|
|
|
|
// If there are validation errors, show modal
|
|
if (validationErrors.length > 0) {
|
|
setDocumentError({
|
|
show: true,
|
|
errors: validationErrors
|
|
});
|
|
}
|
|
|
|
// If no valid files, stop here
|
|
if (validFiles.length === 0) {
|
|
if (event.target) {
|
|
event.target.value = '';
|
|
}
|
|
return;
|
|
}
|
|
|
|
setUploadingDocument(true);
|
|
|
|
try {
|
|
// Upload only the first valid file (backend currently supports single file)
|
|
const file = validFiles[0];
|
|
|
|
// Validate: Ensure request ID is available
|
|
// Note: Backend requires UUID, not request number
|
|
const requestId = apiRequest?.requestId;
|
|
if (!requestId) {
|
|
toast.error('Request ID not found');
|
|
return;
|
|
}
|
|
|
|
// API Call: Upload document to backend
|
|
// Document type is 'SUPPORTING' (as opposed to 'REQUIRED')
|
|
if (file) {
|
|
await uploadDocument(file, requestId, 'SUPPORTING');
|
|
}
|
|
|
|
// Refresh: Reload request details to show newly uploaded document
|
|
// This also updates the activity timeline
|
|
await refreshDetails();
|
|
|
|
// Success feedback
|
|
if (validFiles.length < fileArray.length) {
|
|
toast.warning(`${validFiles.length} of ${fileArray.length} file(s) were uploaded. ${validationErrors.length} file(s) were rejected.`);
|
|
} else {
|
|
toast.success('Document uploaded successfully');
|
|
}
|
|
} catch (error: any) {
|
|
console.error('[useDocumentUpload] Upload error:', error);
|
|
|
|
// Error feedback with backend error message if available
|
|
toast.error(error?.response?.data?.error || 'Failed to upload document');
|
|
} finally {
|
|
setUploadingDocument(false);
|
|
|
|
// Cleanup: Clear the file input to allow re-uploading same file
|
|
if (event.target) {
|
|
event.target.value = '';
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Function: triggerFileInput
|
|
*
|
|
* Purpose: Programmatically open file picker dialog
|
|
*
|
|
* Process:
|
|
* 1. Create temporary file input element
|
|
* 2. Configure accepted file types based on document policy
|
|
* 3. Attach upload handler
|
|
* 4. Trigger click to open file picker
|
|
*/
|
|
const triggerFileInput = () => {
|
|
const input = document.createElement('input');
|
|
input.type = 'file';
|
|
input.accept = documentPolicy.allowedFileTypes.map(ext => `.${ext}`).join(',');
|
|
input.onchange = handleDocumentUpload as any;
|
|
input.click();
|
|
};
|
|
|
|
return {
|
|
uploadingDocument,
|
|
handleDocumentUpload,
|
|
triggerFileInput,
|
|
previewDocument,
|
|
setPreviewDocument,
|
|
documentPolicy,
|
|
documentError,
|
|
setDocumentError
|
|
};
|
|
}
|
|
|