import { useMutation, useQuery } from "@tanstack/react-query"; import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; import { Check, ChevronDown, ChevronRight, ChevronsUpDown, ChevronUp, Clipboard, ClipboardCheck, Download, LogOut, Search, Users, X, } from "lucide-react"; import { useMemo, useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { authClient } from "@/lib/auth-client"; import { orpc } from "@/utils/orpc"; export const Route = createFileRoute("/admin/")({ component: AdminPage, }); type Guest = { firstName: string; lastName: string; email?: string; phone?: string; }; function parseGuests(raw: unknown): Guest[] { if (!raw) return []; try { const parsed = typeof raw === "string" ? JSON.parse(raw) : raw; return Array.isArray(parsed) ? parsed : []; } catch { return []; } } function AdminPage() { const navigate = useNavigate(); const [search, setSearch] = useState(""); const [registrationType, setRegistrationType] = useState< "performer" | "watcher" | "" >(""); const [artForm, setArtForm] = useState(""); const [fromDate, setFromDate] = useState(""); const [toDate, setToDate] = useState(""); const [page, setPage] = useState(1); const pageSize = 20; const [copiedId, setCopiedId] = useState(null); const [expandedGuests, setExpandedGuests] = useState>(new Set()); // Get current session to check user role const sessionQuery = useQuery({ queryKey: ["session"], queryFn: () => authClient.getSession(), }); const handleCopyManageUrl = (token: string, id: string) => { const url = `${window.location.origin}/manage/${token}`; navigator.clipboard.writeText(url).then(() => { setCopiedId(id); toast.success("Beheerlink gekopieerd"); setTimeout(() => setCopiedId(null), 2000); }); }; const toggleGuestExpand = (id: string) => { setExpandedGuests((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; type SortKey = | "naam" | "email" | "type" | "details" | "gasten" | "gift" | "betaling" | "datum"; type SortDir = "asc" | "desc"; const [sortKey, setSortKey] = useState("datum"); const [sortDir, setSortDir] = useState("desc"); const handleSort = (key: SortKey) => { if (sortKey === key) { setSortDir((d) => (d === "asc" ? "desc" : "asc")); } else { setSortKey(key); setSortDir("asc"); } }; const statsQuery = useQuery(orpc.getRegistrationStats.queryOptions()); const registrationsQuery = useQuery( orpc.getRegistrations.queryOptions({ input: { search: search || undefined, registrationType: registrationType || undefined, artForm: artForm || undefined, fromDate: fromDate || undefined, toDate: toDate || undefined, page, pageSize, }, }), ); const adminRequestsQuery = useQuery(orpc.getAdminRequests.queryOptions()); const exportMutation = useMutation({ ...orpc.exportRegistrations.mutationOptions(), onSuccess: (data: { csv: string; filename: string }) => { const blob = new Blob([data.csv], { type: "text/csv" }); const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = data.filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); }, }); const approveRequestMutation = useMutation({ ...orpc.approveAdminRequest.mutationOptions(), onSuccess: () => { toast.success("Admin toegang goedgekeurd"); adminRequestsQuery.refetch(); }, onError: (error) => { toast.error(`Fout: ${error.message}`); }, }); const rejectRequestMutation = useMutation({ ...orpc.rejectAdminRequest.mutationOptions(), onSuccess: () => { toast.success("Admin toegang geweigerd"); adminRequestsQuery.refetch(); }, onError: (error) => { toast.error(`Fout: ${error.message}`); }, }); const requestAdminMutation = useMutation({ ...orpc.requestAdminAccess.mutationOptions(), onSuccess: () => { toast.success("Admin toegang aangevraagd!"); }, onError: (error) => { toast.error(`Aanvraag mislukt: ${error.message}`); }, }); const handleSignOut = async () => { await authClient.signOut(); navigate({ to: "/" }); }; const handleExport = () => { exportMutation.mutate(undefined); }; const handleApprove = (requestId: string) => { approveRequestMutation.mutate({ requestId }); }; const handleReject = (requestId: string) => { rejectRequestMutation.mutate({ requestId }); }; const handleRequestAdmin = () => { requestAdminMutation.mutate(undefined); }; const stats = statsQuery.data; const registrations = registrationsQuery.data?.data ?? []; const pagination = registrationsQuery.data?.pagination; const adminRequests = adminRequestsQuery.data ?? []; const pendingRequests = adminRequests.filter((r) => r.status === "pending"); const performerCount = stats?.byType.find((t) => t.registrationType === "performer")?.count ?? 0; const watcherCount = stats?.byType.find((t) => t.registrationType === "watcher")?.count ?? 0; // Use server-side totalGuestCount from stats (accurate across all pages) const totalGuestCount = (stats as { totalGuestCount?: number } | undefined)?.totalGuestCount ?? 0; const totalWatcherAttendees = watcherCount + totalGuestCount; const totalDrinkCardValue = registrations .filter((r) => r.registrationType === "watcher") .reduce((sum, r) => sum + (r.drinkCardValue ?? 0), 0); const totalGiftRevenue = (stats?.totalGiftRevenue as number | undefined) ?? 0; const sortedRegistrations = useMemo(() => { const sorted = [...registrations].sort((a, b) => { let valA: string | number = 0; let valB: string | number = 0; switch (sortKey) { case "naam": valA = `${a.firstName} ${a.lastName}`.toLowerCase(); valB = `${b.firstName} ${b.lastName}`.toLowerCase(); break; case "email": valA = a.email.toLowerCase(); valB = b.email.toLowerCase(); break; case "type": valA = a.registrationType ?? ""; valB = b.registrationType ?? ""; break; case "details": valA = (a.registrationType === "performer" ? a.artForm : `€${a.drinkCardValue ?? 5}`) ?? ""; valB = (b.registrationType === "performer" ? b.artForm : `€${b.drinkCardValue ?? 5}`) ?? ""; break; case "gasten": { valA = parseGuests(a.guests).length; valB = parseGuests(b.guests).length; break; } case "gift": valA = a.giftAmount ?? 0; valB = b.giftAmount ?? 0; break; case "betaling": valA = a.paymentStatus ?? "pending"; valB = b.paymentStatus ?? "pending"; break; case "datum": valA = new Date(a.createdAt).getTime(); valB = new Date(b.createdAt).getTime(); break; } if (valA < valB) return sortDir === "asc" ? -1 : 1; if (valA > valB) return sortDir === "asc" ? 1 : -1; return 0; }); return sorted; }, [registrations, sortKey, sortDir]); const SortIcon = ({ col }: { col: SortKey }) => { if (sortKey !== col) return ; return sortDir === "asc" ? ( ) : ( ); }; const thClass = "px-4 py-3 text-left font-medium text-xs text-white/60 uppercase tracking-wider cursor-pointer select-none hover:text-white/90 whitespace-nowrap"; const formatCents = (cents: number | null | undefined) => { if (!cents || cents <= 0) return "-"; const euros = cents / 100; return `€${euros.toFixed(euros % 1 === 0 ? 0 : 2)}`; }; // Check if user is admin const user = sessionQuery.data?.data?.user as | { role?: string; name?: string } | undefined; const isAdmin = user?.role === "admin"; const isAuthenticated = !!sessionQuery.data?.data?.user; // Show loading state while checking session if (sessionQuery.isLoading) { return (

Laden...

); } // Show login prompt for non-authenticated users if (!isAuthenticated) { return (
Inloggen Vereist Je moet ingelogd zijn om admin toegang aan te vragen
← Terug naar website
); } // Show request admin UI for non-admin users if (!isAdmin) { return (
Admin Toegang Vereist Je hebt geen admin rechten voor dit dashboard

Admin-toegang aanvragen?

← Terug naar website
); } return (
{/* Header */}
← Terug naar website

Admin Dashboard

Drinkkaart beheer
{/* Main Content */}
{/* Pending Admin Requests */} {pendingRequests.length > 0 && ( Openstaande Admin Aanvragen ({pendingRequests.length}) Gebruikers die admin toegang hebben aangevraagd
{pendingRequests.map((request) => (

{request.userName}

{request.userEmail}

Aangevraagd:{" "} {new Date(request.requestedAt).toLocaleDateString( "nl-BE", )}

))}
)} {/* Stats Cards */}
{/* Total registrations */} Totaal inschrijvingen {stats?.total ?? 0} {/* Today */} Vandaag ingeschreven {stats?.today ?? 0} Nieuwe registraties vandaag {/* Performers */} Artiesten {performerCount}
{stats?.byArtForm.slice(0, 2).map((item) => (
{item.artForm || "Onbekend"} {item.count}
))}
{/* Watchers + guests (real attendees count) */} Bezoekers {totalWatcherAttendees}
Inschrijvingen {watcherCount}
{totalGuestCount > 0 && (
Gasten +{totalGuestCount}
)}
Drinkkaart €{totalDrinkCardValue}
{/* Gifts */} Vrijwillige Gifts €{Math.round(totalGiftRevenue / 100)} Totale gift opbrengst
{/* Filters */} Filters
setSearch(e.target.value)} className="border-white/20 bg-white/10 pl-10 text-sm text-white placeholder:text-white/40 sm:text-base" />
setArtForm(e.target.value)} className="border-white/20 bg-white/10 text-sm text-white placeholder:text-white/40 sm:text-base" />
setFromDate(e.target.value)} className="border-white/20 bg-white/10 text-sm text-white [color-scheme:dark] sm:text-base" />
setToDate(e.target.value)} className="border-white/20 bg-white/10 text-sm text-white [color-scheme:dark] sm:text-base" />
{/* Export Button */}

{pagination?.total ?? 0} registraties gevonden {totalGuestCount > 0 && ( (+{totalGuestCount} gasten ={" "} {(pagination?.total ?? 0) + totalGuestCount} aanwezigen totaal) )}

{/* Registrations Table / Cards */} {/* Desktop Table */}
{registrationsQuery.isLoading ? ( ) : sortedRegistrations.length === 0 ? ( ) : ( sortedRegistrations.map((reg) => { const isPerformer = reg.registrationType === "performer"; const guests = parseGuests(reg.guests); const guestCount = guests.length; const isExpanded = expandedGuests.has(reg.id); const detailLabel = isPerformer ? reg.artForm || "-" : `€${reg.drinkCardValue ?? 5} drinkkaart`; const dateLabel = (() => { try { return new Date(reg.createdAt).toLocaleDateString( "nl-BE", { day: "2-digit", month: "2-digit", year: "numeric", }, ); } catch { return "-"; } })(); return ( <> {/* Expandable guest details row */} {isExpanded && guestCount > 0 && ( )} ); }) )}
handleSort("naam")}> Naam handleSort("email")}> Email Telefoon handleSort("type")}> Type handleSort("details")} > Details handleSort("gasten")} > Gasten handleSort("gift")}> Gift handleSort("betaling")} > Betaling handleSort("datum")}> Datum Link
Laden...
Geen registraties gevonden
{reg.firstName} {reg.lastName} {reg.email} {reg.phone || "-"} {isPerformer ? "Artiest" : "Bezoeker"} {detailLabel} {guestCount > 0 ? ( ) : ( "-" )} {formatCents(reg.giftAmount)} {isPerformer ? ( - ) : reg.paymentStatus === "paid" ? ( Betaald ) : reg.paymentStatus === "extra_payment_pending" ? ( Extra (€ {((reg.paymentAmount ?? 0) / 100).toFixed(0)}) ) : ( Open )} {dateLabel} {reg.managementToken ? ( ) : ( )}

Gasten van {reg.firstName} {reg.lastName}

{guests.map((g, i) => (
{i + 1}. {g.firstName} {g.lastName} {g.email && ( {g.email} )} {g.phone && ( {g.phone} )} {!g.email && !g.phone && ( geen contactgegevens )}
))}
{/* Mobile Cards */}
{registrationsQuery.isLoading ? (
Laden...
) : sortedRegistrations.length === 0 ? (
Geen registraties gevonden
) : (
{sortedRegistrations.map((reg) => { const isPerformer = reg.registrationType === "performer"; const guests = parseGuests(reg.guests); const guestCount = guests.length; const isExpanded = expandedGuests.has(reg.id); const detailLabel = isPerformer ? reg.artForm || "-" : `€${reg.drinkCardValue ?? 5} drinkkaart`; const dateLabel = (() => { try { return new Date(reg.createdAt).toLocaleDateString( "nl-BE", { day: "2-digit", month: "2-digit", year: "2-digit", }, ); } catch { return "-"; } })(); return (
{reg.firstName} {reg.lastName}
{reg.email} {reg.phone && ( • {reg.phone} )}
{isPerformer ? "Artiest" : "Bezoeker"} {reg.managementToken && ( )}
Details:{" "} {detailLabel}
{guestCount > 0 && ( )} {(reg.giftAmount ?? 0) > 0 && (
Gift:{" "} {formatCents(reg.giftAmount)}
)} {!isPerformer && (
Betaling:{" "} {reg.paymentStatus === "paid" ? ( Betaald ) : reg.paymentStatus === "extra_payment_pending" ? ( Extra (€ {((reg.paymentAmount ?? 0) / 100).toFixed( 0, )} ) ) : ( Open )}
)}
{dateLabel}
{/* Expandable guest details — mobile */} {isExpanded && guestCount > 0 && (

Gasten

{guests.map((g, i) => (
{i + 1}. {g.firstName} {g.lastName} {g.email && (
{g.email}
)} {g.phone && (
{g.phone}
)}
))}
)}
); })}
)}
{/* Pagination */} {pagination && pagination.totalPages > 1 && (
{page}/{pagination.totalPages} Pagina {page} van {pagination.totalPages}
)}
); }