diff --git a/src/components/dashboard/Officer_EventManagement.astro b/src/components/dashboard/Officer_EventManagement.astro index 6c6a1a3..99b2445 100644 --- a/src/components/dashboard/Officer_EventManagement.astro +++ b/src/components/dashboard/Officer_EventManagement.astro @@ -2,882 +2,14 @@ import { Icon } from "astro-icon/components"; import { Get } from "../pocketbase/Get"; import { Authentication } from "../pocketbase/Authentication"; -import { Update } from "../pocketbase/Update"; -import { FileManager } from "../pocketbase/FileManager"; -import { SendLog } from "../pocketbase/SendLog"; +import EventEditor from "./Officer_EventManagement/EventEditor"; import FilePreview from "../modals/FilePreview"; // Get instances const get = Get.getInstance(); const auth = Authentication.getInstance(); -const update = Update.getInstance(); -const fileManager = FileManager.getInstance(); -const sendLog = SendLog.getInstance(); -// Interface for Event type interface Event { - id: string; - event_name: string; - event_description: string; - event_code: string; - location: string; - files: string[]; - points_to_reward: number; - start_date: string; - end_date: string; - published: boolean; - has_food: boolean; - attendees: AttendeeEntry[]; -} - -interface AttendeeEntry { - user_id: string; - time_checked_in: string; - food: string; -} - -interface ListResponse { - page: number; - perPage: number; - totalItems: number; - totalPages: number; - items: T[]; -} - -// Initialize variables -let eventResponse: ListResponse = { - page: 1, - perPage: 5, - totalItems: 0, - totalPages: 0, - items: [], -}; -let upcomingEvents: Event[] = []; - -// Fetch events -try { - if (auth.isAuthenticated()) { - eventResponse = await get.getList("events", 1, 5, "", "-start_date"); - upcomingEvents = eventResponse.items; - } -} catch (error) { - console.error("Failed to fetch events:", error); -} - -const totalEvents = eventResponse.totalItems; -const totalPages = eventResponse.totalPages; -const currentPage = eventResponse.page; - -// Add type declaration for window -declare global { - interface Window { - [key: string]: any; - openEditModal: (event?: any) => void; - deleteFile: (eventId: string, filename: string) => void; - undoDeleteFile: (eventId: string, filename: string) => void; - previewFile: (url: string, filename: string) => void; - openDetailsModal: (event: Event) => void; - showFilePreview: (file: { - url: string; - type: string; - name: string; - }) => void; - backToFileList: () => void; - handlePreviewError: () => void; - showLoading: () => void; - hideLoading: () => void; - deleteEvent: (eventId: string, eventName: string) => Promise; - resetAndCloseModal: () => void; - previewFileInEditModal: (url: string, filename: string) => void; - } -} ---- - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/components/dashboard/Officer_EventManagement/EventEditor.tsx b/src/components/dashboard/Officer_EventManagement/EventEditor.tsx new file mode 100644 index 0000000..7834373 --- /dev/null +++ b/src/components/dashboard/Officer_EventManagement/EventEditor.tsx @@ -0,0 +1,565 @@ +import { useState, useEffect } from "react"; + +// Extend Window interface +declare global { + interface Window { + showLoading?: () => void; + hideLoading?: () => void; + } +} +import FilePreview from "../../modals/FilePreview"; +import { Get } from "../../pocketbase/Get"; +import { Authentication } from "../../pocketbase/Authentication"; +import { Update } from "../../pocketbase/Update"; +import { FileManager } from "../../pocketbase/FileManager"; +import { SendLog } from "../../pocketbase/SendLog"; + +interface Event { + id: string; + event_name: string; + event_description: string; + event_code: string; + location: string; + files: string[]; + points_to_reward: number; + start_date: string; + end_date: string; + published: boolean; + has_food: boolean; + attendees: AttendeeEntry[]; +} + +interface AttendeeEntry { + user_id: string; + time_checked_in: string; + food: string; +} + +interface EventEditorProps { + onEventSaved?: () => void; +} + +export default function EventEditor({ onEventSaved }: EventEditorProps) { + // State for form data and UI + const [event, setEvent] = useState(null); + const [previewUrl, setPreviewUrl] = useState(""); + const [previewFilename, setPreviewFilename] = useState(""); + const [showPreview, setShowPreview] = useState(false); + const [selectedFiles, setSelectedFiles] = useState>(new Map()); + const [filesToDelete, setFilesToDelete] = useState>(new Set()); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Initialize services + const get = Get.getInstance(); + const auth = Authentication.getInstance(); + const update = Update.getInstance(); + const fileManager = FileManager.getInstance(); + const sendLog = SendLog.getInstance(); + + // Method to initialize form with event data + const initializeEventData = async (eventId: string) => { + try { + const eventData = await get.getOne("events", eventId); + setEvent(eventData); + setSelectedFiles(new Map()); + setFilesToDelete(new Set()); + setShowPreview(false); + } catch (error) { + console.error("Failed to fetch event data:", error); + alert("Failed to load event data. Please try again."); + } + }; + + // Expose initializeEventData to window + useEffect(() => { + (window as any).openEditModal = async (event?: Event) => { + const modal = document.getElementById("editEventModal") as HTMLDialogElement; + if (!modal) return; + + if (event?.id) { + await initializeEventData(event.id); + } else { + setEvent(null); + setSelectedFiles(new Map()); + setFilesToDelete(new Set()); + setShowPreview(false); + } + + modal.showModal(); + }; + + return () => { + delete (window as any).openEditModal; + }; + }, []); + + // Handle file selection + const handleFileSelect = (e: React.ChangeEvent) => { + if (e.target.files) { + const newFiles = Array.from(e.target.files); + const updatedFiles = new Map(selectedFiles); + + newFiles.forEach(file => { + updatedFiles.set(file.name, file); + }); + + setSelectedFiles(updatedFiles); + } + }; + + // Handle file deletion + const handleFileDelete = (filename: string) => { + if (confirm("Are you sure you want to remove this file?")) { + const updatedFilesToDelete = new Set(filesToDelete); + updatedFilesToDelete.add(filename); + setFilesToDelete(updatedFilesToDelete); + } + }; + + // Handle file deletion undo + const handleUndoFileDelete = (filename: string) => { + const updatedFilesToDelete = new Set(filesToDelete); + updatedFilesToDelete.delete(filename); + setFilesToDelete(updatedFilesToDelete); + }; + + // Handle file preview + const handlePreviewFile = (url: string, filename: string) => { + setPreviewUrl(url); + setPreviewFilename(filename); + setShowPreview(true); + }; + // Handle form submission + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const form = e.target as HTMLFormElement; + const formData = new FormData(form); + + setIsSubmitting(true); + try { + window.showLoading?.(); + const eventData = { + event_name: formData.get("editEventName"), + event_code: formData.get("editEventCode"), + event_description: formData.get("editEventDescription"), + location: formData.get("editEventLocation"), + points_to_reward: Number(formData.get("editEventPoints")), + start_date: new Date(formData.get("editEventStartDate") as string).toISOString(), + end_date: new Date(formData.get("editEventEndDate") as string).toISOString(), + published: formData.get("editEventPublished") === "on", + has_food: formData.get("editEventHasFood") === "on", + }; + + const pb = auth.getPocketBase(); + + if (event?.id) { + // Update existing event + const currentEvent = await pb.collection("events").getOne(event.id); + const currentFiles = currentEvent.files || []; + + // Filter out files marked for deletion + const remainingFiles = currentFiles.filter( + (filename: string) => !filesToDelete.has(filename) + ); + + // Create FormData for update + const updateFormData = new FormData(); + + // Add event data + Object.entries(eventData).forEach(([key, value]) => { + updateFormData.append(key, String(value)); + }); + + // Add remaining and new files + const filePromises = remainingFiles.map(async (filename: string) => { + try { + const response = await fetch( + fileManager.getFileUrl("events", event.id, filename) + ); + const blob = await response.blob(); + return new File([blob], filename, { type: blob.type }); + } catch (error) { + console.error(`Failed to fetch file ${filename}:`, error); + return null; + } + }); + + const existingFiles = (await Promise.all(filePromises)).filter( + (file): file is File => file !== null + ); + + [...existingFiles, ...Array.from(selectedFiles.values())].forEach( + (file: File) => { + updateFormData.append("files", file); + } + ); + + await pb.collection("events").update(event.id, updateFormData); + await sendLog.send( + "update", + "event", + `Updated event: ${eventData.event_name}` + ); + + // Log file deletions + for (const filename of filesToDelete) { + await sendLog.send( + "delete", + "event_file", + `Deleted file ${filename} from event ${eventData.event_name}` + ); + } + } else { + // Create new event + const createFormData = new FormData(); + + // Add event data + Object.entries(eventData).forEach(([key, value]) => { + createFormData.append(key, String(value)); + }); + + // Initialize empty attendees array + createFormData.append("attendees", JSON.stringify([])); + + // Add new files + Array.from(selectedFiles.values()).forEach((file: File) => { + createFormData.append("files", file); + }); + + await pb.collection("events").create(createFormData); + await sendLog.send( + "create", + "event", + `Created event: ${eventData.event_name}` + ); + } + + // Close modal and reset state + const modal = document.getElementById("editEventModal") as HTMLDialogElement; + if (modal) modal.close(); + + setEvent(null); + setSelectedFiles(new Map()); + setFilesToDelete(new Set()); + setShowPreview(false); + form.reset(); + + // Notify parent component + onEventSaved?.(); + + } catch (error) { + console.error("Failed to save event:", error); + alert("Failed to save event. Please try again."); + } finally { + setIsSubmitting(false); + window.hideLoading?.(); + } + }; + + // Add preview section + const previewSection = ( +
+
+ +

