import React, { useCallback, useEffect, useState } from "react"; import { ClipboardCheck, Plus, Search, Download, Link2, Pencil, Trash2, ChevronLeft, UserCheck, Copy, RefreshCw, X, } from "lucide-react"; import { useAppNav } from "../../contexts/AppNavContext"; import { usePermission } from "../../contexts/PermissionContext"; import { attendancesApi } from "../../../services/api"; import type { AttendanceRecord, AttendanceSearchParticipant, AttendanceCheckInRow, } from "../../../services/api/types"; function publicAttendanceUrl(token: string) { return `${window.location.origin}/attendance/${token}`; } function formatDt(iso?: string | null) { if (!iso) return "—"; const d = new Date(iso); if (Number.isNaN(d.getTime())) return "—"; return d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); } interface FormState { name: string; description: string; allowDuplicateCheckIn: boolean; publicLinkEnabled: boolean; } const emptyForm: FormState = { name: "", description: "", allowDuplicateCheckIn: false, publicLinkEnabled: true, }; export function AttendancePage() { const { selectedEventId } = useAppNav(); const { canModule } = usePermission(); const canReadAttendance = canModule("attendance", "read"); const canWriteAttendance = canModule("attendance", "write"); const canDeleteAttendance = canModule("attendance", "delete"); const [sessions, setSessions] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [selectedId, setSelectedId] = useState(null); const [panelOpen, setPanelOpen] = useState(false); const [editingId, setEditingId] = useState(null); const [form, setForm] = useState(emptyForm); const [saving, setSaving] = useState(false); const [copyMsg, setCopyMsg] = useState(null); const [searchQ, setSearchQ] = useState(""); const [searchResults, setSearchResults] = useState([]); const [searching, setSearching] = useState(false); const [checkInBusy, setCheckInBusy] = useState(null); const [checkIns, setCheckIns] = useState([]); const [checkInsTotal, setCheckInsTotal] = useState(0); const [checkInsLoading, setCheckInsLoading] = useState(false); const selected = sessions.find((s) => s._id === selectedId) ?? null; const loadSessions = useCallback(async () => { if (!selectedEventId || !canReadAttendance) return; setLoading(true); setError(null); const res = await attendancesApi.list(selectedEventId); setLoading(false); if (res.error) { setError(res.error); return; } setSessions(res.data?.attendances ?? []); }, [selectedEventId, canReadAttendance]); useEffect(() => { loadSessions(); }, [loadSessions]); const loadCheckIns = useCallback(async () => { if (!selectedEventId || !selectedId) return; setCheckInsLoading(true); const res = await attendancesApi.listCheckIns(selectedEventId, selectedId, { limit: 50 }); setCheckInsLoading(false); if (res.data) { setCheckIns(res.data.checkIns); setCheckInsTotal(res.data.total); } }, [selectedEventId, selectedId]); useEffect(() => { if (selectedId) loadCheckIns(); }, [selectedId, loadCheckIns]); useEffect(() => { if (!selectedEventId || !selectedId) return; const t = setTimeout(async () => { setSearching(true); const res = await attendancesApi.searchParticipants( selectedEventId, selectedId, searchQ.trim() || undefined ); setSearching(false); if (res.data) setSearchResults(res.data.participants); }, 300); return () => clearTimeout(t); }, [searchQ, selectedEventId, selectedId]); const openCreate = () => { setEditingId(null); setForm(emptyForm); setPanelOpen(true); }; const openEdit = (s: AttendanceRecord) => { setEditingId(s._id); setForm({ name: s.name, description: s.description || "", allowDuplicateCheckIn: Boolean(s.settings?.allowDuplicateCheckIn), publicLinkEnabled: s.settings?.publicLinkEnabled !== false, }); setPanelOpen(true); }; const saveSession = async () => { if (!selectedEventId || !form.name.trim()) return; setSaving(true); const payload = { name: form.name.trim(), description: form.description.trim(), settings: { allowDuplicateCheckIn: form.allowDuplicateCheckIn, publicLinkEnabled: form.publicLinkEnabled, }, }; const res = editingId ? await attendancesApi.update(selectedEventId, editingId, payload) : await attendancesApi.create(selectedEventId, payload); setSaving(false); if (res.error) { setError(res.error); return; } setPanelOpen(false); await loadSessions(); if (res.data?._id && !editingId) setSelectedId(res.data._id); }; const archiveSession = async (id: string) => { if (!selectedEventId || !confirm("Archive this attendance session?")) return; const res = await attendancesApi.remove(selectedEventId, id); if (res.error) { setError(res.error); return; } if (selectedId === id) setSelectedId(null); await loadSessions(); }; const handleCheckIn = async (participantId: string) => { if (!selectedEventId || !selectedId) return; setCheckInBusy(participantId); const res = await attendancesApi.checkIn(selectedEventId, selectedId, participantId); setCheckInBusy(null); if (res.error) { setError(res.error); return; } await loadSessions(); await loadCheckIns(); const sr = await attendancesApi.searchParticipants( selectedEventId, selectedId, searchQ.trim() || undefined ); if (sr.data) setSearchResults(sr.data.participants); }; const copyLink = async (token: string) => { try { await navigator.clipboard.writeText(publicAttendanceUrl(token)); setCopyMsg("Link copied"); setTimeout(() => setCopyMsg(null), 2000); } catch { setCopyMsg("Could not copy"); } }; const regenerateLink = async (id: string) => { if (!selectedEventId || !confirm("Regenerate public link? Old links will stop working.")) return; const res = await attendancesApi.regenerateToken(selectedEventId, id); if (res.error) { setError(res.error); return; } await loadSessions(); }; const exportCsv = async () => { if (!selectedEventId || !selectedId || !selected) return; const safe = selected.name.replace(/[^a-z0-9-_]+/gi, "-").slice(0, 40); await attendancesApi.downloadExport(selectedEventId, selectedId, `${safe}-check-ins.csv`); }; if (!selectedEventId) { return (
Select an event to manage attendance.
); } if (!canReadAttendance) { return (
You do not have permission to view Attendance.
); } return (

Attendance

{canWriteAttendance && ( )}
{error && (
{error}
)} {copyMsg && (
{copyMsg}
)}
{loading &&

Loading…

} {!loading && sessions.length === 0 && (

No attendance sessions yet. Create one to start check-ins.

)} {sessions.map((s) => ( ))}
{selected && selectedEventId && (

{selected.name}

{selected.description && (

{selected.description}

)}
{canWriteAttendance && ( )} {canWriteAttendance && ( )} {canDeleteAttendance && ( )}
setSearchQ(e.target.value)} placeholder="Name, email, or phone…" style={inputStyle} /> {searching &&

Searching…

}
{searchResults.map((p) => { const canCheckIn = !p.alreadyCheckedIn || Boolean(selected.settings?.allowDuplicateCheckIn); return (
{p.displayName}
{[p.email, p.phone].filter(Boolean).join(" · ") || "—"}
{p.alreadyCheckedIn && (
Checked in {p.checkInCount} time{p.checkInCount === 1 ? "" : "s"} {p.lastCheckedInAt ? ` · ${formatDt(p.lastCheckedInAt)}` : ""}
)}
); })} {!searching && searchResults.length === 0 && searchQ.trim() && (

No participants found.

)}

Check-ins ({checkInsTotal})

{checkInsLoading &&

Loading…

} {!checkInsLoading && checkIns.length === 0 && (

No check-ins yet.

)}
{checkIns.map((row) => { const name = [row.firstName, row.lastName].filter(Boolean).join(" ").trim() || "—"; return ( ); })}
Name Email Phone Last check-in Count
{name} {row.email || "—"} {row.phone || "—"} {formatDt(row.lastCheckedInAt)} {row.checkInCount}
)} {!selected && sessions.length > 0 && (
Select a session to manage check-ins
)}
{panelOpen && (
setPanelOpen(false)} >
e.stopPropagation()} >

{editingId ? "Edit session" : "New attendance session"}