import PocketBase from "pocketbase"; import type { RecordModel } from "pocketbase"; import yaml from "js-yaml"; import configYaml from "../../data/storeConfig.yaml?raw"; import JSZip from 'jszip'; // Configuration type definitions interface Config { api: { baseUrl: string; }; ui: { messages: { event: { saving: string; success: string; error: string; deleting: string; deleteSuccess: string; deleteError: string; messageTimeout: number; }; }; tables: { events: { title: string; columns: { event_name: string; event_id: string; event_code: string; start_date: string; end_date: string; points_to_reward: string; location: string; registered_users: string; actions: string; }; form: { buttons: { edit: string; delete: string; }; }; }; }; defaults: { pageSize: number; sortField: string; }; }; } // Parse YAML configuration with type const config = yaml.load(configYaml) as Config; const { columns, form } = config.ui.tables.events; interface Event { id: string; event_id: string; event_name: string; event_code: string; attendees: string; // JSON string of attendee IDs points_to_reward: number; start_date: string; end_date: string; location: string; files: string[]; // Array of file URLs collectionId: string; collectionName: string; } interface AuthElements { eventList: HTMLTableSectionElement; eventSearch: HTMLInputElement; searchEvents: HTMLButtonElement; addEvent: HTMLButtonElement; eventEditor: HTMLDialogElement; editorEventId: HTMLInputElement; editorEventName: HTMLInputElement; editorEventCode: HTMLInputElement; editorStartDate: HTMLInputElement; editorStartTime: HTMLInputElement; editorEndDate: HTMLInputElement; editorEndTime: HTMLInputElement; editorPointsToReward: HTMLInputElement; editorLocation: HTMLInputElement; editorFiles: HTMLInputElement; currentFiles: HTMLDivElement; saveEventButton: HTMLButtonElement; } interface ValidationErrors { eventId?: string; eventName?: string; eventCode?: string; startDate?: string; startTime?: string; endDate?: string; endTime?: string; points?: string; } export class EventAuth { private pb: PocketBase; private elements: AuthElements; private cachedEvents: Event[] = []; private abortController: AbortController | null = null; private currentUploadXHR: XMLHttpRequest | null = null; constructor() { this.pb = new PocketBase(config.api.baseUrl); this.elements = this.getElements(); this.init(); } private getElements(): AuthElements { const eventList = document.getElementById("eventList") as HTMLTableSectionElement; const eventSearch = document.getElementById("eventSearch") as HTMLInputElement; const searchEvents = document.getElementById("searchEvents") as HTMLButtonElement; const addEvent = document.getElementById("addEvent") as HTMLButtonElement; const eventEditor = document.getElementById("eventEditor") as HTMLDialogElement; const editorEventId = document.getElementById("editorEventId") as HTMLInputElement; const editorEventName = document.getElementById("editorEventName") as HTMLInputElement; const editorEventCode = document.getElementById("editorEventCode") as HTMLInputElement; const editorStartDate = document.getElementById("editorStartDate") as HTMLInputElement; const editorStartTime = document.getElementById("editorStartTime") as HTMLInputElement; const editorEndDate = document.getElementById("editorEndDate") as HTMLInputElement; const editorEndTime = document.getElementById("editorEndTime") as HTMLInputElement; const editorPointsToReward = document.getElementById("editorPointsToReward") as HTMLInputElement; const editorLocation = document.getElementById("editorLocation") as HTMLInputElement; const editorFiles = document.getElementById("editorFiles") as HTMLInputElement; const currentFiles = document.getElementById("currentFiles") as HTMLDivElement; const saveEventButton = document.getElementById("saveEventButton") as HTMLButtonElement; if ( !eventList || !eventSearch || !searchEvents || !addEvent || !eventEditor || !editorEventId || !editorEventName || !editorEventCode || !editorStartDate || !editorStartTime || !editorEndDate || !editorEndTime || !editorPointsToReward || !editorLocation || !editorFiles || !currentFiles || !saveEventButton ) { throw new Error("Required DOM elements not found"); } return { eventList, eventSearch, searchEvents, addEvent, eventEditor, editorEventId, editorEventName, editorEventCode, editorStartDate, editorStartTime, editorEndDate, editorEndTime, editorPointsToReward, editorLocation, editorFiles, currentFiles, saveEventButton, }; } private getRegisteredUsersCount(registeredUsers: string): number { // Handle different cases for registered_users field if (!registeredUsers) return 0; try { // Try to parse if it's a JSON string const users = JSON.parse(registeredUsers); // Ensure users is an array if (!Array.isArray(users)) { return 0; } return users.length; } catch (err) { console.warn("Failed to parse registered_users, using 0"); return 0; } } private parseArrayField(field: any, defaultValue: any[] = []): any[] { if (!field) return defaultValue; if (Array.isArray(field)) return field; try { const parsed = JSON.parse(field); return Array.isArray(parsed) ? parsed : defaultValue; } catch (err) { console.warn("Failed to parse array field:", err); return defaultValue; } } private async fetchEvents(searchQuery: string = "") { try { // Only fetch from API if we don't have cached data if (this.cachedEvents.length === 0) { const records = await this.pb.collection("events").getList(1, config.ui.defaults.pageSize, { sort: config.ui.defaults.sortField, }); this.cachedEvents = records.items as Event[]; } // Filter cached data based on search query let filteredEvents = this.cachedEvents; if (searchQuery) { const terms = searchQuery.toLowerCase().split(" ").filter(term => term.length > 0); if (terms.length > 0) { filteredEvents = this.cachedEvents.filter(event => { return terms.every(term => (event.event_name?.toLowerCase().includes(term) || event.event_id?.toLowerCase().includes(term) || event.event_code?.toLowerCase().includes(term) || event.location?.toLowerCase().includes(term)) ); }); } } const { eventList } = this.elements; const fragment = document.createDocumentFragment(); if (filteredEvents.length === 0) { const row = document.createElement("tr"); row.innerHTML = ` ${searchQuery ? "No events found matching your search." : "No events found."} `; fragment.appendChild(row); } else { filteredEvents.forEach((event) => { const row = document.createElement("tr"); const startDate = event.start_date ? new Date(event.start_date).toLocaleString() : "N/A"; const endDate = event.end_date ? new Date(event.end_date).toLocaleString() : "N/A"; // Parse attendees using the new helper method const attendees = this.parseArrayField(event.attendees); const attendeeCount = attendees.length; // Handle files display const filesHtml = event.files && Array.isArray(event.files) && event.files.length > 0 ? `` : 'No files'; // Format dates for display const formatDateTime = (dateStr: string) => { if (!dateStr) return { dateDisplay: 'N/A', timeDisplay: 'N/A' }; const date = new Date(dateStr); return { dateDisplay: date.toLocaleDateString(), timeDisplay: date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }; }; const startDateTime = formatDateTime(event.start_date); const endDateTime = formatDateTime(event.end_date); row.innerHTML = `
${event.event_name || "N/A"}
${columns.event_id}: ${event.event_id || "N/A"}
${columns.event_code}: ${event.event_code || "N/A"}
${columns.start_date}: ${startDateTime.dateDisplay}
${startDateTime.timeDisplay}
${columns.end_date}: ${endDateTime.dateDisplay}
${endDateTime.timeDisplay}
${columns.points_to_reward}: ${event.points_to_reward || 0}
${columns.location}: ${event.location || "N/A"}
Files: ${filesHtml}
Attendees: ${attendeeCount}
${event.event_id || "N/A"} ${event.event_code || "N/A"}
${startDateTime.dateDisplay} ${startDateTime.timeDisplay}
${endDateTime.dateDisplay} ${endDateTime.timeDisplay}
${event.points_to_reward || 0} ${event.location || "N/A"} ${filesHtml}
`; fragment.appendChild(row); }); } eventList.innerHTML = ""; eventList.appendChild(fragment); // Setup event listeners for buttons const editButtons = eventList.querySelectorAll(".edit-event"); editButtons.forEach((button) => { button.addEventListener("click", () => { const eventId = (button as HTMLButtonElement).dataset.eventId; if (eventId) { this.handleEventEdit(eventId); } }); }); const deleteButtons = eventList.querySelectorAll(".delete-event"); deleteButtons.forEach((button) => { button.addEventListener("click", () => { const eventId = (button as HTMLButtonElement).dataset.eventId; if (eventId) { this.handleEventDelete(eventId); } }); }); const viewAttendeesButtons = eventList.querySelectorAll(".view-attendees"); viewAttendeesButtons.forEach((button) => { button.addEventListener("click", () => { const eventId = (button as HTMLButtonElement).dataset.eventId; if (eventId) { this.handleViewAttendees(eventId); } }); }); const viewFilesButtons = eventList.querySelectorAll(".view-files"); viewFilesButtons.forEach((button) => { button.addEventListener("click", async () => { const eventId = (button as HTMLButtonElement).dataset.eventId; if (eventId) { await this.handleViewFiles(eventId); } }); }); } catch (err) { console.error("Failed to fetch events:", err); const { eventList } = this.elements; eventList.innerHTML = ` Failed to fetch events. Please try again. `; } } private splitDateTime(dateTimeStr: string): { date: string; time: string } { if (!dateTimeStr) return { date: "", time: "" }; // Create a date object in local timezone const dateTime = new Date(dateTimeStr); // Format date as YYYY-MM-DD const date = dateTime.toLocaleDateString('en-CA'); // en-CA gives YYYY-MM-DD format // Format time as HH:mm const time = dateTime.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit' }); return { date, time }; } private combineDateTime(date: string, time: string): string { if (!date || !time) return ""; // Create a new Date object from the date and time strings const [year, month, day] = date.split('-').map(Number); const [hours, minutes] = time.split(':').map(Number); // Create date in local timezone const dateTime = new Date(year, month - 1, day, hours, minutes); // Format the date to ISO string with timezone offset return dateTime.toISOString(); } private getFileNameFromUrl(url: string): string { try { const urlObj = new URL(url); const pathParts = urlObj.pathname.split("/"); return decodeURIComponent(pathParts[pathParts.length - 1]); } catch (e) { // If URL parsing fails, try to get the filename from the path directly return url.split("/").pop() || "Unknown File"; } } private async handleEventEdit(eventId: string) { try { const event = await this.pb.collection("events").getOne(eventId); const { eventEditor, editorEventId, editorEventName, editorEventCode, editorStartDate, editorStartTime, editorEndDate, editorEndTime, editorPointsToReward, editorLocation, currentFiles, saveEventButton, } = this.elements; // Split start and end dates into separate date and time const startDateTime = this.splitDateTime(event.start_date); const endDateTime = this.splitDateTime(event.end_date); // Populate the form editorEventId.value = event.event_id || ""; editorEventName.value = event.event_name || ""; editorEventCode.value = event.event_code || ""; editorStartDate.value = startDateTime.date; editorStartTime.value = startDateTime.time; editorEndDate.value = endDateTime.date; editorEndTime.value = endDateTime.time; editorPointsToReward.value = event.points_to_reward?.toString() || "0"; editorLocation.value = event.location || ""; // Display current files this.updateFilesDisplay(event); // Update file input to support multiple files const fileInput = this.elements.editorFiles; fileInput.setAttribute('multiple', 'true'); fileInput.setAttribute('accept', '*/*'); // Store the event ID for saving saveEventButton.dataset.eventId = eventId; // Disable event_id field for existing events editorEventId.disabled = true; // Show the dialog eventEditor.showModal(); } catch (err) { console.error("Failed to load event for editing:", err); } } private validateForm(): ValidationErrors | null { const { editorEventId, editorEventName, editorEventCode, editorStartDate, editorStartTime, editorEndDate, editorEndTime, editorPointsToReward, } = this.elements; const errors: ValidationErrors = {}; // Reset all error messages const errorElements = document.querySelectorAll('.label-text-alt.text-error'); errorElements.forEach(el => el.classList.add('hidden')); // Event ID validation if (!editorEventId.disabled) { // Only validate if it's a new event if (!editorEventId.value) { errors.eventId = "Event ID is required"; } else if (!editorEventId.value.match(/^[A-Za-z0-9_-]+$/)) { errors.eventId = "Event ID can only contain letters, numbers, underscores, and hyphens"; } else if (editorEventId.value.length < 3) { errors.eventId = "Event ID must be at least 3 characters"; } else if (editorEventId.value.length > 50) { errors.eventId = "Event ID must be less than 50 characters"; } } // Event Name validation if (!editorEventName.value) { errors.eventName = "Event Name is required"; } else if (editorEventName.value.length < 3) { errors.eventName = "Event Name must be at least 3 characters"; } else if (editorEventName.value.length > 100) { errors.eventName = "Event Name must be less than 100 characters"; } // Event Code validation if (!editorEventCode.value) { errors.eventCode = "Event Code is required"; } else if (!editorEventCode.value.match(/^[A-Za-z0-9_-]+$/)) { errors.eventCode = "Event Code can only contain letters, numbers, underscores, and hyphens"; } else if (editorEventCode.value.length < 3) { errors.eventCode = "Event Code must be at least 3 characters"; } else if (editorEventCode.value.length > 20) { errors.eventCode = "Event Code must be less than 20 characters"; } // Date and Time validation if (!editorStartDate.value) { errors.startDate = "Start Date is required"; } if (!editorStartTime.value) { errors.startTime = "Start Time is required"; } if (!editorEndDate.value) { errors.endDate = "End Date is required"; } if (!editorEndTime.value) { errors.endTime = "End Time is required"; } // Validate that end date/time is after start date/time if (editorStartDate.value && editorStartTime.value && editorEndDate.value && editorEndTime.value) { const startDateTime = new Date(`${editorStartDate.value}T${editorStartTime.value}`); const endDateTime = new Date(`${editorEndDate.value}T${editorEndTime.value}`); if (endDateTime <= startDateTime) { errors.endDate = "End date/time must be after start date/time"; } } // Points validation const points = parseInt(editorPointsToReward.value); if (!editorPointsToReward.value) { errors.points = "Points are required"; } else if (isNaN(points) || points < 0) { errors.points = "Points must be a positive number"; } else if (points > 1000) { errors.points = "Points must be less than 1000"; } // Show error messages if (errors.eventId) { const errorEl = document.getElementById('eventIdError'); if (errorEl) { errorEl.textContent = errors.eventId; errorEl.classList.remove('hidden'); } } if (errors.eventName) { const errorEl = document.getElementById('eventNameError'); if (errorEl) { errorEl.textContent = errors.eventName; errorEl.classList.remove('hidden'); } } if (errors.eventCode) { const errorEl = document.getElementById('eventCodeError'); if (errorEl) { errorEl.textContent = errors.eventCode; errorEl.classList.remove('hidden'); } } if (errors.startDate) { const errorEl = document.getElementById('startDateError'); if (errorEl) { errorEl.textContent = errors.startDate; errorEl.classList.remove('hidden'); } } if (errors.startTime) { const errorEl = document.getElementById('startTimeError'); if (errorEl) { errorEl.textContent = errors.startTime; errorEl.classList.remove('hidden'); } } if (errors.endDate) { const errorEl = document.getElementById('endDateError'); if (errorEl) { errorEl.textContent = errors.endDate; errorEl.classList.remove('hidden'); } } if (errors.endTime) { const errorEl = document.getElementById('endTimeError'); if (errorEl) { errorEl.textContent = errors.endTime; errorEl.classList.remove('hidden'); } } if (errors.points) { const errorEl = document.getElementById('pointsError'); if (errorEl) { errorEl.textContent = errors.points; errorEl.classList.remove('hidden'); } } return Object.keys(errors).length > 0 ? errors : null; } private async handleEventSave() { const { eventEditor, editorEventId, editorEventName, editorEventCode, editorStartDate, editorStartTime, editorEndDate, editorEndTime, editorPointsToReward, editorLocation, editorFiles, saveEventButton, } = this.elements; const eventId = saveEventButton.dataset.eventId; const isNewEvent = !eventId; // Validate form before proceeding const errors = this.validateForm(); if (errors) { return; // Stop if there are validation errors } try { // Combine date and time inputs const startDateTime = this.combineDateTime(editorStartDate.value, editorStartTime.value); const endDateTime = this.combineDateTime(editorEndDate.value, editorEndTime.value); // Create FormData for file upload const formData = new FormData(); formData.append("event_name", editorEventName.value); formData.append("event_code", editorEventCode.value); formData.append("start_date", startDateTime); formData.append("end_date", endDateTime); formData.append("points_to_reward", editorPointsToReward.value); formData.append("location", editorLocation.value); // For new events, set event_id and initialize attendees as empty array if (isNewEvent) { formData.append("event_id", editorEventId.value); formData.append("attendees", "[]"); // Initialize with empty array } else { // For existing events, preserve current attendees and files const currentEvent = await this.pb.collection("events").getOne(eventId); const currentAttendees = this.parseArrayField(currentEvent.attendees, []); formData.append("attendees", JSON.stringify(currentAttendees)); // Preserve existing files if no new files are being uploaded if (currentEvent.files && (!editorFiles.files || editorFiles.files.length === 0)) { formData.append("files", JSON.stringify(currentEvent.files)); } } // Handle file uploads if (editorFiles.files && editorFiles.files.length > 0) { Array.from(editorFiles.files).forEach(file => { formData.append("files", file); }); } if (isNewEvent) { await this.pb.collection("events").create(formData); } else { await this.pb.collection("events").update(eventId, formData); } // Close the dialog and refresh the table eventEditor.close(); this.cachedEvents = []; // Clear cache to force refresh this.fetchEvents(); } catch (err) { console.error("Failed to save event:", err); } } private async handleEventDelete(eventId: string) { if (confirm("Are you sure you want to delete this event?")) { try { await this.pb.collection("events").delete(eventId); this.cachedEvents = []; // Clear cache to force refresh this.fetchEvents(); } catch (err) { console.error("Failed to delete event:", err); } } } private async handleViewAttendees(eventId: string) { try { const event = await this.pb.collection("events").getOne(eventId); const attendees = this.parseArrayField(event.attendees); // Fetch user details for each attendee const userDetails = await Promise.all( attendees.map(async (userId: string) => { try { const user = await this.pb.collection("users").getOne(userId); return { name: user.name || "N/A", email: user.email || "N/A", member_id: user.member_id || "N/A" }; } catch (err) { console.warn(`Failed to fetch user ${userId}:`, err); return { name: "Unknown User", email: "N/A", member_id: "N/A" }; } }) ); // Create and show modal const modal = document.createElement("dialog"); modal.className = "modal"; modal.innerHTML = ` `; document.body.appendChild(modal); modal.showModal(); // Add search functionality const searchInput = modal.querySelector("#attendeeSearch") as HTMLInputElement; const attendeeRows = modal.querySelectorAll(".attendee-row"); if (searchInput) { searchInput.addEventListener("input", () => { const searchTerm = searchInput.value.toLowerCase(); attendeeRows.forEach((row) => { const text = row.textContent?.toLowerCase() || ""; if (text.includes(searchTerm)) { (row as HTMLElement).style.display = ""; } else { (row as HTMLElement).style.display = "none"; } }); }); } // Remove modal when closed modal.addEventListener("close", () => { document.body.removeChild(modal); }); } catch (err) { console.error("Failed to view attendees:", err); } } private async syncAttendees() { try { // Cancel any existing sync operation if (this.abortController) { this.abortController.abort(); } // Create new abort controller for this sync operation this.abortController = new AbortController(); console.log("=== STARTING ATTENDEE SYNC ==="); // Fetch all events first with abort signal const events = await this.pb.collection("events").getFullList({ sort: config.ui.defaults.sortField, $cancelKey: "syncAttendees", }); // Early return if aborted if (this.abortController.signal.aborted) { console.log("Sync operation was cancelled"); return; } console.log("=== EVENTS DATA ==="); events.forEach(event => { console.log(`Event: ${event.event_name} (ID: ${event.id})`); console.log("- event_id:", event.event_id); console.log("- Raw attendees field:", event.attendees); }); // Create a map of event_id to event record for faster lookup const eventMap = new Map(); events.forEach(event => { const currentAttendees = this.parseArrayField(event.attendees); console.log(`Parsed attendees for event ${event.event_name}:`, currentAttendees); eventMap.set(event.event_id, { id: event.id, event_id: event.event_id, event_name: event.event_name, attendees: new Set(currentAttendees) }); console.log(`Mapped event ${event.event_name} with event_id ${event.event_id}`); }); // Check if aborted before fetching users if (this.abortController.signal.aborted) { console.log("Sync operation was cancelled"); return; } // Fetch all users with abort signal const users = await this.pb.collection("users").getFullList({ fields: "id,name,email,member_id,events_attended", $cancelKey: "syncAttendees", }); console.log("=== USERS DATA ==="); users.forEach(user => { console.log(`User: ${user.name || 'Unknown'} (ID: ${user.id})`); console.log("- Raw events_attended:", user.events_attended); }); // Process each user's events_attended for (const user of users) { // Check if aborted before processing each user if (this.abortController.signal.aborted) { console.log("Sync operation was cancelled"); return; } console.log(`\nProcessing user: ${user.name || 'Unknown'} (ID: ${user.id})`); const eventsAttended = this.parseArrayField(user.events_attended); console.log("Parsed events_attended:", eventsAttended); // For each event the user attended for (const eventId of eventsAttended) { console.log(`\nChecking event ${eventId} for user ${user.id}`); // Find the event by event_id const eventRecord = eventMap.get(eventId); if (eventRecord) { console.log(`Found event record:`, eventRecord); // If user not already in attendees, add them if (!eventRecord.attendees.has(user.id)) { eventRecord.attendees.add(user.id); console.log(`Added user ${user.id} to event ${eventId}`); } else { console.log(`User ${user.id} already in event ${eventId}`); } } else { console.log(`Event ${eventId} not found in event map. Available event_ids:`, Array.from(eventMap.keys())); } } } // Update all events with new attendee lists console.log("\n=== UPDATING EVENTS ==="); for (const [eventId, record] of eventMap.entries()) { // Check if aborted before each update if (this.abortController.signal.aborted) { console.log("Sync operation was cancelled"); return; } try { const attendeeArray = Array.from(record.attendees); console.log(`Updating event ${eventId}:`); console.log("- Current attendees:", attendeeArray); await this.pb.collection("events").update(record.id, { attendees: JSON.stringify(attendeeArray) }, { $cancelKey: "syncAttendees", }); console.log(`Successfully updated event ${eventId}`); } catch (err: any) { if (err?.name === "AbortError") { console.log("Update was cancelled"); return; } console.error(`Failed to update attendees for event ${eventId}:`, err); console.error("Failed record:", record); } } // Clear the cache to force a refresh of the events list this.cachedEvents = []; console.log("\n=== SYNC COMPLETE ==="); await this.fetchEvents(); } catch (err: any) { if (err?.name === "AbortError") { console.log("Sync operation was cancelled"); return; } console.error("Failed to sync attendees:", err); console.error("Full error:", err); } finally { // Clear the abort controller when done this.abortController = null; } } private cleanup() { if (this.abortController) { this.abortController.abort(); this.abortController = null; } } private async refreshEventsAndSync() { try { // Clear the cache to force a fresh fetch this.cachedEvents = []; // Check if user is authorized to sync const user = this.pb.authStore.model; if (user && (user.member_type === "IEEE Officer" || user.member_type === "IEEE Administrator")) { await this.syncAttendees().catch(console.error); } else { // If not authorized to sync, just refresh the events await this.fetchEvents(); } } catch (err) { console.error("Failed to refresh events:", err); } } private init() { // Add file input change handler for automatic upload const fileInput = this.elements.editorFiles; // Add dialog close handler to cancel uploads this.elements.eventEditor.addEventListener('close', () => { if (this.currentUploadXHR) { this.currentUploadXHR.abort(); this.currentUploadXHR = null; // Hide progress bar const progressContainer = document.getElementById('uploadProgress'); if (progressContainer) { progressContainer.classList.add('hidden'); } // Clear file input fileInput.value = ''; } }); fileInput.addEventListener('change', async () => { const selectedFiles = fileInput.files; if (selectedFiles && selectedFiles.length > 0) { try { // Validate file sizes const MAX_FILE_SIZE = 999999999; // ~1GB max per file (server limit) const MAX_TOTAL_SIZE = 999999999; // ~1GB max total (server limit) let totalSize = 0; for (const file of selectedFiles) { if (file.size > MAX_FILE_SIZE) { throw new Error(`File "${file.name}" is too large. Maximum size per file is ${(MAX_FILE_SIZE / 1024 / 1024).toFixed(0)}MB.`); } totalSize += file.size; } if (totalSize > MAX_TOTAL_SIZE) { throw new Error(`Total file size (${(totalSize / 1024 / 1024).toFixed(1)}MB) exceeds the limit of ${(MAX_TOTAL_SIZE / 1024 / 1024).toFixed(0)}MB.`); } // Get the current event ID const eventId = this.elements.saveEventButton.dataset.eventId; if (!eventId) { throw new Error('No event ID found'); } // Get current event to preserve existing files const currentEvent = await this.pb.collection("events").getOne(eventId); const formData = new FormData(); // Preserve existing files by adding them to formData if (currentEvent.files && Array.isArray(currentEvent.files)) { formData.append("files", JSON.stringify(currentEvent.files)); } // Add new files to the formData Array.from(selectedFiles).forEach(file => { formData.append("files", file); }); // Show progress bar container const progressContainer = document.getElementById('uploadProgress'); const progressBar = document.getElementById('uploadProgressBar') as HTMLProgressElement; const progressText = document.getElementById('uploadProgressText'); if (progressContainer) { progressContainer.classList.remove('hidden'); // Reset progress if (progressBar) progressBar.value = 0; if (progressText) progressText.textContent = '0%'; } // Create XMLHttpRequest for better progress tracking const xhr = new XMLHttpRequest(); this.currentUploadXHR = xhr; // Store the XHR request const url = `${this.pb.baseUrl}/api/collections/events/records/${eventId}`; xhr.upload.onprogress = (e) => { if (e.lengthComputable) { const progress = Math.round((e.loaded * 100) / e.total); if (progressBar) progressBar.value = progress; if (progressText) progressText.textContent = `${progress}%`; } }; xhr.onload = async () => { this.currentUploadXHR = null; // Clear the XHR reference if (xhr.status === 200) { // Update was successful const response = JSON.parse(xhr.responseText); const event = await this.pb.collection("events").getOne(eventId); this.updateFilesDisplay(event); // Clear the file input fileInput.value = ''; // Hide progress bar after a short delay setTimeout(() => { if (progressContainer) { progressContainer.classList.add('hidden'); if (progressBar) progressBar.value = 0; if (progressText) progressText.textContent = '0%'; } }, 1000); } else { let errorMessage = 'Failed to upload files. '; try { const errorResponse = JSON.parse(xhr.responseText); errorMessage += errorResponse.message || 'Please try again.'; } catch { if (xhr.status === 413) { errorMessage += 'Files are too large. Please reduce file sizes and try again.'; } else if (xhr.status === 401) { errorMessage += 'Your session has expired. Please log in again.'; } else { errorMessage += 'Please try again.'; } } console.error('Upload failed:', errorMessage); alert(errorMessage); if (progressContainer) progressContainer.classList.add('hidden'); fileInput.value = ''; } }; xhr.onerror = () => { this.currentUploadXHR = null; // Clear the XHR reference console.error('Upload failed'); let errorMessage = 'Failed to upload files. '; if (xhr.status === 413) { errorMessage += 'Files are too large. Please reduce file sizes and try again.'; } else if (xhr.status === 0) { errorMessage += 'Network error or CORS issue. Please try again later.'; } else { errorMessage += 'Please try again.'; } alert(errorMessage); if (progressContainer) progressContainer.classList.add('hidden'); fileInput.value = ''; }; xhr.onabort = () => { this.currentUploadXHR = null; // Clear the XHR reference console.log('Upload cancelled'); if (progressContainer) progressContainer.classList.add('hidden'); fileInput.value = ''; }; // Get the auth token const token = this.pb.authStore.token; // Send the request xhr.open('PATCH', url, true); xhr.setRequestHeader('Authorization', token); xhr.send(formData); } catch (err) { console.error('Failed to upload files:', err); alert(err instanceof Error ? err.message : 'Failed to upload files. Please try again.'); // Hide progress bar on error const progressContainer = document.getElementById('uploadProgress'); if (progressContainer) progressContainer.classList.add('hidden'); // Clear file input fileInput.value = ''; } } }); // Only sync attendees if user is an officer or administrator setTimeout(async () => { const user = this.pb.authStore.model; if (user && (user.member_type === "IEEE Officer" || user.member_type === "IEEE Administrator")) { await this.syncAttendees().catch(console.error); } }, 100); // Initial fetch this.fetchEvents(); // Search functionality const handleSearch = () => { const searchQuery = this.elements.eventSearch.value.trim(); this.fetchEvents(searchQuery); }; // Real-time search this.elements.eventSearch.addEventListener("input", handleSearch); // Search button click handler this.elements.searchEvents.addEventListener("click", handleSearch); // Refresh button click handler const refreshButton = document.getElementById("refreshEvents"); if (refreshButton) { refreshButton.addEventListener("click", () => { this.refreshEventsAndSync(); }); } // Add event button this.elements.addEvent.addEventListener("click", () => { const { eventEditor, editorEventId, saveEventButton } = this.elements; // Clear the form this.elements.editorEventId.value = ""; this.elements.editorEventName.value = ""; this.elements.editorEventCode.value = ""; this.elements.editorStartDate.value = ""; this.elements.editorStartTime.value = ""; this.elements.editorEndDate.value = ""; this.elements.editorEndTime.value = ""; this.elements.editorPointsToReward.value = "0"; // Enable event_id field for new events editorEventId.disabled = false; // Clear the event ID to indicate this is a new event saveEventButton.dataset.eventId = ""; // Show the dialog eventEditor.showModal(); }); // Event editor dialog const { eventEditor, saveEventButton } = this.elements; // Close dialog when clicking outside eventEditor.addEventListener("click", (e) => { if (e.target === eventEditor) { eventEditor.close(); } }); // Save event button saveEventButton.addEventListener("click", (e) => { e.preventDefault(); this.handleEventSave(); }); } // Add this new method to handle files display update private updateFilesDisplay(event: RecordModel) { const { currentFiles } = this.elements; currentFiles.innerHTML = ""; if (event.files && Array.isArray(event.files) && event.files.length > 0) { const filesList = document.createElement("div"); filesList.className = "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4"; event.files.forEach((file: string) => { const fileUrl = this.pb.files.getURL(event, file); const fileName = this.getFileNameFromUrl(file); const fileExt = fileName.split('.').pop()?.toLowerCase() || ''; const nameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.')); const fileItem = document.createElement("div"); fileItem.className = "bg-base-200 rounded-lg overflow-hidden"; // Generate preview based on file type let previewHtml = ''; if (['jpg', 'jpeg', 'png', 'gif'].includes(fileExt)) { previewHtml = `
${fileName}
`; } else { // For other file types, show an icon based on type const iconHtml = fileExt === 'txt' || fileExt === 'md' ? ` ` : ` `; previewHtml = `
${iconHtml}
`; } fileItem.innerHTML = ` ${previewHtml}

