Files
hope-events/frontend/src/app/dashboard/admin/cashup/page.tsx
T
joshuaandClaude Sonnet 5 8e6cb542d9 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>
2026-08-06 15:00:10 +02:00

120 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import React, { useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "@/hooks/useAuth";
import { apiFetch } from "@/lib/api";
import { useDismissingState } from "@/hooks/useDismissingState";
import { Wallet } from "lucide-react";
export default function CashupLandingPage() {
const { token } = useAuth();
const router = useRouter();
const [events, setEvents] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useDismissingState<string | null>(null);
const [search, setSearch] = useState("");
const [showPast, setShowPast] = useState(true);
const [showInactive, setShowInactive] = useState(false);
const [showClosed, setShowClosed] = useState(false);
useEffect(() => {
if (!token) return;
(async () => {
setLoading(true);
setError(null);
try {
const evs = await apiFetch<any[]>("/api/events/all?includePast=true&includeInactive=true", { authToken: token });
setEvents(Array.isArray(evs) ? evs : []);
} catch (e: any) {
setError(e?.message || "Failed to load events");
} finally {
setLoading(false);
}
})();
}, [token]);
const filtered = useMemo(() => {
const now = new Date();
return events
.filter(ev => showPast || !ev.endDate || new Date(ev.endDate) >= now)
.filter(ev => showInactive || ev.isActive !== false)
.filter(ev => showClosed || ev.cashupStatus !== "closed")
.filter(ev => !search.trim() || ev.title?.toLowerCase().includes(search.trim().toLowerCase()))
.sort((a, b) => new Date(b.startDate).getTime() - new Date(a.startDate).getTime());
}, [events, showPast, showInactive, showClosed, search]);
return (
<div className="max-w-3xl mx-auto space-y-4">
<div className="flex items-center justify-between gap-3 flex-wrap">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-brand-50 flex items-center justify-center shrink-0">
<Wallet className="w-5 h-5 text-brand-600" />
</div>
<div>
<h1 className="text-xl font-semibold text-gray-900">Post-event Cashup</h1>
<p className="text-sm text-gray-500 mt-1">Set costs, reconcile takings, and close out an event. Admin only.</p>
</div>
</div>
<button
type="button"
className="px-3 py-1.5 text-sm rounded-lg bg-gray-100 hover:bg-gray-200 text-gray-800 shrink-0"
onClick={() => router.push("/dashboard")}
>Back</button>
</div>
{error && <div className="text-sm text-red-600 bg-red-50 border border-red-100 rounded p-2">{error}</div>}
<div className="bg-white border rounded-lg p-3 flex flex-wrap items-center gap-3">
<input
className="border rounded px-3 py-1.5 text-sm flex-1 min-w-48"
placeholder="Search events…"
value={search}
onChange={e => setSearch(e.target.value)}
/>
<label className="flex items-center gap-1.5 text-sm text-gray-600">
<input type="checkbox" checked={showPast} onChange={e => setShowPast(e.target.checked)} /> Past events
</label>
<label className="flex items-center gap-1.5 text-sm text-gray-600">
<input type="checkbox" checked={showInactive} onChange={e => setShowInactive(e.target.checked)} /> Inactive events
</label>
<label className="flex items-center gap-1.5 text-sm text-gray-600">
<input type="checkbox" checked={showClosed} onChange={e => setShowClosed(e.target.checked)} /> Closed events
</label>
</div>
{loading && <div className="text-sm text-gray-400">Loading</div>}
{!loading && (
<ul className="space-y-2">
{filtered.map(ev => {
const isClosed = ev.cashupStatus === "closed";
return (
<li
key={ev.id}
className="border rounded-lg p-3 bg-white hover:bg-brand-50/40 cursor-pointer transition-colors flex items-center justify-between gap-3"
onClick={() => router.push(`/dashboard/admin/cashup/${ev.id}`)}
>
<div className="min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-medium text-sm">{ev.title}</span>
<span className={"text-[10px] px-1.5 py-0.5 rounded " + (isClosed ? "bg-rose-50 text-rose-700" : "bg-emerald-50 text-emerald-700")}>
{isClosed ? "Closed" : "Open"}
</span>
</div>
<div className="text-xs text-gray-500 mt-0.5">
{ev.startDate ? new Date(ev.startDate).toLocaleDateString() : ""}{ev.endDate ? ` ${new Date(ev.endDate).toLocaleDateString()}` : ""}
</div>
</div>
<span className="text-xs text-brand-600 shrink-0">Manage </span>
</li>
);
})}
{filtered.length === 0 && <div className="text-sm text-gray-400">No events match the current filters.</div>}
</ul>
)}
</div>
);
}