Files
hope-events/frontend/src/lib/waitlist.ts
T
joshuaandClaude Opus 5.5 f99b84858e Session gaps: per-session waitlist, door session picker, all-sessions choice
- Waitlist per session: entries for pick-sessions tickets name a session;
  offers use that session's free places, never run past its start, and
  claim links tick it. Held offers count towards session capacity.
- At the door: the Select items pop-up has the session picker, with
  today's session preselected for new attendees.
- On pick-sessions events every ticket shows the session list straight
  away. "All sessions" tickets record the ticked sessions
  (RegistrationOption.sessionIds) at one price; scanning, attendance,
  certificates, reminders and capacity follow them.
- Fix: per-session stock limits weren't enforced (takenCount filtered
  WaitlistEntry by a sessionId column that didn't exist, and the error
  was swallowed), and the public event page lost its stock info.
- Fix: editing a door registration with picked sessions couldn't be saved.
- Fix: the offline-scanning service worker served /_next/static cache-first
  forever, so dev browsers that had opened the scanner ran stale code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 11:38:21 +02:00

32 lines
1.9 KiB
TypeScript

// Pure helpers shared by server and client components. Must not be "use client",
// or server pages calling these get a client reference instead of the function.
// sessionId: one session of a "pick sessions" option (those are waited for per session).
export type WaitlistLine = { eventOptionId: string; variantId?: string | null; sessionId?: string | null; label: string };
/** "Drop-in (Adult)" or "Drop-in — Class 2, Wed 4 Nov" for a waitlist entry. */
export function entryLineName(e: { eventOption: { name: string }; variant?: { name: string } | null; session?: { label?: string | null; startsAt: string } | null }): string {
const base = `${e.eventOption.name}${e.variant ? ` (${e.variant.name})` : ""}`;
if (!e.session) return base;
const when = new Date(e.session.startsAt).toLocaleDateString(undefined, { weekday: "short", day: "numeric", month: "short" });
return `${base} — ${e.session.label ? `${e.session.label}, ` : ""}${when}`;
}
/** The sold-out lines of an event, for the waitlist button. */
export function soldOutLines(options: { id: string; name: string; stockLimit?: number; availableCount?: number | null; variants?: { id: string; name: string; stockLimit?: number; availableCount?: number | null }[] | null }[]): WaitlistLine[] {
const out: WaitlistLine[] = [];
for (const o of options || []) {
const optOut = (o.stockLimit ?? 0) > 0 && o.availableCount !== undefined && o.availableCount !== null && o.availableCount <= 0;
const variants = o.variants || [];
if (variants.length > 0) {
for (const v of variants) {
const vOut = (v.stockLimit ?? 0) > 0 && v.availableCount !== undefined && v.availableCount !== null && v.availableCount <= 0;
if (optOut || vOut) out.push({ eventOptionId: o.id, variantId: v.id, label: `${o.name} (${v.name})` });
}
} else if (optOut) {
out.push({ eventOptionId: o.id, label: o.name });
}
}
return out;
}