From 4349b4d0348f81d320acd98e0e98f8128719f6a7 Mon Sep 17 00:00:00 2001 From: chark1es Date: Fri, 30 May 2025 23:00:56 -0700 Subject: [PATCH] fix file uploads --- .../ASFundingSection.tsx | 67 +++++++-- .../EventRequestForm.tsx | 81 ++++++---- .../Officer_EventRequestForm/PRSection.tsx | 68 +++++++-- .../TAPFormSection.tsx | 141 ++++++++++++++---- .../EventRequestDetails.tsx | 53 ++++--- src/schemas/pocketbase/schema.ts | 2 +- 6 files changed, 307 insertions(+), 105 deletions(-) diff --git a/src/components/dashboard/Officer_EventRequestForm/ASFundingSection.tsx b/src/components/dashboard/Officer_EventRequestForm/ASFundingSection.tsx index c4713ae..29dab2c 100644 --- a/src/components/dashboard/Officer_EventRequestForm/ASFundingSection.tsx +++ b/src/components/dashboard/Officer_EventRequestForm/ASFundingSection.tsx @@ -86,11 +86,26 @@ const ASFundingSection: React.FC = ({ formData, onDataCha const handleInvoiceFileChange = (e: React.ChangeEvent) => { if (e.target.files && e.target.files.length > 0) { const newFiles = Array.from(e.target.files) as File[]; - setInvoiceFiles(newFiles); - onDataChange({ invoice_files: newFiles }); + // Combine existing files with new files instead of replacing + const combinedFiles = [...invoiceFiles, ...newFiles]; + setInvoiceFiles(combinedFiles); + onDataChange({ invoice_files: combinedFiles }); } }; + // Handle removing individual files + const handleRemoveFile = (indexToRemove: number) => { + const updatedFiles = invoiceFiles.filter((_, index) => index !== indexToRemove); + setInvoiceFiles(updatedFiles); + onDataChange({ invoice_files: updatedFiles }); + }; + + // Handle clearing all files + const handleClearAllFiles = () => { + setInvoiceFiles([]); + onDataChange({ invoice_files: [] }); + }; + // Handle JSON input change const handleJsonInputChange = (e: React.ChangeEvent) => { setJsonInput(e.target.value); @@ -234,8 +249,10 @@ const ASFundingSection: React.FC = ({ formData, onDataCha if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { const newFiles = Array.from(e.dataTransfer.files) as File[]; - setInvoiceFiles(newFiles); - onDataChange({ invoice_files: newFiles }); + // Combine existing files with new files instead of replacing + const combinedFiles = [...invoiceFiles, ...newFiles]; + setInvoiceFiles(combinedFiles); + onDataChange({ invoice_files: combinedFiles }); } }; @@ -311,20 +328,44 @@ const ASFundingSection: React.FC = ({ formData, onDataCha {invoiceFiles.length > 0 ? ( <> -

{invoiceFiles.length} file(s) selected:

-
-
    - {invoiceFiles.map((file, index) => ( -
  • {file.name}
  • - ))} -
+
+

{invoiceFiles.length} file(s) selected:

+
-

Click or drag to replace

+
+ {invoiceFiles.map((file, index) => ( +
+ {file.name} + +
+ ))} +
+

Click or drag to add more files

) : ( <>

Drop your invoice files here or click to browse

-

Supports PDF, JPG, JPEG, PNG

+

Supports PDF, JPG, JPEG, PNG (multiple files allowed)

)}
diff --git a/src/components/dashboard/Officer_EventRequestForm/EventRequestForm.tsx b/src/components/dashboard/Officer_EventRequestForm/EventRequestForm.tsx index e8e5f39..0e84cd7 100644 --- a/src/components/dashboard/Officer_EventRequestForm/EventRequestForm.tsx +++ b/src/components/dashboard/Officer_EventRequestForm/EventRequestForm.tsx @@ -70,13 +70,13 @@ export interface EventRequestFormData { flyer_advertising_start_date: string; flyer_additional_requests: string; required_logos: string[]; - other_logos: File[]; // Form uses File objects, schema uses strings + other_logos: File[]; // Form uses File objects, schema uses strings - MULTIPLE FILES advertising_format: string; will_or_have_room_booking: boolean; expected_attendance: number; - room_booking: File | null; + room_booking_files: File[]; // CHANGED: Multiple room booking files instead of single invoice: File | null; - invoice_files: File[]; + invoice_files: File[]; // MULTIPLE FILES invoiceData: InvoiceData; needs_graphics?: boolean | null; needs_as_funding?: boolean | null; @@ -108,7 +108,7 @@ const EventRequestForm: React.FC = () => { advertising_format: '', will_or_have_room_booking: false, expected_attendance: 0, - room_booking: null, + room_booking_files: [], as_funding_required: false, food_drinks_being_served: false, itemized_invoice: '', @@ -134,7 +134,7 @@ const EventRequestForm: React.FC = () => { const dataToStore = { ...formDataToSave, other_logos: [], - room_booking: null, + room_booking_files: [], invoice: null, invoice_files: [] }; @@ -184,7 +184,7 @@ const EventRequestForm: React.FC = () => { ...updatedData, // Remove file objects before saving to localStorage other_logos: [], - room_booking: null, + room_booking_files: [], invoice: null, invoice_files: [] }; @@ -221,7 +221,7 @@ const EventRequestForm: React.FC = () => { advertising_format: '', will_or_have_room_booking: false, expected_attendance: 0, - room_booking: null, // No room booking by default + room_booking_files: [], as_funding_required: false, food_drinks_being_served: false, itemized_invoice: '', @@ -377,16 +377,28 @@ const EventRequestForm: React.FC = () => { } } - // Upload room booking - if (formData.room_booking) { + // Upload room booking files + if (formData.room_booking_files && formData.room_booking_files.length > 0) { try { - console.log('Uploading room booking file:', { name: formData.room_booking.name, size: formData.room_booking.size, type: formData.room_booking.type }); - await fileManager.uploadFile('event_request', record.id, 'room_booking', formData.room_booking); - console.log('Room booking file uploaded successfully'); + console.log('Uploading room booking files:', formData.room_booking_files.length, 'files'); + console.log('Room booking files:', formData.room_booking_files.map(f => ({ name: f.name, size: f.size, type: f.type }))); + console.log('Using collection:', 'event_request', 'record ID:', record.id, 'field:', 'room_booking'); + + // Use the correct field name 'room_booking' instead of 'room_booking_files' + await fileManager.uploadFiles('event_request', record.id, 'room_booking', formData.room_booking_files); + console.log('Room booking files uploaded successfully'); } catch (error) { - console.error('Failed to upload room booking:', error); + console.error('Failed to upload room booking files:', error); + console.error('Error details:', { + message: error instanceof Error ? error.message : 'Unknown error', + stack: error instanceof Error ? error.stack : undefined, + collection: 'event_request', + recordId: record.id, + field: 'room_booking', + fileCount: formData.room_booking_files.length + }); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - fileUploadErrors.push(`Failed to upload room booking file: ${errorMessage}`); + fileUploadErrors.push(`Failed to upload room booking files: ${errorMessage}`); } } @@ -395,17 +407,21 @@ const EventRequestForm: React.FC = () => { try { console.log('Uploading invoice files:', formData.invoice_files.length, 'files'); console.log('Invoice files:', formData.invoice_files.map(f => ({ name: f.name, size: f.size, type: f.type }))); - await fileManager.appendFiles('event_request', record.id, 'invoice_files', formData.invoice_files); - - // For backward compatibility, also upload the first file as the main invoice - if (formData.invoice || formData.invoice_files[0]) { - const mainInvoice = formData.invoice || formData.invoice_files[0]; - console.log('Uploading main invoice file:', { name: mainInvoice.name, size: mainInvoice.size, type: mainInvoice.type }); - await fileManager.uploadFile('event_request', record.id, 'invoice', mainInvoice); - } + console.log('Using collection:', 'event_request', 'record ID:', record.id, 'field:', 'invoice'); + + // Use the correct field name 'invoice' instead of 'invoice_files' + await fileManager.uploadFiles('event_request', record.id, 'invoice', formData.invoice_files); console.log('Invoice files uploaded successfully'); } catch (error) { console.error('Failed to upload invoice files:', error); + console.error('Error details:', { + message: error instanceof Error ? error.message : 'Unknown error', + stack: error instanceof Error ? error.stack : undefined, + collection: 'event_request', + recordId: record.id, + field: 'invoice', + fileCount: formData.invoice_files.length + }); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; fileUploadErrors.push(`Failed to upload invoice files: ${errorMessage}`); } @@ -416,6 +432,13 @@ const EventRequestForm: React.FC = () => { console.log('Invoice file uploaded successfully'); } catch (error) { console.error('Failed to upload invoice file:', error); + console.error('Error details:', { + message: error instanceof Error ? error.message : 'Unknown error', + stack: error instanceof Error ? error.stack : undefined, + collection: 'event_request', + recordId: record.id, + field: 'invoice' + }); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; fileUploadErrors.push(`Failed to upload invoice file: ${errorMessage}`); } @@ -601,9 +624,9 @@ const EventRequestForm: React.FC = () => { return false; } - // Only require room booking file if will_or_have_room_booking is true - if (formData.will_or_have_room_booking && !formData.room_booking) { - toast.error('Please upload your room booking confirmation'); + // REQUIRED: Room booking files if will_or_have_room_booking is true + if (formData.will_or_have_room_booking && formData.room_booking_files.length === 0) { + toast.error('Room booking files are required when you need a room booking'); return false; } @@ -623,7 +646,13 @@ const EventRequestForm: React.FC = () => { // Validate AS Funding Section const validateASFundingSection = () => { - if (formData.as_funding_required) { + if (formData.as_funding_required || formData.needs_as_funding) { + // REQUIRED: Invoice files if AS funding is needed + if (!formData.invoice_files || formData.invoice_files.length === 0) { + toast.error('Invoice files are required when requesting AS funding'); + return false; + } + // Check if invoice data is present and has items if (!formData.invoiceData || !formData.invoiceData.items || formData.invoiceData.items.length === 0) { toast.error('Please add at least one item to your invoice'); diff --git a/src/components/dashboard/Officer_EventRequestForm/PRSection.tsx b/src/components/dashboard/Officer_EventRequestForm/PRSection.tsx index d55cf2a..bdd2094 100644 --- a/src/components/dashboard/Officer_EventRequestForm/PRSection.tsx +++ b/src/components/dashboard/Officer_EventRequestForm/PRSection.tsx @@ -4,6 +4,7 @@ import type { EventRequestFormData } from './EventRequestForm'; import type { EventRequest } from '../../../schemas/pocketbase'; import { FlyerTypes, LogoOptions } from '../../../schemas/pocketbase'; import CustomAlert from '../universal/CustomAlert'; +import { Icon } from '@iconify/react'; // Enhanced animation variants const containerVariants = { @@ -122,11 +123,26 @@ const PRSection: React.FC = ({ formData, onDataChange }) => { const handleLogoFileChange = (e: React.ChangeEvent) => { if (e.target.files && e.target.files.length > 0) { const newFiles = Array.from(e.target.files) as File[]; - setOtherLogoFiles(newFiles); - onDataChange({ other_logos: newFiles }); + // Combine existing files with new files instead of replacing + const combinedFiles = [...otherLogoFiles, ...newFiles]; + setOtherLogoFiles(combinedFiles); + onDataChange({ other_logos: combinedFiles }); } }; + // Handle removing individual files + const handleRemoveLogoFile = (indexToRemove: number) => { + const updatedFiles = otherLogoFiles.filter((_, index) => index !== indexToRemove); + setOtherLogoFiles(updatedFiles); + onDataChange({ other_logos: updatedFiles }); + }; + + // Handle clearing all files + const handleClearAllLogoFiles = () => { + setOtherLogoFiles([]); + onDataChange({ other_logos: [] }); + }; + // Handle drag events for file upload const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); @@ -144,8 +160,10 @@ const PRSection: React.FC = ({ formData, onDataChange }) => { if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { const newFiles = Array.from(e.dataTransfer.files) as File[]; - setOtherLogoFiles(newFiles); - onDataChange({ other_logos: newFiles }); + // Combine existing files with new files instead of replacing + const combinedFiles = [...otherLogoFiles, ...newFiles]; + setOtherLogoFiles(combinedFiles); + onDataChange({ other_logos: combinedFiles }); } }; @@ -349,20 +367,44 @@ const PRSection: React.FC = ({ formData, onDataChange }) => { {otherLogoFiles.length > 0 ? ( <> -

{otherLogoFiles.length} file(s) selected:

-
-
    - {otherLogoFiles.map((file, index) => ( -
  • {file.name}
  • - ))} -
+
+

{otherLogoFiles.length} file(s) selected:

+
-

Click or drag to replace

+
+ {otherLogoFiles.map((file, index) => ( +
+ {file.name} + +
+ ))} +
+

Click or drag to add more files

) : ( <>

Drop your logo files here or click to browse

-

Please upload transparent logo files (PNG preferred)

+

Please upload transparent logo files (PNG preferred, multiple files allowed)

)}
diff --git a/src/components/dashboard/Officer_EventRequestForm/TAPFormSection.tsx b/src/components/dashboard/Officer_EventRequestForm/TAPFormSection.tsx index 040e418..0b809bd 100644 --- a/src/components/dashboard/Officer_EventRequestForm/TAPFormSection.tsx +++ b/src/components/dashboard/Officer_EventRequestForm/TAPFormSection.tsx @@ -4,6 +4,7 @@ import type { EventRequestFormData } from './EventRequestForm'; import type { EventRequest } from '../../../schemas/pocketbase'; import CustomAlert from '../universal/CustomAlert'; import FilePreview from '../universal/FilePreview'; +import { Icon } from '@iconify/react'; // Enhanced animation variants const containerVariants = { @@ -69,11 +70,12 @@ interface TAPFormSectionProps { } const TAPFormSection: React.FC = ({ formData, onDataChange }) => { - const [roomBookingFile, setRoomBookingFile] = useState(formData.room_booking); + const [roomBookingFiles, setRoomBookingFiles] = useState(formData.room_booking_files || []); const [isDragging, setIsDragging] = useState(false); const [fileError, setFileError] = useState(null); const [showFilePreview, setShowFilePreview] = useState(false); const [filePreviewUrl, setFilePreviewUrl] = useState(null); + const [selectedPreviewFile, setSelectedPreviewFile] = useState(null); // Add style tag for hidden arrows useEffect(() => { @@ -89,27 +91,58 @@ const TAPFormSection: React.FC = ({ formData, onDataChange // Handle room booking file upload with size limit const handleRoomBookingFileChange = (e: React.ChangeEvent) => { if (e.target.files && e.target.files.length > 0) { - const file = e.target.files[0]; - - // Check file size - 1MB limit - if (file.size > 1024 * 1024) { - setFileError("Room booking file size must be under 1MB"); + const newFiles = Array.from(e.target.files) as File[]; + + // Check file sizes - 1MB limit for each file + const oversizedFiles = newFiles.filter(file => file.size > 1024 * 1024); + if (oversizedFiles.length > 0) { + setFileError(`${oversizedFiles.length} file(s) exceed 1MB limit`); return; } setFileError(null); - setRoomBookingFile(file); - onDataChange({ room_booking: file }); + // Combine existing files with new files instead of replacing + const combinedFiles = [...roomBookingFiles, ...newFiles]; + setRoomBookingFiles(combinedFiles); + onDataChange({ room_booking_files: combinedFiles }); - // Create preview URL + // Create preview URL for the first new file if (filePreviewUrl) { URL.revokeObjectURL(filePreviewUrl); } - const url = URL.createObjectURL(file); + const url = URL.createObjectURL(newFiles[0]); setFilePreviewUrl(url); + setSelectedPreviewFile(newFiles[0]); } }; + // Handle removing individual files + const handleRemoveFile = (indexToRemove: number) => { + const updatedFiles = roomBookingFiles.filter((_, index) => index !== indexToRemove); + setRoomBookingFiles(updatedFiles); + onDataChange({ room_booking_files: updatedFiles }); + + // Clear preview if we removed the previewed file + if (selectedPreviewFile && updatedFiles.length === 0) { + if (filePreviewUrl) { + URL.revokeObjectURL(filePreviewUrl); + setFilePreviewUrl(null); + } + setSelectedPreviewFile(null); + } + }; + + // Handle clearing all files + const handleClearAllFiles = () => { + setRoomBookingFiles([]); + onDataChange({ room_booking_files: [] }); + if (filePreviewUrl) { + URL.revokeObjectURL(filePreviewUrl); + setFilePreviewUrl(null); + } + setSelectedPreviewFile(null); + }; + // Handle drag events for file upload const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); @@ -126,24 +159,28 @@ const TAPFormSection: React.FC = ({ formData, onDataChange setIsDragging(false); if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { - const file = e.dataTransfer.files[0]; + const newFiles = Array.from(e.dataTransfer.files) as File[]; - // Check file size - 1MB limit - if (file.size > 1024 * 1024) { - setFileError("Room booking file size must be under 1MB"); + // Check file sizes - 1MB limit for each file + const oversizedFiles = newFiles.filter(file => file.size > 1024 * 1024); + if (oversizedFiles.length > 0) { + setFileError(`${oversizedFiles.length} file(s) exceed 1MB limit`); return; } setFileError(null); - setRoomBookingFile(file); - onDataChange({ room_booking: file }); + // Combine existing files with new files instead of replacing + const combinedFiles = [...roomBookingFiles, ...newFiles]; + setRoomBookingFiles(combinedFiles); + onDataChange({ room_booking_files: combinedFiles }); - // Create preview URL + // Create preview URL for the first new file if (filePreviewUrl) { URL.revokeObjectURL(filePreviewUrl); } - const url = URL.createObjectURL(file); + const url = URL.createObjectURL(newFiles[0]); setFilePreviewUrl(url); + setSelectedPreviewFile(newFiles[0]); } }; @@ -262,6 +299,11 @@ const TAPFormSection: React.FC = ({ formData, onDataChange Room Booking Confirmation {formData.will_or_have_room_booking && *} + {formData.will_or_have_room_booking && ( +

+ Required: Upload your room booking confirmation document. +

+ )} {fileError && (
@@ -292,6 +334,7 @@ const TAPFormSection: React.FC = ({ formData, onDataChange className="hidden" onChange={handleRoomBookingFileChange} accept=".pdf,.png,.jpg,.jpeg" + multiple />
@@ -304,16 +347,46 @@ const TAPFormSection: React.FC = ({ formData, onDataChange - {roomBookingFile ? ( + {roomBookingFiles.length > 0 ? ( <> -

File selected:

-

{roomBookingFile.name}

-

Click or drag to replace (Max size: 1MB)

+
+

{roomBookingFiles.length} file(s) selected:

+ +
+
+ {roomBookingFiles.map((file, index) => ( +
+ {file.name} + +
+ ))} +
+

Click or drag to add more files (Max size: 1MB each)

) : ( <> -

Drop your file here or click to browse

-

Accepted formats: PDF, PNG, JPG (Max size: 1MB)

+

Drop your files here or click to browse

+

Accepted formats: PDF, PNG, JPG (Max size: 1MB each, multiple files allowed)

)}
@@ -329,20 +402,20 @@ const TAPFormSection: React.FC = ({ formData, onDataChange )} {/* Preview File Button - Outside the upload area */} - {formData.will_or_have_room_booking && roomBookingFile && ( + {formData.will_or_have_room_booking && roomBookingFiles.length > 0 && (
)} {/* File Preview Component */} - {showFilePreview && filePreviewUrl && roomBookingFile && ( + {showFilePreview && roomBookingFiles.length > 0 && ( = ({ formData, onDataChange className="mt-4 p-4 bg-base-200 rounded-lg" >
-

File Preview

+

File Preview ({roomBookingFiles.length} files)

- +
+ {roomBookingFiles.map((file, index) => { + const url = URL.createObjectURL(file); + return ( +
+

{file.name}

+ +
+ ); + })} +
)} diff --git a/src/components/dashboard/Officer_EventRequestManagement/EventRequestDetails.tsx b/src/components/dashboard/Officer_EventRequestManagement/EventRequestDetails.tsx index 70d257d..b58eef0 100644 --- a/src/components/dashboard/Officer_EventRequestManagement/EventRequestDetails.tsx +++ b/src/components/dashboard/Officer_EventRequestManagement/EventRequestDetails.tsx @@ -29,7 +29,7 @@ interface ExtendedEventRequest extends SchemaEventRequest { flyer_files?: string[]; // Add this for PR-related files files?: string[]; // Generic files field will_or_have_room_booking?: boolean; - room_booking?: string; // Single file for room booking + room_booking_files?: string[]; // CHANGED: Multiple room booking files instead of single room_reservation_needed?: boolean; // Keep for backward compatibility additional_notes?: string; flyers_completed?: boolean; // Track if flyers have been completed by PR team @@ -82,7 +82,7 @@ const FilePreviewModal: React.FC = ({ setFileUrl(secureUrl); // Determine file type from extension - const extension = fileName.split('.').pop()?.toLowerCase() || ''; + const extension = (typeof fileName === 'string' ? fileName.split('.').pop()?.toLowerCase() : '') || ''; setFileType(extension); setIsLoading(false); @@ -1125,6 +1125,7 @@ const PRMaterialsTab: React.FC<{ request: ExtendedEventRequest }> = ({ request } // Use the same utility functions as in the ASFundingTab const getFileExtension = (filename: string): string => { + if (!filename || typeof filename !== 'string') return ''; const parts = filename.split('.'); return parts.length > 1 ? parts.pop()?.toLowerCase() || '' : ''; }; @@ -1139,6 +1140,7 @@ const PRMaterialsTab: React.FC<{ request: ExtendedEventRequest }> = ({ request } }; const getFriendlyFileName = (filename: string, maxLength: number = 20): string => { + if (!filename || typeof filename !== 'string') return 'Unknown File'; const basename = filename.split('/').pop() || filename; if (basename.length <= maxLength) return basename; const extension = getFileExtension(basename); @@ -1836,29 +1838,34 @@ const EventRequestDetails = ({
-
- {request.room_booking ? 'Booking File Uploaded' : 'No Booking File'} +
0 ? 'badge-success' : 'badge-warning'}`}> + {request.room_booking_files && request.room_booking_files.length > 0 ? 'Booking Files Uploaded' : 'No Booking Files'}
- {request.room_booking && ( - + // Open the modal + const modal = document.getElementById('file-preview-modal') as HTMLDialogElement; + if (modal) modal.showModal(); + }} + className="btn btn-xs btn-primary" + > + View File {index + 1} + + ))} +
)}
diff --git a/src/schemas/pocketbase/schema.ts b/src/schemas/pocketbase/schema.ts index 2b0e9e1..543f587 100644 --- a/src/schemas/pocketbase/schema.ts +++ b/src/schemas/pocketbase/schema.ts @@ -119,7 +119,7 @@ export interface EventRequest extends BaseRecord { advertising_format?: string; will_or_have_room_booking?: boolean; expected_attendance?: number; - room_booking?: string; // signle file + room_booking_files?: string[]; // CHANGED: Multiple files instead of single file as_funding_required: boolean; food_drinks_being_served: boolean; itemized_invoice?: string; // JSON string