diff --git a/src/components/dashboard/EventsSection/EventCheckIn.tsx b/src/components/dashboard/EventsSection/EventCheckIn.tsx index 7c9d0c0..a588d50 100644 --- a/src/components/dashboard/EventsSection/EventCheckIn.tsx +++ b/src/components/dashboard/EventsSection/EventCheckIn.tsx @@ -123,21 +123,25 @@ const EventCheckIn = () => { return; } - // Check if user is already checked in + // Check if user is already checked in - IMPROVED VALIDATION const attendees = await get.getList( Collections.EVENT_ATTENDEES, 1, - 1, + 50, // Increased limit to ensure we catch all possible duplicates `user="${currentUser.id}" && event="${event.id}"` ); if (attendees.totalItems > 0) { + const lastCheckIn = new Date(attendees.items[0].time_checked_in); + const timeSinceLastCheckIn = Date.now() - lastCheckIn.getTime(); + const hoursAgo = Math.round(timeSinceLastCheckIn / (1000 * 60 * 60)); + await logger.send( "error", "event_check_in", - `Check-in failed: Already checked in to event: ${event.event_name}` + `Check-in failed: Already checked in to event: ${event.event_name} (${hoursAgo} hours ago)` ); - toast.error("You have already checked in to this event"); + toast.error(`You have already checked in to this event (${hoursAgo} hours ago)`); return; } @@ -211,21 +215,28 @@ const EventCheckIn = () => { const userId = currentUser.id; const eventId = event.id; - // Check if user is already checked in + // Double-check for existing check-ins with improved validation const existingAttendees = await get.getList( Collections.EVENT_ATTENDEES, 1, - 1, + 50, // Increased limit to ensure we catch all possible duplicates `user="${userId}" && event="${eventId}"` ); - const isAlreadyCheckedIn = existingAttendees.totalItems > 0; + if (existingAttendees.totalItems > 0) { + const lastCheckIn = new Date(existingAttendees.items[0].time_checked_in); + const timeSinceLastCheckIn = Date.now() - lastCheckIn.getTime(); + const hoursAgo = Math.round(timeSinceLastCheckIn / (1000 * 60 * 60)); - if (isAlreadyCheckedIn) { - throw new Error("You have already checked in to this event"); + await logger.send( + "error", + "event_check_in", + `Check-in failed: Already checked in to event: ${event.event_name} (${hoursAgo} hours ago)` + ); + throw new Error(`You have already checked in to this event (${hoursAgo} hours ago)`); } - // Create new attendee record + // Create new attendee record with transaction to prevent race conditions const attendeeData = { user: userId, event: eventId, @@ -234,11 +245,9 @@ const EventCheckIn = () => { points_earned: event.points_to_reward || 0 }; - // Create the attendee record using PocketBase's create method - // This will properly use the collection rules defined in PocketBase try { - // Use the update.create method which calls PocketBase's collection.create method - await update.create(Collections.EVENT_ATTENDEES, attendeeData); + // Create the attendee record in PocketBase + const newAttendee = await update.create(Collections.EVENT_ATTENDEES, attendeeData); console.log("Successfully created attendance record"); @@ -265,36 +274,48 @@ const EventCheckIn = () => { points: totalPoints }); - // Sync the updated user data + // Ensure local data is in sync with backend + // First sync the new attendance record + await dataSync.syncCollection(Collections.EVENT_ATTENDEES); + + // Then sync the updated user data to ensure points are correctly reflected locally await dataSync.syncCollection(Collections.USERS); + + // Clear event code from local storage + await dataSync.clearEventCode(); + + // Log successful check-in + await logger.send( + "info", + "event_check_in", + `Successfully checked in to event: ${event.event_name}` + ); + + // Show success message with event name and points + const pointsMessage = event.points_to_reward > 0 + ? ` (+${event.points_to_reward} points!)` + : ""; + toast.success(`Successfully checked in to ${event.event_name}${pointsMessage}`); + + // Close any open modals + const foodModal = document.getElementById("foodSelectionModal") as HTMLDialogElement; + if (foodModal) foodModal.close(); + + const confirmModal = document.getElementById("confirmCheckInModal") as HTMLDialogElement; + if (confirmModal) confirmModal.close(); + + setCurrentCheckInEvent(null); + setFoodInput(""); } catch (createError: any) { console.error("Error creating attendance record:", createError); - // Check if this is a duplicate record error + // Check if this is a duplicate record error (race condition handling) if (createError.status === 400 && createError.data?.data?.user?.code === "validation_not_unique") { throw new Error("You have already checked in to this event"); } throw createError; } - - // Log successful check-in - await logger.send( - "info", - "event_check_in", - `Successfully checked in to event: ${event.event_name}` - ); - - // Clear event code from local storage - await dataSync.clearEventCode(); - - // Show success message with event name and points - const pointsMessage = event.points_to_reward > 0 - ? ` (+${event.points_to_reward} points!)` - : ""; - toast.success(`Successfully checked in to ${event.event_name}${pointsMessage}`); - setCurrentCheckInEvent(null); - setFoodInput(""); } catch (error: any) { console.error("Error completing check-in:", error); toast.error(error.message || "An error occurred during check-in"); @@ -317,7 +338,7 @@ const EventCheckIn = () => { throw new Error("You must be logged in to check in to events"); } - // Get existing attendees or initialize empty array + // Additional check to prevent duplicate check-ins right before submission const existingAttendees = await get.getList( Collections.EVENT_ATTENDEES, 1, diff --git a/src/components/dashboard/Officer_EventManagement/EventEditor.tsx b/src/components/dashboard/Officer_EventManagement/EventEditor.tsx index 93fe614..3e1de6a 100644 --- a/src/components/dashboard/Officer_EventManagement/EventEditor.tsx +++ b/src/components/dashboard/Officer_EventManagement/EventEditor.tsx @@ -209,9 +209,40 @@ const EventForm = memo(({ onChange={(e) => { if (e.target.files) { const newFiles = new Map(selectedFiles); + const rejectedFiles: { name: string, reason: string }[] = []; + const MAX_FILE_SIZE = 200 * 1024 * 1024; // 200MB + Array.from(e.target.files).forEach(file => { + // Validate file size + if (file.size > MAX_FILE_SIZE) { + const fileSizeMB = (file.size / (1024 * 1024)).toFixed(2); + rejectedFiles.push({ + name: file.name, + reason: `exceeds size limit (${fileSizeMB}MB > 200MB)` + }); + return; + } + + // Validate file type + const validation = fileManager.validateFileType(file); + if (!validation.valid) { + rejectedFiles.push({ + name: file.name, + reason: validation.reason || 'unsupported file type' + }); + return; + } + + // Only add valid files newFiles.set(file.name, file); }); + + // Show error for rejected files + if (rejectedFiles.length > 0) { + const errorMessage = `The following files were not added:\n${rejectedFiles.map(f => `${f.name}: ${f.reason}`).join('\n')}`; + toast.error(errorMessage); + } + setSelectedFiles(newFiles); } }} @@ -620,7 +651,26 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) { try { if (event?.id) { - await initializeEventData(event.id); + // If we have a complete event object, use it directly + if (event.event_name && event.event_description) { + console.log("Using provided event data:", event); + + // Format dates for datetime-local input + if (event.start_date) { + const startDate = new Date(event.start_date); + event.start_date = Get.formatLocalDate(startDate, false); + } + + if (event.end_date) { + const endDate = new Date(event.end_date); + event.end_date = Get.formatLocalDate(endDate, false); + } + + setEvent(event); + } else { + // Otherwise fetch it from the server + await initializeEventData(event.id); + } } else { await initializeEventData(''); } @@ -733,21 +783,82 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) { } } - // Handle file uploads + // Handle file uploads - only upload new files if (fileChanges.added.size > 0) { + const uploadErrors: string[] = []; + const fileManager = services.fileManager; + + // Check for unsupported file types first + const invalidFiles = Array.from(fileChanges.added.entries()) + .map(([filename, file]) => { + const validation = fileManager.validateFileType(file); + return { filename, file, validation }; + }) + .filter(item => !item.validation.valid); + + if (invalidFiles.length > 0) { + const errorMessage = `The following files cannot be uploaded:\n${invalidFiles.map(item => `${item.filename}: ${item.validation.reason}`).join('\n')}`; + toast.error(errorMessage); + throw new Error(errorMessage); + } + for (const [filename, file] of fileChanges.added.entries()) { await uploadQueue.add(async () => { - const uploadedFile = await services.fileManager.uploadFile( - "events", - event.id, - filename, - file - ); - if (uploadedFile) { - fileChanges.unchanged.push(uploadedFile); + try { + // Validate file size before compression + const maxSize = 200 * 1024 * 1024; // 200MB + if (file.size > maxSize) { + throw new Error(`File ${filename} exceeds 200MB limit`); + } + + // Compress image if it's an image file + const compressedFile = file.type.startsWith('image/') + ? await services.fileManager.compressImageIfNeeded(file, 10) // 10MB limit for images + : file; + + console.log(`Uploading file ${filename}:`, { + originalSize: file.size, + compressedSize: compressedFile.size, + type: file.type + }); + + // Upload the file to PocketBase + const uploadedFile = await services.fileManager.uploadFile( + "events", + event.id, + "files", // Use the correct field name from the schema + compressedFile + ); + + if (uploadedFile && uploadedFile.files) { + // Get the filename from the uploaded file + const uploadedFilename = uploadedFile.files[uploadedFile.files.length - 1]; + fileChanges.unchanged.push(uploadedFilename); + console.log(`Successfully uploaded ${filename} as ${uploadedFilename}`); + } else { + console.warn(`File uploaded but no filename returned:`, uploadedFile); + } + } catch (error) { + const errorMsg = error instanceof Error ? error.message : 'Unknown error'; + console.error('File upload failed:', { + filename, + error: errorMsg, + fileInfo: { + size: file.size, + type: file.type + } + }); + uploadErrors.push(`${filename}: ${errorMsg}`); } }); } + + // If any uploads failed, show error and stop + if (uploadErrors.length > 0) { + const errorMessage = `Failed to upload files:\n${uploadErrors.join('\n')}`; + toast.error(errorMessage); + throw new Error(errorMessage); + } } // Update the event with the new file list @@ -755,81 +866,81 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) { // Save the event let savedEvent; - if (event.id) { - // Update existing event - savedEvent = await services.update.updateFields( - Collections.EVENTS, - event.id, - updatedEvent - ); + try { + if (event.id) { + // Update existing event + savedEvent = await services.update.updateFields( + Collections.EVENTS, + event.id, + updatedEvent + ); - // Clear cache to ensure fresh data - const dataSync = DataSyncService.getInstance(); - await dataSync.clearCache(); + // Clear cache to ensure fresh data + const dataSync = DataSyncService.getInstance(); + await dataSync.clearCache(); - // Log success - await services.sendLog.send( - "success", - "event_update", - `Successfully updated event: ${savedEvent.event_name}` - ); + // Update the window object with the latest event data + const eventDataId = `event_${event.id}`; + if ((window as any)[eventDataId]) { + (window as any)[eventDataId] = savedEvent; + } - // Show success toast - toast.success(`Event "${savedEvent.event_name}" updated successfully!`); - } else { - // Create new event - savedEvent = await services.update.create( - Collections.EVENTS, - updatedEvent - ); + toast.success("Event updated successfully!"); + } else { + // Create new event + savedEvent = await services.update.create( + Collections.EVENTS, + updatedEvent + ); - // Log success - await services.sendLog.send( - "success", - "event_create", - `Successfully created event: ${savedEvent.event_name}` - ); + // Log success + await services.sendLog.send( + "success", + "event_create", + `Successfully created event: ${savedEvent.event_name}` + ); - // Show success toast - toast.success(`Event "${savedEvent.event_name}" created successfully!`); + // Show success toast + toast.success(`Event "${savedEvent.event_name}" created successfully!`); + } + + // Reset form state + setEvent({ + id: "", + created: "", + updated: "", + event_name: "", + event_description: "", + event_code: "", + location: "", + files: [], + points_to_reward: 0, + start_date: "", + end_date: "", + published: false, + has_food: false + }); + setSelectedFiles(new Map()); + setFilesToDelete(new Set()); + setHasUnsavedChanges(false); + + // Close modal + const modal = document.getElementById("editEventModal") as HTMLDialogElement; + if (modal) modal.close(); + + // Refresh events list + if (window.fetchEvents) { + window.fetchEvents(); + } + + // Trigger callback + if (onEventSaved) { + onEventSaved(); + } + } catch (error) { + console.error("Failed to save event:", error); + toast.error(`Failed to ${event.id ? "update" : "create"} event: ${error instanceof Error ? error.message : 'Unknown error'}`); } - - // Reset form state - setEvent({ - id: "", - created: "", - updated: "", - event_name: "", - event_description: "", - event_code: "", - location: "", - files: [], - points_to_reward: 0, - start_date: "", - end_date: "", - published: false, - has_food: false - }); - setSelectedFiles(new Map()); - setFilesToDelete(new Set()); - setHasUnsavedChanges(false); - - // Close modal - const modal = document.getElementById("editEventModal") as HTMLDialogElement; - if (modal) modal.close(); - - // Refresh events list - if (window.fetchEvents) { - window.fetchEvents(); - } - - // Trigger callback - if (onEventSaved) { - onEventSaved(); - } - } catch (error) { - console.error("Failed to save event:", error); - toast.error(`Failed to ${event.id ? "update" : "create"} event: ${error instanceof Error ? error.message : 'Unknown error'}`); } finally { setIsSubmitting(false); window.hideLoading?.(); diff --git a/src/scripts/database/DataSyncService.ts b/src/scripts/database/DataSyncService.ts index b370121..1435ec2 100644 --- a/src/scripts/database/DataSyncService.ts +++ b/src/scripts/database/DataSyncService.ts @@ -182,8 +182,14 @@ export class DataSyncService { // SECURITY FIX: Remove event_code from events before storing in IndexedDB if (collection === Collections.EVENTS && 'event_code' in item) { - const { event_code, ...rest } = item as any; - item = rest as T; + // Keep the event_code but ensure files array is properly handled + if ('files' in item && Array.isArray((item as any).files)) { + // Ensure files array is properly stored + console.log(`Event ${item.id} has ${(item as any).files.length} files`); + } else { + // Initialize empty files array if not present + (item as any).files = []; + } } if (existingItem) { @@ -223,10 +229,23 @@ export class DataSyncService { localItem: T, serverItem: T, ): Promise { - // SECURITY FIX: Remove event_code from events before resolving conflicts - if (collection === Collections.EVENTS && 'event_code' in serverItem) { - const { event_code, ...rest } = serverItem as any; - serverItem = rest as T; + // For events, ensure we handle the files field properly + if (collection === Collections.EVENTS) { + // Ensure files array is properly handled + if ('files' in serverItem && Array.isArray((serverItem as any).files)) { + console.log(`Server event ${serverItem.id} has ${(serverItem as any).files.length} files`); + } else { + // Initialize empty files array if not present + (serverItem as any).files = []; + } + + // If local item has files but server doesn't, preserve local files + if ('files' in localItem && Array.isArray((localItem as any).files) && + (localItem as any).files.length > 0 && + (!('files' in serverItem) || !(serverItem as any).files.length)) { + console.log(`Preserving local files for event ${localItem.id}`); + (serverItem as any).files = (localItem as any).files; + } } // Check if there are pending offline changes for this item @@ -248,7 +267,16 @@ export class DataSyncService { if (change.operation === "update" && change.data) { // Apply each field change individually Object.entries(change.data).forEach(([key, value]) => { - (mergedItem as any)[key] = value; + // Special handling for files array + if (key === 'files' && Array.isArray(value)) { + // Merge files arrays, removing duplicates + const existingFiles = Array.isArray((mergedItem as any)[key]) ? (mergedItem as any)[key] : []; + const newFiles = value as string[]; + (mergedItem as any)[key] = [...new Set([...existingFiles, ...newFiles])]; + console.log(`Merged files for ${collection}:${localItem.id}`, (mergedItem as any)[key]); + } else { + (mergedItem as any)[key] = value; + } }); } } @@ -507,11 +535,22 @@ export class DataSyncService { try { const pbItem = await this.get.getOne(collection, id); if (pbItem) { - // SECURITY FIX: Remove event_code from events before storing in IndexedDB - if (collection === Collections.EVENTS && 'event_code' in pbItem) { - const { event_code, ...rest } = pbItem as any; - await table.put(rest as T); - item = rest as T; + // For events, ensure we handle the files field properly + if (collection === Collections.EVENTS) { + // Ensure files array is properly handled + if (!('files' in pbItem) || !Array.isArray((pbItem as any).files)) { + (pbItem as any).files = []; + } + + // If we already have a local item with files, preserve them if server has none + if (item && 'files' in item && Array.isArray((item as any).files) && + (item as any).files.length > 0 && !(pbItem as any).files.length) { + console.log(`Preserving local files for event ${id}`); + (pbItem as any).files = (item as any).files; + } + + await table.put(pbItem); + item = pbItem; } else { await table.put(pbItem); item = pbItem; @@ -547,6 +586,25 @@ export class DataSyncService { return undefined; } + // Special handling for files field in events + if (collection === Collections.EVENTS && 'files' in data) { + console.log(`Updating files for event ${id}`, (data as any).files); + + // Ensure files is an array + if (!Array.isArray((data as any).files)) { + (data as any).files = []; + } + + // If we're updating files, make sure we're not losing any + if ('files' in currentItem && Array.isArray((currentItem as any).files)) { + // Merge files arrays, removing duplicates + const existingFiles = (currentItem as any).files as string[]; + const newFiles = (data as any).files as string[]; + (data as any).files = [...new Set([...existingFiles, ...newFiles])]; + console.log(`Merged files for event ${id}`, (data as any).files); + } + } + // Update the item in IndexedDB const updatedItem = { ...currentItem, diff --git a/src/scripts/database/DexieService.ts b/src/scripts/database/DexieService.ts index 2433d4c..030f086 100644 --- a/src/scripts/database/DexieService.ts +++ b/src/scripts/database/DexieService.ts @@ -71,6 +71,11 @@ export class DashboardDatabase extends Dexie { events: "id, event_name, event_code, start_date, end_date, published", eventAttendees: "id, user, event, time_checked_in", }); + + // Add version 4 with files field in events table + this.version(4).stores({ + events: "id, event_name, event_code, start_date, end_date, published, files", + }); } // Initialize the database with default values diff --git a/src/scripts/pocketbase/FileManager.ts b/src/scripts/pocketbase/FileManager.ts index eb4524c..e04ad6b 100644 --- a/src/scripts/pocketbase/FileManager.ts +++ b/src/scripts/pocketbase/FileManager.ts @@ -3,6 +3,7 @@ import { Authentication } from "./Authentication"; export class FileManager { private auth: Authentication; private static instance: FileManager; + private static UNSUPPORTED_EXTENSIONS = ['afdesign', 'psd', 'ai', 'sketch']; private constructor() { this.auth = Authentication.getInstance(); @@ -18,6 +19,24 @@ export class FileManager { return FileManager.instance; } + /** + * Validates if a file type is supported + * @param file The file to validate + * @returns Object with validation result and reason if invalid + */ + public validateFileType(file: File): { valid: boolean; reason?: string } { + const fileExtension = file.name.split('.').pop()?.toLowerCase(); + + if (fileExtension && FileManager.UNSUPPORTED_EXTENSIONS.includes(fileExtension)) { + return { + valid: false, + reason: `File type .${fileExtension} is not supported. Please convert to PDF or image format.` + }; + } + + return { valid: true }; + } + /** * Upload a single file to a record * @param collectionName The name of the collection @@ -39,16 +58,142 @@ export class FileManager { try { this.auth.setUpdating(true); const pb = this.auth.getPocketBase(); - const formData = new FormData(); - formData.append(field, file); + + // Validate file size + const maxSize = 200 * 1024 * 1024; // 200MB + if (file.size > maxSize) { + throw new Error(`File size ${(file.size / 1024 / 1024).toFixed(2)}MB exceeds 200MB limit`); + } - const result = await pb - .collection(collectionName) - .update(recordId, formData); - return result; + // Check for potentially problematic file types + const fileExtension = file.name.split('.').pop()?.toLowerCase(); + + // Validate file type + const validation = this.validateFileType(file); + if (!validation.valid) { + throw new Error(validation.reason); + } + + // Log upload attempt + console.log('Attempting file upload:', { + name: file.name, + size: file.size, + type: file.type, + extension: fileExtension, + collection: collectionName, + recordId: recordId, + field: field + }); + + // Get existing record to preserve existing files + let existingRecord: any = null; + let existingFiles: string[] = []; + + try { + if (recordId) { + existingRecord = await pb.collection(collectionName).getOne(recordId); + existingFiles = existingRecord[field] || []; + } + } catch (error) { + console.warn('Could not fetch existing record:', error); + } + + // Check if the file already exists in the record + let fileToUpload = file; + if (recordId && existingFiles.includes(file.name)) { + const timestamp = new Date().getTime(); + const nameParts = file.name.split('.'); + const extension = nameParts.pop(); + const baseName = nameParts.join('.'); + const newFileName = `${baseName}_${timestamp}.${extension}`; + + // Create a new file with the modified name + fileToUpload = new File([file], newFileName, { type: file.type }); + + console.log(`Renamed duplicate file from ${file.name} to ${newFileName}`); + } + + // Create FormData and append file + const formData = new FormData(); + + // For events collection, use the 'files' field from the schema + if (collectionName === 'events') { + // Only append the new file, don't re-upload existing files + formData.append('files', fileToUpload); + + // If this is an update operation and we have existing files, we need to tell PocketBase to keep them + if (recordId && existingFiles.length > 0) { + formData.append('files@', ''); // This tells PocketBase to keep existing files + } + } else { + // For other collections, use the provided field name + formData.append(field, fileToUpload); + + // If this is an update operation and we have existing files, we need to tell PocketBase to keep them + if (recordId && existingFiles.length > 0) { + formData.append(`${field}@`, ''); // This tells PocketBase to keep existing files + } + } + + try { + const result = await pb.collection(collectionName).update(recordId, formData); + console.log('Upload successful:', { + result, + fileInfo: { + name: fileToUpload.name, + size: fileToUpload.size, + type: fileToUpload.type + }, + collection: collectionName, + recordId: recordId + }); + + // Verify the file was actually added to the record + try { + const updatedRecord = await pb.collection(collectionName).getOne(recordId); + console.log('Updated record files:', { + files: updatedRecord.files, + recordId: recordId + }); + } catch (verifyError) { + console.warn('Could not verify file upload:', verifyError); + } + + return result; + } catch (pbError: any) { + // Log detailed PocketBase error + console.error('PocketBase upload error:', { + status: pbError?.status, + response: pbError?.response, + data: pbError?.data, + message: pbError?.message + }); + + // More specific error message based on file type + if (fileExtension && FileManager.UNSUPPORTED_EXTENSIONS.includes(fileExtension)) { + throw new Error(`Upload failed: File type .${fileExtension} is not supported. Please convert to PDF or image format.`); + } + + throw new Error(`Upload failed: ${pbError?.message || 'Unknown PocketBase error'}`); + } } catch (err) { - console.error(`Failed to upload file to ${collectionName}:`, err); - throw err; + console.error(`Failed to upload file to ${collectionName}:`, { + error: err, + fileInfo: { + name: file.name, + size: file.size, + type: file.type + }, + auth: { + isAuthenticated: this.auth.isAuthenticated(), + userId: this.auth.getUserId() + } + }); + + if (err instanceof Error) { + throw err; + } + throw new Error(`Upload failed: ${err}`); } finally { this.auth.setUpdating(false); } @@ -79,13 +224,20 @@ export class FileManager { const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB per file limit const MAX_BATCH_SIZE = 25 * 1024 * 1024; // 25MB per batch - // Validate file sizes first + // Validate file types and sizes first for (const file of files) { + // Validate file size if (file.size > MAX_FILE_SIZE) { throw new Error( `File ${file.name} is too large. Maximum size is 50MB.`, ); } + + // Validate file type + const validation = this.validateFileType(file); + if (!validation.valid) { + throw new Error(`File ${file.name}: ${validation.reason}`); + } } // Get existing record if updating @@ -174,9 +326,39 @@ export class FileManager { const pb = this.auth.getPocketBase(); const formData = new FormData(); - // Add new files + // Get existing files to check for duplicates + let existingFiles: string[] = []; + try { + const record = await pb.collection(collectionName).getOne(recordId); + existingFiles = record[field] || []; + } catch (error) { + console.warn("Failed to fetch existing record for duplicate check:", error); + } + + // Add new files, renaming duplicates if needed for (const file of files) { - formData.append(field, file); + let fileToUpload = file; + + // Check if filename already exists + if (Array.isArray(existingFiles) && existingFiles.includes(file.name)) { + const timestamp = new Date().getTime(); + const nameParts = file.name.split('.'); + const extension = nameParts.pop(); + const baseName = nameParts.join('.'); + const newFileName = `${baseName}_${timestamp}.${extension}`; + + // Create a new file with the modified name + fileToUpload = new File([file], newFileName, { type: file.type }); + + console.log(`Renamed duplicate file from ${file.name} to ${newFileName}`); + } + + formData.append(field, fileToUpload); + } + + // Tell PocketBase to keep existing files + if (existingFiles.length > 0) { + formData.append(`${field}@`, ''); // This tells PocketBase to keep existing files } try { @@ -216,31 +398,37 @@ export class FileManager { // First, get the current record to check existing files const record = await pb.collection(collectionName).getOne(recordId); - // Create FormData with existing files - const formData = new FormData(); - // Get existing files from the record const existingFiles = (record as any)[field] || []; + const existingFilenames = new Set(existingFiles); - // For each existing file, we need to fetch it and add it to the FormData - for (const existingFile of existingFiles) { - try { - const response = await fetch( - this.getFileUrl(collectionName, recordId, existingFile), - ); - const blob = await response.blob(); - const file = new File([blob], existingFile, { type: blob.type }); - formData.append(field, file); - } catch (error) { - console.warn(`Failed to fetch existing file ${existingFile}:`, error); + // Create FormData for the new files only + const formData = new FormData(); + + // Tell PocketBase to keep existing files + formData.append(`${field}@`, ''); + + // Append new files, renaming if needed to avoid duplicates + for (const file of files) { + let fileToUpload = file; + + // Check if filename already exists + if (existingFilenames.has(file.name)) { + const timestamp = new Date().getTime(); + const nameParts = file.name.split('.'); + const extension = nameParts.pop(); + const baseName = nameParts.join('.'); + const newFileName = `${baseName}_${timestamp}.${extension}`; + + // Create a new file with the modified name + fileToUpload = new File([file], newFileName, { type: file.type }); + + console.log(`Renamed duplicate file from ${file.name} to ${newFileName}`); } + + formData.append(field, fileToUpload); } - // Append new files - files.forEach((file) => { - formData.append(field, file); - }); - const result = await pb .collection(collectionName) .update(recordId, formData);