fix bugs relating to the event editor
This commit is contained in:
parent
addfb479b1
commit
3123f6c00c
5 changed files with 539 additions and 156 deletions
|
@ -123,21 +123,25 @@ const EventCheckIn = () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user is already checked in
|
// Check if user is already checked in - IMPROVED VALIDATION
|
||||||
const attendees = await get.getList<EventAttendee>(
|
const attendees = await get.getList<EventAttendee>(
|
||||||
Collections.EVENT_ATTENDEES,
|
Collections.EVENT_ATTENDEES,
|
||||||
1,
|
1,
|
||||||
1,
|
50, // Increased limit to ensure we catch all possible duplicates
|
||||||
`user="${currentUser.id}" && event="${event.id}"`
|
`user="${currentUser.id}" && event="${event.id}"`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (attendees.totalItems > 0) {
|
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(
|
await logger.send(
|
||||||
"error",
|
"error",
|
||||||
"event_check_in",
|
"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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,21 +215,28 @@ const EventCheckIn = () => {
|
||||||
const userId = currentUser.id;
|
const userId = currentUser.id;
|
||||||
const eventId = event.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<EventAttendee>(
|
const existingAttendees = await get.getList<EventAttendee>(
|
||||||
Collections.EVENT_ATTENDEES,
|
Collections.EVENT_ATTENDEES,
|
||||||
1,
|
1,
|
||||||
1,
|
50, // Increased limit to ensure we catch all possible duplicates
|
||||||
`user="${userId}" && event="${eventId}"`
|
`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) {
|
await logger.send(
|
||||||
throw new Error("You have already checked in to this event");
|
"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 = {
|
const attendeeData = {
|
||||||
user: userId,
|
user: userId,
|
||||||
event: eventId,
|
event: eventId,
|
||||||
|
@ -234,11 +245,9 @@ const EventCheckIn = () => {
|
||||||
points_earned: event.points_to_reward || 0
|
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 {
|
try {
|
||||||
// Use the update.create method which calls PocketBase's collection.create method
|
// Create the attendee record in PocketBase
|
||||||
await update.create(Collections.EVENT_ATTENDEES, attendeeData);
|
const newAttendee = await update.create(Collections.EVENT_ATTENDEES, attendeeData);
|
||||||
|
|
||||||
console.log("Successfully created attendance record");
|
console.log("Successfully created attendance record");
|
||||||
|
|
||||||
|
@ -265,36 +274,48 @@ const EventCheckIn = () => {
|
||||||
points: totalPoints
|
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);
|
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) {
|
} catch (createError: any) {
|
||||||
console.error("Error creating attendance record:", createError);
|
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") {
|
if (createError.status === 400 && createError.data?.data?.user?.code === "validation_not_unique") {
|
||||||
throw new Error("You have already checked in to this event");
|
throw new Error("You have already checked in to this event");
|
||||||
}
|
}
|
||||||
|
|
||||||
throw createError;
|
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) {
|
} catch (error: any) {
|
||||||
console.error("Error completing check-in:", error);
|
console.error("Error completing check-in:", error);
|
||||||
toast.error(error.message || "An error occurred during check-in");
|
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");
|
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<EventAttendee>(
|
const existingAttendees = await get.getList<EventAttendee>(
|
||||||
Collections.EVENT_ATTENDEES,
|
Collections.EVENT_ATTENDEES,
|
||||||
1,
|
1,
|
||||||
|
|
|
@ -209,9 +209,40 @@ const EventForm = memo(({
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (e.target.files) {
|
if (e.target.files) {
|
||||||
const newFiles = new Map(selectedFiles);
|
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 => {
|
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);
|
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);
|
setSelectedFiles(newFiles);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
@ -620,7 +651,26 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (event?.id) {
|
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 {
|
} else {
|
||||||
await initializeEventData('');
|
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) {
|
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()) {
|
for (const [filename, file] of fileChanges.added.entries()) {
|
||||||
await uploadQueue.add(async () => {
|
await uploadQueue.add(async () => {
|
||||||
const uploadedFile = await services.fileManager.uploadFile(
|
try {
|
||||||
"events",
|
// Validate file size before compression
|
||||||
event.id,
|
const maxSize = 200 * 1024 * 1024; // 200MB
|
||||||
filename,
|
if (file.size > maxSize) {
|
||||||
file
|
throw new Error(`File ${filename} exceeds 200MB limit`);
|
||||||
);
|
}
|
||||||
if (uploadedFile) {
|
|
||||||
fileChanges.unchanged.push(uploadedFile);
|
// 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
|
// Update the event with the new file list
|
||||||
|
@ -755,81 +866,81 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
|
||||||
|
|
||||||
// Save the event
|
// Save the event
|
||||||
let savedEvent;
|
let savedEvent;
|
||||||
if (event.id) {
|
try {
|
||||||
// Update existing event
|
if (event.id) {
|
||||||
savedEvent = await services.update.updateFields<Event>(
|
// Update existing event
|
||||||
Collections.EVENTS,
|
savedEvent = await services.update.updateFields<Event>(
|
||||||
event.id,
|
Collections.EVENTS,
|
||||||
updatedEvent
|
event.id,
|
||||||
);
|
updatedEvent
|
||||||
|
);
|
||||||
|
|
||||||
// Clear cache to ensure fresh data
|
// Clear cache to ensure fresh data
|
||||||
const dataSync = DataSyncService.getInstance();
|
const dataSync = DataSyncService.getInstance();
|
||||||
await dataSync.clearCache();
|
await dataSync.clearCache();
|
||||||
|
|
||||||
// Log success
|
// Update the window object with the latest event data
|
||||||
await services.sendLog.send(
|
const eventDataId = `event_${event.id}`;
|
||||||
"success",
|
if ((window as any)[eventDataId]) {
|
||||||
"event_update",
|
(window as any)[eventDataId] = savedEvent;
|
||||||
`Successfully updated event: ${savedEvent.event_name}`
|
}
|
||||||
);
|
|
||||||
|
|
||||||
// Show success toast
|
toast.success("Event updated successfully!");
|
||||||
toast.success(`Event "${savedEvent.event_name}" updated successfully!`);
|
} else {
|
||||||
} else {
|
// Create new event
|
||||||
// Create new event
|
savedEvent = await services.update.create<Event>(
|
||||||
savedEvent = await services.update.create<Event>(
|
Collections.EVENTS,
|
||||||
Collections.EVENTS,
|
updatedEvent
|
||||||
updatedEvent
|
);
|
||||||
);
|
|
||||||
|
|
||||||
// Log success
|
// Log success
|
||||||
await services.sendLog.send(
|
await services.sendLog.send(
|
||||||
"success",
|
"success",
|
||||||
"event_create",
|
"event_create",
|
||||||
`Successfully created event: ${savedEvent.event_name}`
|
`Successfully created event: ${savedEvent.event_name}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Show success toast
|
// Show success toast
|
||||||
toast.success(`Event "${savedEvent.event_name}" created successfully!`);
|
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 {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
window.hideLoading?.();
|
window.hideLoading?.();
|
||||||
|
|
|
@ -182,8 +182,14 @@ export class DataSyncService {
|
||||||
|
|
||||||
// SECURITY FIX: Remove event_code from events before storing in IndexedDB
|
// SECURITY FIX: Remove event_code from events before storing in IndexedDB
|
||||||
if (collection === Collections.EVENTS && 'event_code' in item) {
|
if (collection === Collections.EVENTS && 'event_code' in item) {
|
||||||
const { event_code, ...rest } = item as any;
|
// Keep the event_code but ensure files array is properly handled
|
||||||
item = rest as T;
|
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) {
|
if (existingItem) {
|
||||||
|
@ -223,10 +229,23 @@ export class DataSyncService {
|
||||||
localItem: T,
|
localItem: T,
|
||||||
serverItem: T,
|
serverItem: T,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
// SECURITY FIX: Remove event_code from events before resolving conflicts
|
// For events, ensure we handle the files field properly
|
||||||
if (collection === Collections.EVENTS && 'event_code' in serverItem) {
|
if (collection === Collections.EVENTS) {
|
||||||
const { event_code, ...rest } = serverItem as any;
|
// Ensure files array is properly handled
|
||||||
serverItem = rest as T;
|
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
|
// Check if there are pending offline changes for this item
|
||||||
|
@ -248,7 +267,16 @@ export class DataSyncService {
|
||||||
if (change.operation === "update" && change.data) {
|
if (change.operation === "update" && change.data) {
|
||||||
// Apply each field change individually
|
// Apply each field change individually
|
||||||
Object.entries(change.data).forEach(([key, value]) => {
|
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 {
|
try {
|
||||||
const pbItem = await this.get.getOne<T>(collection, id);
|
const pbItem = await this.get.getOne<T>(collection, id);
|
||||||
if (pbItem) {
|
if (pbItem) {
|
||||||
// SECURITY FIX: Remove event_code from events before storing in IndexedDB
|
// For events, ensure we handle the files field properly
|
||||||
if (collection === Collections.EVENTS && 'event_code' in pbItem) {
|
if (collection === Collections.EVENTS) {
|
||||||
const { event_code, ...rest } = pbItem as any;
|
// Ensure files array is properly handled
|
||||||
await table.put(rest as T);
|
if (!('files' in pbItem) || !Array.isArray((pbItem as any).files)) {
|
||||||
item = rest as T;
|
(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 {
|
} else {
|
||||||
await table.put(pbItem);
|
await table.put(pbItem);
|
||||||
item = pbItem;
|
item = pbItem;
|
||||||
|
@ -547,6 +586,25 @@ export class DataSyncService {
|
||||||
return undefined;
|
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
|
// Update the item in IndexedDB
|
||||||
const updatedItem = {
|
const updatedItem = {
|
||||||
...currentItem,
|
...currentItem,
|
||||||
|
|
|
@ -71,6 +71,11 @@ export class DashboardDatabase extends Dexie {
|
||||||
events: "id, event_name, event_code, start_date, end_date, published",
|
events: "id, event_name, event_code, start_date, end_date, published",
|
||||||
eventAttendees: "id, user, event, time_checked_in",
|
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
|
// Initialize the database with default values
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { Authentication } from "./Authentication";
|
||||||
export class FileManager {
|
export class FileManager {
|
||||||
private auth: Authentication;
|
private auth: Authentication;
|
||||||
private static instance: FileManager;
|
private static instance: FileManager;
|
||||||
|
private static UNSUPPORTED_EXTENSIONS = ['afdesign', 'psd', 'ai', 'sketch'];
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
this.auth = Authentication.getInstance();
|
this.auth = Authentication.getInstance();
|
||||||
|
@ -18,6 +19,24 @@ export class FileManager {
|
||||||
return FileManager.instance;
|
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
|
* Upload a single file to a record
|
||||||
* @param collectionName The name of the collection
|
* @param collectionName The name of the collection
|
||||||
|
@ -39,16 +58,142 @@ export class FileManager {
|
||||||
try {
|
try {
|
||||||
this.auth.setUpdating(true);
|
this.auth.setUpdating(true);
|
||||||
const pb = this.auth.getPocketBase();
|
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
|
// Check for potentially problematic file types
|
||||||
.collection(collectionName)
|
const fileExtension = file.name.split('.').pop()?.toLowerCase();
|
||||||
.update<T>(recordId, formData);
|
|
||||||
return result;
|
// 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<T>(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) {
|
} catch (err) {
|
||||||
console.error(`Failed to upload file to ${collectionName}:`, err);
|
console.error(`Failed to upload file to ${collectionName}:`, {
|
||||||
throw err;
|
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 {
|
} finally {
|
||||||
this.auth.setUpdating(false);
|
this.auth.setUpdating(false);
|
||||||
}
|
}
|
||||||
|
@ -79,13 +224,20 @@ export class FileManager {
|
||||||
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB per file limit
|
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB per file limit
|
||||||
const MAX_BATCH_SIZE = 25 * 1024 * 1024; // 25MB per batch
|
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) {
|
for (const file of files) {
|
||||||
|
// Validate file size
|
||||||
if (file.size > MAX_FILE_SIZE) {
|
if (file.size > MAX_FILE_SIZE) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`File ${file.name} is too large. Maximum size is 50MB.`,
|
`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
|
// Get existing record if updating
|
||||||
|
@ -174,9 +326,39 @@ export class FileManager {
|
||||||
const pb = this.auth.getPocketBase();
|
const pb = this.auth.getPocketBase();
|
||||||
const formData = new FormData();
|
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) {
|
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 {
|
try {
|
||||||
|
@ -216,31 +398,37 @@ export class FileManager {
|
||||||
// First, get the current record to check existing files
|
// First, get the current record to check existing files
|
||||||
const record = await pb.collection(collectionName).getOne<T>(recordId);
|
const record = await pb.collection(collectionName).getOne<T>(recordId);
|
||||||
|
|
||||||
// Create FormData with existing files
|
|
||||||
const formData = new FormData();
|
|
||||||
|
|
||||||
// Get existing files from the record
|
// Get existing files from the record
|
||||||
const existingFiles = (record as any)[field] || [];
|
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
|
// Create FormData for the new files only
|
||||||
for (const existingFile of existingFiles) {
|
const formData = new FormData();
|
||||||
try {
|
|
||||||
const response = await fetch(
|
// Tell PocketBase to keep existing files
|
||||||
this.getFileUrl(collectionName, recordId, existingFile),
|
formData.append(`${field}@`, '');
|
||||||
);
|
|
||||||
const blob = await response.blob();
|
// Append new files, renaming if needed to avoid duplicates
|
||||||
const file = new File([blob], existingFile, { type: blob.type });
|
for (const file of files) {
|
||||||
formData.append(field, file);
|
let fileToUpload = file;
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Failed to fetch existing file ${existingFile}:`, error);
|
// 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
|
const result = await pb
|
||||||
.collection(collectionName)
|
.collection(collectionName)
|
||||||
.update<T>(recordId, formData);
|
.update<T>(recordId, formData);
|
||||||
|
|
Loading…
Reference in a new issue