import { useState, useEffect } from "react"; import { Get } from "../../../scripts/pocketbase/Get"; import { Authentication } from "../../../scripts/pocketbase/Authentication"; import { Update } from "../../../scripts/pocketbase/Update"; import { SendLog } from "../../../scripts/pocketbase/SendLog"; 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, EventAttendee } from "../../../schemas/pocketbase"; // Extended Event interface with additional properties needed for this component interface ExtendedEvent extends Event { description?: string; // This component uses 'description' but schema has 'event_description' } // Note: Date conversion is now handled automatically by the Get and Update classes. // When fetching events, UTC dates are converted to local time. // When saving events, local dates are converted back to UTC. const EventCheckIn = () => { const [currentCheckInEvent, setCurrentCheckInEvent] = useState(null); const [isLoading, setIsLoading] = useState(false); const [foodInput, setFoodInput] = useState(""); // SECURITY FIX: Purge event codes when component mounts useEffect(() => { const dataSync = DataSyncService.getInstance(); dataSync.purgeEventCodes().catch(err => { console.error("Error purging event codes:", err); }); }, []); async function handleEventCheckIn(eventCode: string): Promise { try { const get = Get.getInstance(); const auth = Authentication.getInstance(); const dataSync = DataSyncService.getInstance(); const logger = SendLog.getInstance(); const currentUser = auth.getCurrentUser(); if (!currentUser) { await logger.send( "error", "event_check_in", "Check-in failed: User not logged in" ); toast.error("You must be logged in to check in to events"); return; } // Log the check-in attempt await logger.send( "info", "event_check_in", `Attempting to check in with code: ${eventCode}` ); // Validate event code if (!eventCode || eventCode.trim() === "") { await logger.send( "error", "event_check_in", "Check-in failed: Empty event code" ); toast.error("Please enter an event code"); return; } // Get event by code const events = await get.getList( Collections.EVENTS, 1, 1, `event_code="${eventCode}"` ); if (events.totalItems === 0) { await logger.send( "error", "event_check_in", `Check-in failed: Invalid event code: ${eventCode}` ); 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); const eventEndDate = new Date(event.end_date); if (currentTime < eventStartDate) { await logger.send( "error", "event_check_in", `Check-in failed: Event has not started yet: ${event.event_name}` ); toast.error(`This event hasn't started yet. It begins on ${eventStartDate.toLocaleDateString()} at ${eventStartDate.toLocaleTimeString()}`); return; } if (currentTime > eventEndDate) { await logger.send( "error", "event_check_in", `Check-in failed: Event has already ended: ${event.event_name}` ); toast.error("This event has already ended"); return; } // Check if user is already checked in - IMPROVED VALIDATION const attendees = await get.getList( Collections.EVENT_ATTENDEES, 1, 50, // Increased limit to ensure we catch all possible duplicates `user="${currentUser.id}" && event="${event.id}"` ); if (attendees.totalItems > 0) { const lastCheckIn = new Date(attendees.items[0].time_checked_in); const timeSinceLastCheckIn = Date.now() - lastCheckIn.getTime(); const hoursAgo = Math.round(timeSinceLastCheckIn / (1000 * 60 * 60)); await logger.send( "error", "event_check_in", `Check-in failed: Already checked in to event: ${event.event_name} (${hoursAgo} hours ago)` ); toast.error(`You have already checked in to this event (${hoursAgo} hours ago)`); return; } // Set current event for check-in setCurrentCheckInEvent(event); // Log successful event lookup await logger.send( "info", "event_check_in", `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(
Event found!

{event.event_name}

{event.points_to_reward > 0 ? `${event.points_to_reward} points` : "No points"}

, { duration: 5000 } ); } // If event has food, show food selection modal if (event.has_food) { // Show food-specific toast toast.success(
Event with food found!

{event.event_name}

Please select your food preference

