Files
hope-events/frontend/src/app/dashboard/admin/cashup/page.tsx
T
joshuaandClaude Sonnet 5 f3e6525467 Add registration status badges and auto-dismissing dashboard messages
Two consistency fixes requested after the payment-method work:

1. Registration status (pending/confirmed/partial_paid/paid/cancelled)
   was printed as a raw string on the user dashboard. Added
   RegistrationStatusBadge mirroring the existing EventStatusBadge
   pattern, using the same status colors already established on
   dashboard/admin/registrations.

2. Inline success/error banners across dashboard pages persisted
   indefinitely. Added a shared useDismissingState hook (drop-in
   useState replacement that auto-clears a truthy value after 7s,
   resetting the timer on each update) and swapped it in across ~24
   dashboard files. Excluded: message-only modal dialogs (ticket-
   scanning's success/error confirmations) and two states that mix
   live form-validation feedback with async results inside actively-
   open forms (the registration-edit modal's editError, the event
   create/edit modal's error) - those keep persisting until the user
   acts, since auto-hiding a "fix this field" message mid-edit would
   be a regression. Also fixed at-the-door's existing bespoke
   auto-dismiss timers (10s/15s, one mislabeled as "5s") to the same
   consistent 7s, and removed admin/settings' manual x dismiss button
   in favor of the same auto-only behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 10:17:53 +02:00

114 lines
4.8 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";
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">
<div>
<h1 className="text-xl font-semibold">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>
<button
type="button"
className="px-3 py-1.5 text-sm rounded 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-indigo-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-indigo-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>
);
}