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:
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user