event attendees changes

still broken
This commit is contained in:
chark1es 2025-03-02 18:11:46 -08:00
parent 9481f433de
commit 36e1f4663b
7 changed files with 2819 additions and 2400 deletions

View file

@ -7,7 +7,7 @@ import { DataSyncService } from "../../../scripts/database/DataSyncService";
import { Collections } from "../../../schemas/pocketbase/schema";
import { Icon } from "@iconify/react";
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
interface ExtendedEvent extends Event {
@ -51,91 +51,152 @@ const EventCheckIn = () => {
// Log the check-in attempt
await logger.send(
"attempt",
"info",
"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,
// directly query PocketBase for the event with the given code
// 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) {
// Validate event code
if (!eventCode || eventCode.trim() === "") {
await logger.send(
"error",
"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
const attendees = event.attendees || [];
if (attendees.some((entry) => entry.user_id === currentUser.id)) {
// Get event by code
const events = await get.getList<Event>(
Collections.EVENTS,
1,
1,
`event_code="${eventCode}"`
);
if (events.totalItems === 0) {
await logger.send(
"error",
"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)
const currentTime = new Date();
const eventStartDate = new Date(event.start_date); // Now properly converted to local time by Get
const eventEndDate = new Date(event.end_date); // Now properly converted to local time by Get
const eventStartDate = new Date(event.start_date);
const eventEndDate = new Date(event.end_date);
if (eventStartDate > currentTime) {
if (currentTime < eventStartDate) {
await logger.send(
"error",
"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(
"error",
"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(
"info",
"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) {
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;
modal.showModal();
if (modal) modal.showModal();
} else {
// If no food, complete check-in directly
await completeCheckIn(event, null);
// If no food, show confirmation modal
const modal = document.getElementById("confirmCheckInModal") as HTMLDialogElement;
if (modal) modal.showModal();
}
} 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> {
try {
setIsLoading(true);
const auth = Authentication.getInstance();
const update = Update.getInstance();
const logger = SendLog.getInstance();
@ -146,119 +207,108 @@ const EventCheckIn = () => {
throw new Error("You must be logged in to check in to events");
}
// Check if user is already checked in
const userId = auth.getUserId();
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 || [];
const userId = currentUser.id;
const eventId = event.id;
// Check if user is already checked in
const isAlreadyCheckedIn = attendees.some(
(attendee) => attendee.user_id === userId
const get = Get.getInstance();
const existingAttendees = await get.getList<EventAttendee>(
Collections.EVENT_ATTENDEES,
1,
1,
`user="${userId}" && event="${eventId}"`
);
const isAlreadyCheckedIn = existingAttendees.totalItems > 0;
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");
}
// Add new attendee entry to the array
const updatedAttendees = [...existingAttendees, attendeeEntry];
// Create new attendee record
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
await update.updateField("events", event.id, "attendees", updatedAttendees);
// Create the attendee record using PocketBase's create method
// This will properly use the collection rules defined in PocketBase
try {
// Use the update.create method which calls PocketBase's collection.create method
await update.create(Collections.EVENT_ATTENDEES, attendeeData);
// SECURITY FIX: Instead of syncing the entire events collection which would store event codes in IndexedDB,
// only sync the user's collection to update their points
if (event.points_to_reward > 0) {
await dataSync.syncCollection(Collections.USERS);
console.log("Successfully created attendance record");
} catch (createError: any) {
console.error("Error creating attendance record:", createError);
// 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
if (foodSelection) {
throw createError;
}
// Log successful check-in
await logger.send(
"update",
"event check-in",
`Food selection for ${event.event_name}: ${foodSelection}`
);
}
// 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
"info",
"event_check_in",
`Successfully checked in to event: ${event.event_name}`
);
// Log the points award
await logger.send(
"update",
"event check-in",
`Awarded ${event.points_to_reward} points for checking in to ${event.event_name}`
);
}
// Clear event code from local storage
await dataSync.clearEventCode();
// Show success message with points if awarded
toast.success(
`Successfully checked in to ${event.event_name}${event.points_to_reward > 0
// Show success message with event name and points
const pointsMessage = event.points_to_reward > 0
? ` (+${event.points_to_reward} points!)`
: ""
}`
);
// 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();
: "";
toast.success(`Successfully checked in to ${event.event_name}${pointsMessage}`);
setCurrentCheckInEvent(null);
setFoodInput("");
}
} 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) => {
e.preventDefault();
if (currentCheckInEvent) {
await completeCheckIn(currentCheckInEvent, foodInput.trim());
setCurrentCheckInEvent(null);
if (!currentCheckInEvent) return;
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 */}
<dialog id="foodSelectionModal" className="modal">
<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>
<form onSubmit={handleSubmit}>
<div className="form-control">
@ -345,7 +401,17 @@ const EventCheckIn = () => {
modal.close();
setCurrentCheckInEvent(null);
}}>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>
</form>
</div>
@ -353,6 +419,58 @@ const EventCheckIn = () => {
<button>close</button>
</form>
</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 { DexieService } from "../../../scripts/database/DexieService";
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
interface ExtendedEvent extends Event {
@ -132,20 +132,51 @@ const EventLoad = () => {
);
const renderEventCard = (event: Event) => {
const startDate = new Date(event.start_date);
const endDate = new Date(event.end_date);
const now = new Date();
const isPastEvent = endDate < now;
// Get current user to check attendance
try {
// Get authentication instance
const auth = Authentication.getInstance();
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
const eventDataId = `event_${event.id}`;
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 (
<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">
@ -202,6 +233,10 @@ const EventLoad = () => {
</div>
</div>
);
} catch (error) {
console.error("Error rendering event card:", error);
return null;
}
};
const loadEvents = async () => {

View file

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

View file

@ -5,13 +5,22 @@ import { SendLog } from '../../../scripts/pocketbase/SendLog';
import { DataSyncService } from '../../../scripts/database/DataSyncService';
import { Collections } from '../../../schemas/pocketbase/schema';
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
interface User extends SchemaUser {
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
const userCache = new Map<string, {
data: User;
@ -58,7 +67,6 @@ const HighlightText = ({ text, searchTerms }: { text: string | number | null | u
interface EventFields {
id: true;
event_name: true;
attendees: true;
}
interface UserFields {
@ -73,7 +81,7 @@ interface UserFields {
}
// 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'];
export default function Attendees() {
@ -86,6 +94,7 @@ export default function Attendees() {
const [searchTerm, setSearchTerm] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [processedSearchTerms, setProcessedSearchTerms] = useState<string[]>([]);
const [refreshKey, setRefreshKey] = useState(0); // Add a refresh key to force re-fetching
const get = Get.getInstance();
const auth = Authentication.getInstance();
@ -141,6 +150,8 @@ export default function Attendees() {
// Optimized user data fetching with cache
const fetchUserData = useCallback(async (userIds: string[]) => {
if (!userIds.length) return new Map<string, User>();
const now = Date.now();
const uncachedIds: string[] = [];
const cachedUsers = new Map<string, User>();
@ -160,48 +171,86 @@ export default function Attendees() {
return cachedUsers;
}
// Fetch uncached users
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
if (uncachedIds.length > 0) {
const idFilter = uncachedIds.map(id => `id = "${id}"`).join(' || ');
await dataSync.syncCollection(Collections.USERS, idFilter);
}
// 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;
}
})
// Fetch all uncached users in one request
const usersResponse = await get.getAll<User>(
Collections.USERS,
userFilter
);
// Update cache and merge with cached users
users.forEach(user => {
if (user) {
userCache.set(user.id, { data: user, timestamp: now });
cachedUsers.set(user.id, user);
// Process the fetched users
usersResponse.forEach(user => {
// Add member_type if it doesn't exist
const userWithMemberType = {
...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) {
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;
}, []);
// Function to refresh attendees data
const refreshAttendees = useCallback(() => {
setRefreshKey(prev => prev + 1);
}, []);
// Listen for the custom event
useEffect(() => {
const handleUpdateAttendees = async (e: CustomEvent<{ eventId: string; eventName: string }>) => {
setEventId(e.detail.eventId);
setEventName(e.detail.eventName);
setCurrentPage(1); // Reset pagination on new event
setSearchTerm(''); // Clear search on new event
// Log the attendees view action
try {
@ -217,17 +266,22 @@ export default function Attendees() {
};
window.addEventListener('updateAttendees', handleUpdateAttendees as unknown as EventListener);
// Expose refresh function to window
(window as any).refreshAttendees = refreshAttendees;
return () => {
window.removeEventListener('updateAttendees', handleUpdateAttendees as unknown as EventListener);
delete (window as any).refreshAttendees;
};
}, []);
}, [refreshAttendees]);
// Update search terms when search input changes
useEffect(() => {
updateProcessedSearchTerms(searchTerm);
}, [searchTerm, updateProcessedSearchTerms]);
// Fetch event data when eventId changes
// Fetch event data when eventId changes or refreshKey changes
useEffect(() => {
let isMounted = true;
const fetchEventData = async () => {
@ -243,44 +297,118 @@ export default function Attendees() {
setLoading(true);
setError(null);
const dataSync = DataSyncService.getInstance();
if (!eventId) {
setAttendeesList([]);
setUsers(new Map());
return;
}
// Sync the event data
// Clear cache to ensure fresh data
const dataSync = DataSyncService.getInstance();
await dataSync.clearCache();
await dataSync.syncCollection(Collections.EVENTS, `id="${eventId}"`);
// Get the event from IndexedDB
const event = await dataSync.getItem<Event>(Collections.EVENTS, eventId);
if (!isMounted) return;
const event = await get.getOne<Event>(Collections.EVENTS, eventId);
if (!event) {
setError('Event not found');
setError("Event not found");
setAttendeesList([]);
setUsers(new Map());
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([]);
setUsers(new Map());
}
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
const userIds = [...new Set(event.attendees.map(a => a.user_id))];
const userMap = await fetchUserData(userIds);
if (isMounted) {
setAttendeesList(transformedAttendees);
}
// 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) {
setUsers(userMap);
}
} catch (error) {
console.error("Failed to fetch users:", error);
// Fallback to individual user fetching
const userMap = await fetchUserData(userIds);
if (isMounted) {
console.error('Failed to fetch event data:', error);
setError('Failed to load event data');
setAttendeesList([]);
setUsers(userMap);
}
}
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 {
if (isMounted) {
setLoading(false);
@ -290,15 +418,18 @@ export default function Attendees() {
fetchEventData();
return () => { isMounted = false; };
}, [eventId, auth, fetchUserData]);
}, [eventId, auth, fetchUserData, refreshKey]);
// Reset state when modal is closed
useEffect(() => {
const handleModalClose = () => {
setEventId('');
setEventName('');
setAttendeesList([]);
setUsers(new Map());
setError(null);
setSearchTerm('');
setCurrentPage(1);
};
const modal = document.getElementById('attendeesModal');
@ -332,15 +463,22 @@ export default function Attendees() {
'Graduation Year',
'Major',
'Check-in Time',
'Food Choice'
'Food Choice',
'Points Earned'
].map(escapeCSV);
// Create CSV rows
const rows = attendeesList.map(attendee => {
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 [
user?.name || 'Unknown User',
user?.name || `User ${attendee.user_id}`,
user?.email || 'N/A',
user?.pid || 'N/A',
user?.member_id || 'N/A',
@ -348,7 +486,8 @@ export default function Attendees() {
user?.graduation_year || 'N/A',
user?.major || 'N/A',
checkInTime,
attendee.food || 'N/A'
attendee.food || 'N/A',
attendee.points_earned || 'N/A'
].map(escapeCSV);
});
@ -375,6 +514,8 @@ export default function Attendees() {
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url); // Clean up the URL object
toast.success(`Downloaded ${rows.length} attendee records`);
};
if (loading) {
@ -390,6 +531,13 @@ export default function Attendees() {
<div className="alert alert-error">
<Icon icon="heroicons:exclamation-circle" className="h-6 w-6" />
<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>
);
}
@ -403,6 +551,13 @@ export default function Attendees() {
<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" />
<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>
);
}
@ -426,6 +581,14 @@ export default function Attendees() {
/>
</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
className="btn btn-primary btn-sm gap-2"
onClick={downloadAttendeesCSV}
@ -434,6 +597,7 @@ export default function Attendees() {
Download CSV
</button>
</div>
</div>
{/* Stats Row */}
<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">Check-in Time</th>
<th className="bg-base-100">Food Choice</th>
<th className="bg-base-100">Points</th>
</tr>
</thead>
<tbody>
{paginatedAttendees.map((attendee, index) => {
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 (
<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?.pid || '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={checkInTime} 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>
);
})}

View file

@ -9,6 +9,7 @@ import FilePreview from "../universal/FilePreview";
import type { Event as SchemaEvent, AttendeeEntry } from "../../../schemas/pocketbase";
import { DataSyncService } from '../../../scripts/database/DataSyncService';
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.
// 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) {
// State for form data and UI
const [event, setEvent] = useState<Event>({
id: '',
event_name: '',
event_description: '',
event_code: '',
location: '',
id: "",
created: "",
updated: "",
event_name: "",
event_description: "",
event_code: "",
location: "",
files: [],
points_to_reward: 0,
start_date: Get.formatLocalDate(new Date(), false),
end_date: Get.formatLocalDate(new Date(), false),
start_date: "",
end_date: "",
published: false,
has_food: false,
attendees: []
has_food: false
});
const [previewUrl, setPreviewUrl] = useState("");
@ -557,7 +559,16 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
const initializeEventData = useCallback(async (eventId: string) => {
try {
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
if (eventData.start_date) {
@ -573,20 +584,22 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
}
setEvent(eventData);
console.log("Event data loaded successfully:", eventData);
} else {
setEvent({
id: '',
created: '',
updated: '',
event_name: '',
event_description: '',
event_code: '',
location: '',
files: [],
points_to_reward: 0,
start_date: Get.formatLocalDate(new Date(), false),
end_date: Get.formatLocalDate(new Date(), false),
start_date: '',
end_date: '',
published: false,
has_food: false,
attendees: []
has_food: false
});
}
setSelectedFiles(new Map());
@ -595,7 +608,7 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
setHasUnsavedChanges(false);
} catch (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]);
@ -614,7 +627,7 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
modal.showModal();
} catch (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({
id: '',
event_name: '',
event_description: '',
event_code: '',
location: '',
id: "",
created: "",
updated: "",
event_name: "",
event_description: "",
event_code: "",
location: "",
files: [],
points_to_reward: 0,
start_date: Get.formatLocalDate(new Date(), false),
end_date: Get.formatLocalDate(new Date(), false),
start_date: "",
end_date: "",
published: false,
has_food: false,
attendees: []
has_food: false
});
setSelectedFiles(new Map());
setFilesToDelete(new Set());
setShowPreview(false);
setHasUnsavedChanges(false);
setPreviewUrl("");
setPreviewFilename("");
const modal = document.getElementById("editEventModal") as HTMLDialogElement;
if (modal) modal.close();
@ -663,176 +678,160 @@ export default function EventEditor({ onEventSaved }: EventEditorProps) {
e.preventDefault();
if (isSubmitting) return;
const form = e.target as HTMLFormElement;
const submitButton = form.querySelector('button[type="submit"]') as HTMLButtonElement;
const cancelButton = form.querySelector('button[type="button"]') as HTMLButtonElement;
try {
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;
try {
window.showLoading?.();
const pb = services.auth.getPocketBase();
// Get form data
const formData = new FormData(e.currentTarget);
console.log('Form submission started');
console.log('Event data:', event);
const formData = new FormData(form);
const eventData = {
event_name: formData.get("editEventName"),
event_code: formData.get("editEventCode"),
event_description: formData.get("editEventDescription"),
location: formData.get("editEventLocation"),
points_to_reward: Number(formData.get("editEventPoints")),
// Create updated event object
const updatedEvent: Event = {
id: event.id,
created: event.created,
updated: event.updated,
event_name: formData.get("editEventName") as string,
event_description: formData.get("editEventDescription") as string,
event_code: formData.get("editEventCode") as string,
location: formData.get("editEventLocation") as string,
files: event.files || [],
points_to_reward: parseInt(formData.get("editEventPoints") as string) || 0,
start_date: formData.get("editEventStartDate") as string,
end_date: formData.get("editEventEndDate") as string,
published: formData.get("editEventPublished") === "on",
has_food: formData.get("editEventHasFood") === "on",
attendees: event.attendees || []
has_food: formData.get("editEventHasFood") === "on"
};
// 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) {
// Update existing event
console.log('Updating event:', event.id);
await services.update.updateFields("events", event.id, eventData);
savedEvent = await services.update.updateFields<Event>(
Collections.EVENTS,
event.id,
updatedEvent
);
// Handle file deletions first
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
// Clear cache to ensure fresh data
const dataSync = DataSyncService.getInstance();
await dataSync.syncCollection(Collections.EVENTS);
}
await dataSync.clearCache();
// Handle file additions
if (selectedFiles.size > 0) {
try {
// Convert Map to array of Files
const filesToUpload = Array.from(selectedFiles.values());
console.log('Uploading files:', filesToUpload.map(f => f.name));
// Log success
await services.sendLog.send(
"success",
"event_update",
`Successfully updated event: ${savedEvent.event_name}`
);
// Use appendFiles to preserve existing files
await services.fileManager.appendFiles("events", event.id, "files", filesToUpload);
// 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;
}
}
// Show success toast
toast.success(`Event "${savedEvent.event_name}" updated successfully!`);
} else {
// Create new event
console.log('Creating new event');
const newEvent = await pb.collection("events").create(eventData);
console.log('New event created:', newEvent);
savedEvent = await services.update.create<Event>(
Collections.EVENTS,
updatedEvent
);
// Sync the events collection to update IndexedDB
const dataSync = DataSyncService.getInstance();
await dataSync.syncCollection(Collections.EVENTS);
// Log success
await services.sendLog.send(
"success",
"event_create",
`Successfully created event: ${savedEvent.event_name}`
);
// Upload files if any
if (selectedFiles.size > 0) {
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 toast
toast.success(`Event "${savedEvent.event_name}" created successfully!`);
}
// Show success message
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());
// Reset form state
setEvent({
id: '',
event_name: '',
event_description: '',
event_code: '',
location: '',
id: "",
created: "",
updated: "",
event_name: "",
event_description: "",
event_code: "",
location: "",
files: [],
points_to_reward: 0,
start_date: Get.formatLocalDate(new Date(), false),
end_date: Get.formatLocalDate(new Date(), false),
start_date: "",
end_date: "",
published: false,
has_food: false,
attendees: []
has_food: false
});
setSelectedFiles(new Map());
setFilesToDelete(new Set());
setHasUnsavedChanges(false);
// Reset cache timestamp to force refresh
if (window.lastCacheUpdate) {
window.lastCacheUpdate = 0;
}
// Trigger the callback
onEventSaved?.();
// Close modal directly instead of using handleModalClose
// Close modal
const modal = document.getElementById("editEventModal") as HTMLDialogElement;
if (modal) modal.close();
// Force refresh of events list
if (typeof window.fetchEvents === 'function') {
// Refresh events list
if (window.fetchEvents) {
window.fetchEvents();
}
} catch (error: any) {
console.error("Failed to save event:", error);
if (submitButton) {
submitButton.classList.remove("btn-disabled");
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);
// Trigger callback
if (onEventSaved) {
onEventSaved();
}
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 {
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?.();
}
}, [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 { Collections } from "../../../schemas/pocketbase/schema";
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
interface ExtendedUser extends User {
@ -18,109 +20,68 @@ export function Stats() {
const [memberSince, setMemberSince] = useState<string | null>(null);
const [upcomingEvents, setUpcomingEvents] = useState(0);
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(() => {
const fetchStats = async () => {
try {
setIsLoading(true);
setError(null);
const auth = Authentication.getInstance();
const dataSync = DataSyncService.getInstance();
const userId = auth.getCurrentUser()?.id;
const get = Get.getInstance();
const currentUser = auth.getCurrentUser();
if (!userId) return;
// Get current date
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
if (!currentUser) {
setError("User not logged in");
return;
}
// Sync user data to ensure we have the latest
await dataSync.syncCollection(Collections.USERS, `id = "${userId}"`);
const userId = currentUser.id;
// Get user from IndexedDB
const user = await dataSync.getItem<ExtendedUser>(Collections.USERS, userId);
const totalPoints = user?.points || 0;
// Set membership status and date
setMembershipStatus(user?.member_type || "Member");
if (user?.created) {
const createdDate = new Date(user.created);
setMemberSince(createdDate.toLocaleDateString(undefined, {
year: 'numeric',
month: 'long'
}));
// Get user data
const userData = await get.getOne<ExtendedUser>("users", userId);
if (!userData) {
setError("Failed to load user data");
return;
}
// Sync logs for the current quarter to calculate points change
await dataSync.syncCollection(
Collections.LOGS,
`user_id = "${userId}" && created >= "${quarterStart.toISOString()}" && type = "update" && part = "event check-in"`,
"-created"
// Set user data
setUser(userData);
// Get events attended by the user
const attendedEvents = await get.getList<EventAttendee>(
"event_attendees",
1,
1000,
`user="${userId}"`
);
// Get logs from IndexedDB
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"
);
setEventsAttended(attendedEvents.totalItems);
// Calculate quarterly points
const quarterlyPoints = logs.reduce((total, log) => {
const pointsMatch = log.message?.match(/Awarded (\d+) points/);
if (pointsMatch) {
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;
// Calculate total points earned
let totalPoints = 0;
attendedEvents.items.forEach(attendee => {
totalPoints += attendee.points_earned || 0;
});
setUpcomingEvents(upcoming.length);
// Set loyalty points
setLoyaltyPoints(totalPoints);
setPointsEarned(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) {
console.error("Error fetching stats:", error);
setError("Failed to load stats");
} finally {
setIsLoading(false);
}

View file

@ -4,7 +4,6 @@ import UserProfileSettings from "./SettingsSection/UserProfileSettings";
import AccountSecuritySettings from "./SettingsSection/AccountSecuritySettings";
import NotificationSettings from "./SettingsSection/NotificationSettings";
import DisplaySettings from "./SettingsSection/DisplaySettings";
import { Toaster } from "react-hot-toast";
---
<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>
</div>
<Toaster position="top-right" client:load />
<!-- Profile Settings Card -->
<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"