event attendees changes
still broken
This commit is contained in:
parent
9481f433de
commit
36e1f4663b
7 changed files with 2819 additions and 2400 deletions
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
if (!eventId) {
|
||||
setAttendeesList([]);
|
||||
setUsers(new Map());
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear cache to ensure fresh data
|
||||
const dataSync = DataSyncService.getInstance();
|
||||
await dataSync.clearCache();
|
||||
await dataSync.syncCollection(Collections.EVENTS, `id="${eventId}"`);
|
||||
|
||||
// Sync the event data
|
||||
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>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue