From eb46ed82647908285606270cceefba553d0fabce Mon Sep 17 00:00:00 2001 From: joshua Date: Thu, 23 Jul 2026 16:43:33 +0200 Subject: [PATCH 1/3] 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 --- CHANGELOG.md | 5 +++ .../src/controllers/registrationController.js | 2 +- frontend/src/app/dashboard/user/page.tsx | 35 ++++++++++--------- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cb9d58..bf0e723 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project follows [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Fixed + +- User dashboard: registration-edit errors now show inside the edit popup instead of being hidden behind it. +- User dashboard: closed events are now hidden by default alongside past events (revealed via "Show past events"), and the Edit button no longer appears for closed or past registrations. + ## [1.0.0] - 2026-07-23 ### Added diff --git a/backend/src/controllers/registrationController.js b/backend/src/controllers/registrationController.js index 6a9ebcf..64116d9 100644 --- a/backend/src/controllers/registrationController.js +++ b/backend/src/controllers/registrationController.js @@ -415,7 +415,7 @@ const getUserRegistrations = async (req, res) => { const whereClause = showPast ? { userId: req.user.id } : { userId: req.user.id, status: { not: 'cancelled' }, - event: { endDate: { gte: now } } + event: { endDate: { gte: now }, cashupStatus: { not: 'closed' } } }; const registrations = await prisma.registration.findMany({ diff --git a/frontend/src/app/dashboard/user/page.tsx b/frontend/src/app/dashboard/user/page.tsx index 0b08938..8c54856 100644 --- a/frontend/src/app/dashboard/user/page.tsx +++ b/frontend/src/app/dashboard/user/page.tsx @@ -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>({}); const [editBaseQty, setEditBaseQty] = useState>({}); const [editMinQty, setEditMinQty] = useState>({}); + const [editError, setEditError] = useState(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() { )} - {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 ? (

No tickets yet.

) : (
    {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() { {activeReg && ( -
    setActiveRegId(null)}> +
    { setEditMode(false); setEditError(null); setShowCancelConfirm(false); setActiveRegId(null); }}>
    e.stopPropagation()}>

    {activeReg.event?.title || activeReg.eventId}

    - +
    @@ -870,6 +870,7 @@ export default function UserDashboardPage() {
) : (
+ {editError &&

{editError}

} {editLoading ? (
Loading options…
) : ( @@ -960,7 +961,7 @@ export default function UserDashboardPage() { })()}
- +
From 349686e1826f5f64c4377cd33821787533117964 Mon Sep 17 00:00:00 2001 From: joshua Date: Thu, 23 Jul 2026 16:51:10 +0200 Subject: [PATCH 2/3] Block self-service payment and cancellation on closed/past registrations Extends the earlier user-dashboard fix: canEditActive is renamed canModifyActive and now also gates the "Make payment" and "Cancel registration" actions, not just editing. Backend enforcement added to cancelRegistration and createYocoCheckout to block past-event self-service payment/cancellation server-side (cashup-closed events were already blocked via assertEventOpen; admins/supervisors are exempt from the past-date check, consistent with existing overrides). Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 2 +- backend/src/controllers/paymentController.js | 6 +++++- backend/src/controllers/registrationController.js | 9 ++++++++- frontend/src/app/dashboard/user/page.tsx | 12 +++++++----- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf0e723..1d0496e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ and this project follows [Semantic Versioning](https://semver.org/). ### Fixed - User dashboard: registration-edit errors now show inside the edit popup instead of being hidden behind it. -- User dashboard: closed events are now hidden by default alongside past events (revealed via "Show past events"), and the Edit button no longer appears for closed or past registrations. +- User dashboard: closed events are now hidden by default alongside past events (revealed via "Show past events"), and the Edit, Make payment, and Cancel registration actions no longer appear for closed or past registrations (also enforced server-side). ## [1.0.0] - 2026-07-23 diff --git a/backend/src/controllers/paymentController.js b/backend/src/controllers/paymentController.js index 804ba0e..00cdc9e 100644 --- a/backend/src/controllers/paymentController.js +++ b/backend/src/controllers/paymentController.js @@ -788,11 +788,15 @@ const createYocoCheckout = async (req, res) => { // Branch 1: Registration payment — delegate to internal helper if (registrationId) { // Authorisation check (the internal helper skips this) - const reg = await prisma.registration.findUnique({ where: { id: registrationId }, select: { userId: true, eventId: true } }); + const reg = await prisma.registration.findUnique({ where: { id: registrationId }, select: { userId: true, eventId: true, event: { select: { endDate: true } } } }); if (!reg) { res.status(404); throw new Error('Registration not found'); } if (reg.userId !== userId && req.user.role !== 'admin' && req.user.role !== 'supervisor') { res.status(403); throw new Error('Not authorized to create checkout for this registration'); } + // If event is in the past, block self-service payment (mirrors the registration-edit block) + if (req.user.role !== 'admin' && req.user.role !== 'supervisor' && reg.event?.endDate && new Date(reg.event.endDate).getTime() < Date.now()) { + res.status(400); throw new Error('This event has already ended; payment can no longer be made for this registration'); + } await assertEventOpen(reg.eventId, res); // Refresh early-bird pricing — updates priceSnapshot if any tier has expired or sold out. diff --git a/backend/src/controllers/registrationController.js b/backend/src/controllers/registrationController.js index 64116d9..a8fc6a9 100644 --- a/backend/src/controllers/registrationController.js +++ b/backend/src/controllers/registrationController.js @@ -583,7 +583,8 @@ const updateRegistrationStatus = async (req, res) => { const cancelRegistration = async (req, res) => { try { const registration = await prisma.registration.findUnique({ - where: { id: req.params.id } + where: { id: req.params.id }, + include: { event: { select: { endDate: true } } } }); if (!registration) { @@ -597,6 +598,12 @@ const cancelRegistration = async (req, res) => { throw new Error('Not authorized to cancel this registration'); } + // If event is in the past, block self-cancellation (mirrors the edit-block above) + if (req.user.role !== 'admin' && registration.event?.endDate && new Date(registration.event.endDate).getTime() < Date.now()) { + res.status(400); + throw new Error('This event has already ended; registration can no longer be cancelled'); + } + await assertEventOpen(registration.eventId, res); // Check if registration can be cancelled diff --git a/frontend/src/app/dashboard/user/page.tsx b/frontend/src/app/dashboard/user/page.tsx index 8c54856..b97b271 100644 --- a/frontend/src/app/dashboard/user/page.tsx +++ b/frontend/src/app/dashboard/user/page.tsx @@ -447,7 +447,9 @@ export default function UserDashboardPage() { const [editMinQty, setEditMinQty] = useState>({}); const [editError, setEditError] = useState(null); - const canEditActive = useMemo(() => { + // Whether the active registration's event still permits self-service changes + // (edit items, pay, cancel) — false once the event is past or its cashup is closed. + const canModifyActive = useMemo(() => { if (!activeReg) return false; return !isEventOver(activeReg.event); }, [activeReg]); @@ -847,7 +849,7 @@ export default function UserDashboardPage() {
Registration items - {!editMode && canEditActive && ( + {!editMode && canModifyActive && ( )}
@@ -987,7 +989,7 @@ export default function UserDashboardPage() { onClick={() => router.push(`/dashboard/user/forms?registrationId=${encodeURIComponent(activeReg.id)}`)} >Attendee forms )} - {activeBill && activeBill.outstanding > 0 ? ( + {canModifyActive && activeBill && activeBill.outstanding > 0 ? (
- {/* Cancel registration — only shown when no payments have been made */} - {activeReg.status !== 'cancelled' && (activeBill?.totalPaid ?? 0) === 0 && ( + {/* Cancel registration — only shown when no payments have been made and the event still permits changes */} + {canModifyActive && activeReg.status !== 'cancelled' && (activeBill?.totalPaid ?? 0) === 0 && (
{!showCancelConfirm ? (
{type}
@@ -832,7 +856,10 @@ export default function UserDashboardPage() {
{ setEditMode(false); setEditError(null); setShowCancelConfirm(false); setActiveRegId(null); }}>
e.stopPropagation()}>
-

{activeReg.event?.title || activeReg.eventId}

+

+ {activeReg.event?.title || activeReg.eventId} + +