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]
|
## [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
|
## [1.0.0] - 2026-07-23
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -415,7 +415,7 @@ const getUserRegistrations = async (req, res) => {
|
|||||||
const whereClause = showPast ? { userId: req.user.id } : {
|
const whereClause = showPast ? { userId: req.user.id } : {
|
||||||
userId: req.user.id,
|
userId: req.user.id,
|
||||||
status: { not: 'cancelled' },
|
status: { not: 'cancelled' },
|
||||||
event: { endDate: { gte: now } }
|
event: { endDate: { gte: now }, cashupStatus: { not: 'closed' } }
|
||||||
};
|
};
|
||||||
|
|
||||||
const registrations = await prisma.registration.findMany({
|
const registrations = await prisma.registration.findMany({
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { QrImage } from "@/components/shared/QrImage";
|
|||||||
// Helper formatters
|
// Helper formatters
|
||||||
const formatRand = (n: number) => `R ${n.toFixed(2)}`;
|
const formatRand = (n: number) => `R ${n.toFixed(2)}`;
|
||||||
const isFuture = (d: string | Date) => new Date(d).getTime() > Date.now();
|
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.
|
// 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
|
// 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 [editQuantities, setEditQuantities] = useState<Record<string, number>>({});
|
||||||
const [editBaseQty, setEditBaseQty] = useState<Record<string, number>>({});
|
const [editBaseQty, setEditBaseQty] = useState<Record<string, number>>({});
|
||||||
const [editMinQty, setEditMinQty] = useState<Record<string, number>>({});
|
const [editMinQty, setEditMinQty] = useState<Record<string, number>>({});
|
||||||
|
const [editError, setEditError] = useState<string | null>(null);
|
||||||
|
|
||||||
const canEditActive = useMemo(() => {
|
const canEditActive = useMemo(() => {
|
||||||
if (!activeReg) return false;
|
if (!activeReg) return false;
|
||||||
const end = activeReg.event?.endDate ? new Date(activeReg.event.endDate).getTime() : Infinity;
|
return !isEventOver(activeReg.event);
|
||||||
return end >= Date.now();
|
|
||||||
}, [activeReg]);
|
}, [activeReg]);
|
||||||
|
|
||||||
const editTotal = useMemo(() => {
|
const editTotal = useMemo(() => {
|
||||||
@@ -464,6 +466,7 @@ export default function UserDashboardPage() {
|
|||||||
|
|
||||||
const beginEdit = async () => {
|
const beginEdit = async () => {
|
||||||
if (!activeReg || !token) return;
|
if (!activeReg || !token) return;
|
||||||
|
setEditError(null);
|
||||||
setEditLoading(true);
|
setEditLoading(true);
|
||||||
try {
|
try {
|
||||||
// Load event options (with variants) for this event
|
// Load event options (with variants) for this event
|
||||||
@@ -507,7 +510,7 @@ export default function UserDashboardPage() {
|
|||||||
setEditMinQty(minQtyMap);
|
setEditMinQty(minQtyMap);
|
||||||
setEditMode(true);
|
setEditMode(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError((e as any)?.message || 'Failed to load event options');
|
setEditError((e as any)?.message || 'Failed to load event options');
|
||||||
} finally {
|
} finally {
|
||||||
setEditLoading(false);
|
setEditLoading(false);
|
||||||
}
|
}
|
||||||
@@ -515,6 +518,7 @@ export default function UserDashboardPage() {
|
|||||||
|
|
||||||
const saveEdit = async () => {
|
const saveEdit = async () => {
|
||||||
if (!activeReg || !token) return;
|
if (!activeReg || !token) return;
|
||||||
|
setEditError(null);
|
||||||
|
|
||||||
for (const [key, minQty] of Object.entries(editMinQty)) {
|
for (const [key, minQty] of Object.entries(editMinQty)) {
|
||||||
if (minQty <= 0) continue;
|
if (minQty <= 0) continue;
|
||||||
@@ -523,7 +527,7 @@ export default function UserDashboardPage() {
|
|||||||
const eo = editEventOptions.find((o: any) => o.id === eventOptionId);
|
const eo = editEventOptions.find((o: any) => o.id === eventOptionId);
|
||||||
const variant = variantId ? (eo?.variants || []).find((v: any) => v.id === variantId) : null;
|
const variant = variantId ? (eo?.variants || []).find((v: any) => v.id === variantId) : null;
|
||||||
const label = variant ? `${eo?.name || 'item'} (${variant.name})` : (eo?.name || 'item');
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -539,13 +543,13 @@ export default function UserDashboardPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (payloadOptions.length === 0) {
|
if (payloadOptions.length === 0) {
|
||||||
setError('Please select at least one item');
|
setEditError('Please select at least one item');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Enforce client-side: do not reduce below paid
|
// Enforce client-side: do not reduce below paid
|
||||||
const paid = activeBill?.totalPaid || 0;
|
const paid = activeBill?.totalPaid || 0;
|
||||||
if (editTotal < paid) {
|
if (editTotal < paid) {
|
||||||
setError('Cannot reduce items below the amount already paid');
|
setEditError('Cannot reduce items below the amount already paid');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -588,7 +592,7 @@ export default function UserDashboardPage() {
|
|||||||
setInfo('Registration updated successfully');
|
setInfo('Registration updated successfully');
|
||||||
setEditMode(false);
|
setEditMode(false);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(e?.message || 'Failed to update registration');
|
setEditError(e?.message || 'Failed to update registration');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -635,11 +639,7 @@ export default function UserDashboardPage() {
|
|||||||
{registrations
|
{registrations
|
||||||
// Hide cancelled always; hide past unless showPast
|
// Hide cancelled always; hide past unless showPast
|
||||||
.filter((r: any) => r.status !== 'cancelled')
|
.filter((r: any) => r.status !== 'cancelled')
|
||||||
.filter((r: any) => {
|
.filter((r: any) => showPast || !isEventOver(r.event))
|
||||||
if (showPast) return true;
|
|
||||||
const end = r.event?.endDate ? new Date(r.event.endDate).getTime() : Infinity;
|
|
||||||
return end >= Date.now();
|
|
||||||
})
|
|
||||||
.map((r: any) => {
|
.map((r: any) => {
|
||||||
const bill = billing[r.id];
|
const bill = billing[r.id];
|
||||||
const isCancelled = r.status === 'cancelled';
|
const isCancelled = r.status === 'cancelled';
|
||||||
@@ -786,12 +786,12 @@ export default function UserDashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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>
|
<p className="text-gray-600 text-sm">No tickets yet.</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="grid sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
<ul className="grid sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||||
{tickets
|
{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) => {
|
.map((t: any) => {
|
||||||
const eventDate = t.event?.startDate ? new Date(t.event.startDate) : null;
|
const eventDate = t.event?.startDate ? new Date(t.event.startDate) : null;
|
||||||
const optionName = t.registrationOption?.eventOption?.name || "Ticket";
|
const optionName = t.registrationOption?.eventOption?.name || "Ticket";
|
||||||
@@ -827,11 +827,11 @@ export default function UserDashboardPage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{activeReg && (
|
{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="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">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<h3 className="text-lg font-semibold">{activeReg.event?.title || activeReg.eventId}</h3>
|
<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>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
@@ -870,6 +870,7 @@ export default function UserDashboardPage() {
|
|||||||
</ul>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
<div className="border rounded p-3 space-y-2 bg-gray-50">
|
<div className="border rounded p-3 space-y-2 bg-gray-50">
|
||||||
|
{editError && <p className="text-xs text-red-600">{editError}</p>}
|
||||||
{editLoading ? (
|
{editLoading ? (
|
||||||
<div className="text-sm text-gray-600">Loading options…</div>
|
<div className="text-sm text-gray-600">Loading options…</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -960,7 +961,7 @@ export default function UserDashboardPage() {
|
|||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user