+ {previewFilename} +

+
+
+
+ +
+
+ +
+
+
+ ); + + return ( + + {/* Preview Section */} + {previewSection} +
+ {/* Main Edit Form Section */} +
+

Edit Event

+
+ +
+ {/* Event Name */} +
+ + setEvent(prev => prev ? { ...prev, event_name: e.target.value } : null)} + required + /> +
+ + {/* Event Code */} +
+ + setEvent(prev => prev ? { ...prev, event_code: e.target.value } : null)} + required + /> +
+ + {/* Location */} +
+ + setEvent(prev => prev ? { ...prev, location: e.target.value } : null)} + required + /> +
+ + {/* Points to Reward */} +
+ + setEvent(prev => prev ? { ...prev, points_to_reward: Number(e.target.value) } : null)} + min="0" + required + /> +
+ + {/* Start Date */} +
+ + setEvent(prev => prev ? { ...prev, start_date: e.target.value } : null)} + required + /> +
+ + {/* End Date */} +
+ + setEvent(prev => prev ? { ...prev, end_date: e.target.value } : null)} + required + /> +
+
+ + {/* Description */} +
+ + +
+ + {/* Files */} +
+ + +
+ {/* New Files */} + {Array.from(selectedFiles.entries()).map(([name, file]) => ( +
+ {name} +
+
New
+ +
+
+ ))} + + {/* Current Files */} + {event?.files && event.files.length > 0 && ( + <> +
Current Files
+ {event.files.map((filename) => ( +
+ {filename} +
+ +
+ {filesToDelete.has(filename) ? ( + + ) : ( + + )} +
+
+
+ ))} + + )} +
+
+ + {/* Published */} +
+ + +
+ + {/* Has Food */} +
+ + +
+
+
+ +
+ + +
+
+
+ +
+
+ ); +}