Add authentication #17

Manually merged
Webmaster merged 225 commits from auth into main 2025-03-08 10:37:06 +00:00
7 changed files with 2819 additions and 2400 deletions
Showing only changes of commit 36e1f4663b - Show all commits

View file

@ -7,7 +7,7 @@ import { DataSyncService } from "../../../scripts/database/DataSyncService";
import { Collections } from "../../../schemas/pocketbase/schema"; import { Collections } from "../../../schemas/pocketbase/schema";
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import type { Event, AttendeeEntry } from "../../../schemas/pocketbase"; import type { Event, EventAttendee } from "../../../schemas/pocketbase";
// Extended Event interface with additional properties needed for this component // Extended Event interface with additional properties needed for this component
interface ExtendedEvent extends Event { interface ExtendedEvent extends Event {
@ -51,91 +51,152 @@ const EventCheckIn = () => {
// Log the check-in attempt // Log the check-in attempt
await logger.send( await logger.send(
"attempt", "info",
"event_check_in", "event_check_in",
`User ${currentUser.id} attempted to check in with code: ${eventCode}` `Attempting to check in with code: ${eventCode}`
); );
// SECURITY FIX: Instead of syncing and querying IndexedDB with the event code, // Validate event code
// directly query PocketBase for the event with the given code if (!eventCode || eventCode.trim() === "") {
// This prevents the event code from being stored in IndexedDB
const pb = auth.getPocketBase();
const records = await pb.collection(Collections.EVENTS).getList(1, 1, {
filter: `event_code = "${eventCode}"`,
});
// Convert the first result to our Event type
let event: Event | null = null;
if (records.items.length > 0) {
event = Get.convertUTCToLocal(records.items[0] as unknown as Event);
}
if (!event) {
await logger.send( await logger.send(
"error", "error",
"event_check_in", "event_check_in",
`Check-in failed: Invalid event code "${eventCode}"` "Check-in failed: Empty event code"
); );
throw new Error("Invalid event code"); toast.error("Please enter an event code");
return;
} }
// Check if user is already checked in // Get event by code
const attendees = event.attendees || []; const events = await get.getList<Event>(
if (attendees.some((entry) => entry.user_id === currentUser.id)) { Collections.EVENTS,
1,
1,
`event_code="${eventCode}"`
);
if (events.totalItems === 0) {
await logger.send( await logger.send(
"error", "error",
"event_check_in", "event_check_in",
`Check-in failed: User ${currentUser.id} already checked in to event ${event.event_name} (${event.id})` `Check-in failed: Invalid event code: ${eventCode}`
); );
throw new Error("You have already checked in to this event"); toast.error("Invalid event code. Please try again.");
return;
}
const event = events.items[0];
// Check if event is published
if (!event.published) {
await logger.send(
"error",
"event_check_in",
`Check-in failed: Event not published: ${event.event_name}`
);
toast.error("This event is not currently available for check-in");
return;
} }
// Check if the event is active (has started and hasn't ended yet) // Check if the event is active (has started and hasn't ended yet)
const currentTime = new Date(); const currentTime = new Date();
const eventStartDate = new Date(event.start_date); // Now properly converted to local time by Get const eventStartDate = new Date(event.start_date);
const eventEndDate = new Date(event.end_date); // Now properly converted to local time by Get const eventEndDate = new Date(event.end_date);
if (eventStartDate > currentTime) { if (currentTime < eventStartDate) {
await logger.send( await logger.send(
"error", "error",
"event_check_in", "event_check_in",
`Check-in failed: Event ${event.event_name} (${event.id}) has not started yet` `Check-in failed: Event has not started yet: ${event.event_name}`
); );
throw new Error("This event has not started yet"); toast.error(`This event hasn't started yet. It begins on ${eventStartDate.toLocaleDateString()} at ${eventStartDate.toLocaleTimeString()}`);
return;
} }
if (eventEndDate < currentTime) { if (currentTime > eventEndDate) {
await logger.send( await logger.send(
"error", "error",
"event_check_in", "event_check_in",
`Check-in failed: Event ${event.event_name} (${event.id}) has already ended` `Check-in failed: Event has already ended: ${event.event_name}`
); );
throw new Error("This event has already ended"); toast.error("This event has already ended");
return;
} }
// Log successful validation before proceeding // Check if user is already checked in
const attendees = await get.getList<EventAttendee>(
Collections.EVENT_ATTENDEES,
1,
1,
`user="${currentUser.id}" && event="${event.id}"`
);
if (attendees.totalItems > 0) {
await logger.send(
"error",
"event_check_in",
`Check-in failed: Already checked in to event: ${event.event_name}`
);
toast.error("You have already checked in to this event");
return;
}
// Set current event for check-in
setCurrentCheckInEvent(event);
// Log successful event lookup
await logger.send( await logger.send(
"info", "info",
"event_check_in", "event_check_in",
`Check-in validation successful for user ${currentUser.id} to event ${event.event_name} (${event.id})` `Found event for check-in: ${event.event_name}`
); );
// Store event code in local storage for offline check-in
await dataSync.storeEventCode(eventCode);
// Show event details toast only for non-food events
// For food events, we'll show the toast after food selection
if (!event.has_food) {
toast.success(
<div>
<strong>Event found!</strong>
<p className="text-sm mt-1">{event.event_name}</p>
<p className="text-xs mt-1">
{event.points_to_reward > 0 ? `${event.points_to_reward} points` : "No points"}
</p>
</div>,
{ duration: 5000 }
);
}
// If event has food, show food selection modal // If event has food, show food selection modal
if (event.has_food) { if (event.has_food) {
setCurrentCheckInEvent(event); // Show food-specific toast
toast.success(
<div>
<strong>Event with food found!</strong>
<p className="text-sm mt-1">{event.event_name}</p>
<p className="text-xs mt-1">Please select your food preference</p>
</div>,
{ duration: 5000 }
);
const modal = document.getElementById("foodSelectionModal") as HTMLDialogElement; const modal = document.getElementById("foodSelectionModal") as HTMLDialogElement;
modal.showModal(); if (modal) modal.showModal();
} else { } else {
// If no food, complete check-in directly // If no food, show confirmation modal
await completeCheckIn(event, null); const modal = document.getElementById("confirmCheckInModal") as HTMLDialogElement;
if (modal) modal.showModal();
} }
} catch (error: any) { } catch (error: any) {
toast.error(error?.message || "Failed to check in to event"); console.error("Error checking in:", error);
toast.error(error.message || "An error occurred during check-in");
} }
} }
async function completeCheckIn(event: Event, foodSelection: string | null): Promise<void> { async function completeCheckIn(event: Event, foodSelection: string | null): Promise<void> {
try { try {
setIsLoading(true);
const auth = Authentication.getInstance(); const auth = Authentication.getInstance();
const update = Update.getInstance(); const update = Update.getInstance();
const logger = SendLog.getInstance(); const logger = SendLog.getInstance();
@ -146,119 +207,108 @@ 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");
} }
// Check if user is already checked in const userId = currentUser.id;
const userId = auth.getUserId(); const eventId = event.id;
if (!userId) {
toast.error("You must be logged in to check in to an event");
return;
}
// Initialize attendees array if it doesn't exist
const attendees = event.attendees || [];
// Check if user is already checked in // Check if user is already checked in
const isAlreadyCheckedIn = attendees.some( const get = Get.getInstance();
(attendee) => attendee.user_id === userId const existingAttendees = await get.getList<EventAttendee>(
Collections.EVENT_ATTENDEES,
1,
1,
`user="${userId}" && event="${eventId}"`
); );
const isAlreadyCheckedIn = existingAttendees.totalItems > 0;
if (isAlreadyCheckedIn) { if (isAlreadyCheckedIn) {
toast("You are already checked in to this event", {
icon: '⚠️',
style: {
borderRadius: '10px',
background: '#FFC107',
color: '#000',
},
});
return;
}
// Create attendee entry with check-in details
const attendeeEntry: AttendeeEntry = {
user_id: currentUser.id,
time_checked_in: new Date().toISOString(), // Will be properly converted to UTC by Update
food: foodSelection || "none",
};
// Get existing attendees or initialize empty array
const existingAttendees = event.attendees || [];
// Check if user is already checked in
if (existingAttendees.some((entry) => entry.user_id === currentUser.id)) {
throw new Error("You have already checked in to this event"); throw new Error("You have already checked in to this event");
} }
// Add new attendee entry to the array // Create new attendee record
const updatedAttendees = [...existingAttendees, attendeeEntry]; const attendeeData = {
user: userId,
event: eventId,
food_ate: foodSelection || "",
time_checked_in: new Date().toISOString(),
points_earned: event.points_to_reward || 0
};
// Update attendees array with the new entry // Create the attendee record using PocketBase's create method
await update.updateField("events", event.id, "attendees", updatedAttendees); // 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);
// SECURITY FIX: Instead of syncing the entire events collection which would store event codes in IndexedDB, console.log("Successfully created attendance record");
// only sync the user's collection to update their points } catch (createError: any) {
if (event.points_to_reward > 0) { console.error("Error creating attendance record:", createError);
await dataSync.syncCollection(Collections.USERS);
// Check if this is a duplicate record error
if (createError.status === 400 && createError.data?.data?.user?.code === "validation_not_unique") {
throw new Error("You have already checked in to this event");
} }
// If food selection was made, log it throw createError;
if (foodSelection) { }
// Log successful check-in
await logger.send( await logger.send(
"update", "info",
"event check-in", "event_check_in",
`Food selection for ${event.event_name}: ${foodSelection}` `Successfully checked in to event: ${event.event_name}`
);
}
// Award points to user if available
if (event.points_to_reward > 0) {
const userPoints = currentUser.points || 0;
await update.updateField(
"users",
currentUser.id,
"points",
userPoints + event.points_to_reward
); );
// Log the points award // Clear event code from local storage
await logger.send( await dataSync.clearEventCode();
"update",
"event check-in",
`Awarded ${event.points_to_reward} points for checking in to ${event.event_name}`
);
}
// Show success message with points if awarded // Show success message with event name and points
toast.success( const pointsMessage = event.points_to_reward > 0
`Successfully checked in to ${event.event_name}${event.points_to_reward > 0
? ` (+${event.points_to_reward} points!)` ? ` (+${event.points_to_reward} points!)`
: "" : "";
}` toast.success(`Successfully checked in to ${event.event_name}${pointsMessage}`);
); setCurrentCheckInEvent(null);
// Log the check-in
await logger.send(
"check_in",
"events",
`Checked in to event ${event.event_name}`
);
// Close the food selection modal if it's open
const modal = document.getElementById("foodSelectionModal") as HTMLDialogElement;
if (modal) {
modal.close();
setFoodInput(""); setFoodInput("");
}
} catch (error: any) { } catch (error: any) {
toast.error(error?.message || "Failed to check in to event"); console.error("Error completing check-in:", error);
toast.error(error.message || "An error occurred during check-in");
} finally {
setIsLoading(false);
} }
} }
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (currentCheckInEvent) { if (!currentCheckInEvent) return;
await completeCheckIn(currentCheckInEvent, foodInput.trim());
setCurrentCheckInEvent(null); try {
const auth = Authentication.getInstance();
const logger = SendLog.getInstance();
const get = Get.getInstance();
const currentUser = auth.getCurrentUser();
if (!currentUser) {
throw new Error("You must be logged in to check in to events");
}
// Get existing attendees or initialize empty array
const existingAttendees = await get.getList<EventAttendee>(
Collections.EVENT_ATTENDEES,
1,
1,
`user="${currentUser.id}" && event="${currentCheckInEvent.id}"`
);
// Check if user is already checked in
if (existingAttendees.totalItems > 0) {
throw new Error("You have already checked in to this event");
}
// Complete check-in with food selection
await completeCheckIn(currentCheckInEvent, foodInput);
} catch (error: any) {
console.error("Error submitting check-in:", error);
toast.error(error.message || "An error occurred during check-in");
} }
}; };
@ -326,7 +376,13 @@ const EventCheckIn = () => {
{/* Food Selection Modal */} {/* Food Selection Modal */}
<dialog id="foodSelectionModal" className="modal"> <dialog id="foodSelectionModal" className="modal">
<div className="modal-box"> <div className="modal-box">
<h3 className="font-bold text-lg mb-4">Food Selection</h3> <h3 className="font-bold text-lg mb-2">{currentCheckInEvent?.event_name}</h3>
<div className="text-sm mb-4 opacity-75">
{currentCheckInEvent?.event_description}
</div>
<div className="badge badge-primary mb-4">
{currentCheckInEvent?.points_to_reward} points
</div>
<p className="mb-4">This event has food! Please let us know what you'd like to eat:</p> <p className="mb-4">This event has food! Please let us know what you'd like to eat:</p>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="form-control"> <div className="form-control">
@ -345,7 +401,17 @@ const EventCheckIn = () => {
modal.close(); modal.close();
setCurrentCheckInEvent(null); setCurrentCheckInEvent(null);
}}>Cancel</button> }}>Cancel</button>
<button type="submit" className="btn btn-primary">Submit</button> <button type="submit" className="btn btn-primary" disabled={isLoading}>
{isLoading ? (
<Icon
icon="line-md:loading-twotone-loop"
className="w-5 h-5"
inline={true}
/>
) : (
"Check In"
)}
</button>
</div> </div>
</form> </form>
</div> </div>
@ -353,6 +419,58 @@ const EventCheckIn = () => {
<button>close</button> <button>close</button>
</form> </form>
</dialog> </dialog>
{/* Confirmation Modal (for events without food) */}
<dialog id="confirmCheckInModal" className="modal">
<div className="modal-box">
<h3 className="font-bold text-lg mb-2">{currentCheckInEvent?.event_name}</h3>
<div className="text-sm mb-4 opacity-75">
{currentCheckInEvent?.event_description}
</div>
<div className="badge badge-primary mb-4">
{currentCheckInEvent?.points_to_reward} points
</div>
<p className="mb-4">Are you sure you want to check in to this event?</p>
<div className="modal-action">
<button
type="button"
className="btn"
onClick={() => {
const modal = document.getElementById("confirmCheckInModal") as HTMLDialogElement;
modal.close();
setCurrentCheckInEvent(null);
}}
>
Cancel
</button>
<button
type="button"
className="btn btn-primary"
disabled={isLoading}
onClick={() => {
if (currentCheckInEvent) {
completeCheckIn(currentCheckInEvent, null);
const modal = document.getElementById("confirmCheckInModal") as HTMLDialogElement;
modal.close();
}
}}
>
{isLoading ? (
<Icon
icon="line-md:loading-twotone-loop"
className="w-5 h-5"
inline={true}
/>
) : (
"Confirm Check In"
)}
</button>
</div>
</div>
<form method="dialog" className="modal-backdrop">
<button>close</button>
</form>
</dialog>
</> </>
); );
}; };