${nameWithoutExt} ${fileExt}

Open
`; // Add delete handler const deleteButton = fileItem.querySelector('.text-error'); if (deleteButton) { deleteButton.addEventListener('click', async (e) => { e.preventDefault(); if (confirm('Are you sure you want to remove this file?')) { try { const fileToRemove = deleteButton.getAttribute('data-file'); if (!fileToRemove) throw new Error('File not found'); // Get the current event data const currentEvent = await this.pb.collection('events').getOne(event.id); // Filter out the file to be removed const updatedFiles = currentEvent.files.filter((f: string) => f !== fileToRemove); // Update the event with the new files array await this.pb.collection('events').update(event.id, { files: updatedFiles }); // Update the local event object event.files = updatedFiles; // Remove the file item from the UI fileItem.remove(); // If no files left, show the "No files" message if (!event.files || event.files.length === 0) { currentFiles.innerHTML = 'No files'; } } catch (err) { console.error('Failed to remove file:', err); alert('Failed to remove file. Please try again.'); } } }); } filesList.appendChild(fileItem); }); currentFiles.appendChild(filesList); } else { currentFiles.innerHTML = 'No files'; } } private async handleViewFiles(eventId: string): Promise { try { const event = await this.pb.collection("events").getOne(eventId); // Create and show modal const modal = document.createElement("dialog"); modal.className = "modal"; const modalContent = ` `; modal.innerHTML = modalContent; document.body.appendChild(modal); modal.showModal(); // Add download all functionality const downloadAllButton = modal.querySelector('.download-all') as HTMLButtonElement; if (downloadAllButton) { downloadAllButton.addEventListener('click', async () => { try { // Create a new JSZip instance const zip = new JSZip(); // Add each file to the zip for (const file of event.files) { const fileUrl = this.pb.files.getURL(event, file); const fileName = this.getFileNameFromUrl(file); const response = await fetch(fileUrl); const blob = await response.blob(); zip.file(fileName, blob); } // Generate the zip file const content = await zip.generateAsync({ type: "blob" }); // Create a download link and trigger it const downloadUrl = URL.createObjectURL(content); const link = document.createElement('a'); link.href = downloadUrl; link.download = `${event.event_name} Files.zip`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(downloadUrl); } catch (err) { console.error('Failed to download files:', err); alert('Failed to download files. Please try again.'); } }); } // Remove modal when closed modal.addEventListener("close", () => { document.body.removeChild(modal); }); } catch (err) { console.error("Failed to view files:", err); } } }