fix file uploads
This commit is contained in:
parent
a57a4e6889
commit
4349b4d034
6 changed files with 307 additions and 105 deletions
|
@ -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>
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue