"use client"; import React, { useEffect, useMemo, useRef, useState } from "react"; import { useAuth } from "@/hooks/useAuth"; import { useRouter } from "next/navigation"; import { apiFetch } from "@/lib/api"; import { useStableState } from "@/hooks/useStableState"; import { useVisiblePolling } from "@/hooks/useVisiblePolling"; import { Calendar, Banknote, Gift, Users, Ticket, QrCode, DoorOpen, Wallet, UserPlus, FileText, MessageCircle, BarChart2, Mail, } from "lucide-react"; import { StatCard, StatCardRow } from "@/components/shared/StatCard"; import { QuickActionTile, QuickActionGrid } from "@/components/shared/QuickActionTile"; import { AreaTrendChart } from "@/components/charts/AreaTrendChart"; import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from "@/components/ui/table"; const formatRand = (n: number) => `R ${(n || 0).toFixed(2)}`; const formatRandAxis = (n: number) => `R${new Intl.NumberFormat(undefined, { notation: "compact", maximumFractionDigits: 1 }).format(n)}`; const formatCount = (n: number) => (n || 0).toLocaleString(); const REPORTS_URL = "/dashboard/supervisor/reports"; const QUICK_ACTIONS = [ { href: "/dashboard/supervisor/manual", label: "Manual registration", description: "Register a guest and issue tickets", icon: UserPlus }, { href: "/dashboard/supervisor/events", label: "Manage events", description: "Create, edit, and update ticket types", icon: Calendar }, { href: "/dashboard/supervisor/payments", label: "Payments & donations", description: "Manual payments and assignment", icon: Wallet }, { href: "/dashboard/staff/ticket-scanning", label: "Open scanner", description: "Use your device camera to validate tickets", icon: QrCode }, { href: "/dashboard/supervisor/at-the-door", label: "At the door", description: "Walk-ins, payments, ticket printing", icon: DoorOpen }, { href: "/dashboard/supervisor/reports", label: "Reports", description: "View, export, and email reports", icon: BarChart2 }, { href: "/dashboard/supervisor/forms", label: "Attendee forms", description: "View submitted attendee forms", icon: FileText }, { href: "/dashboard/supervisor/email-attendees", label: "Email attendees", description: "Send a message to attendees of an event", icon: Mail }, { href: "/dashboard/supervisor/whatsapp-attendees", label: "WhatsApp attendees", description: "Send a WhatsApp message to event attendees", icon: MessageCircle }, ] as const; type OverviewMetric = { thisMonth: number; lastMonth: number; pctChange: number | null }; type Overview = { activeEvents: number; registrations: OverviewMetric; ticketsSold: OverviewMetric; revenue: OverviewMetric; donations: OverviewMetric; trend: { date: string; revenue: number }[]; topEvents: { eventId: string; title: string; revenue: number; registrations: number; ticketsSold: number }[]; }; export default function SupervisorDashboardPage() { const { user, loading, token } = useAuth(); const router = useRouter(); const canView = useMemo(() => { const role = user?.role; return role === "admin" || role === "supervisor"; }, [user]); useEffect(() => { if (loading) return; if (!user) router.replace("/login"); }, [user, loading, router]); // /api/stats/supervisor covers scan activity + today's payments (unchanged from before); // /api/stats/overview is the month-over-month KPI/trend/top-events endpoint, shared // with the admin dashboard since supervisors already have full revenue/donation visibility. const [scanStats, setScanStats] = useStableState(null); const [paymentStats, setPaymentStats] = useStableState(null); const [recentScans, setRecentScans] = useStableState([]); const [overview, setOverview] = useStableState(null); const [loadingStats, setLoadingStats] = useState(false); const hasLoadedOnce = useRef(false); const loadStats = async () => { if (!token) return; const isFirstLoad = !hasLoadedOnce.current; try { if (isFirstLoad) setLoadingStats(true); const [data, overviewData] = await Promise.all([ apiFetch("/api/stats/supervisor", { authToken: token }), apiFetch("/api/stats/overview", { authToken: token }), ]); setScanStats(data.scanStats); setPaymentStats(data.paymentStats); setRecentScans(Array.isArray(data.recentScans) ? data.recentScans : []); setOverview(overviewData); } catch (e) { // ignore } finally { hasLoadedOnce.current = true; if (isFirstLoad) setLoadingStats(false); } }; useEffect(() => { if (!token) return; loadStats(); }, [token]); // Poll every 15s while the tab is visible; pause in the background and refetch // immediately on return instead of leaving stale numbers up. useVisiblePolling(() => { if (!token) return; loadStats(); }, 15000, !!token); return (

Welcome back{user ? `, ${user.name}` : ""} đź‘‹

Here's what's happening with your events today.

{!canView && (
You need supervisor or admin access to use these tools.
)}
Quick actions
{QUICK_ACTIONS.map(a => ( ))}

Revenue trend — past month

{overview && overview.trend.length > 0 ? ( ({ label: t.date.slice(5), value: t.revenue }))} valueFormatter={formatRand} axisFormatter={formatRandAxis} /> ) : (
No revenue recorded in the past month.
)}

Top performing events

{overview && overview.topEvents.length > 0 ? ( Event Registrations Revenue Tickets sold {overview.topEvents.map(e => ( {e.title} {formatCount(e.registrations)} {formatRand(e.revenue)} {formatCount(e.ticketsSold)} ))}
) : (
No event revenue recorded yet.
)}

Recent scans

{loadingStats && Refreshing…}
    {recentScans.map((u: any) => (
  • {u.ticket?.event?.title || u.ticket?.eventId || 'Event'}
    {new Date(u.scannedAt).toLocaleString()}
    {u.ticket?.registrationOption?.eventOption?.name || 'Ticket'} — #{String(u.ticket?.id || '').slice(0, 8)}
    Scanned by: {u.scannedBy?.name || u.scannedById}
  • ))} {recentScans.length === 0 &&
  • No scans yet.
  • }

Scanner activity

{loadingStats && Refreshing…}
{scanStats ? (
Today
{scanStats.totalToday}
My scans
{scanStats.myToday}
Last hour
{scanStats.lastHour}
) : (
No scanner data yet.
)}

Payments today

{paymentStats ? (
Today
{formatRand(paymentStats.totalToday)}
) : (
No payment data yet.
)} {scanStats?.byStaff?.length > 0 && (
Today by staff
    {scanStats.byStaff.map((s: any) => (
  • {s.name || 'Staff'} {s.count}
  • ))}
)}
); }