fix file uploads

This commit is contained in:
chark1es 2025-05-30 23:00:56 -07:00
parent a57a4e6889
commit 4349b4d034
6 changed files with 307 additions and 105 deletions

View file

@ -86,11 +86,26 @@ const ASFundingSection: React.FC<ASFundingSectionProps> = ({ formData, onDataCha
const handleInvoiceFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleInvoiceFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) { if (e.target.files && e.target.files.length > 0) {
const newFiles = Array.from(e.target.files) as File[]; const newFiles = Array.from(e.target.files) as File[];
setInvoiceFiles(newFiles); // Combine existing files with new files instead of replacing
onDataChange({ invoice_files: newFiles }); 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 // Handle JSON input change
const handleJsonInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { const handleJsonInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setJsonInput(e.target.value); setJsonInput(e.target.value);
@ -234,8 +249,10 @@ const ASFundingSection: React.FC<ASFundingSectionProps> = ({ formData, onDataCha
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
const newFiles = Array.from(e.dataTransfer.files) as File[]; const newFiles = Array.from(e.dataTransfer.files) as File[];
setInvoiceFiles(newFiles); // Combine existing files with new files instead of replacing
onDataChange({ invoice_files: newFiles }); const combinedFiles = [...invoiceFiles, ...newFiles];
setInvoiceFiles(combinedFiles);
onDataChange({ invoice_files: combinedFiles });
} }
}; };
@ -311,20 +328,44 @@ const ASFundingSection: React.FC<ASFundingSectionProps> = ({ formData, onDataCha
{invoiceFiles.length > 0 ? ( {invoiceFiles.length > 0 ? (
<> <>
<p className="font-medium text-primary">{invoiceFiles.length} file(s) selected:</p> <div className="flex items-center justify-between w-full">
<div className="max-h-24 overflow-y-auto text-left w-full"> <p className="font-medium text-primary">{invoiceFiles.length} file(s) selected:</p>
<ul className="list-disc list-inside text-sm"> <button
{invoiceFiles.map((file, index) => ( type="button"
<li key={index} className="truncate">{file.name}</li> onClick={(e) => {
))} e.stopPropagation();
</ul> handleClearAllFiles();
}}
className="btn btn-xs btn-outline btn-error"
title="Clear all files"
>
Clear All
</button>
</div> </div>
<p className="text-xs text-gray-500">Click or drag to replace</p> <div className="max-h-32 overflow-y-auto text-left w-full space-y-1">
{invoiceFiles.map((file, index) => (
<div key={index} className="flex items-center justify-between bg-base-100 p-2 rounded">
<span className="text-sm truncate flex-1">{file.name}</span>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
handleRemoveFile(index);
}}
className="btn btn-xs btn-error ml-2"
title="Remove file"
>
<Icon icon="heroicons:x-mark" className="w-3 h-3" />
</button>
</div>
))}
</div>
<p className="text-xs text-gray-500">Click or drag to add more files</p>
</> </>
) : ( ) : (
<> <>
<p className="font-medium">Drop your invoice files here or click to browse</p> <p className="font-medium">Drop your invoice files here or click to browse</p>
<p className="text-xs text-gray-500">Supports PDF, JPG, JPEG, PNG</p> <p className="text-xs text-gray-500">Supports PDF, JPG, JPEG, PNG (multiple files allowed)</p>
</> </>
)} )}
</div> </div>

View file

@ -70,13 +70,13 @@ export interface EventRequestFormData {
flyer_advertising_start_date: string; flyer_advertising_start_date: string;
flyer_additional_requests: string; flyer_additional_requests: string;
required_logos: 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; advertising_format: string;
will_or_have_room_booking: boolean; will_or_have_room_booking: boolean;
expected_attendance: number; expected_attendance: number;
room_booking: File | null; room_booking_files: File[]; // CHANGED: Multiple room booking files instead of single
invoice: File | null; invoice: File | null;
invoice_files: File[]; invoice_files: File[]; // MULTIPLE FILES
invoiceData: InvoiceData; invoiceData: InvoiceData;
needs_graphics?: boolean | null; needs_graphics?: boolean | null;
needs_as_funding?: boolean | null; needs_as_funding?: boolean | null;
@ -108,7 +108,7 @@ const EventRequestForm: React.FC = () => {
advertising_format: '', advertising_format: '',
will_or_have_room_booking: false, will_or_have_room_booking: false,
expected_attendance: 0, expected_attendance: 0,
room_booking: null, room_booking_files: [],
as_funding_required: false, as_funding_required: false,
food_drinks_being_served: false, food_drinks_being_served: false,
itemized_invoice: '', itemized_invoice: '',
@ -134,7 +134,7 @@ const EventRequestForm: React.FC = () => {
const dataToStore = { const dataToStore = {
...formDataToSave, ...formDataToSave,
other_logos: [], other_logos: [],
room_booking: null, room_booking_files: [],
invoice: null, invoice: null,
invoice_files: [] invoice_files: []
}; };
@ -184,7 +184,7 @@ const EventRequestForm: React.FC = () => {
...updatedData, ...updatedData,
// Remove file objects before saving to localStorage // Remove file objects before saving to localStorage
other_logos: [], other_logos: [],
room_booking: null, room_booking_files: [],
invoice: null, invoice: null,
invoice_files: [] invoice_files: []
}; };
@ -221,7 +221,7 @@ const EventRequestForm: React.FC = () => {
advertising_format: '', advertising_format: '',
will_or_have_room_booking: false, will_or_have_room_booking: false,
expected_attendance: 0, expected_attendance: 0,
room_booking: null, // No room booking by default room_booking_files: [],
as_funding_required: false, as_funding_required: false,
food_drinks_being_served: false, food_drinks_being_served: false,
itemized_invoice: '', itemized_invoice: '',
@ -377,16 +377,28 @@ const EventRequestForm: React.FC = () => {
} }
} }
// Upload room booking // Upload room booking files
if (formData.room_booking) { if (formData.room_booking_files && formData.room_booking_files.length > 0) {
try { try {
console.log('Uploading room booking file:', { name: formData.room_booking.name, size: formData.room_booking.size, type: formData.room_booking.type }); console.log('Uploading room booking files:', formData.room_booking_files.length, 'files');
await fileManager.uploadFile('event_request', record.id, 'room_booking', formData.room_booking); console.log('Room booking files:', formData.room_booking_files.map(f => ({ name: f.name, size: f.size, type: f.type })));
console.log('Room booking file uploaded successfully'); 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) { } 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'; 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 { try {
console.log('Uploading invoice files:', formData.invoice_files.length, 'files'); 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 }))); 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); console.log('Using collection:', 'event_request', 'record ID:', record.id, 'field:', 'invoice');
// For backward compatibility, also upload the first file as the main invoice // Use the correct field name 'invoice' instead of 'invoice_files'
if (formData.invoice || formData.invoice_files[0]) { await fileManager.uploadFiles('event_request', record.id, 'invoice', formData.invoice_files);
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('Invoice files uploaded successfully'); console.log('Invoice files uploaded successfully');
} catch (error) { } catch (error) {
console.error('Failed to upload invoice files:', 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'; const errorMessage = error instanceof Error ? error.message : 'Unknown error';
fileUploadErrors.push(`Failed to upload invoice files: ${errorMessage}`); fileUploadErrors.push(`Failed to upload invoice files: ${errorMessage}`);
} }
@ -416,6 +432,13 @@ const EventRequestForm: React.FC = () => {
console.log('Invoice file uploaded successfully'); console.log('Invoice file uploaded successfully');
} catch (error) { } catch (error) {
console.error('Failed to upload invoice file:', 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'; const errorMessage = error instanceof Error ? error.message : 'Unknown error';
fileUploadErrors.push(`Failed to upload invoice file: ${errorMessage}`); fileUploadErrors.push(`Failed to upload invoice file: ${errorMessage}`);
} }
@ -601,9 +624,9 @@ const EventRequestForm: React.FC = () => {
return false; return false;
} }
// Only require room booking file if will_or_have_room_booking is true // REQUIRED: Room booking files if will_or_have_room_booking is true
if (formData.will_or_have_room_booking && !formData.room_booking) { if (formData.will_or_have_room_booking && formData.room_booking_files.length === 0) {
toast.error('Please upload your room booking confirmation'); toast.error('Room booking files are required when you need a room booking');
return false; return false;
} }
@ -623,7 +646,13 @@ const EventRequestForm: React.FC = () => {
// Validate AS Funding Section // Validate AS Funding Section
const validateASFundingSection = () => { 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 // Check if invoice data is present and has items
if (!formData.invoiceData || !formData.invoiceData.items || formData.invoiceData.items.length === 0) { if (!formData.invoiceData || !formData.invoiceData.items || formData.invoiceData.items.length === 0) {
toast.error('Please add at least one item to your invoice'); toast.error('Please add at least one item to your invoice');

View file

@ -4,6 +4,7 @@ import type { EventRequestFormData } from './EventRequestForm';
import type { EventRequest } from '../../../schemas/pocketbase'; import type { EventRequest } from '../../../schemas/pocketbase';
import { FlyerTypes, LogoOptions } from '../../../schemas/pocketbase'; import { FlyerTypes, LogoOptions } from '../../../schemas/pocketbase';
import CustomAlert from '../universal/CustomAlert'; import CustomAlert from '../universal/CustomAlert';
import { Icon } from '@iconify/react';
// Enhanced animation variants // Enhanced animation variants
const containerVariants = { const containerVariants = {
@ -122,11 +123,26 @@ const PRSection: React.FC<PRSectionProps> = ({ formData, onDataChange }) => {
const handleLogoFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleLogoFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) { if (e.target.files && e.target.files.length > 0) {
const newFiles = Array.from(e.target.files) as File[]; const newFiles = Array.from(e.target.files) as File[];
setOtherLogoFiles(newFiles); // Combine existing files with new files instead of replacing
onDataChange({ other_logos: newFiles }); 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 // Handle drag events for file upload
const handleDragOver = (e: React.DragEvent) => { const handleDragOver = (e: React.DragEvent) => {
e.preventDefault(); e.preventDefault();
@ -144,8 +160,10 @@ const PRSection: React.FC<PRSectionProps> = ({ formData, onDataChange }) => {
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
const newFiles = Array.from(e.dataTransfer.files) as File[]; const newFiles = Array.from(e.dataTransfer.files) as File[];
setOtherLogoFiles(newFiles); // Combine existing files with new files instead of replacing
onDataChange({ other_logos: newFiles }); const combinedFiles = [...otherLogoFiles, ...newFiles];
setOtherLogoFiles(combinedFiles);
onDataChange({ other_logos: combinedFiles });
} }
}; };
@ -349,20 +367,44 @@ const PRSection: React.FC<PRSectionProps> = ({ formData, onDataChange }) => {
{otherLogoFiles.length > 0 ? ( {otherLogoFiles.length > 0 ? (
<> <>
<p className="font-medium text-primary">{otherLogoFiles.length} file(s) selected:</p> <div className="flex items-center justify-between w-full">
<div className="max-h-24 overflow-y-auto text-left w-full"> <p className="font-medium text-primary">{otherLogoFiles.length} file(s) selected:</p>
<ul className="list-disc list-inside text-sm"> <button
{otherLogoFiles.map((file, index) => ( type="button"
<li key={index} className="truncate">{file.name}</li> onClick={(e) => {
))} e.stopPropagation();
</ul> handleClearAllLogoFiles();
}}
className="btn btn-xs btn-outline btn-error"
title="Clear all files"
>
Clear All
</button>
</div> </div>
<p className="text-xs text-gray-500">Click or drag to replace</p> <div className="max-h-32 overflow-y-auto text-left w-full space-y-1">
{otherLogoFiles.map((file, index) => (
<div key={index} className="flex items-center justify-between bg-base-100 p-2 rounded">
<span className="text-sm truncate flex-1">{file.name}</span>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
handleRemoveLogoFile(index);
}}
className="btn btn-xs btn-error ml-2"
title="Remove file"
>
<Icon icon="heroicons:x-mark" className="w-3 h-3" />
</button>
</div>
))}
</div>
<p className="text-xs text-gray-500">Click or drag to add more files</p>
</> </>
) : ( ) : (
<> <>
<p className="font-medium">Drop your logo files here or click to browse</p> <p className="font-medium">Drop your logo files here or click to browse</p>
<p className="text-xs text-gray-500">Please upload transparent logo files (PNG preferred)</p> <p className="text-xs text-gray-500">Please upload transparent logo files (PNG preferred, multiple files allowed)</p>
</> </>
)} )}
</div> </div>

View file

@ -4,6 +4,7 @@ import type { EventRequestFormData } from './EventRequestForm';
import type { EventRequest } from '../../../schemas/pocketbase'; import type { EventRequest } from '../../../schemas/pocketbase';
import CustomAlert from '../universal/CustomAlert'; import CustomAlert from '../universal/CustomAlert';
import FilePreview from '../universal/FilePreview'; import FilePreview from '../universal/FilePreview';
import { Icon } from '@iconify/react';
// Enhanced animation variants // Enhanced animation variants
const containerVariants = { const containerVariants = {
@ -69,11 +70,12 @@ interface TAPFormSectionProps {
} }
const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange }) => { const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange }) => {
const [roomBookingFile, setRoomBookingFile] = useState<File | null>(formData.room_booking); const [roomBookingFiles, setRoomBookingFiles] = useState<File[]>(formData.room_booking_files || []);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
const [fileError, setFileError] = useState<string | null>(null); const [fileError, setFileError] = useState<string | null>(null);
const [showFilePreview, setShowFilePreview] = useState(false); const [showFilePreview, setShowFilePreview] = useState(false);
const [filePreviewUrl, setFilePreviewUrl] = useState<string | null>(null); const [filePreviewUrl, setFilePreviewUrl] = useState<string | null>(null);
const [selectedPreviewFile, setSelectedPreviewFile] = useState<File | null>(null);
// Add style tag for hidden arrows // Add style tag for hidden arrows
useEffect(() => { useEffect(() => {
@ -89,27 +91,58 @@ const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange
// Handle room booking file upload with size limit // Handle room booking file upload with size limit
const handleRoomBookingFileChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleRoomBookingFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) { if (e.target.files && e.target.files.length > 0) {
const file = e.target.files[0]; const newFiles = Array.from(e.target.files) as File[];
// Check file size - 1MB limit // Check file sizes - 1MB limit for each file
if (file.size > 1024 * 1024) { const oversizedFiles = newFiles.filter(file => file.size > 1024 * 1024);
setFileError("Room booking file size must be under 1MB"); if (oversizedFiles.length > 0) {
setFileError(`${oversizedFiles.length} file(s) exceed 1MB limit`);
return; return;
} }
setFileError(null); setFileError(null);
setRoomBookingFile(file); // Combine existing files with new files instead of replacing
onDataChange({ room_booking: file }); const combinedFiles = [...roomBookingFiles, ...newFiles];
setRoomBookingFiles(combinedFiles);
onDataChange({ room_booking_files: combinedFiles });
// Create preview URL // Create preview URL for the first new file
if (filePreviewUrl) { if (filePreviewUrl) {
URL.revokeObjectURL(filePreviewUrl); URL.revokeObjectURL(filePreviewUrl);
} }
const url = URL.createObjectURL(file); const url = URL.createObjectURL(newFiles[0]);
setFilePreviewUrl(url); 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 // Handle drag events for file upload
const handleDragOver = (e: React.DragEvent) => { const handleDragOver = (e: React.DragEvent) => {
e.preventDefault(); e.preventDefault();
@ -126,24 +159,28 @@ const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange
setIsDragging(false); setIsDragging(false);
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { 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 // Check file sizes - 1MB limit for each file
if (file.size > 1024 * 1024) { const oversizedFiles = newFiles.filter(file => file.size > 1024 * 1024);
setFileError("Room booking file size must be under 1MB"); if (oversizedFiles.length > 0) {
setFileError(`${oversizedFiles.length} file(s) exceed 1MB limit`);
return; return;
} }
setFileError(null); setFileError(null);
setRoomBookingFile(file); // Combine existing files with new files instead of replacing
onDataChange({ room_booking: file }); const combinedFiles = [...roomBookingFiles, ...newFiles];
setRoomBookingFiles(combinedFiles);
onDataChange({ room_booking_files: combinedFiles });
// Create preview URL // Create preview URL for the first new file
if (filePreviewUrl) { if (filePreviewUrl) {
URL.revokeObjectURL(filePreviewUrl); URL.revokeObjectURL(filePreviewUrl);
} }
const url = URL.createObjectURL(file); const url = URL.createObjectURL(newFiles[0]);
setFilePreviewUrl(url); setFilePreviewUrl(url);
setSelectedPreviewFile(newFiles[0]);
} }
}; };
@ -262,6 +299,11 @@ const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange
<span className="label-text font-medium text-lg">Room Booking Confirmation</span> <span className="label-text font-medium text-lg">Room Booking Confirmation</span>
{formData.will_or_have_room_booking && <span className="label-text-alt text-error">*</span>} {formData.will_or_have_room_booking && <span className="label-text-alt text-error">*</span>}
</label> </label>
{formData.will_or_have_room_booking && (
<p className="text-sm text-gray-500 mb-3">
<strong>Required:</strong> Upload your room booking confirmation document.
</p>
)}
{fileError && ( {fileError && (
<div className="mt-2 mb-2"> <div className="mt-2 mb-2">
@ -292,6 +334,7 @@ const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange
className="hidden" className="hidden"
onChange={handleRoomBookingFileChange} onChange={handleRoomBookingFileChange}
accept=".pdf,.png,.jpg,.jpeg" accept=".pdf,.png,.jpg,.jpeg"
multiple
/> />
<div className="flex flex-col items-center justify-center gap-3"> <div className="flex flex-col items-center justify-center gap-3">
@ -304,16 +347,46 @@ const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange
</svg> </svg>
</motion.div> </motion.div>
{roomBookingFile ? ( {roomBookingFiles.length > 0 ? (
<> <>
<p className="font-medium text-primary">File selected:</p> <div className="flex items-center justify-between w-full">
<p className="text-sm">{roomBookingFile.name}</p> <p className="font-medium text-primary">{roomBookingFiles.length} file(s) selected:</p>
<p className="text-xs text-gray-500">Click or drag to replace (Max size: 1MB)</p> <button
type="button"
onClick={(e) => {
e.stopPropagation();
handleClearAllFiles();
}}
className="btn btn-xs btn-outline btn-error"
title="Clear all files"
>
Clear All
</button>
</div>
<div className="max-h-32 overflow-y-auto text-left w-full space-y-1">
{roomBookingFiles.map((file, index) => (
<div key={index} className="flex items-center justify-between bg-base-100 p-2 rounded">
<span className="text-sm truncate flex-1">{file.name}</span>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
handleRemoveFile(index);
}}
className="btn btn-xs btn-error ml-2"
title="Remove file"
>
<Icon icon="heroicons:x-mark" className="w-3 h-3" />
</button>
</div>
))}
</div>
<p className="text-xs text-gray-500">Click or drag to add more files (Max size: 1MB each)</p>
</> </>
) : ( ) : (
<> <>
<p className="font-medium">Drop your file here or click to browse</p> <p className="font-medium">Drop your files here or click to browse</p>
<p className="text-xs text-gray-500">Accepted formats: PDF, PNG, JPG (Max size: 1MB)</p> <p className="text-xs text-gray-500">Accepted formats: PDF, PNG, JPG (Max size: 1MB each, multiple files allowed)</p>
</> </>
)} )}
</div> </div>
@ -329,20 +402,20 @@ const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange
)} )}
{/* Preview File Button - Outside the upload area */} {/* Preview File Button - Outside the upload area */}
{formData.will_or_have_room_booking && roomBookingFile && ( {formData.will_or_have_room_booking && roomBookingFiles.length > 0 && (
<div className="mt-3 flex justify-end"> <div className="mt-3 flex justify-end">
<button <button
type="button" type="button"
className="btn btn-primary btn-sm" className="btn btn-primary btn-sm"
onClick={toggleFilePreview} onClick={toggleFilePreview}
> >
{showFilePreview ? 'Hide Preview' : 'Preview File'} {showFilePreview ? 'Hide Preview' : `Preview Files (${roomBookingFiles.length})`}
</button> </button>
</div> </div>
)} )}
{/* File Preview Component */} {/* File Preview Component */}
{showFilePreview && filePreviewUrl && roomBookingFile && ( {showFilePreview && roomBookingFiles.length > 0 && (
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
@ -350,7 +423,7 @@ const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange
className="mt-4 p-4 bg-base-200 rounded-lg" className="mt-4 p-4 bg-base-200 rounded-lg"
> >
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<h3 className="font-medium">File Preview</h3> <h3 className="font-medium">File Preview ({roomBookingFiles.length} files)</h3>
<button <button
type="button" type="button"
className="btn btn-sm btn-circle" className="btn btn-sm btn-circle"
@ -361,7 +434,17 @@ const TAPFormSection: React.FC<TAPFormSectionProps> = ({ formData, onDataChange
</svg> </svg>
</button> </button>
</div> </div>
<FilePreview url={filePreviewUrl} filename={roomBookingFile.name} /> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{roomBookingFiles.map((file, index) => {
const url = URL.createObjectURL(file);
return (
<div key={index} className="border rounded-lg p-2">
<p className="text-sm font-medium mb-2 truncate">{file.name}</p>
<FilePreview url={url} filename={file.name} />
</div>
);
})}
</div>
</motion.div> </motion.div>
)} )}
</motion.div> </motion.div>

View file

@ -29,7 +29,7 @@ interface ExtendedEventRequest extends SchemaEventRequest {
flyer_files?: string[]; // Add this for PR-related files flyer_files?: string[]; // Add this for PR-related files
files?: string[]; // Generic files field files?: string[]; // Generic files field
will_or_have_room_booking?: boolean; 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 room_reservation_needed?: boolean; // Keep for backward compatibility
additional_notes?: string; additional_notes?: string;
flyers_completed?: boolean; // Track if flyers have been completed by PR team flyers_completed?: boolean; // Track if flyers have been completed by PR team
@ -82,7 +82,7 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
setFileUrl(secureUrl); setFileUrl(secureUrl);
// Determine file type from extension // Determine file type from extension
const extension = fileName.split('.').pop()?.toLowerCase() || ''; const extension = (typeof fileName === 'string' ? fileName.split('.').pop()?.toLowerCase() : '') || '';
setFileType(extension); setFileType(extension);
setIsLoading(false); setIsLoading(false);
@ -1125,6 +1125,7 @@ const PRMaterialsTab: React.FC<{ request: ExtendedEventRequest }> = ({ request }
// Use the same utility functions as in the ASFundingTab // Use the same utility functions as in the ASFundingTab
const getFileExtension = (filename: string): string => { const getFileExtension = (filename: string): string => {
if (!filename || typeof filename !== 'string') return '';
const parts = filename.split('.'); const parts = filename.split('.');
return parts.length > 1 ? parts.pop()?.toLowerCase() || '' : ''; 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 => { const getFriendlyFileName = (filename: string, maxLength: number = 20): string => {
if (!filename || typeof filename !== 'string') return 'Unknown File';
const basename = filename.split('/').pop() || filename; const basename = filename.split('/').pop() || filename;
if (basename.length <= maxLength) return basename; if (basename.length <= maxLength) return basename;
const extension = getFileExtension(basename); const extension = getFileExtension(basename);
@ -1836,29 +1838,34 @@ const EventRequestDetails = ({
<div className="bg-base-200/30 p-3 rounded-lg"> <div className="bg-base-200/30 p-3 rounded-lg">
<label className="text-xs text-gray-400 block mb-1">Confirmation Status</label> <label className="text-xs text-gray-400 block mb-1">Confirmation Status</label>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className={`badge ${request.room_booking ? 'badge-success' : 'badge-warning'}`}> <div className={`badge ${request.room_booking_files && request.room_booking_files.length > 0 ? 'badge-success' : 'badge-warning'}`}>
{request.room_booking ? 'Booking File Uploaded' : 'No Booking File'} {request.room_booking_files && request.room_booking_files.length > 0 ? 'Booking Files Uploaded' : 'No Booking Files'}
</div> </div>
{request.room_booking && ( {request.room_booking_files && request.room_booking_files.length > 0 && (
<button <div className="flex gap-2">
onClick={() => { {request.room_booking_files.map((fileId, index) => (
// Dispatch event to update file preview modal <button
const event = new CustomEvent('filePreviewStateChange', { key={index}
detail: { onClick={() => {
url: `https://pocketbase.ieeeucsd.org/api/files/event_request/${request.id}/${request.room_booking}`, // Dispatch event to update file preview modal
filename: request.room_booking const event = new CustomEvent('filePreviewStateChange', {
} detail: {
}); url: `https://pocketbase.ieeeucsd.org/api/files/event_request/${request.id}/${fileId}`,
window.dispatchEvent(event); filename: fileId
}
});
window.dispatchEvent(event);
// Open the modal // Open the modal
const modal = document.getElementById('file-preview-modal') as HTMLDialogElement; const modal = document.getElementById('file-preview-modal') as HTMLDialogElement;
if (modal) modal.showModal(); if (modal) modal.showModal();
}} }}
className="btn btn-xs btn-primary ml-2" className="btn btn-xs btn-primary"
> >
View File View File {index + 1}
</button> </button>
))}
</div>
)} )}
</div> </div>
</div> </div>

View file

@ -119,7 +119,7 @@ export interface EventRequest extends BaseRecord {
advertising_format?: string; advertising_format?: string;
will_or_have_room_booking?: boolean; will_or_have_room_booking?: boolean;
expected_attendance?: number; expected_attendance?: number;
room_booking?: string; // signle file room_booking_files?: string[]; // CHANGED: Multiple files instead of single file
as_funding_required: boolean; as_funding_required: boolean;
food_drinks_being_served: boolean; food_drinks_being_served: boolean;
itemized_invoice?: string; // JSON string itemized_invoice?: string; // JSON string