Fix user dashboard: buried edit errors, closed events not hidden, closed/past registrations editable

Registration-edit errors were set into a page-level error state rendered
behind the edit modal overlay; they now render inside the modal. The
"Show past events" toggle only hid date-based past events, letting
cashup-closed events leak through by default; a shared isEventOver()
check now covers both, applied to registrations, tickets, and the
Edit button visibility (mirroring the backend's own edit block).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 16:43:33 +02:00
parent 890970fffe
commit eb46ed8264
3 changed files with 24 additions and 18 deletions
+18 -17
View File
@@ -9,6 +9,8 @@ import { QrImage } from "@/components/shared/QrImage";
// Helper formatters
const formatRand = (n: number) => `R ${n.toFixed(2)}`;
const isFuture = (d: string | Date) => new Date(d).getTime() > Date.now();
const isEventOver = (ev: any) =>
!!ev && ((ev.endDate && new Date(ev.endDate).getTime() < Date.now()) || ev.cashupStatus === 'closed');
// Effective unit price for an event option (or one of its variants), early-bird aware.
// Used by the registration editor, which works off raw /api/events/:id data rather than
@@ -443,11 +445,11 @@ export default function UserDashboardPage() {
const [editQuantities, setEditQuantities] = useState<Record<string, number>>({});
const [editBaseQty, setEditBaseQty] = useState<Record<string, number>>({});
const [editMinQty, setEditMinQty] = useState<Record<string, number>>({});
const [editError, setEditError] = useState<string | null>(null);
const canEditActive = useMemo(() => {
if (!activeReg) return false;
const end = activeReg.event?.endDate ? new Date(activeReg.event.endDate).getTime() : Infinity;
return end >= Date.now();
return !isEventOver(activeReg.event);
}, [activeReg]);
const editTotal = useMemo(() => {
@@ -464,6 +466,7 @@ export default function UserDashboardPage() {
const beginEdit = async () => {
if (!activeReg || !token) return;
setEditError(null);
setEditLoading(true);
try {
// Load event options (with variants) for this event
@@ -507,7 +510,7 @@ export default function UserDashboardPage() {
setEditMinQty(minQtyMap);
setEditMode(true);
} catch (e) {
setError((e as any)?.message || 'Failed to load event options');
setEditError((e as any)?.message || 'Failed to load event options');
} finally {
setEditLoading(false);
}
@@ -515,6 +518,7 @@ export default function UserDashboardPage() {
const saveEdit = async () => {
if (!activeReg || !token) return;
setEditError(null);
for (const [key, minQty] of Object.entries(editMinQty)) {
if (minQty <= 0) continue;
@@ -523,7 +527,7 @@ export default function UserDashboardPage() {
const eo = editEventOptions.find((o: any) => o.id === eventOptionId);
const variant = variantId ? (eo?.variants || []).find((v: any) => v.id === variantId) : null;
const label = variant ? `${eo?.name || 'item'} (${variant.name})` : (eo?.name || 'item');
setError(`Cannot reduce "${label}" below the ${minQty} already issued`);
setEditError(`Cannot reduce "${label}" below the ${minQty} already issued`);
return;
}
}
@@ -539,13 +543,13 @@ export default function UserDashboardPage() {
}
}
if (payloadOptions.length === 0) {
setError('Please select at least one item');
setEditError('Please select at least one item');
return;
}
// Enforce client-side: do not reduce below paid
const paid = activeBill?.totalPaid || 0;
if (editTotal < paid) {
setError('Cannot reduce items below the amount already paid');
setEditError('Cannot reduce items below the amount already paid');
return;
}
setLoading(true);
@@ -588,7 +592,7 @@ export default function UserDashboardPage() {
setInfo('Registration updated successfully');
setEditMode(false);
} catch (e: any) {
setError(e?.message || 'Failed to update registration');
setEditError(e?.message || 'Failed to update registration');
} finally {
setLoading(false);
}
@@ -635,11 +639,7 @@ export default function UserDashboardPage() {
{registrations
// Hide cancelled always; hide past unless showPast
.filter((r: any) => r.status !== 'cancelled')
.filter((r: any) => {
if (showPast) return true;
const end = r.event?.endDate ? new Date(r.event.endDate).getTime() : Infinity;
return end >= Date.now();
})
.filter((r: any) => showPast || !isEventOver(r.event))
.map((r: any) => {
const bill = billing[r.id];
const isCancelled = r.status === 'cancelled';
@@ -786,12 +786,12 @@ export default function UserDashboardPage() {
</div>
)}
</div>
{tickets.filter(t => showPast || (t.event?.endDate ? new Date(t.event.endDate).getTime() >= Date.now() : true)).length === 0 ? (
{tickets.filter(t => showPast || !isEventOver(t.event)).length === 0 ? (
<p className="text-gray-600 text-sm">No tickets yet.</p>
) : (
<ul className="grid sm:grid-cols-2 lg:grid-cols-3 gap-3">
{tickets
.filter((t: any) => showPast || (t.event?.endDate ? new Date(t.event.endDate).getTime() >= Date.now() : true))
.filter((t: any) => showPast || !isEventOver(t.event))
.map((t: any) => {
const eventDate = t.event?.startDate ? new Date(t.event.startDate) : null;
const optionName = t.registrationOption?.eventOption?.name || "Ticket";
@@ -827,11 +827,11 @@ export default function UserDashboardPage() {
</section>
{activeReg && (
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50" onClick={() => setActiveRegId(null)}>
<div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50" onClick={() => { setEditMode(false); setEditError(null); setShowCancelConfirm(false); setActiveRegId(null); }}>
<div className="bg-white rounded-lg shadow-lg max-w-xl w-full mx-4 p-5 max-h-[90vh] overflow-y-auto" onClick={e => e.stopPropagation()}>
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-semibold">{activeReg.event?.title || activeReg.eventId}</h3>
<button className="px-2.5 py-1 text-sm rounded bg-gray-100 hover:bg-gray-200 text-gray-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1" onClick={() => { setEditMode(false); setShowCancelConfirm(false); setActiveRegId(null); }}>Close</button>
<button className="px-2.5 py-1 text-sm rounded bg-gray-100 hover:bg-gray-200 text-gray-700 shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1" onClick={() => { setEditMode(false); setEditError(null); setShowCancelConfirm(false); setActiveRegId(null); }}>Close</button>
</div>
<div className="space-y-3">
<div>
@@ -870,6 +870,7 @@ export default function UserDashboardPage() {
</ul>
) : (
<div className="border rounded p-3 space-y-2 bg-gray-50">
{editError && <p className="text-xs text-red-600">{editError}</p>}
{editLoading ? (
<div className="text-sm text-gray-600">Loading options</div>
) : (
@@ -960,7 +961,7 @@ export default function UserDashboardPage() {
})()}
</div>
<div className="flex gap-2">
<button onClick={() => setEditMode(false)} className="px-2.5 py-1 text-xs rounded bg-gray-100 hover:bg-gray-200">Cancel</button>
<button onClick={() => { setEditMode(false); setEditError(null); }} className="px-2.5 py-1 text-xs rounded bg-gray-100 hover:bg-gray-200">Cancel</button>
<button onClick={saveEdit} className="px-2.5 py-1 text-xs bg-green-600 text-white rounded hover:bg-green-700">Save</button>
</div>
</div>