Re_Figma_Code/src/hooks/useDocumentUpload.ts

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
};
}