, { duration: 5000 } ); const modal = document.getElementById("foodSelectionModal") as HTMLDialogElement; if (modal) modal.showModal(); } else { // If no food, show confirmation modal const modal = document.getElementById("confirmCheckInModal") as HTMLDialogElement; if (modal) modal.showModal(); } } catch (error: any) { 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 { try { setIsLoading(true); const auth = Authentication.getInstance(); const update = Update.getInstance(); const logger = SendLog.getInstance(); const dataSync = DataSyncService.getInstance(); const get = Get.getInstance(); const currentUser = auth.getCurrentUser(); if (!currentUser) { throw new Error("You must be logged in to check in to events"); } const userId = currentUser.id; const eventId = event.id; // Double-check for existing check-ins with improved validation const existingAttendees = await get.getList( Collections.EVENT_ATTENDEES, 1, 50, // Increased limit to ensure we catch all possible duplicates `user="${userId}" && event="${eventId}"` ); if (existingAttendees.totalItems > 0) { const lastCheckIn = new Date(existingAttendees.items[0].time_checked_in); const timeSinceLastCheckIn = Date.now() - lastCheckIn.getTime(); const hoursAgo = Math.round(timeSinceLastCheckIn / (1000 * 60 * 60)); await logger.send( "error", "event_check_in", `Check-in failed: Already checked in to event: ${event.event_name} (${hoursAgo} hours ago)` ); throw new Error(`You have already checked in to this event (${hoursAgo} hours ago)`); } // Create new attendee record with transaction to prevent race conditions const attendeeData = { user: userId, event: eventId, food_ate: foodSelection || "", time_checked_in: new Date().toISOString(), points_earned: event.points_to_reward || 0 }; try { // Create the attendee record in PocketBase const newAttendee = await update.create(Collections.EVENT_ATTENDEES, attendeeData); console.log("Successfully created attendance record"); // Update user's total points // First, get all the user's attendance records to calculate total points const userAttendance = await get.getList( Collections.EVENT_ATTENDEES, 1, 1000, `user="${userId}"` ); // Calculate total points let totalPoints = 0; userAttendance.items.forEach(attendee => { totalPoints += attendee.points_earned || 0; }); // Log the points update console.log(`Updating user points to: ${totalPoints}`); // Update the user record with the new total points await update.updateFields(Collections.USERS, userId, { points: totalPoints }); // Ensure local data is in sync with backend // First sync the new attendance record await dataSync.syncCollection(Collections.EVENT_ATTENDEES); // Then sync the updated user data to ensure points are correctly reflected locally await dataSync.syncCollection(Collections.USERS); // Clear event code from local storage await dataSync.clearEventCode(); // Log successful check-in await logger.send( "info", "event_check_in", `Successfully checked in to event: ${event.event_name}` ); // Show success message with event name and points const pointsMessage = event.points_to_reward > 0 ? ` (+${event.points_to_reward} points!)` : ""; toast.success(`Successfully checked in to ${event.event_name}${pointsMessage}`); // Close any open modals const foodModal = document.getElementById("foodSelectionModal") as HTMLDialogElement; if (foodModal) foodModal.close(); const confirmModal = document.getElementById("confirmCheckInModal") as HTMLDialogElement; if (confirmModal) confirmModal.close(); setCurrentCheckInEvent(null); setFoodInput(""); } catch (createError: any) { console.error("Error creating attendance record:", createError); // Check if this is a duplicate record error (race condition handling) if (createError.status === 400 && createError.data?.data?.user?.code === "validation_not_unique") { throw new Error("You have already checked in to this event"); } throw createError; } } catch (error: any) { 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) 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"); } // Additional check to prevent duplicate check-ins right before submission const existingAttendees = await get.getList( 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"); } }; return ( <>

Event Check-in

{ e.preventDefault(); const input = e.currentTarget.querySelector('input') as HTMLInputElement; if (input.value.trim()) { setIsLoading(true); handleEventCheckIn(input.value.trim()).finally(() => { setIsLoading(false); input.value = ""; }); } else { toast("Please enter an event code", { icon: '⚠️', style: { borderRadius: '10px', background: '#FFC107', color: '#000', }, }); } }}>
{ if (e.key === "Enter") { e.preventDefault(); } }} />
{/* Food Selection Modal */}

{currentCheckInEvent?.event_name}

{currentCheckInEvent?.event_description}
{currentCheckInEvent?.points_to_reward} points

This event has food! Please let us know what you'd like to eat:

setFoodInput(e.target.value)} required />
{/* Confirmation Modal (for events without food) */}

{currentCheckInEvent?.event_name}

{currentCheckInEvent?.event_description}
{currentCheckInEvent?.points_to_reward} points

Are you sure you want to check in to this event?

); }; export default EventCheckIn;