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>
114 lines
4.8 KiB
TypeScript
114 lines
4.8 KiB
TypeScript
"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>
|
||
);
|
||
}
|