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:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { apiFetch, API_BASE } from "@/lib/api";
|
||||
import { downloadReportExcel, emailReportPdf, whatsappReportPdf, viewReportPdf, type ReportPdfPayload } from "@/lib/export";
|
||||
@@ -8,7 +9,6 @@ import { Printer, Mail, FileSpreadsheet, MessageCircle, ShoppingCart, CreditCard
|
||||
import { REPORTS, type ReportKey } from "./ReportCatalog";
|
||||
import ReportsShell from "./ReportsShell";
|
||||
import ReportViewerModal, { ReportActionButton } from "./ReportViewerModal";
|
||||
import ReportingGuideModal, { GUIDE_DISMISSED_KEY } from "./ReportingGuideModal";
|
||||
import { StatTile, StatTileRow } from "./StatTile";
|
||||
import { HorizontalBarChart } from "./charts/HorizontalBarChart";
|
||||
|
||||
@@ -91,6 +91,7 @@ export default function ReportsV2({ onBack }: { onBack?: () => void } = {}) {
|
||||
const { token, user } = useAuth();
|
||||
const role = user?.role || "user";
|
||||
const canView = role === "admin" || role === "supervisor";
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
// Events list
|
||||
const [events, setEvents] = useState<any[]>([]);
|
||||
@@ -135,23 +136,6 @@ export default function ReportsV2({ onBack }: { onBack?: () => void } = {}) {
|
||||
const [ready, setReady] = useState(false); // toggled by View button
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [popupOpen, setPopupOpen] = useState(false);
|
||||
const [guideOpen, setGuideOpen] = useState(false);
|
||||
|
||||
// Show the reporting guide automatically on first visit, unless the user dismissed it for good.
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (typeof window !== "undefined" && window.localStorage.getItem(GUIDE_DISMISSED_KEY) !== "1") {
|
||||
setGuideOpen(true);
|
||||
}
|
||||
} catch {}
|
||||
}, []);
|
||||
|
||||
const closeGuide = (dontShowAgain: boolean) => {
|
||||
setGuideOpen(false);
|
||||
if (dontShowAgain) {
|
||||
try { window.localStorage.setItem(GUIDE_DISMISSED_KEY, "1"); } catch {}
|
||||
}
|
||||
};
|
||||
|
||||
// Universal filters — apply to every report's onView loader (per-report filters below stay
|
||||
// report-specific). A single events selection replaces what used to be ~10 separate
|
||||
@@ -190,10 +174,48 @@ export default function ReportsV2({ onBack }: { onBack?: () => void } = {}) {
|
||||
const [financialsByEvent, setFinancialsByEvent] = useState<Record<string, any>>({});
|
||||
const [auditRows, setAuditRows] = useState<any[]>([]);
|
||||
|
||||
// Deep-linking: dashboards link here with ?report=<key>&range=trailing_month to jump
|
||||
// straight into a specific report, pre-filtered to every currently-visible event and the
|
||||
// trailing month (today back one month, matching the dashboard KPI window — see
|
||||
// trailingMonthRanges in backend/src/controllers/statsController.js), instead of landing
|
||||
// on the plain report grid. deepLinkHandledRef is a ref (not state) so it updates
|
||||
// synchronously — the "Initialize default selection" effect right below reads it in the
|
||||
// same commit to skip its own single-event default.
|
||||
const deepLinkHandledRef = useRef(false);
|
||||
const [autoViewPending, setAutoViewPending] = useState(false);
|
||||
useEffect(() => {
|
||||
if (deepLinkHandledRef.current) return;
|
||||
if (filteredEvents.length === 0) return;
|
||||
const reportParam = searchParams.get("report");
|
||||
if (!reportParam || !REPORTS.some(r => r.key === reportParam)) return;
|
||||
deepLinkHandledRef.current = true;
|
||||
setReport(reportParam as ReportKey);
|
||||
setSelectedEventIds(filteredEvents.map((e: any) => e.id));
|
||||
if (searchParams.get("range") === "trailing_month") {
|
||||
const now = new Date();
|
||||
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 30);
|
||||
const iso = (d: Date) => d.toISOString().slice(0, 10);
|
||||
setDateFrom(iso(start));
|
||||
setDateTo(iso(now));
|
||||
}
|
||||
setPopupOpen(true);
|
||||
setAutoViewPending(true);
|
||||
}, [filteredEvents, searchParams]);
|
||||
|
||||
// Fires once the state set above has actually committed (selectedEventIds reflects the
|
||||
// deep-link's full event list), so onView() below closes over the updated values instead
|
||||
// of the stale defaults from the render that scheduled it.
|
||||
useEffect(() => {
|
||||
if (!autoViewPending || selectedEventIds.length === 0) return;
|
||||
setAutoViewPending(false);
|
||||
onView();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [autoViewPending, selectedEventIds, report]);
|
||||
|
||||
// Initialize default selection when events load
|
||||
useEffect(() => {
|
||||
if (filteredEvents.length === 0) return;
|
||||
if (selectedEventIds.length === 0) setSelectedEventIds(filteredEvents.slice(0, 1).map((e: any) => e.id));
|
||||
if (!deepLinkHandledRef.current && selectedEventIds.length === 0) setSelectedEventIds(filteredEvents.slice(0, 1).map((e: any) => e.id));
|
||||
if (!attEventId) setAttEventId(filteredEvents[0].id);
|
||||
}, [filteredEvents]);
|
||||
|
||||
@@ -1213,12 +1235,9 @@ export default function ReportsV2({ onBack }: { onBack?: () => void } = {}) {
|
||||
report={report} setReport={(r: ReportKey) => { setReport(r); setReady(false); }}
|
||||
onViewReport={() => { setPopupOpen(true); onView(); }}
|
||||
busy={busy}
|
||||
onOpenGuide={() => setGuideOpen(true)}
|
||||
onBack={onBack}
|
||||
/>
|
||||
|
||||
{guideOpen && <ReportingGuideModal onClose={closeGuide} />}
|
||||
|
||||
{popupOpen && (
|
||||
<ReportViewerModal
|
||||
title={activeReportMeta?.label || "Report"}
|
||||
@@ -1697,7 +1716,7 @@ export default function ReportsV2({ onBack }: { onBack?: () => void } = {}) {
|
||||
<td className="py-2 px-3 text-right">R {masterTotals?.paidViaDonation.toFixed(2)}</td>
|
||||
<td className="py-2 px-3 text-right">R {masterTotals?.outstanding.toFixed(2)}</td>
|
||||
</tr>
|
||||
<tr className="text-xs bg-blue-50/60">
|
||||
<tr className="text-xs bg-brand-50/60">
|
||||
<td className="py-1.5 px-3" colSpan={3}>Revenue per Ticket</td>
|
||||
|
||||
{masterOptions.map(opt => (
|
||||
|
||||
Reference in New Issue
Block a user