View file

@ -5,7 +5,7 @@ import { Authentication } from "../../../scripts/pocketbase/Authentication";
import { DataSyncService } from "../../../scripts/database/DataSyncService"; import { DataSyncService } from "../../../scripts/database/DataSyncService";
import { DexieService } from "../../../scripts/database/DexieService"; import { DexieService } from "../../../scripts/database/DexieService";
import { Collections } from "../../../schemas/pocketbase/schema"; import { Collections } from "../../../schemas/pocketbase/schema";
import type { Event, AttendeeEntry } from "../../../schemas/pocketbase"; import type { Event, AttendeeEntry, EventAttendee } from "../../../schemas/pocketbase";
// Extended Event interface with additional properties needed for this component // Extended Event interface with additional properties needed for this component
interface ExtendedEvent extends Event { interface ExtendedEvent extends Event {
@ -132,20 +132,51 @@ const EventLoad = () => {
); );
const renderEventCard = (event: Event) => { const renderEventCard = (event: Event) => {
const startDate = new Date(event.start_date); try {
const endDate = new Date(event.end_date); // Get authentication instance
const now = new Date();
const isPastEvent = endDate < now;
// Get current user to check attendance
const auth = Authentication.getInstance(); const auth = Authentication.getInstance();
const currentUser = auth.getCurrentUser(); const currentUser = auth.getCurrentUser();
const hasAttended = currentUser && event.attendees?.some(entry => entry.user_id === currentUser.id);
// Check if user has attended this event by querying the event_attendees collection
let hasAttended = false;
if (currentUser) {
// We'll check attendance status when displaying the card
// This will be done asynchronously after rendering
setTimeout(async () => {
try {
const get = Get.getInstance();
const attendees = await get.getList<EventAttendee>(
"event_attendees",
1,
1,
`user="${currentUser.id}" && event="${event.id}"`
);
const hasAttendedEvent = attendees.totalItems > 0;
// Update the card UI based on attendance status
const cardElement = document.getElementById(`event-card-${event.id}`);
if (cardElement && hasAttendedEvent) {
const attendedBadge = cardElement.querySelector('.attended-badge');
if (attendedBadge) {
(attendedBadge as HTMLElement).style.display = 'flex';
}
}
} catch (error) {
console.error("Error checking attendance status:", error);
}
}, 0);
}
// Store event data in window object with unique ID // Store event data in window object with unique ID
const eventDataId = `event_${event.id}`; const eventDataId = `event_${event.id}`;
window[eventDataId] = event; window[eventDataId] = event;
const startDate = new Date(event.start_date);
const endDate = new Date(event.end_date);
const now = new Date();
const isPastEvent = endDate < now;
return ( return (
<div key={event.id} className="card bg-base-200 shadow-lg hover:shadow-xl transition-all duration-300 relative overflow-hidden"> <div key={event.id} className="card bg-base-200 shadow-lg hover:shadow-xl transition-all duration-300 relative overflow-hidden">
<div className="card-body p-3 sm:p-4"> <div className="card-body p-3 sm:p-4">
@ -202,6 +233,10 @@ const EventLoad = () => {
</div> </div>
</div> </div>
); );
} catch (error) {
console.error("Error rendering event card:", error);
return null;
}
}; };
const loadEvents = async () => { const loadEvents = async () => {

View file

@ -6,6 +6,8 @@ import EventEditor from "./Officer_EventManagement/EventEditor";
import FilePreview from "./universal/FilePreview"; import FilePreview from "./universal/FilePreview";
import Attendees from "./Officer_EventManagement/Attendees"; import Attendees from "./Officer_EventManagement/Attendees";
import type { Event, AttendeeEntry } from "../../schemas/pocketbase"; import type { Event, AttendeeEntry } from "../../schemas/pocketbase";
import { DataSyncService } from "../../scripts/database/DataSyncService";
import toast from "react-hot-toast";
// Get instances // Get instances
const get = Get.getInstance(); const get = Get.getInstance();
@ -32,7 +34,13 @@ let upcomingEvents: Event[] = [];
// Fetch events // Fetch events
try { try {
if (auth.isAuthenticated()) { if (auth.isAuthenticated()) {
eventResponse = await get.getList<Event>("events", 1, 5, "", "-start_date"); eventResponse = await get.getList<Event>(
"events",
1,
5,
"",
"-start_date"
);
upcomingEvents = eventResponse.items; upcomingEvents = eventResponse.items;
} }
} catch (error) { } catch (error) {
@ -64,7 +72,9 @@ const currentPage = eventResponse.page;
class="stats shadow-lg bg-base-100 rounded-2xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform" class="stats shadow-lg bg-base-100 rounded-2xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform"
> >
<div class="stat p-4 md:p-6"> <div class="stat p-4 md:p-6">
<div class="stat-title text-sm md:text-base font-medium opacity-80"> <div
class="stat-title text-sm md:text-base font-medium opacity-80"
>
Total Events Total Events
</div> </div>
<div <div
@ -84,7 +94,9 @@ const currentPage = eventResponse.page;
class="stats shadow-lg bg-base-100 rounded-2xl border border-base-200 hover:border-secondary transition-all duration-300 hover:-translate-y-1 transform" class="stats shadow-lg bg-base-100 rounded-2xl border border-base-200 hover:border-secondary transition-all duration-300 hover:-translate-y-1 transform"
> >
<div class="stat p-4 md:p-6"> <div class="stat p-4 md:p-6">
<div class="stat-title text-sm md:text-base font-medium opacity-80"> <div
class="stat-title text-sm md:text-base font-medium opacity-80"
>
Unique Attendees Unique Attendees
</div> </div>
<div <div
@ -104,7 +116,9 @@ const currentPage = eventResponse.page;
class="stats shadow-lg bg-base-100 rounded-2xl border border-base-200 hover:border-accent transition-all duration-300 hover:-translate-y-1 transform sm:col-span-2 md:col-span-1" class="stats shadow-lg bg-base-100 rounded-2xl border border-base-200 hover:border-accent transition-all duration-300 hover:-translate-y-1 transform sm:col-span-2 md:col-span-1"
> >
<div class="stat p-4 md:p-6"> <div class="stat p-4 md:p-6">
<div class="stat-title text-sm md:text-base font-medium opacity-80"> <div
class="stat-title text-sm md:text-base font-medium opacity-80"
>
Recurring Attendees Recurring Attendees
</div> </div>
<div <div
@ -141,14 +155,20 @@ const currentPage = eventResponse.page;
class="btn btn-ghost btn-sm md:btn-md gap-2" class="btn btn-ghost btn-sm md:btn-md gap-2"
onclick="window.refreshEvents()" onclick="window.refreshEvents()"
> >
<Icon name="heroicons:arrow-path" class="h-4 w-4 md:h-5 md:w-5" /> <Icon
name="heroicons:arrow-path"
class="h-4 w-4 md:h-5 md:w-5"
/>
Refresh Refresh
</button> </button>
<button <button
class="btn btn-primary btn-sm md:btn-md gap-2" class="btn btn-primary btn-sm md:btn-md gap-2"
onclick="window.openEditModal()" onclick="window.openEditModal()"
> >
<Icon name="heroicons:plus" class="h-4 w-4 md:h-5 md:w-5" /> <Icon
name="heroicons:plus"
class="h-4 w-4 md:h-5 md:w-5"
/>
Add New Event Add New Event
</button> </button>
</div> </div>
@ -162,7 +182,8 @@ const currentPage = eventResponse.page;
<div class="flex flex-wrap gap-4"> <div class="flex flex-wrap gap-4">
<div class="form-control w-full sm:w-auto"> <div class="form-control w-full sm:w-auto">
<label class="label"> <label class="label">
<span class="label-text text-sm md:text-base font-medium" <span
class="label-text text-sm md:text-base font-medium"
>Time Filter</span >Time Filter</span
> >
</label> </label>
@ -202,7 +223,8 @@ const currentPage = eventResponse.page;
<!-- Other filters with similar responsive adjustments --> <!-- Other filters with similar responsive adjustments -->
<div class="form-control w-full sm:w-auto"> <div class="form-control w-full sm:w-auto">
<label class="label"> <label class="label">
<span class="label-text text-sm md:text-base font-medium" <span
class="label-text text-sm md:text-base font-medium"
>Year</span >Year</span
> >
</label> </label>
@ -232,7 +254,8 @@ const currentPage = eventResponse.page;
> >
<div class="form-control"> <div class="form-control">
<label class="label cursor-pointer"> <label class="label cursor-pointer">
<span class="label-text">All Years</span> <span class="label-text">All Years</span
>
<input <input
type="checkbox" type="checkbox"
class="checkbox" class="checkbox"
@ -250,7 +273,8 @@ const currentPage = eventResponse.page;
<div class="form-control w-full sm:w-auto"> <div class="form-control w-full sm:w-auto">
<label class="label"> <label class="label">
<span class="label-text text-sm md:text-base font-medium" <span
class="label-text text-sm md:text-base font-medium"
>Quarter</span >Quarter</span
> >
</label> </label>
@ -260,7 +284,9 @@ const currentPage = eventResponse.page;
class="btn btn-sm m-1 w-[180px] justify-between items-center" class="btn btn-sm m-1 w-[180px] justify-between items-center"
> >
<div class="flex-1 text-left truncate"> <div class="flex-1 text-left truncate">
<span id="quarterFilterLabel">All Quarters</span> <span id="quarterFilterLabel"
>All Quarters</span
>
</div> </div>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -281,7 +307,9 @@ const currentPage = eventResponse.page;
> >
<div class="form-control"> <div class="form-control">
<label class="label cursor-pointer"> <label class="label cursor-pointer">
<span class="label-text">All Quarters</span> <span class="label-text"
>All Quarters</span
>
<input <input
type="checkbox" type="checkbox"
class="checkbox" class="checkbox"
@ -293,25 +321,41 @@ const currentPage = eventResponse.page;
<div class="form-control"> <div class="form-control">
<label class="label cursor-pointer"> <label class="label cursor-pointer">
<span class="label-text">Fall</span> <span class="label-text">Fall</span>
<input type="checkbox" class="checkbox" value="fall" /> <input
type="checkbox"
class="checkbox"
value="fall"
/>
</label> </label>
</div> </div>
<div class="form-control"> <div class="form-control">
<label class="label cursor-pointer"> <label class="label cursor-pointer">
<span class="label-text">Winter</span> <span class="label-text">Winter</span>
<input type="checkbox" class="checkbox" value="winter" /> <input
type="checkbox"
class="checkbox"
value="winter"
/>
</label> </label>
</div> </div>
<div class="form-control"> <div class="form-control">
<label class="label cursor-pointer"> <label class="label cursor-pointer">
<span class="label-text">Spring</span> <span class="label-text">Spring</span>
<input type="checkbox" class="checkbox" value="spring" /> <input
type="checkbox"
class="checkbox"
value="spring"
/>
</label> </label>
</div> </div>
<div class="form-control"> <div class="form-control">
<label class="label cursor-pointer"> <label class="label cursor-pointer">
<span class="label-text">Summer</span> <span class="label-text">Summer</span>
<input type="checkbox" class="checkbox" value="summer" /> <input
type="checkbox"
class="checkbox"
value="summer"
/>
</label> </label>
</div> </div>
</div> </div>
@ -320,7 +364,8 @@ const currentPage = eventResponse.page;
<div class="form-control w-full sm:w-auto"> <div class="form-control w-full sm:w-auto">
<label class="label"> <label class="label">
<span class="label-text text-sm md:text-base font-medium" <span
class="label-text text-sm md:text-base font-medium"
>Published</span >Published</span
> >
</label> </label>
@ -386,7 +431,8 @@ const currentPage = eventResponse.page;
<div class="form-control w-full sm:w-auto"> <div class="form-control w-full sm:w-auto">
<label class="label"> <label class="label">
<span class="label-text text-sm md:text-base font-medium" <span
class="label-text text-sm md:text-base font-medium"
>Has Files</span >Has Files</span
> >
</label> </label>
@ -452,7 +498,8 @@ const currentPage = eventResponse.page;
<div class="form-control w-full sm:w-auto"> <div class="form-control w-full sm:w-auto">
<label class="label"> <label class="label">
<span class="label-text text-sm md:text-base font-medium" <span
class="label-text text-sm md:text-base font-medium"
>Has Food</span >Has Food</span
> >
</label> </label>
@ -522,7 +569,9 @@ const currentPage = eventResponse.page;
<div class="flex flex-col sm:flex-row gap-4 mb-4"> <div class="flex flex-col sm:flex-row gap-4 mb-4">
<div class="form-control flex-1"> <div class="form-control flex-1">
<div class="join w-full"> <div class="join w-full">
<div class="join-item bg-base-200 flex items-center px-3"> <div
class="join-item bg-base-200 flex items-center px-3"
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 md:h-5 md:w-5 opacity-70" class="h-4 w-4 md:h-5 md:w-5 opacity-70"
@ -569,22 +618,29 @@ const currentPage = eventResponse.page;
</div> </div>
<!-- Pagination --> <!-- Pagination -->
<div class="flex justify-center mt-4 md:mt-6" id="paginationContainer"> <div
<div class="join"> class="flex justify-center mt-4 md:mt-6"
<button class="join-item btn btn-xs md:btn-sm" id="firstPageBtn" id="paginationContainer"
>«</button
> >
<button class="join-item btn btn-xs md:btn-sm" id="prevPageBtn" <div class="join">
></button <button
class="join-item btn btn-xs md:btn-sm"
id="firstPageBtn">«</button
>
<button
class="join-item btn btn-xs md:btn-sm"
id="prevPageBtn"></button
> >
<button class="join-item btn btn-xs md:btn-sm" <button class="join-item btn btn-xs md:btn-sm"
>Page <span id="currentPageNumber">1</span></button >Page <span id="currentPageNumber">1</span></button
> >
<button class="join-item btn btn-xs md:btn-sm" id="nextPageBtn" <button
></button class="join-item btn btn-xs md:btn-sm"
id="nextPageBtn"></button
> >
<button class="join-item btn btn-xs md:btn-sm" id="lastPageBtn" <button
>»</button class="join-item btn btn-xs md:btn-sm"
id="lastPageBtn">»</button
> >
</div> </div>
</div> </div>
@ -679,7 +735,10 @@ const currentPage = eventResponse.page;
<div class="modal-box max-w-4xl"> <div class="modal-box max-w-4xl">
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<h3 class="font-bold text-lg" id="attendeesModalTitle"></h3> <h3 class="font-bold text-lg" id="attendeesModalTitle"></h3>
<button class="btn btn-circle btn-ghost" onclick="attendeesModal.close()"> <button
class="btn btn-circle btn-ghost"
onclick="attendeesModal.close()"
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6" class="h-6 w-6"
@ -709,7 +768,9 @@ const currentPage = eventResponse.page;
import { Update } from "../../scripts/pocketbase/Update"; import { Update } from "../../scripts/pocketbase/Update";
import { FileManager } from "../../scripts/pocketbase/FileManager"; import { FileManager } from "../../scripts/pocketbase/FileManager";
import { SendLog } from "../../scripts/pocketbase/SendLog"; import { SendLog } from "../../scripts/pocketbase/SendLog";
import { DataSyncService } from "../../scripts/database/DataSyncService";
import FilePreview from "./universal/FilePreview"; import FilePreview from "./universal/FilePreview";
import toast from "react-hot-toast";
// Add file storage // Add file storage
const selectedFileStorage = new Map<string, File>(); const selectedFileStorage = new Map<string, File>();
@ -779,7 +840,9 @@ const currentPage = eventResponse.page;
) { ) {
auth.setUpdating(true); auth.setUpdating(true);
const response = await get.getAll<Event>("events"); const response = await get.getAll<Event>("events");
cachedEvents = response.map((event) => Get.convertUTCToLocal(event)); cachedEvents = response.map((event) =>
Get.convertUTCToLocal(event)
);
lastCacheUpdate = now; lastCacheUpdate = now;
// Initialize year filter options from cache // Initialize year filter options from cache
@ -789,7 +852,8 @@ const currentPage = eventResponse.page;
years.add(year); years.add(year);
}); });
const yearCheckboxes = document.getElementById("yearCheckboxes"); const yearCheckboxes =
document.getElementById("yearCheckboxes");
if (yearCheckboxes) { if (yearCheckboxes) {
const sortedYears = Array.from(years).sort((a, b) => b - a); const sortedYears = Array.from(years).sort((a, b) => b - a);
yearCheckboxes.innerHTML = sortedYears yearCheckboxes.innerHTML = sortedYears
@ -799,16 +863,18 @@ const currentPage = eventResponse.page;
<span class="label-text">${year}</span> <span class="label-text">${year}</span>
<input type="checkbox" class="checkbox" value="${year}" /> <input type="checkbox" class="checkbox" value="${year}" />
</label> </label>
`, `
) )
.join(""); .join("");
// Add event listeners to checkboxes // Add event listeners to checkboxes
const allYearsCheckbox = document.querySelector( const allYearsCheckbox = document.querySelector(
'input[type="checkbox"][value="all"]', 'input[type="checkbox"][value="all"]'
) as HTMLInputElement; ) as HTMLInputElement;
const yearInputs = Array.from( const yearInputs = Array.from(
yearCheckboxes.querySelectorAll('input[type="checkbox"]'), yearCheckboxes.querySelectorAll(
'input[type="checkbox"]'
)
) as HTMLInputElement[]; ) as HTMLInputElement[];
if (allYearsCheckbox) { if (allYearsCheckbox) {
@ -819,8 +885,9 @@ const currentPage = eventResponse.page;
input.checked = false; input.checked = false;
}); });
filterState.year = ["all"]; filterState.year = ["all"];
document.getElementById("yearFilterLabel")!.textContent = document.getElementById(
"All Years"; "yearFilterLabel"
)!.textContent = "All Years";
} }
currentPage = 1; currentPage = 1;
fetchEvents(); fetchEvents();
@ -836,12 +903,15 @@ const currentPage = eventResponse.page;
if (checkedYears.length === 0) { if (checkedYears.length === 0) {
allYearsCheckbox.checked = true; allYearsCheckbox.checked = true;
filterState.year = ["all"]; filterState.year = ["all"];
document.getElementById("yearFilterLabel")!.textContent = document.getElementById(
"All Years"; "yearFilterLabel"
)!.textContent = "All Years";
} else { } else {
allYearsCheckbox.checked = false; allYearsCheckbox.checked = false;
filterState.year = checkedYears; filterState.year = checkedYears;
document.getElementById("yearFilterLabel")!.textContent = document.getElementById(
"yearFilterLabel"
)!.textContent =
checkedYears.length === 1 checkedYears.length === 1
? checkedYears[0] ? checkedYears[0]
: `${checkedYears.length} Years Selected`; : `${checkedYears.length} Years Selected`;
@ -940,7 +1010,8 @@ const currentPage = eventResponse.page;
const eventEnd = new Date(event.end_date).toISOString(); const eventEnd = new Date(event.end_date).toISOString();
// Time filter // Time filter
if (filterState.time === "upcoming" && eventStart <= now) return false; if (filterState.time === "upcoming" && eventStart <= now)
return false;
if (filterState.time === "past" && eventEnd >= now) return false; if (filterState.time === "past" && eventEnd >= now) return false;
if ( if (
filterState.time === "ongoing" && filterState.time === "ongoing" &&
@ -957,7 +1028,7 @@ const currentPage = eventResponse.page;
// Check if either the start year or end year matches any selected year // Check if either the start year or end year matches any selected year
const yearMatches = filterState.year.some( const yearMatches = filterState.year.some(
(year) => year === eventStartYear || year === eventEndYear, (year) => year === eventStartYear || year === eventEndYear
); );
if (!yearMatches) return false; if (!yearMatches) return false;
} }
@ -994,7 +1065,8 @@ const currentPage = eventResponse.page;
// Published filter // Published filter
if (filterState.published !== "all") { if (filterState.published !== "all") {
if ((filterState.published === "yes") !== event.published) return false; if ((filterState.published === "yes") !== event.published)
return false;
} }
// Has Files filter // Has Files filter
@ -1005,7 +1077,8 @@ const currentPage = eventResponse.page;
// Has Food filter // Has Food filter
if (filterState.hasFood !== "all") { if (filterState.hasFood !== "all") {
if ((filterState.hasFood === "yes") !== event.has_food) return false; if ((filterState.hasFood === "yes") !== event.has_food)
return false;
} }
// Search query // Search query
@ -1020,7 +1093,7 @@ const currentPage = eventResponse.page;
event.event_name.toLowerCase().includes(term) || event.event_name.toLowerCase().includes(term) ||
event.event_code.toLowerCase().includes(term) || event.event_code.toLowerCase().includes(term) ||
event.location.toLowerCase().includes(term) || event.location.toLowerCase().includes(term) ||
event.event_description.toLowerCase().includes(term), event.event_description.toLowerCase().includes(term)
) )
); );
} }
@ -1032,7 +1105,9 @@ const currentPage = eventResponse.page;
// Fetch and display events using cached data // Fetch and display events using cached data
async function fetchEvents() { async function fetchEvents() {
const eventsList = document.getElementById("eventsList"); const eventsList = document.getElementById("eventsList");
const paginationContainer = document.getElementById("paginationContainer"); const paginationContainer = document.getElementById(
"paginationContainer"
);
if (!eventsList || !paginationContainer) return; if (!eventsList || !paginationContainer) return;
try { try {
@ -1058,7 +1133,8 @@ const currentPage = eventResponse.page;
// Sort events by start date (newest first) // Sort events by start date (newest first)
filteredEvents.sort( filteredEvents.sort(
(a, b) => (a, b) =>
new Date(b.start_date).getTime() - new Date(a.start_date).getTime(), new Date(b.start_date).getTime() -
new Date(a.start_date).getTime()
); );
// Calculate pagination // Calculate pagination
@ -1070,18 +1146,19 @@ const currentPage = eventResponse.page;
// Update pagination UI // Update pagination UI
const firstPageBtn = document.getElementById( const firstPageBtn = document.getElementById(
"firstPageBtn", "firstPageBtn"
) as HTMLButtonElement; ) as HTMLButtonElement;
const prevPageBtn = document.getElementById( const prevPageBtn = document.getElementById(
"prevPageBtn", "prevPageBtn"
) as HTMLButtonElement; ) as HTMLButtonElement;
const nextPageBtn = document.getElementById( const nextPageBtn = document.getElementById(
"nextPageBtn", "nextPageBtn"
) as HTMLButtonElement; ) as HTMLButtonElement;
const lastPageBtn = document.getElementById( const lastPageBtn = document.getElementById(
"lastPageBtn", "lastPageBtn"
) as HTMLButtonElement; ) as HTMLButtonElement;
const currentPageNumber = document.getElementById("currentPageNumber"); const currentPageNumber =
document.getElementById("currentPageNumber");
if (firstPageBtn) firstPageBtn.disabled = currentPage <= 1; if (firstPageBtn) firstPageBtn.disabled = currentPage <= 1;
if (prevPageBtn) prevPageBtn.disabled = currentPage <= 1; if (prevPageBtn) prevPageBtn.disabled = currentPage <= 1;
@ -1129,8 +1206,12 @@ const currentPage = eventResponse.page;
console.error("Error formatting date:", e); console.error("Error formatting date:", e);
} }
const locationStr = event.location ? `${event.location}` : ""; const locationStr = event.location
const codeStr = event.event_code ? `${event.event_code}` : ""; ? `${event.location}`
: "";
const codeStr = event.event_code
? `${event.event_code}`
: "";
const detailsStr = [locationStr, codeStr] const detailsStr = [locationStr, codeStr]
.filter(Boolean) .filter(Boolean)
.join(" | code: "); .join(" | code: ");
@ -1195,7 +1276,8 @@ const currentPage = eventResponse.page;
async function calculateQuarterlyStats() { async function calculateQuarterlyStats() {
try { try {
const { start: termStart, end: termEnd } = getCurrentTerm(); const { start: termStart, end: termEnd } = getCurrentTerm();
const { start: quarterStart, end: quarterEnd } = getCurrentQuarter(); const { start: quarterStart, end: quarterEnd } =
getCurrentQuarter();
// Update quarter name in UI // Update quarter name in UI
const quarterNameEl = document.getElementById("quarterName"); const quarterNameEl = document.getElementById("quarterName");
@ -1236,17 +1318,19 @@ const currentPage = eventResponse.page;
}); });
quarterlyStats.recurringAttendees = Array.from( quarterlyStats.recurringAttendees = Array.from(
quarterAttendees.values(), quarterAttendees.values()
).filter((count) => count > 1).length; ).filter((count) => count > 1).length;
// Update the UI // Update the UI
const totalEventsEl = document.getElementById("totalEvents"); const totalEventsEl = document.getElementById("totalEvents");
const uniqueAttendeesEl = document.getElementById("uniqueAttendees"); const uniqueAttendeesEl =
document.getElementById("uniqueAttendees");
const recurringAttendeesEl = const recurringAttendeesEl =
document.getElementById("recurringAttendees"); document.getElementById("recurringAttendees");
if (totalEventsEl) if (totalEventsEl)
totalEventsEl.textContent = quarterlyStats.totalEvents.toString(); totalEventsEl.textContent =
quarterlyStats.totalEvents.toString();
if (uniqueAttendeesEl) if (uniqueAttendeesEl)
uniqueAttendeesEl.textContent = uniqueAttendeesEl.textContent =
quarterlyStats.uniqueAttendees.toString(); quarterlyStats.uniqueAttendees.toString();
@ -1299,20 +1383,24 @@ const currentPage = eventResponse.page;
}); });
// Year filter // Year filter
document.getElementById("yearCheckboxes")?.addEventListener("change", (e) => { document
.getElementById("yearCheckboxes")
?.addEventListener("change", (e) => {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
if (!target.matches('input[type="checkbox"]')) return; if (!target.matches('input[type="checkbox"]')) return;
const allYearsCheckbox = target const allYearsCheckbox = target
.closest(".dropdown-content") .closest(".dropdown-content")
?.querySelector( ?.querySelector(
'input[type="checkbox"][value="all"]', 'input[type="checkbox"][value="all"]'
) as HTMLInputElement; ) as HTMLInputElement;
const yearInputs = Array.from( const yearInputs = Array.from(
target target
.closest(".dropdown-content") .closest(".dropdown-content")
?.querySelectorAll('#yearCheckboxes input[type="checkbox"]') || [], ?.querySelectorAll(
'#yearCheckboxes input[type="checkbox"]'
) || []
) as HTMLInputElement[]; ) as HTMLInputElement[];
if (target.value === "all" && target.checked) { if (target.value === "all" && target.checked) {
@ -1320,7 +1408,8 @@ const currentPage = eventResponse.page;
input.checked = false; input.checked = false;
}); });
filterState.year = ["all"]; filterState.year = ["all"];
document.getElementById("yearFilterLabel")!.textContent = "All Years"; document.getElementById("yearFilterLabel")!.textContent =
"All Years";
} else { } else {
const checkedYears = yearInputs const checkedYears = yearInputs
.filter((inp) => inp.checked) .filter((inp) => inp.checked)
@ -1329,7 +1418,8 @@ const currentPage = eventResponse.page;
if (checkedYears.length === 0) { if (checkedYears.length === 0) {
allYearsCheckbox.checked = true; allYearsCheckbox.checked = true;
filterState.year = ["all"]; filterState.year = ["all"];
document.getElementById("yearFilterLabel")!.textContent = "All Years"; document.getElementById("yearFilterLabel")!.textContent =
"All Years";
} else { } else {
allYearsCheckbox.checked = false; allYearsCheckbox.checked = false;
filterState.year = checkedYears; filterState.year = checkedYears;
@ -1349,7 +1439,9 @@ const currentPage = eventResponse.page;
?.addEventListener("change", (e) => { ?.addEventListener("change", (e) => {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
const yearInputs = Array.from( const yearInputs = Array.from(
document.querySelectorAll('#yearCheckboxes input[type="checkbox"]'), document.querySelectorAll(
'#yearCheckboxes input[type="checkbox"]'
)
) as HTMLInputElement[]; ) as HTMLInputElement[];
if (target.checked) { if (target.checked) {
@ -1357,7 +1449,8 @@ const currentPage = eventResponse.page;
input.checked = false; input.checked = false;
}); });
filterState.year = ["all"]; filterState.year = ["all"];
document.getElementById("yearFilterLabel")!.textContent = "All Years"; document.getElementById("yearFilterLabel")!.textContent =
"All Years";
currentPage = 1; currentPage = 1;
fetchEvents(); fetchEvents();
} }
@ -1373,15 +1466,15 @@ const currentPage = eventResponse.page;
const allQuartersCheckbox = target const allQuartersCheckbox = target
.closest("#quarterDropdownContent") .closest("#quarterDropdownContent")
?.querySelector( ?.querySelector(
'input[type="checkbox"][value="all"]', 'input[type="checkbox"][value="all"]'
) as HTMLInputElement; ) as HTMLInputElement;
const quarterInputs = Array.from( const quarterInputs = Array.from(
target target
.closest("#quarterDropdownContent") .closest("#quarterDropdownContent")
?.querySelectorAll('input[type="checkbox"]') || [], ?.querySelectorAll('input[type="checkbox"]') || []
).filter( ).filter(
(inp) => (inp as HTMLInputElement).value !== "all", (inp) => (inp as HTMLInputElement).value !== "all"
) as HTMLInputElement[]; ) as HTMLInputElement[];
if (target.value === "all" && target.checked) { if (target.value === "all" && target.checked) {
@ -1423,8 +1516,8 @@ const currentPage = eventResponse.page;
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
filterState.published = target.value; filterState.published = target.value;
document.getElementById("publishedFilterLabel")!.textContent = document.getElementById("publishedFilterLabel")!.textContent =
target.parentElement?.querySelector(".label-text")?.textContent || target.parentElement?.querySelector(".label-text")
"All"; ?.textContent || "All";
currentPage = 1; currentPage = 1;
fetchEvents(); fetchEvents();
}); });
@ -1438,8 +1531,8 @@ const currentPage = eventResponse.page;
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
filterState.hasFiles = target.value; filterState.hasFiles = target.value;
document.getElementById("hasFilesFilterLabel")!.textContent = document.getElementById("hasFilesFilterLabel")!.textContent =
target.parentElement?.querySelector(".label-text")?.textContent || target.parentElement?.querySelector(".label-text")
"All"; ?.textContent || "All";
currentPage = 1; currentPage = 1;
fetchEvents(); fetchEvents();
}); });
@ -1453,8 +1546,8 @@ const currentPage = eventResponse.page;
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
filterState.hasFood = target.value; filterState.hasFood = target.value;
document.getElementById("hasFoodFilterLabel")!.textContent = document.getElementById("hasFoodFilterLabel")!.textContent =
target.parentElement?.querySelector(".label-text")?.textContent || target.parentElement?.querySelector(".label-text")
"All"; ?.textContent || "All";
currentPage = 1; currentPage = 1;
fetchEvents(); fetchEvents();
}); });
@ -1483,7 +1576,9 @@ const currentPage = eventResponse.page;
} }
// Per page select // Per page select
document.getElementById("perPageSelect")?.addEventListener("change", (e) => { document
.getElementById("perPageSelect")
?.addEventListener("change", (e) => {
const target = e.target as HTMLSelectElement; const target = e.target as HTMLSelectElement;
perPage = parseInt(target.value); perPage = parseInt(target.value);
currentPage = 1; currentPage = 1;
@ -1495,7 +1590,7 @@ const currentPage = eventResponse.page;
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", () => {
// Reset all filters to defaults // Reset all filters to defaults
const timeFilterAll = document.querySelector( const timeFilterAll = document.querySelector(
'input[name="timeFilter"][value="all"]', 'input[name="timeFilter"][value="all"]'
) as HTMLInputElement; ) as HTMLInputElement;
if (timeFilterAll) { if (timeFilterAll) {
// Ensure the radio button is properly checked // Ensure the radio button is properly checked
@ -1505,10 +1600,10 @@ const currentPage = eventResponse.page;
// Reset year filter // Reset year filter
const yearAllCheckbox = document.querySelector( const yearAllCheckbox = document.querySelector(
'input[type="checkbox"][value="all"]', 'input[type="checkbox"][value="all"]'
) as HTMLInputElement; ) as HTMLInputElement;
const yearCheckboxes = document.querySelectorAll( const yearCheckboxes = document.querySelectorAll(
'#yearCheckboxes input[type="checkbox"]', '#yearCheckboxes input[type="checkbox"]'
); );
if (yearAllCheckbox && yearCheckboxes) { if (yearAllCheckbox && yearCheckboxes) {
yearAllCheckbox.checked = true; yearAllCheckbox.checked = true;
@ -1516,15 +1611,16 @@ const currentPage = eventResponse.page;
(checkbox as HTMLInputElement).checked = false; (checkbox as HTMLInputElement).checked = false;
}); });
filterState.year = ["all"]; filterState.year = ["all"];
document.getElementById("yearFilterLabel")!.textContent = "All Years"; document.getElementById("yearFilterLabel")!.textContent =
"All Years";
} }
// Reset quarter filter // Reset quarter filter
const quarterAllCheckbox = document.querySelector( const quarterAllCheckbox = document.querySelector(
'#quarterDropdownContent input[type="checkbox"][value="all"]', '#quarterDropdownContent input[type="checkbox"][value="all"]'
) as HTMLInputElement; ) as HTMLInputElement;
const quarterCheckboxes = document.querySelectorAll( const quarterCheckboxes = document.querySelectorAll(
'#quarterDropdownContent input[type="checkbox"]:not([value="all"])', '#quarterDropdownContent input[type="checkbox"]:not([value="all"])'
); );
if (quarterAllCheckbox && quarterCheckboxes) { if (quarterAllCheckbox && quarterCheckboxes) {
quarterAllCheckbox.checked = true; quarterAllCheckbox.checked = true;
@ -1537,27 +1633,27 @@ const currentPage = eventResponse.page;
} }
const publishedFilter = document.getElementById( const publishedFilter = document.getElementById(
"publishedFilter", "publishedFilter"
) as HTMLSelectElement; ) as HTMLSelectElement;
if (publishedFilter) publishedFilter.value = "all"; if (publishedFilter) publishedFilter.value = "all";
const hasFilesFilter = document.getElementById( const hasFilesFilter = document.getElementById(
"hasFilesFilter", "hasFilesFilter"
) as HTMLSelectElement; ) as HTMLSelectElement;
if (hasFilesFilter) hasFilesFilter.value = "all"; if (hasFilesFilter) hasFilesFilter.value = "all";
const hasFoodFilter = document.getElementById( const hasFoodFilter = document.getElementById(
"hasFoodFilter", "hasFoodFilter"
) as HTMLSelectElement; ) as HTMLSelectElement;
if (hasFoodFilter) hasFoodFilter.value = "all"; if (hasFoodFilter) hasFoodFilter.value = "all";
const searchInput = document.getElementById( const searchInput = document.getElementById(
"searchInput", "searchInput"
) as HTMLInputElement; ) as HTMLInputElement;
if (searchInput) searchInput.value = ""; if (searchInput) searchInput.value = "";
const perPageSelect = document.getElementById( const perPageSelect = document.getElementById(
"perPageSelect", "perPageSelect"
) as HTMLSelectElement; ) as HTMLSelectElement;
if (perPageSelect) perPageSelect.value = "5"; if (perPageSelect) perPageSelect.value = "5";
@ -1587,9 +1683,11 @@ const currentPage = eventResponse.page;
// Add openEditModal function // Add openEditModal function
window.openEditModal = async function (event?: any) { window.openEditModal = async function (event?: any) {
const modal = document.getElementById( const modal = document.getElementById(
"editEventModal", "editEventModal"
) as HTMLDialogElement; ) as HTMLDialogElement;
const form = document.getElementById("editEventForm") as HTMLFormElement; const form = document.getElementById(
"editEventForm"
) as HTMLFormElement;
const modalTitle = document.getElementById("editModalTitle"); const modalTitle = document.getElementById("editModalTitle");
const currentFiles = document.getElementById("currentFiles"); const currentFiles = document.getElementById("currentFiles");
const newFiles = document.getElementById("newFiles"); const newFiles = document.getElementById("newFiles");
@ -1609,46 +1707,51 @@ const currentPage = eventResponse.page;
if (event) { if (event) {
// Fetch fresh data from PocketBase for the event // Fetch fresh data from PocketBase for the event
const freshEventData = await get.getOne<Event>("events", event.id); const freshEventData = await get.getOne<Event>(
"events",
event.id
);
// Populate form with fresh data // Populate form with fresh data
const idInput = document.getElementById( const idInput = document.getElementById(
"editEventId", "editEventId"
) as HTMLInputElement; ) as HTMLInputElement;
const nameInput = document.getElementById( const nameInput = document.getElementById(
"editEventName", "editEventName"
) as HTMLInputElement; ) as HTMLInputElement;
const codeInput = document.getElementById( const codeInput = document.getElementById(
"editEventCode", "editEventCode"
) as HTMLInputElement; ) as HTMLInputElement;
const locationInput = document.getElementById( const locationInput = document.getElementById(
"editEventLocation", "editEventLocation"
) as HTMLInputElement; ) as HTMLInputElement;
const pointsInput = document.getElementById( const pointsInput = document.getElementById(
"editEventPoints", "editEventPoints"
) as HTMLInputElement; ) as HTMLInputElement;
const startDateInput = document.getElementById( const startDateInput = document.getElementById(
"editEventStartDate", "editEventStartDate"
) as HTMLInputElement; ) as HTMLInputElement;
const endDateInput = document.getElementById( const endDateInput = document.getElementById(
"editEventEndDate", "editEventEndDate"
) as HTMLInputElement; ) as HTMLInputElement;
const descriptionInput = document.getElementById( const descriptionInput = document.getElementById(
"editEventDescription", "editEventDescription"
) as HTMLTextAreaElement; ) as HTMLTextAreaElement;
const publishedInput = document.getElementById( const publishedInput = document.getElementById(
"editEventPublished", "editEventPublished"
) as HTMLInputElement; ) as HTMLInputElement;
const hasFoodInput = document.getElementById( const hasFoodInput = document.getElementById(
"editEventHasFood", "editEventHasFood"
) as HTMLInputElement; ) as HTMLInputElement;
if (idInput) idInput.value = freshEventData.id; if (idInput) idInput.value = freshEventData.id;
if (nameInput) nameInput.value = freshEventData.event_name; if (nameInput) nameInput.value = freshEventData.event_name;
if (codeInput) codeInput.value = freshEventData.event_code; if (codeInput) codeInput.value = freshEventData.event_code;
if (locationInput) locationInput.value = freshEventData.location; if (locationInput)
locationInput.value = freshEventData.location;
if (pointsInput) if (pointsInput)
pointsInput.value = freshEventData.points_to_reward.toString(); pointsInput.value =
freshEventData.points_to_reward.toString();
if (startDateInput) if (startDateInput)
startDateInput.value = new Date(freshEventData.start_date) startDateInput.value = new Date(freshEventData.start_date)
.toISOString() .toISOString()
@ -1659,8 +1762,10 @@ const currentPage = eventResponse.page;
.slice(0, 16); .slice(0, 16);
if (descriptionInput) if (descriptionInput)
descriptionInput.value = freshEventData.event_description; descriptionInput.value = freshEventData.event_description;
if (publishedInput) publishedInput.checked = freshEventData.published; if (publishedInput)
if (hasFoodInput) hasFoodInput.checked = freshEventData.has_food; publishedInput.checked = freshEventData.published;
if (hasFoodInput)
hasFoodInput.checked = freshEventData.has_food;
// Display current files if any // Display current files if any
if ( if (
@ -1670,7 +1775,7 @@ const currentPage = eventResponse.page;
) { ) {
currentFiles.innerHTML = updateFilePreviewButtons( currentFiles.innerHTML = updateFilePreviewButtons(
freshEventData.files, freshEventData.files,
freshEventData.id, freshEventData.id
); );
} }
} }
@ -1686,10 +1791,12 @@ const currentPage = eventResponse.page;
// Update the previewFileInEditModal function // Update the previewFileInEditModal function
window.previewFileInEditModal = async function ( window.previewFileInEditModal = async function (
url: string, url: string,
filename: string, filename: string
) { ) {
const editFormSection = document.getElementById("editFormSection"); const editFormSection = document.getElementById("editFormSection");
const previewSection = document.getElementById("editModalPreviewSection"); const previewSection = document.getElementById(
"editModalPreviewSection"
);
const editFilePreview = document.getElementById("editFilePreview"); const editFilePreview = document.getElementById("editFilePreview");
const previewFileName = document.getElementById("editPreviewFileName"); const previewFileName = document.getElementById("editPreviewFileName");
const loadingSpinner = document.getElementById("editLoadingSpinner"); const loadingSpinner = document.getElementById("editLoadingSpinner");
@ -1732,7 +1839,9 @@ const currentPage = eventResponse.page;
// Add backToEditForm function // Add backToEditForm function
window.backToEditForm = function () { window.backToEditForm = function () {
const editFormSection = document.getElementById("editFormSection"); const editFormSection = document.getElementById("editFormSection");
const previewSection = document.getElementById("editModalPreviewSection"); const previewSection = document.getElementById(
"editModalPreviewSection"
);
const editFilePreview = document.getElementById("editFilePreview"); const editFilePreview = document.getElementById("editFilePreview");
const previewFileName = document.getElementById("editPreviewFileName"); const previewFileName = document.getElementById("editPreviewFileName");
@ -1757,9 +1866,11 @@ const currentPage = eventResponse.page;
window.previewFile = function (url: string, filename: string) { window.previewFile = function (url: string, filename: string) {
console.log("previewFile called with:", { url, filename }); console.log("previewFile called with:", { url, filename });
const modal = document.getElementById( const modal = document.getElementById(
"filePreviewModal", "filePreviewModal"
) as HTMLDialogElement; ) as HTMLDialogElement;
const filePreview = document.getElementById("officerFilePreview") as any; const filePreview = document.getElementById(
"officerFilePreview"
) as any;
const previewFileName = document.getElementById("previewFileName"); const previewFileName = document.getElementById("previewFileName");
if (filePreview && modal && previewFileName) { if (filePreview && modal && previewFileName) {
@ -1789,9 +1900,11 @@ const currentPage = eventResponse.page;
window.closeFilePreview = function () { window.closeFilePreview = function () {
console.log("closeFilePreview called"); console.log("closeFilePreview called");
const modal = document.getElementById( const modal = document.getElementById(
"filePreviewModal", "filePreviewModal"
) as HTMLDialogElement; ) as HTMLDialogElement;
const filePreview = document.getElementById("officerFilePreview") as any; const filePreview = document.getElementById(
"officerFilePreview"
) as any;
const previewFileName = document.getElementById("previewFileName"); const previewFileName = document.getElementById("previewFileName");
if (modal && filePreview && previewFileName) { if (modal && filePreview && previewFileName) {
@ -1809,7 +1922,7 @@ const currentPage = eventResponse.page;
// Close event details modal // Close event details modal
window.closeEventDetailsModal = function () { window.closeEventDetailsModal = function () {
const modal = document.getElementById( const modal = document.getElementById(
"eventDetailsModal", "eventDetailsModal"
) as HTMLDialogElement; ) as HTMLDialogElement;
const filesContent = document.getElementById("filesContent"); const filesContent = document.getElementById("filesContent");
const attendeesContent = document.getElementById("attendeesContent"); const attendeesContent = document.getElementById("attendeesContent");
@ -1830,7 +1943,11 @@ const currentPage = eventResponse.page;
function updateFilePreviewButtons(files: string[], eventId: string) { function updateFilePreviewButtons(files: string[], eventId: string) {
return files return files
.map((filename) => { .map((filename) => {
const fileUrl = fileManager.getFileUrl("events", eventId, filename); const fileUrl = fileManager.getFileUrl(
"events",
eventId,
filename
);
const previewData = JSON.stringify({ const previewData = JSON.stringify({
url: fileUrl, url: fileUrl,
name: filename, name: filename,
@ -1875,10 +1992,12 @@ const currentPage = eventResponse.page;
if (newFiles && fileInput.files) { if (newFiles && fileInput.files) {
// Get existing files if any // Get existing files if any
const existingFiles = newFiles.querySelectorAll(".file-item"); const existingFiles = newFiles.querySelectorAll(".file-item");
const existingFilesArray = Array.from(existingFiles).map((item) => { const existingFilesArray = Array.from(existingFiles).map(
(item) => {
const nameSpan = item.querySelector(".file-name"); const nameSpan = item.querySelector(".file-name");
return nameSpan ? nameSpan.textContent : ""; return nameSpan ? nameSpan.textContent : "";
}); }
);
// Store new files in the storage and update UI // Store new files in the storage and update UI
Array.from(fileInput.files) Array.from(fileInput.files)
@ -1925,11 +2044,12 @@ const currentPage = eventResponse.page;
const currentFiles = document.getElementById("currentFiles"); const currentFiles = document.getElementById("currentFiles");
if (currentFiles) { if (currentFiles) {
const fileElement = currentFiles.querySelector( const fileElement = currentFiles.querySelector(
`[data-filename="${filename}"]`, `[data-filename="${filename}"]`
); );
if (fileElement) { if (fileElement) {
fileElement.classList.add("opacity-50"); fileElement.classList.add("opacity-50");
const deleteButton = fileElement.querySelector(".text-error"); const deleteButton =
fileElement.querySelector(".text-error");
if (deleteButton) { if (deleteButton) {
deleteButton.innerHTML = ` deleteButton.innerHTML = `
<button type="button" class="btn btn-ghost btn-xs" onclick="window.undoDeleteFile('${eventId}', '${filename}')"> <button type="button" class="btn btn-ghost btn-xs" onclick="window.undoDeleteFile('${eventId}', '${filename}')">
@ -1939,9 +2059,17 @@ const currentPage = eventResponse.page;
} }
} }
} }
// Show success toast
toast.success(
`File "${filename}" marked for deletion. Save changes to confirm.`,
{
icon: "🗑️",
}
);
} catch (error) { } catch (error) {
console.error("Failed to stage file deletion:", error); console.error("Failed to stage file deletion:", error);
alert("Failed to stage file deletion. Please try again."); toast.error("Failed to stage file deletion. Please try again.");
} }
}; };
@ -1953,7 +2081,7 @@ const currentPage = eventResponse.page;
const currentFiles = document.getElementById("currentFiles"); const currentFiles = document.getElementById("currentFiles");
if (currentFiles) { if (currentFiles) {
const fileElement = currentFiles.querySelector( const fileElement = currentFiles.querySelector(
`[data-filename="${filename}"]`, `[data-filename="${filename}"]`
); );
if (fileElement) { if (fileElement) {
fileElement.classList.remove("opacity-50"); fileElement.classList.remove("opacity-50");
@ -1969,6 +2097,11 @@ const currentPage = eventResponse.page;
} }
} }
} }
// Show success toast
toast.success(`Restored file "${filename}"`, {
icon: "↩️",
});
}; };
// Create a custom event for file preview state management // Create a custom event for file preview state management
@ -1977,7 +2110,7 @@ const currentPage = eventResponse.page;
// Universal file preview function for officer section // Universal file preview function for officer section
window.previewFileOfficer = function (url: string, filename: string) { window.previewFileOfficer = function (url: string, filename: string) {
const modal = document.getElementById( const modal = document.getElementById(
"filePreviewModal", "filePreviewModal"
) as HTMLDialogElement; ) as HTMLDialogElement;
if (!modal) { if (!modal) {
@ -1989,7 +2122,7 @@ const currentPage = eventResponse.page;
window.dispatchEvent( window.dispatchEvent(
new CustomEvent(FILE_PREVIEW_STATE_CHANGE, { new CustomEvent(FILE_PREVIEW_STATE_CHANGE, {
detail: { url, filename }, detail: { url, filename },
}), })
); );
// Show modal after event dispatch // Show modal after event dispatch
@ -2001,7 +2134,7 @@ const currentPage = eventResponse.page;
// Close file preview for officer section // Close file preview for officer section
window.closeFilePreviewOfficer = function () { window.closeFilePreviewOfficer = function () {
const modal = document.getElementById( const modal = document.getElementById(
"filePreviewModal", "filePreviewModal"
) as HTMLDialogElement; ) as HTMLDialogElement;
const previewContent = document.getElementById("previewContent"); const previewContent = document.getElementById("previewContent");
@ -2024,7 +2157,7 @@ const currentPage = eventResponse.page;
window.dispatchEvent( window.dispatchEvent(
new CustomEvent(FILE_PREVIEW_STATE_CHANGE, { new CustomEvent(FILE_PREVIEW_STATE_CHANGE, {
detail: { url: "", filename: "" }, detail: { url: "", filename: "" },
}), })
); );
// Close modal after cleanup // Close modal after cleanup
@ -2040,6 +2173,7 @@ const currentPage = eventResponse.page;
}) { }) {
if (!file || !file.url || !file.name) { if (!file || !file.url || !file.name) {
console.error("Invalid file data provided"); console.error("Invalid file data provided");
toast.error("Invalid file data provided");
return; return;
} }
@ -2057,14 +2191,18 @@ const currentPage = eventResponse.page;
} }
// Get fresh URL from FileManager to ensure we have the latest // Get fresh URL from FileManager to ensure we have the latest
const freshUrl = fileManager.getFileUrl("events", eventId, file.name); const freshUrl = fileManager.getFileUrl(
"events",
eventId,
file.name
);
// Show the preview with fresh URL // Show the preview with fresh URL
window.previewFileOfficer(freshUrl, file.name); window.previewFileOfficer(freshUrl, file.name);
} catch (error) { } catch (error) {
console.error("Failed to fetch fresh file data:", error); console.error("Failed to fetch fresh file data:", error);
alert( toast.error(
"Failed to load file preview. The file may have been deleted or modified.", "Failed to load file preview. The file may have been deleted or modified."
); );
} }
}; };
@ -2077,7 +2215,7 @@ const currentPage = eventResponse.page;
console.log("Opening attendees modal for event:", event.id); console.log("Opening attendees modal for event:", event.id);
const modal = document.getElementById( const modal = document.getElementById(
"attendeesModal", "attendeesModal"
) as HTMLDialogElement; ) as HTMLDialogElement;
const modalTitle = document.getElementById("attendeesModalTitle"); const modalTitle = document.getElementById("attendeesModalTitle");
@ -2096,7 +2234,7 @@ const currentPage = eventResponse.page;
eventId: event.id, eventId: event.id,
eventName: event.event_name, eventName: event.event_name,
}, },
}), })
); );
// Show modal // Show modal
@ -2106,7 +2244,7 @@ const currentPage = eventResponse.page;
// Add event listeners when the document loads // Add event listeners when the document loads
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const modal = document.getElementById( const modal = document.getElementById(
"filePreviewModal", "filePreviewModal"
) as HTMLDialogElement; ) as HTMLDialogElement;
if (modal) { if (modal) {
// Handle modal close via backdrop // Handle modal close via backdrop

View file

@ -5,13 +5,22 @@ import { SendLog } from '../../../scripts/pocketbase/SendLog';
import { DataSyncService } from '../../../scripts/database/DataSyncService'; import { DataSyncService } from '../../../scripts/database/DataSyncService';
import { Collections } from '../../../schemas/pocketbase/schema'; import { Collections } from '../../../schemas/pocketbase/schema';
import { Icon } from "@iconify/react"; import { Icon } from "@iconify/react";
import type { Event, AttendeeEntry, User as SchemaUser } from "../../../schemas/pocketbase"; import type { Event, User as SchemaUser, EventAttendee } from "../../../schemas/pocketbase";
import toast from "react-hot-toast";
// Extended User interface with additional properties needed for this component // Extended User interface with additional properties needed for this component
interface User extends SchemaUser { interface User extends SchemaUser {
member_type: string; member_type: string;
} }
// Define AttendeeEntry interface locally
interface AttendeeEntry {
user_id: string;
time_checked_in: string;
food: string;
points_earned?: number;
}
// Cache for storing user data // Cache for storing user data
const userCache = new Map<string, { const userCache = new Map<string, {
data: User; data: User;
@ -58,7 +67,6 @@ const HighlightText = ({ text, searchTerms }: { text: string | number | null | u
interface EventFields { interface EventFields {
id: true; id: true;
event_name: true; event_name: true;
attendees: true;
} }
interface UserFields { interface UserFields {
@ -73,7 +81,7 @@ interface UserFields {
} }
// Constants for field selection // Constants for field selection
const EVENT_FIELDS: (keyof EventFields)[] = ['id', 'event_name', 'attendees']; const EVENT_FIELDS: (keyof EventFields)[] = ['id', 'event_name'];
const USER_FIELDS: (keyof UserFields)[] = ['id', 'name', 'email', 'pid', 'member_id', 'member_type', 'graduation_year', 'major']; const USER_FIELDS: (keyof UserFields)[] = ['id', 'name', 'email', 'pid', 'member_id', 'member_type', 'graduation_year', 'major'];
export default function Attendees() { export default function Attendees() {
@ -86,6 +94,7 @@ export default function Attendees() {
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [processedSearchTerms, setProcessedSearchTerms] = useState<string[]>([]); const [processedSearchTerms, setProcessedSearchTerms] = useState<string[]>([]);
const [refreshKey, setRefreshKey] = useState(0); // Add a refresh key to force re-fetching
const get = Get.getInstance(); const get = Get.getInstance();
const auth = Authentication.getInstance(); const auth = Authentication.getInstance();
@ -141,6 +150,8 @@ export default function Attendees() {
// Optimized user data fetching with cache // Optimized user data fetching with cache
const fetchUserData = useCallback(async (userIds: string[]) => { const fetchUserData = useCallback(async (userIds: string[]) => {
if (!userIds.length) return new Map<string, User>();
const now = Date.now(); const now = Date.now();
const uncachedIds: string[] = []; const uncachedIds: string[] = [];
const cachedUsers = new Map<string, User>(); const cachedUsers = new Map<string, User>();
@ -160,48 +171,86 @@ export default function Attendees() {
return cachedUsers; return cachedUsers;
} }
// Fetch uncached users
try { try {
const dataSync = DataSyncService.getInstance(); // Create a filter to get all uncached users in one request
const userFilter = uncachedIds.map(id => `id="${id}"`).join(" || ");
// Sync users collection for the uncached IDs // Fetch all uncached users in one request
if (uncachedIds.length > 0) { const usersResponse = await get.getAll<User>(
const idFilter = uncachedIds.map(id => `id = "${id}"`).join(' || '); Collections.USERS,
await dataSync.syncCollection(Collections.USERS, idFilter); userFilter
}
// Get users from IndexedDB
const users = await Promise.all(
uncachedIds.map(async id => {
try {
return await dataSync.getItem<User>(Collections.USERS, id);
} catch (error) {
console.error(`Failed to fetch user ${id}:`, error);
return null;
}
})
); );
// Update cache and merge with cached users // Process the fetched users
users.forEach(user => { usersResponse.forEach(user => {
if (user) { // Add member_type if it doesn't exist
userCache.set(user.id, { data: user, timestamp: now }); const userWithMemberType = {
cachedUsers.set(user.id, user); ...user,
member_type: user.member_type || "N/A"
};
cachedUsers.set(user.id, userWithMemberType);
// Update cache
userCache.set(user.id, {
data: userWithMemberType,
timestamp: now
});
});
// Create placeholders for any users that weren't found
const fetchedIds = new Set(usersResponse.map(user => user.id));
uncachedIds.forEach(id => {
if (!fetchedIds.has(id) && !cachedUsers.has(id)) {
// Create a placeholder user
const placeholderUser: User = {
id,
name: `User ${id}`,
email: "N/A",
emailVisibility: false,
verified: false,
created: "",
updated: "",
member_type: "N/A"
};
cachedUsers.set(id, placeholderUser);
} }
}); });
} catch (error) { } catch (error) {
console.error('Failed to fetch uncached users:', error); console.error('Failed to fetch users:', error);
// Create placeholders for all uncached users that failed to fetch
uncachedIds.forEach(id => {
if (!cachedUsers.has(id)) {
const placeholderUser: User = {
id,
name: `User ${id}`,
email: "N/A",
emailVisibility: false,
verified: false,
created: "",
updated: "",
member_type: "N/A"
};
cachedUsers.set(id, placeholderUser);
}
});
} }
return cachedUsers; return cachedUsers;
}, []); }, []);
// Function to refresh attendees data
const refreshAttendees = useCallback(() => {
setRefreshKey(prev => prev + 1);
}, []);
// Listen for the custom event // Listen for the custom event
useEffect(() => { useEffect(() => {
const handleUpdateAttendees = async (e: CustomEvent<{ eventId: string; eventName: string }>) => { const handleUpdateAttendees = async (e: CustomEvent<{ eventId: string; eventName: string }>) => {
setEventId(e.detail.eventId); setEventId(e.detail.eventId);
setEventName(e.detail.eventName); setEventName(e.detail.eventName);
setCurrentPage(1); // Reset pagination on new event setCurrentPage(1); // Reset pagination on new event
setSearchTerm(''); // Clear search on new event
// Log the attendees view action // Log the attendees view action
try { try {
@ -217,17 +266,22 @@ export default function Attendees() {
}; };
window.addEventListener('updateAttendees', handleUpdateAttendees as unknown as EventListener); window.addEventListener('updateAttendees', handleUpdateAttendees as unknown as EventListener);
// Expose refresh function to window
(window as any).refreshAttendees = refreshAttendees;
return () => { return () => {
window.removeEventListener('updateAttendees', handleUpdateAttendees as unknown as EventListener); window.removeEventListener('updateAttendees', handleUpdateAttendees as unknown as EventListener);
delete (window as any).refreshAttendees;
}; };
}, []); }, [refreshAttendees]);
// Update search terms when search input changes // Update search terms when search input changes
useEffect(() => { useEffect(() => {
updateProcessedSearchTerms(searchTerm); updateProcessedSearchTerms(searchTerm);
}, [searchTerm, updateProcessedSearchTerms]); }, [searchTerm, updateProcessedSearchTerms]);
// Fetch event data when eventId changes // Fetch event data when eventId changes or refreshKey changes
useEffect(() => { useEffect(() => {
let isMounted = true; let isMounted = true;
const fetchEventData = async () => { const fetchEventData = async () => {
@ -243,44 +297,118 @@ export default function Attendees() {
setLoading(true); setLoading(true);
setError(null); setError(null);
if (!eventId) {
setAttendeesList([]);
setUsers(new Map());
return;
}
// Clear cache to ensure fresh data
const dataSync = DataSyncService.getInstance(); const dataSync = DataSyncService.getInstance();
await dataSync.clearCache();
await dataSync.syncCollection(Collections.EVENTS, `id="${eventId}"`);
// Sync the event data const event = await get.getOne<Event>(Collections.EVENTS, eventId);
await dataSync.syncCollection(Collections.EVENTS, `id = "${eventId}"`);
// Get the event from IndexedDB
const event = await dataSync.getItem<Event>(Collections.EVENTS, eventId);
if (!isMounted) return;
if (!event) { if (!event) {
setError('Event not found'); setError("Event not found");
setAttendeesList([]); setAttendeesList([]);
setUsers(new Map()); setUsers(new Map());
return; return;
} }
if (!event.attendees?.length) { // Fetch attendees from event_attendees collection with a higher limit
const attendeesList = await get.getList<EventAttendee>(
Collections.EVENT_ATTENDEES,
1,
2000, // Increased limit to handle more attendees
`event="${eventId}"`
);
if (!attendeesList.items.length) {
if (isMounted) {
setAttendeesList([]); setAttendeesList([]);
setUsers(new Map()); setUsers(new Map());
}
return; return;
} }
setAttendeesList(event.attendees); // Transform EventAttendee records to match the expected format
const transformedAttendees = attendeesList.items.map(attendee => ({
user_id: attendee.user, // This is the user ID (relation)
time_checked_in: attendee.time_checked_in,
food: attendee.food_ate,
points_earned: attendee.points_earned
}));
// Fetch user details with cache if (isMounted) {
const userIds = [...new Set(event.attendees.map(a => a.user_id))]; setAttendeesList(transformedAttendees);
const userMap = await fetchUserData(userIds); }
// Fetch all users at once to improve performance
const userIds = transformedAttendees.map(a => a.user_id);
// Create a filter to get all users in one request
const userFilter = userIds.map(id => `id="${id}"`).join(" || ");
try {
// Fetch all users directly from PocketBase in one request
const usersResponse = await get.getAll<User>(
Collections.USERS,
userFilter
);
// Create a map of users
const userMap = new Map<string, User>();
usersResponse.forEach(user => {
// Add member_type if it doesn't exist
const userWithMemberType = {
...user,
member_type: user.member_type || "N/A"
};
userMap.set(user.id, userWithMemberType);
// Update cache
userCache.set(user.id, {
data: userWithMemberType,
timestamp: Date.now()
});
});
// For any missing users, create placeholders
userIds.forEach(id => {
if (!userMap.has(id)) {
const placeholderUser: User = {
id,
name: `User ${id}`,
email: "N/A",
emailVisibility: false,
verified: false,
created: "",
updated: "",
member_type: "N/A"
};
userMap.set(id, placeholderUser);
}
});
if (isMounted) { if (isMounted) {
setUsers(userMap); setUsers(userMap);
} }
} catch (error) { } catch (error) {
console.error("Failed to fetch users:", error);
// Fallback to individual user fetching
const userMap = await fetchUserData(userIds);
if (isMounted) { if (isMounted) {
console.error('Failed to fetch event data:', error); setUsers(userMap);
setError('Failed to load event data');
setAttendeesList([]);
} }
}
toast.success(`Loaded ${attendeesList.items.length} attendees for ${event.event_name}`);
} catch (error) {
console.error("Error fetching event data:", error);
setError("Failed to load event data. Please try refreshing.");
} finally { } finally {
if (isMounted) { if (isMounted) {
setLoading(false); setLoading(false);
@ -290,15 +418,18 @@ export default function Attendees() {
fetchEventData(); fetchEventData();
return () => { isMounted = false; }; return () => { isMounted = false; };
}, [eventId, auth, fetchUserData]); }, [eventId, auth, fetchUserData, refreshKey]);
// Reset state when modal is closed // Reset state when modal is closed
useEffect(() => { useEffect(() => {
const handleModalClose = () => { const handleModalClose = () => {
setEventId(''); setEventId('');
setEventName('');
setAttendeesList([]); setAttendeesList([]);
setUsers(new Map()); setUsers(new Map());
setError(null); setError(null);
setSearchTerm('');
setCurrentPage(1);
}; };
const modal = document.getElementById('attendeesModal'); const modal = document.getElementById('attendeesModal');
@ -332,15 +463,22 @@ export default function Attendees() {
'Graduation Year', 'Graduation Year',
'Major', 'Major',
'Check-in Time', 'Check-in Time',
'Food Choice' 'Food Choice',
'Points Earned'
].map(escapeCSV); ].map(escapeCSV);
// Create CSV rows // Create CSV rows
const rows = attendeesList.map(attendee => { const rows = attendeesList.map(attendee => {
const user = users.get(attendee.user_id); const user = users.get(attendee.user_id);
const checkInTime = new Date(attendee.time_checked_in).toLocaleString(); let checkInTime = '';
try {
checkInTime = new Date(attendee.time_checked_in).toLocaleString();
} catch (e) {
checkInTime = attendee.time_checked_in || 'N/A';
}
return [ return [
user?.name || 'Unknown User', user?.name || `User ${attendee.user_id}`,
user?.email || 'N/A', user?.email || 'N/A',
user?.pid || 'N/A', user?.pid || 'N/A',
user?.member_id || 'N/A', user?.member_id || 'N/A',
@ -348,7 +486,8 @@ export default function Attendees() {
user?.graduation_year || 'N/A', user?.graduation_year || 'N/A',
user?.major || 'N/A', user?.major || 'N/A',
checkInTime, checkInTime,
attendee.food || 'N/A' attendee.food || 'N/A',
attendee.points_earned || 'N/A'
].map(escapeCSV); ].map(escapeCSV);
}); });
@ -375,6 +514,8 @@ export default function Attendees() {
link.click(); link.click();
document.body.removeChild(link); document.body.removeChild(link);
URL.revokeObjectURL(url); // Clean up the URL object URL.revokeObjectURL(url); // Clean up the URL object
toast.success(`Downloaded ${rows.length} attendee records`);
}; };
if (loading) { if (loading) {
@ -390,6 +531,13 @@ export default function Attendees() {
<div className="alert alert-error"> <div className="alert alert-error">
<Icon icon="heroicons:exclamation-circle" className="h-6 w-6" /> <Icon icon="heroicons:exclamation-circle" className="h-6 w-6" />
<span>{error}</span> <span>{error}</span>
<button
className="btn btn-sm btn-outline"
onClick={refreshAttendees}
>
<Icon icon="heroicons:arrow-path" className="h-4 w-4 mr-1" />
Retry
</button>
</div> </div>
); );
} }
@ -403,6 +551,13 @@ export default function Attendees() {
<div className="text-center py-8 text-base-content/70"> <div className="text-center py-8 text-base-content/70">
<Icon icon="heroicons:user-group" className="h-12 w-12 mx-auto mb-4 opacity-50" /> <Icon icon="heroicons:user-group" className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p>No attendees yet</p> <p>No attendees yet</p>
<button
className="btn btn-sm btn-outline mt-4"
onClick={refreshAttendees}
>
<Icon icon="heroicons:arrow-path" className="h-4 w-4 mr-1" />
Refresh
</button>
</div> </div>
); );
} }
@ -426,6 +581,14 @@ export default function Attendees() {
/> />
</div> </div>
</div> </div>
<div className="flex gap-2">
<button
className="btn btn-outline btn-sm gap-2"
onClick={refreshAttendees}
>
<Icon icon="heroicons:arrow-path" className="h-4 w-4" />
Refresh
</button>
<button <button
className="btn btn-primary btn-sm gap-2" className="btn btn-primary btn-sm gap-2"
onClick={downloadAttendeesCSV} onClick={downloadAttendeesCSV}
@ -434,6 +597,7 @@ export default function Attendees() {
Download CSV Download CSV
</button> </button>
</div> </div>
</div>
{/* Stats Row */} {/* Stats Row */}
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
@ -462,16 +626,22 @@ export default function Attendees() {
<th className="bg-base-100">Major</th> <th className="bg-base-100">Major</th>
<th className="bg-base-100">Check-in Time</th> <th className="bg-base-100">Check-in Time</th>
<th className="bg-base-100">Food Choice</th> <th className="bg-base-100">Food Choice</th>
<th className="bg-base-100">Points</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{paginatedAttendees.map((attendee, index) => { {paginatedAttendees.map((attendee, index) => {
const user = users.get(attendee.user_id); const user = users.get(attendee.user_id);
const checkInTime = new Date(attendee.time_checked_in).toLocaleString(); let checkInTime = '';
try {
checkInTime = new Date(attendee.time_checked_in).toLocaleString();
} catch (e) {
checkInTime = attendee.time_checked_in || 'N/A';
}
return ( return (
<tr key={`${attendee.user_id}-${index}`}> <tr key={`${attendee.user_id}-${index}`}>
<td><HighlightText text={user?.name || 'Unknown User'} searchTerms={processedSearchTerms} /></td> <td><HighlightText text={user?.name || `User ${attendee.user_id}`} searchTerms={processedSearchTerms} /></td>
<td><HighlightText text={user?.email || 'N/A'} searchTerms={processedSearchTerms} /></td> <td><HighlightText text={user?.email || 'N/A'} searchTerms={processedSearchTerms} /></td>
<td><HighlightText text={user?.pid || 'N/A'} searchTerms={processedSearchTerms} /></td> <td><HighlightText text={user?.pid || 'N/A'} searchTerms={processedSearchTerms} /></td>
<td><HighlightText text={user?.member_id || 'N/A'} searchTerms={processedSearchTerms} /></td> <td><HighlightText text={user?.member_id || 'N/A'} searchTerms={processedSearchTerms} /></td>
@ -480,6 +650,7 @@ export default function Attendees() {
<td><HighlightText text={user?.major || 'N/A'} searchTerms={processedSearchTerms} /></td> <td><HighlightText text={user?.major || 'N/A'} searchTerms={processedSearchTerms} /></td>
<td><HighlightText text={checkInTime} searchTerms={processedSearchTerms} /></td> <td><HighlightText text={checkInTime} searchTerms={processedSearchTerms} /></td>
<td><HighlightText text={attendee.food || 'N/A'} searchTerms={processedSearchTerms} /></td> <td><HighlightText text={attendee.food || 'N/A'} searchTerms={processedSearchTerms} /></td>
<td><HighlightText text={attendee.points_earned || 'N/A'} searchTerms={processedSearchTerms} /></td>
</tr> </tr>
); );
})} })}

View file

@ -9,6 +9,7 @@ import FilePreview from "../universal/FilePreview";
import type { Event as SchemaEvent, AttendeeEntry } from "../../../schemas/pocketbase"; import type { Event as SchemaEvent, AttendeeEntry } from "../../../schemas/pocketbase";
import { DataSyncService } from '../../../scripts/database/DataSyncService'; import { DataSyncService } from '../../../scripts/database/DataSyncService';
import { Collections } from '../../../schemas/pocketbase/schema'; import { Collections } from '../../../schemas/pocketbase/schema';
import toast from "react-hot-toast";
// Note: Date conversion is now handled automatically by the Get and Update classes. // Note: Date conversion is now handled automatically by the Get and Update classes.
// When fetching events, UTC dates are converted to local time by the Get class. // When fetching events, UTC dates are converted to local time by the Get class.
@ -510,18 +511,19 @@ const ErrorDisplay = memo(({ error, onRetry }: { error: string; onRetry: () => v
export default function EventEditor({ onEventSaved }: EventEditorProps) { export default function EventEditor({ onEventSaved }: EventEditorProps) {
// State for form data and UI // State for form data and UI
const [event, setEvent] = useState<Event>({ const [event, setEvent] = useState<Event>({
id: '', id: "",
event_name: '', created: "",
event_description: '', updated: "",
event_code: '', event_name: "",
location: '', event_description: "",
event_code: "",
location: "",
files: [], files: [],
points_to_reward: 0, points_to_reward: 0,
start_date: Get.formatLocalDate(new Date(), false), start_date: "",
end_date: Get.formatLocalDate(new Date(), false), end_date: "",
published: false, published: false,
has_food: false, has_food: false
attendees: []
}); });
const [previewUrl, setPreviewUrl] = useState(""); const [previewUrl, setPreviewUrl] = useState("");
@ -557,7 +559,16 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
const initializeEventData = useCallback(async (eventId: string) => { const initializeEventData = useCallback(async (eventId: string) => {
try { try {
if (eventId) { if (eventId) {
const eventData = await services.get.getOne<Event>("events", eventId); // Clear cache to ensure fresh data
const dataSync = DataSyncService.getInstance();
await dataSync.clearCache();
// Fetch fresh event data
const eventData = await services.get.getOne<Event>(Collections.EVENTS, eventId);
if (!eventData) {
throw new Error("Event not found");
}
// Ensure dates are properly formatted for datetime-local input // Ensure dates are properly formatted for datetime-local input
if (eventData.start_date) { if (eventData.start_date) {
@ -573,20 +584,22 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
} }
setEvent(eventData); setEvent(eventData);
console.log("Event data loaded successfully:", eventData);
} else { } else {
setEvent({ setEvent({
id: '', id: '',
created: '',
updated: '',
event_name: '', event_name: '',
event_description: '', event_description: '',
event_code: '', event_code: '',
location: '', location: '',
files: [], files: [],
points_to_reward: 0, points_to_reward: 0,
start_date: Get.formatLocalDate(new Date(), false), start_date: '',
end_date: Get.formatLocalDate(new Date(), false), end_date: '',
published: false, published: false,
has_food: false, has_food: false
attendees: []
}); });
} }
setSelectedFiles(new Map()); setSelectedFiles(new Map());
@ -595,7 +608,7 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
setHasUnsavedChanges(false); setHasUnsavedChanges(false);
} catch (error) { } catch (error) {
console.error("Failed to initialize event data:", error); console.error("Failed to initialize event data:", error);
alert("Failed to load event data. Please try again."); toast.error("Failed to load event data. Please try again.");
} }
}, [services.get]); }, [services.get]);
@ -614,7 +627,7 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
modal.showModal(); modal.showModal();
} catch (error) { } catch (error) {
console.error("Failed to open edit modal:", error); console.error("Failed to open edit modal:", error);
alert("Failed to open edit modal. Please try again."); toast.error("Failed to open edit modal. Please try again.");
} }
}; };
@ -637,23 +650,25 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
} }
setEvent({ setEvent({
id: '', id: "",
event_name: '', created: "",
event_description: '', updated: "",
event_code: '', event_name: "",
location: '', event_description: "",
event_code: "",
location: "",
files: [], files: [],
points_to_reward: 0, points_to_reward: 0,
start_date: Get.formatLocalDate(new Date(), false), start_date: "",
end_date: Get.formatLocalDate(new Date(), false), end_date: "",
published: false, published: false,
has_food: false, has_food: false
attendees: []
}); });
setSelectedFiles(new Map()); setSelectedFiles(new Map());
setFilesToDelete(new Set()); setFilesToDelete(new Set());
setShowPreview(false); setShowPreview(false);
setHasUnsavedChanges(false); setPreviewUrl("");
setPreviewFilename("");
const modal = document.getElementById("editEventModal") as HTMLDialogElement; const modal = document.getElementById("editEventModal") as HTMLDialogElement;
if (modal) modal.close(); if (modal) modal.close();
@ -663,176 +678,160 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
e.preventDefault(); e.preventDefault();
if (isSubmitting) return; if (isSubmitting) return;
const form = e.target as HTMLFormElement; try {
const submitButton = form.querySelector('button[type="submit"]') as HTMLButtonElement;
const cancelButton = form.querySelector('button[type="button"]') as HTMLButtonElement;
setIsSubmitting(true); setIsSubmitting(true);
if (submitButton) submitButton.disabled = true; window.showLoading?.();
const submitButton = document.getElementById("submitEventButton") as HTMLButtonElement;
const cancelButton = document.getElementById("cancelEventButton") as HTMLButtonElement;
if (submitButton) {
submitButton.disabled = true;
submitButton.classList.add("btn-disabled");
}
if (cancelButton) cancelButton.disabled = true; if (cancelButton) cancelButton.disabled = true;
try { // Get form data
window.showLoading?.(); const formData = new FormData(e.currentTarget);
const pb = services.auth.getPocketBase();
console.log('Form submission started'); // Create updated event object
console.log('Event data:', event); const updatedEvent: Event = {
id: event.id,
const formData = new FormData(form); created: event.created,
const eventData = { updated: event.updated,
event_name: formData.get("editEventName"), event_name: formData.get("editEventName") as string,
event_code: formData.get("editEventCode"), event_description: formData.get("editEventDescription") as string,
event_description: formData.get("editEventDescription"), event_code: formData.get("editEventCode") as string,
location: formData.get("editEventLocation"), location: formData.get("editEventLocation") as string,
points_to_reward: Number(formData.get("editEventPoints")), files: event.files || [],
points_to_reward: parseInt(formData.get("editEventPoints") as string) || 0,
start_date: formData.get("editEventStartDate") as string, start_date: formData.get("editEventStartDate") as string,
end_date: formData.get("editEventEndDate") as string, end_date: formData.get("editEventEndDate") as string,
published: formData.get("editEventPublished") === "on", published: formData.get("editEventPublished") === "on",
has_food: formData.get("editEventHasFood") === "on", has_food: formData.get("editEventHasFood") === "on"
attendees: event.attendees || []
}; };
// Log the update attempt
await services.sendLog.send(
"update",
"event",
`Updating event: ${updatedEvent.event_name} (${updatedEvent.id})`
);
// Process file changes
const uploadQueue = new UploadQueue();
const fileChanges: FileChanges = {
added: selectedFiles,
deleted: filesToDelete,
unchanged: event.files?.filter(file => !filesToDelete.has(file)) || []
};
// Handle file deletions
if (fileChanges.deleted.size > 0) {
for (const fileId of fileChanges.deleted) {
await services.fileManager.deleteFile("events", event.id, fileId);
}
}
// Handle file uploads
if (fileChanges.added.size > 0) {
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);
}
});
}
}
// Update the event with the new file list
updatedEvent.files = fileChanges.unchanged;
// Save the event
let savedEvent;
if (event.id) { if (event.id) {
// Update existing event // Update existing event
console.log('Updating event:', event.id); savedEvent = await services.update.updateFields<Event>(
await services.update.updateFields("events", event.id, eventData); Collections.EVENTS,
event.id,
updatedEvent
);
// Handle file deletions first // Clear cache to ensure fresh data
if (filesToDelete.size > 0) {
console.log('Deleting files:', Array.from(filesToDelete));
// Get current files
const currentRecord = await pb.collection("events").getOne(event.id);
let remainingFiles = [...currentRecord.files];
// Remove files marked for deletion
for (const filename of filesToDelete) {
const fileIndex = remainingFiles.indexOf(filename);
if (fileIndex > -1) {
remainingFiles.splice(fileIndex, 1);
}
}
// Update record with remaining files
await pb.collection("events").update(event.id, {
files: remainingFiles
});
// Sync the events collection to update IndexedDB
const dataSync = DataSyncService.getInstance(); const dataSync = DataSyncService.getInstance();
await dataSync.syncCollection(Collections.EVENTS); await dataSync.clearCache();
}
// Handle file additions // Log success
if (selectedFiles.size > 0) { await services.sendLog.send(
try { "success",
// Convert Map to array of Files "event_update",
const filesToUpload = Array.from(selectedFiles.values()); `Successfully updated event: ${savedEvent.event_name}`
console.log('Uploading files:', filesToUpload.map(f => f.name)); );
// Use appendFiles to preserve existing files // Show success toast
await services.fileManager.appendFiles("events", event.id, "files", filesToUpload); toast.success(`Event "${savedEvent.event_name}" updated successfully!`);
// Sync the events collection to update IndexedDB
const dataSync = DataSyncService.getInstance();
await dataSync.syncCollection(Collections.EVENTS);
} catch (error: any) {
if (error.status === 413) {
throw new Error("Files are too large. Please try uploading smaller files or fewer files at once.");
}
throw error;
}
}
} else { } else {
// Create new event // Create new event
console.log('Creating new event'); savedEvent = await services.update.create<Event>(
const newEvent = await pb.collection("events").create(eventData); Collections.EVENTS,
console.log('New event created:', newEvent); updatedEvent
);
// Sync the events collection to update IndexedDB // Log success
const dataSync = DataSyncService.getInstance(); await services.sendLog.send(
await dataSync.syncCollection(Collections.EVENTS); "success",
"event_create",
`Successfully created event: ${savedEvent.event_name}`
);
// Upload files if any // Show success toast
if (selectedFiles.size > 0) { toast.success(`Event "${savedEvent.event_name}" created successfully!`);
try {
const filesToUpload = Array.from(selectedFiles.values());
console.log('Uploading files:', filesToUpload.map(f => f.name));
// Use uploadFiles for new event
await services.fileManager.uploadFiles("events", newEvent.id, "files", filesToUpload);
} catch (error: any) {
if (error.status === 413) {
throw new Error("Files are too large. Please try uploading smaller files or fewer files at once.");
}
throw error;
}
}
} }
// Show success message // Reset form state
if (submitButton) {
submitButton.classList.remove("btn-disabled");
submitButton.classList.add("btn-success");
const successIcon = document.createElement('span');
successIcon.innerHTML = '<i class="iconify" data-icon="heroicons:check" style="width: 20px; height: 20px;"></i>';
submitButton.textContent = '';
submitButton.appendChild(successIcon);
}
// Reset all state
setHasUnsavedChanges(false);
setSelectedFiles(new Map());
setFilesToDelete(new Set());
setEvent({ setEvent({
id: '', id: "",
event_name: '', created: "",
event_description: '', updated: "",
event_code: '', event_name: "",
location: '', event_description: "",
event_code: "",
location: "",
files: [], files: [],
points_to_reward: 0, points_to_reward: 0,
start_date: Get.formatLocalDate(new Date(), false), start_date: "",
end_date: Get.formatLocalDate(new Date(), false), end_date: "",
published: false, published: false,
has_food: false, has_food: false
attendees: []
}); });
setSelectedFiles(new Map());
setFilesToDelete(new Set());
setHasUnsavedChanges(false);
// Reset cache timestamp to force refresh // Close modal
if (window.lastCacheUpdate) {
window.lastCacheUpdate = 0;
}
// Trigger the callback
onEventSaved?.();
// Close modal directly instead of using handleModalClose
const modal = document.getElementById("editEventModal") as HTMLDialogElement; const modal = document.getElementById("editEventModal") as HTMLDialogElement;
if (modal) modal.close(); if (modal) modal.close();
// Force refresh of events list // Refresh events list
if (typeof window.fetchEvents === 'function') { if (window.fetchEvents) {
window.fetchEvents(); window.fetchEvents();
} }
} catch (error: any) {
console.error("Failed to save event:", error); // Trigger callback
if (submitButton) { if (onEventSaved) {
submitButton.classList.remove("btn-disabled"); onEventSaved();
submitButton.classList.add("btn-error");
const errorIcon = document.createElement('span');
errorIcon.innerHTML = '<i class="iconify" data-icon="heroicons:x-circle" style="width: 20px; height: 20px;"></i>';
submitButton.textContent = '';
submitButton.appendChild(errorIcon);
} }
alert(error.message || "Failed to save event. Please try again."); } 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);
if (submitButton) {
submitButton.disabled = false;
submitButton.classList.remove("btn-disabled", "btn-success", "btn-error");
submitButton.textContent = 'Save Changes';
}
if (cancelButton) cancelButton.disabled = false;
window.hideLoading?.(); window.hideLoading?.();
} }
}, [event, selectedFiles, filesToDelete, services, onEventSaved, isSubmitting]); }, [event, selectedFiles, filesToDelete, services, onEventSaved, isSubmitting]);

View file

@ -3,6 +3,8 @@ import { Authentication } from "../../../scripts/pocketbase/Authentication";
import { DataSyncService } from "../../../scripts/database/DataSyncService"; import { DataSyncService } from "../../../scripts/database/DataSyncService";
import { Collections } from "../../../schemas/pocketbase/schema"; import { Collections } from "../../../schemas/pocketbase/schema";
import type { Event, Log, User } from "../../../schemas/pocketbase"; import type { Event, Log, User } from "../../../schemas/pocketbase";
import { Get } from "../../../scripts/pocketbase/Get";
import type { EventAttendee } from "../../../schemas/pocketbase";
// Extended User interface with points property // Extended User interface with points property
interface ExtendedUser extends User { interface ExtendedUser extends User {
@ -18,109 +20,68 @@ export function Stats() {
const [memberSince, setMemberSince] = useState<string | null>(null); const [memberSince, setMemberSince] = useState<string | null>(null);
const [upcomingEvents, setUpcomingEvents] = useState(0); const [upcomingEvents, setUpcomingEvents] = useState(0);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [user, setUser] = useState<ExtendedUser | null>(null);
const [pointsEarned, setPointsEarned] = useState(0);
const [attendancePercentage, setAttendancePercentage] = useState(0);
useEffect(() => { useEffect(() => {
const fetchStats = async () => { const fetchStats = async () => {
try { try {
setIsLoading(true); setIsLoading(true);
setError(null);
const auth = Authentication.getInstance(); const auth = Authentication.getInstance();
const dataSync = DataSyncService.getInstance(); const get = Get.getInstance();
const userId = auth.getCurrentUser()?.id; const currentUser = auth.getCurrentUser();
if (!userId) return; if (!currentUser) {
setError("User not logged in");
// Get current date return;
const now = new Date();
// Get current quarter dates for points calculation
const month = now.getMonth(); // 0-11
let quarterStart = new Date();
// Fall: Sept-Dec
if (month >= 8 && month <= 11) {
quarterStart = new Date(now.getFullYear(), 8, 1); // Sept 1
}
// Winter: Jan-Mar
else if (month >= 0 && month <= 2) {
quarterStart = new Date(now.getFullYear(), 0, 1); // Jan 1
}
// Spring: Apr-Jun
else if (month >= 3 && month <= 5) {
quarterStart = new Date(now.getFullYear(), 3, 1); // Apr 1
}
// Summer: Jul-Aug
else {
quarterStart = new Date(now.getFullYear(), 6, 1); // Jul 1
} }
// Sync user data to ensure we have the latest const userId = currentUser.id;
await dataSync.syncCollection(Collections.USERS, `id = "${userId}"`);
// Get user from IndexedDB // Get user data
const user = await dataSync.getItem<ExtendedUser>(Collections.USERS, userId); const userData = await get.getOne<ExtendedUser>("users", userId);
const totalPoints = user?.points || 0; if (!userData) {
setError("Failed to load user data");
// Set membership status and date return;
setMembershipStatus(user?.member_type || "Member");
if (user?.created) {
const createdDate = new Date(user.created);
setMemberSince(createdDate.toLocaleDateString(undefined, {
year: 'numeric',
month: 'long'
}));
} }
// Sync logs for the current quarter to calculate points change // Set user data
await dataSync.syncCollection( setUser(userData);
Collections.LOGS,
`user_id = "${userId}" && created >= "${quarterStart.toISOString()}" && type = "update" && part = "event check-in"`, // Get events attended by the user
"-created" const attendedEvents = await get.getList<EventAttendee>(
"event_attendees",
1,
1000,
`user="${userId}"`
); );
// Get logs from IndexedDB setEventsAttended(attendedEvents.totalItems);
const logs = await dataSync.getData<Log>(
Collections.LOGS,
false, // Don't force sync again
`user_id = "${userId}" && created >= "${quarterStart.toISOString()}" && type = "update" && part = "event check-in"`,
"-created"
);
// Calculate quarterly points // Calculate total points earned
const quarterlyPoints = logs.reduce((total, log) => { let totalPoints = 0;
const pointsMatch = log.message?.match(/Awarded (\d+) points/); attendedEvents.items.forEach(attendee => {
if (pointsMatch) { totalPoints += attendee.points_earned || 0;
return total + parseInt(pointsMatch[1]);
}
return total;
}, 0);
// Set points change message
setPointsChange(quarterlyPoints > 0 ? `+${quarterlyPoints} this quarter` : "No activity");
// Sync events collection
await dataSync.syncCollection(Collections.EVENTS);
// Get events from IndexedDB
const events = await dataSync.getData<Event>(Collections.EVENTS);
// Count attended events
const attendedEvents = events.filter(event =>
event.attendees?.some(attendee => attendee.user_id === userId)
);
setEventsAttended(attendedEvents.length);
// Count upcoming events (events that haven't ended yet)
const upcoming = events.filter(event => {
if (!event.end_date) return false;
const endDate = new Date(event.end_date);
return endDate > now && event.published;
}); });
setUpcomingEvents(upcoming.length);
// Set loyalty points setPointsEarned(totalPoints);
setLoyaltyPoints(totalPoints);
// Get all events to calculate percentage
const allEvents = await get.getList<Event>("events", 1, 1000);
if (allEvents.totalItems > 0) {
const percentage = (attendedEvents.totalItems / allEvents.totalItems) * 100;
setAttendancePercentage(Math.round(percentage));
} else {
setAttendancePercentage(0);
}
} catch (error) { } catch (error) {
console.error("Error fetching stats:", error); console.error("Error fetching stats:", error);
setError("Failed to load stats");
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }

View file

@ -4,7 +4,6 @@ import UserProfileSettings from "./SettingsSection/UserProfileSettings";
import AccountSecuritySettings from "./SettingsSection/AccountSecuritySettings"; import AccountSecuritySettings from "./SettingsSection/AccountSecuritySettings";
import NotificationSettings from "./SettingsSection/NotificationSettings"; import NotificationSettings from "./SettingsSection/NotificationSettings";
import DisplaySettings from "./SettingsSection/DisplaySettings"; import DisplaySettings from "./SettingsSection/DisplaySettings";
import { Toaster } from "react-hot-toast";
--- ---
<div id="settings-section" class=""> <div id="settings-section" class="">
@ -13,8 +12,6 @@ import { Toaster } from "react-hot-toast";
<p class="opacity-70">Manage your account settings and preferences</p> <p class="opacity-70">Manage your account settings and preferences</p>
</div> </div>
<Toaster position="top-right" client:load />
<!-- Profile Settings Card --> <!-- Profile Settings Card -->
<div <div
class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-6" class="card bg-base-100 shadow-xl border border-base-200 hover:border-primary transition-all duration-300 hover:-translate-y-1 transform mb-6"