Full site redesign, help system, and dashboard stats fixes
Multi-phase visual facelift (design tokens, dashboards, sidebar/navbar shell, per-page help guides, and a layout/content pass across every remaining page) plus backend fixes to the dashboard KPI stats: - Admin/Supervisor dashboard KPIs (revenue, donations, registrations, tickets sold) now use a rolling trailing-month window (today back one calendar month, e.g. 9 May - 8 June if today is 8 June) instead of calendar month-to-date, which under-counted for most of the month. The comparison window shifts the same way, so like is still compared with like. - Reports deep-links from those stat tiles now match the same window (range=trailing_month, replacing range=this_month). - Design tokens (brand-* Tailwind scale + shadcn CSS variables), a site-wide contextual help button, fixed dashboard sidebar/navbar, Admin/Supervisor/Staff/User dashboard rebuilds backed by a new GET /api/stats/overview endpoint, a dedicated Contact page, Site Settings restyle with WhatsApp config folded in, and an Account activity feed backed by a new SecurityEvent model. - Every remaining page (home, events, registration flow, auth, legal, payment results, and every Admin/Supervisor/Staff/User tool page) restyled onto the same design tokens, several with real layout upgrades (home hero, events list/detail, donate page, auth pages). - 20+ new dedicated help guides so the whole site has page-specific help content instead of falling back to a generic guide. - Assorted fixes surfaced along the way: donation-leg double-counting in payment stats, donations not counting toward revenue, refund netting in per-method report breakdowns, and donation over-allocation after a refund. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,44 @@ 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, ClipboardList,
|
||||
UserPlus, FileText, MessageCircle, BarChart2, Mail, Wallet,
|
||||
} 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/admin/users", label: "Manage users", description: "Create, edit, change roles and passwords", icon: Users },
|
||||
{ href: "/dashboard/supervisor/events", label: "Manage events", description: "Create, edit, and update ticket types", icon: Calendar },
|
||||
{ href: "/dashboard/admin/registrations", label: "Manage registrations", description: "Cancel, update status, and search registrations", icon: ClipboardList },
|
||||
{ href: "/dashboard/supervisor/manual", label: "Manual registration", description: "Register a guest and issue tickets", icon: UserPlus },
|
||||
{ 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/reports", label: "Reports", description: "View, export, and email reports", icon: BarChart2 },
|
||||
{ href: "/dashboard/admin/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 },
|
||||
{ href: "/dashboard/admin/cashup", label: "Post-event Cashup", description: "Set costs, reconcile takings, and close out events", icon: Wallet },
|
||||
] 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 AdminDashboardPage() {
|
||||
const { user, loading, token } = useAuth();
|
||||
@@ -18,29 +56,26 @@ export default function AdminDashboardPage() {
|
||||
if (!user) router.replace("/login");
|
||||
}, [user, loading, router]);
|
||||
|
||||
// Everything this dashboard displays comes from one endpoint (/api/stats/admin) that
|
||||
// computes it all server-side — no more separate calls plus a full payments/events pull
|
||||
// just to reduce them down to a couple of numbers client-side.
|
||||
// useStableState skips the re-render entirely when a poll returns identical data, and
|
||||
// hasLoadedOnce below means "Refreshing…" only ever shows for the very first load —
|
||||
// together these stop the stats panels from flickering on every 15s poll.
|
||||
// /api/stats/admin covers today/week/month payment totals (unchanged from before);
|
||||
// /api/stats/overview is the month-over-month KPI/trend/top-events endpoint. Both are
|
||||
// computed server-side — no full payments/events list ever ships to the client.
|
||||
const [paymentStats, setPaymentStats] = useStableState<any | null>(null);
|
||||
const [scanStats, setScanStats] = useStableState<any | null>(null);
|
||||
const [activeEventsCount, setActiveEventsCount] = useStableState<number>(0);
|
||||
const [recentScans, setRecentScans] = useStableState<any[]>([]);
|
||||
const [overview, setOverview] = useStableState<Overview | null>(null);
|
||||
const [loadingStats, setLoadingStats] = useState(false);
|
||||
const hasLoadedOnce = useRef(false);
|
||||
const [paymentsTab, setPaymentsTab] = useState<"today" | "week" | "month">("today");
|
||||
|
||||
const loadStats = async () => {
|
||||
if (!token) return;
|
||||
const isFirstLoad = !hasLoadedOnce.current;
|
||||
try {
|
||||
if (isFirstLoad) setLoadingStats(true);
|
||||
const data = await apiFetch<any>("/api/stats/admin", { authToken: token });
|
||||
setScanStats(data.scanStats);
|
||||
setPaymentStats(data.paymentStats);
|
||||
setActiveEventsCount(data.activeEventsCount || 0);
|
||||
setRecentScans(Array.isArray(data.recentScans) ? data.recentScans : []);
|
||||
const [dash, overviewData] = await Promise.all([
|
||||
apiFetch<any>("/api/stats/admin", { authToken: token }),
|
||||
apiFetch<Overview>("/api/stats/overview", { authToken: token }),
|
||||
]);
|
||||
setPaymentStats(dash.paymentStats);
|
||||
setOverview(overviewData);
|
||||
} catch (e) {
|
||||
// ignore errors for dashboard summaries
|
||||
} finally {
|
||||
@@ -61,18 +96,15 @@ export default function AdminDashboardPage() {
|
||||
loadStats();
|
||||
}, 15000, !!token);
|
||||
|
||||
const paymentsTabValue = paymentStats
|
||||
? paymentsTab === "today" ? paymentStats.totalToday : paymentsTab === "week" ? paymentStats.totalWeek : paymentStats.totalMonth
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className="max-w-6xl mx-auto w-full p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h1 className="text-2xl font-semibold">Admin Dashboard{user ? ` — ${user.name}` : ""}</h1>
|
||||
<div className="hidden sm:flex gap-2">
|
||||
<button className="px-3 py-1.5 text-sm rounded bg-indigo-600 text-white hover:bg-indigo-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1" onClick={() => router.push("/dashboard/admin/users")}>Manage users</button>
|
||||
<button className="px-3 py-1.5 text-sm rounded bg-indigo-600 text-white hover:bg-indigo-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1" onClick={() => router.push("/dashboard/supervisor/events")}>Manage events</button>
|
||||
<button className="px-3 py-1.5 text-sm rounded bg-indigo-600 text-white hover:bg-indigo-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1" onClick={() => router.push("/dashboard/admin/registrations")}>Manage registrations</button>
|
||||
<button className="px-3 py-1.5 text-sm rounded bg-indigo-600 text-white hover:bg-indigo-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1" onClick={() => router.push("/dashboard/supervisor/manual")}>Manual registration</button>
|
||||
<button className="px-3 py-1.5 text-sm rounded bg-indigo-600 text-white hover:bg-indigo-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1" onClick={() => router.push("/dashboard/supervisor/payments")}>Payments</button>
|
||||
<button className="px-3 py-1.5 text-sm rounded bg-indigo-600 text-white hover:bg-indigo-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1" onClick={() => router.push("/dashboard/admin/whatsapp")}>WhatsApp API</button>
|
||||
</div>
|
||||
<div className="max-w-6xl mx-auto w-full">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-semibold text-gray-900">Welcome back{user ? `, ${user.name}` : ""} 👋</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">Here's what's happening with your events today.</p>
|
||||
</div>
|
||||
|
||||
{!isAdmin && (
|
||||
@@ -81,158 +113,89 @@ export default function AdminDashboardPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2">
|
||||
<div className="border rounded-xl p-4 bg-white shadow-sm mb-6">
|
||||
<div className="text-lg font-semibold mb-2">Quick actions</div>
|
||||
<div className="grid sm:grid-cols-3 gap-3">
|
||||
<button className="rounded-lg p-3 text-left bg-indigo-600 text-white hover:bg-indigo-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1" onClick={() => router.push("/dashboard/admin/users")}>
|
||||
Manage users
|
||||
<div className="text-xs text-white/90">Create, edit, change roles and passwords</div>
|
||||
</button>
|
||||
<button className="rounded-lg p-3 text-left bg-indigo-600 text-white hover:bg-indigo-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1" onClick={() => router.push("/dashboard/supervisor/events")}>
|
||||
Manage events
|
||||
<div className="text-xs text-white/90">Create, edit, and update ticket types</div>
|
||||
</button>
|
||||
<button className="rounded-lg p-3 text-left bg-indigo-600 text-white hover:bg-indigo-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1" onClick={() => router.push("/dashboard/supervisor/sections")}>Manage sections
|
||||
<div className="text-xs text-white/90">Create sections and assign ticket types</div>
|
||||
</button>
|
||||
<button className="rounded-lg p-3 text-left bg-indigo-600 text-white hover:bg-indigo-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1" onClick={() => router.push("/dashboard/admin/registrations")}>
|
||||
Manage registrations
|
||||
<div className="text-xs text-white/90">Cancel, update status, and search registrations</div>
|
||||
</button>
|
||||
<button className="rounded-lg p-3 text-left bg-indigo-600 text-white hover:bg-indigo-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1" onClick={() => router.push("/dashboard/supervisor/manual")}>
|
||||
Create manual registration
|
||||
<div className="text-xs text-white/90">Register a guest and issue tickets</div>
|
||||
</button>
|
||||
<button className="rounded-lg p-3 text-left bg-indigo-600 text-white hover:bg-indigo-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1" onClick={() => router.push("/dashboard/supervisor/payments")}>
|
||||
Record payment / donations
|
||||
<div className="text-xs text-white/90">Manual payments and assignment</div>
|
||||
</button>
|
||||
<button className="rounded-lg p-3 text-left bg-indigo-600 text-white hover:bg-indigo-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1" onClick={() => router.push("/dashboard/staff/ticket-scanning")}>
|
||||
Open scanner
|
||||
<div className="text-xs text-white/90">Use your device camera to validate tickets</div>
|
||||
</button>
|
||||
<button className="rounded-lg p-3 text-left bg-indigo-600 text-white hover:bg-indigo-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1" onClick={() => router.push("/dashboard/staff/event-tickets")}>
|
||||
Event tickets & printing
|
||||
<div className="text-xs text-white/90">Browse event tickets and print lists</div>
|
||||
</button>
|
||||
<button className="rounded-lg p-3 text-left bg-indigo-600 text-white hover:bg-indigo-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1" onClick={() => router.push("/dashboard/supervisor/at-the-door")}>At the door
|
||||
<div className="text-xs text-white/90">Walk-ins, payments, ticket printing</div>
|
||||
</button>
|
||||
<button className="rounded-lg p-3 text-left bg-indigo-600 text-white hover:bg-indigo-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1" onClick={() => router.push("/dashboard/supervisor/reports")}>
|
||||
Reports
|
||||
<div className="text-xs text-white/90">View, export, and email reports</div>
|
||||
</button>
|
||||
<button className="rounded-lg p-3 text-left bg-indigo-600 text-white hover:bg-indigo-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1" onClick={() => router.push("/dashboard/admin/forms")}>
|
||||
Attendee forms
|
||||
<div className="text-xs text-white/90">View submitted attendee forms</div>
|
||||
</button>
|
||||
<button className="rounded-lg p-3 text-left bg-indigo-600 text-white hover:bg-indigo-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1" onClick={() => router.push("/dashboard/supervisor/email-attendees")}>
|
||||
Email attendees
|
||||
<div className="text-xs text-white/90">Send message to attendees of an event</div>
|
||||
</button>
|
||||
<button className="rounded-lg p-3 text-left bg-indigo-600 text-white hover:bg-indigo-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1" onClick={() => router.push("/dashboard/supervisor/whatsapp-attendees")}>
|
||||
WhatsApp attendees
|
||||
<div className="text-xs text-white/90">Send WhatsApp message to event attendees</div>
|
||||
</button>
|
||||
<button className="rounded-lg p-3 text-left bg-indigo-600 text-white hover:bg-indigo-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1" onClick={() => router.push("/dashboard/admin/whatsapp")}>
|
||||
Manage WhatsApp API
|
||||
<div className="text-xs text-white/90">Manage the WhatsApp API config</div>
|
||||
</button>
|
||||
<button className="rounded-lg p-3 text-left bg-indigo-600 text-white hover:bg-indigo-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1" onClick={() => router.push("/dashboard/admin/cashup")}>
|
||||
Post-event Cashup
|
||||
<div className="text-xs text-white/90">Set costs, reconcile takings, and close out events</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<StatCardRow>
|
||||
<StatCard icon={Calendar} label="Active events" value={overview ? formatCount(overview.activeEvents) : "—"} tone="brand" href="/dashboard/supervisor/events" linkLabel="View all events" />
|
||||
<StatCard icon={Banknote} label="Revenue (past month)" value={overview ? formatRand(overview.revenue.thisMonth) : "—"} tone="green" href={`${REPORTS_URL}?report=revenue&range=trailing_month`} linkLabel="View report" delta={overview ? { value: overview.revenue.pctChange } : undefined} />
|
||||
<StatCard icon={Gift} label="Donations (past month)" value={overview ? formatRand(overview.donations.thisMonth) : "—"} tone="rose" href={`${REPORTS_URL}?report=donations&range=trailing_month`} linkLabel="View report" delta={overview ? { value: overview.donations.pctChange } : undefined} />
|
||||
<StatCard icon={Users} label="Registrations (past month)" value={overview ? formatCount(overview.registrations.thisMonth) : "—"} tone="blue" href="/dashboard/admin/registrations" linkLabel="View registrations" delta={overview ? { value: overview.registrations.pctChange } : undefined} />
|
||||
<StatCard icon={Ticket} label="Tickets sold (past month)" value={overview ? formatCount(overview.ticketsSold.thisMonth) : "—"} tone="amber" href={`${REPORTS_URL}?report=usage&range=trailing_month`} linkLabel="View report" delta={overview ? { value: overview.ticketsSold.pctChange } : undefined} />
|
||||
</StatCardRow>
|
||||
|
||||
<div className="border rounded-xl p-4 bg-white shadow-sm mb-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-lg font-semibold">Recent scans</h2>
|
||||
{loadingStats && <span className="text-xs text-gray-500">Refreshing…</span>}
|
||||
</div>
|
||||
<ul className="text-sm space-y-2 max-h-96 overflow-auto pr-2">
|
||||
{recentScans.map((u: any) => (
|
||||
<li key={u.id} className="border rounded p-2">
|
||||
<div className="flex justify-between">
|
||||
<div className="font-medium">{u.ticket?.event?.title || u.ticket?.eventId || 'Event'}</div>
|
||||
<div className="text-xs text-gray-500">{new Date(u.scannedAt).toLocaleString()}</div>
|
||||
</div>
|
||||
<div className="text-xs text-gray-600">{u.ticket?.registrationOption?.eventOption?.name || 'Ticket'} — #{String(u.ticket?.id || '').slice(0,8)}</div>
|
||||
<div className="text-xs text-gray-500">Scanned by: {u.scannedBy?.name || u.scannedById}</div>
|
||||
</li>
|
||||
))}
|
||||
{recentScans.length === 0 && <li className="text-gray-500">No scans yet.</li>}
|
||||
</ul>
|
||||
<div className="border rounded-xl p-4 bg-white shadow-sm my-6">
|
||||
<div className="text-lg font-semibold mb-3">Quick actions</div>
|
||||
<QuickActionGrid>
|
||||
{QUICK_ACTIONS.map(a => (
|
||||
<QuickActionTile key={a.href} icon={a.icon} title={a.label} description={a.description} href={a.href} />
|
||||
))}
|
||||
</QuickActionGrid>
|
||||
</div>
|
||||
|
||||
<div className="grid lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<div className="border rounded-xl p-4 bg-white shadow-sm">
|
||||
<h2 className="text-lg font-semibold mb-3">Revenue trend — past month</h2>
|
||||
{overview && overview.trend.length > 0 ? (
|
||||
<AreaTrendChart data={overview.trend.map(t => ({ label: t.date.slice(5), value: t.revenue }))} valueFormatter={formatRand} axisFormatter={formatRandAxis} />
|
||||
) : (
|
||||
<div className="text-sm text-gray-400">No revenue recorded in the past month.</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border rounded-xl p-4 bg-white shadow-sm">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-lg font-semibold">Payments</h2>
|
||||
{loadingStats && <span className="text-xs text-gray-500">Refreshing…</span>}
|
||||
</div>
|
||||
{paymentStats ? (
|
||||
<div className="grid grid-cols-3 gap-2 mb-3">
|
||||
<div className="border rounded p-3 bg-white">
|
||||
<div className="text-xs text-gray-500">Today</div>
|
||||
<div className="text-lg font-semibold">R{paymentStats.totalToday}</div>
|
||||
</div>
|
||||
<div className="border rounded p-3 bg-white">
|
||||
<div className="text-xs text-gray-500">Past Week</div>
|
||||
<div className="text-lg font-semibold">R{paymentStats.totalWeek}</div>
|
||||
</div>
|
||||
<div className="border rounded p-3 bg-white">
|
||||
<div className="text-xs text-gray-500">Past Month</div>
|
||||
<div className="text-lg font-semibold">R{paymentStats.totalMonth}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-gray-500">No payment data yet.</div>
|
||||
)}
|
||||
|
||||
{scanStats?.byStaff?.length > 0 && (
|
||||
<div className="mb-1">
|
||||
<div className="text-sm font-medium mb-1">Today by staff</div>
|
||||
<ul className="text-sm text-gray-700 space-y-1">
|
||||
{scanStats.byStaff.map((s: any) => (
|
||||
<li key={s.scannedById} className="flex justify-between">
|
||||
<span>{s.name || 'Staff'}</span>
|
||||
<span className="font-medium">{s.count}</span>
|
||||
</li>
|
||||
<h2 className="text-lg font-semibold mb-3">Top performing events</h2>
|
||||
{overview && overview.topEvents.length > 0 ? (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Event</TableHead>
|
||||
<TableHead className="text-right">Registrations</TableHead>
|
||||
<TableHead className="text-right">Revenue</TableHead>
|
||||
<TableHead className="text-right">Tickets sold</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{overview.topEvents.map(e => (
|
||||
<TableRow key={e.eventId}>
|
||||
<TableCell className="font-medium">{e.title}</TableCell>
|
||||
<TableCell className="text-right">{formatCount(e.registrations)}</TableCell>
|
||||
<TableCell className="text-right">{formatRand(e.revenue)}</TableCell>
|
||||
<TableCell className="text-right">{formatCount(e.ticketsSold)}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : (
|
||||
<div className="text-sm text-gray-400">No event revenue recorded yet.</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="space-y-6">
|
||||
<div className="border rounded-xl p-4 bg-white shadow-sm">
|
||||
<h2 className="text-lg font-semibold mb-3">Admin stats</h2>
|
||||
{loadingStats && <div className="text-sm text-gray-500 mb-2">Loading…</div>}
|
||||
<div className="space-y-2">
|
||||
<div className="border rounded p-3 bg-white flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-xs text-gray-500">Active events</div>
|
||||
<div className="text-lg font-semibold">{activeEventsCount}</div>
|
||||
</div>
|
||||
<button className="text-xs px-2 py-1 rounded bg-gray-100 hover:bg-gray-200" onClick={() => router.push("/dashboard/staff/event-tickets")}>View</button>
|
||||
</div>
|
||||
<div className="border rounded p-3 bg-white">
|
||||
<div className="text-xs text-gray-500">Revenue today</div>
|
||||
<div className="text-lg font-semibold">R {(paymentStats?.totalToday || 0).toFixed(2)}</div>
|
||||
</div>
|
||||
<div className="border rounded p-3 bg-white">
|
||||
<div className="text-xs text-gray-500">Donations today</div>
|
||||
<div className="text-lg font-semibold">{paymentStats?.donationsToday || 0}</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h2 className="text-lg font-semibold">Payments overview</h2>
|
||||
{loadingStats && <span className="text-xs text-gray-500">Refreshing…</span>}
|
||||
</div>
|
||||
<div className="flex gap-1 mb-3 bg-gray-100 rounded-lg p-1">
|
||||
{(["today", "week", "month"] as const).map(t => (
|
||||
<button
|
||||
key={t}
|
||||
type="button"
|
||||
onClick={() => setPaymentsTab(t)}
|
||||
className={"flex-1 text-xs font-medium py-1.5 rounded-md transition-colors " + (paymentsTab === t ? "bg-white text-brand-700 shadow-sm" : "text-gray-500 hover:text-gray-700")}
|
||||
>
|
||||
{t === "today" ? "Today" : t === "week" ? "Past week" : "Past month"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{paymentStats ? (
|
||||
<div className="text-2xl font-semibold text-gray-900">{formatRand(paymentsTabValue)}</div>
|
||||
) : (
|
||||
<div className="text-sm text-gray-500">No payment data yet.</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-600 mt-6">
|
||||
<p>As an admin you can access Supervisor and Staff tools. Use the quick actions above to jump to common tasks.</p>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600">As an admin you can access Supervisor and Staff tools. Use the quick actions above to jump to common tasks.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user