feat(classes): session roster + manual waitlist promotion

Adds the roster and manual promotion controls deferred from the booking
lifecycle milestones. Businesses can now inspect who holds a seat and who
is queueing on a class session, and promote waitlisted customers by hand
— including a deliberate over-capacity override when the session is full.

Backend:
- GET /v1/catalog/class-sessions/{id}/roster — session metadata plus
  confirmed and waitlist entry lists with queue positions
- POST /v1/catalog/bookings/{id}/promote — 409 on a full session unless
  allowOverCapacity is set; 400 for non-waitlisted, 404 for missing or
  foreign bookings; reuses the shared confirm path so waitlist ledger
  rows resolve and reminder jobs re-arm
- ListBookingsBySession repository method (pgx + memory); ClassSession
  carries waitlistCount; UpcomingBooking carries classSessionId and
  waitlistPosition
- OpenAPI paths + schemas, regenerated client, catalog service tests

Frontend:
- classes tab session rows expand into a two-column roster panel with
  per-entry promote buttons and an explicit confirm-anyway step on 409
- booking detail modal shows waitlist position and offers promote for
  waitlisted bookings; list badges show #N queue positions
- CS/EN i18n keys and seeded demo roster data; demo mutations surface
  the demo notice

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-18 14:37:40 +02:00
1 parent a82a48e1c8
commit 1cd5b1ba91
18 files changed
+1022 -59

No files matched your search

+140
View File
@@ -303,6 +303,22 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/catalog/bookings/{bookingID}/promote": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post: operations["promoteDashboardBooking"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/catalog/class-sessions": {
parameters: {
query?: never;
@@ -335,6 +351,22 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/catalog/class-sessions/{sessionID}/roster": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["getSessionRoster"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/catalog/class-templates": {
parameters: {
query?: never;
@@ -1024,6 +1056,7 @@ export interface components {
templateTitle: string;
/** Format: uuid */
tenantId: string;
waitlistCount: number;
};
ClassTemplate: {
capacity: number;
@@ -1196,6 +1229,10 @@ export interface components {
/** Format: uri */
url: string;
};
PromoteBookingRequest: {
/** @description Confirm even when the session is full (deliberate overbooking) */
allowOverCapacity?: boolean;
};
PublicAvailabilityResponse: {
/** @enum {string} */
locale: "cs" | "en";
@@ -1244,6 +1281,31 @@ export interface components {
/** Format: uuid */
tenantId: string;
};
SessionRoster: {
capacity: number;
confirmed: components["schemas"]["SessionRosterEntry"][];
/** Format: date-time */
endsAt: string;
locationName?: string;
/** Format: uuid */
sessionId: string;
/** Format: date-time */
startsAt: string;
templateTitle: string;
waitlist: components["schemas"]["SessionRosterEntry"][];
};
SessionRosterEntry: {
/** Format: uuid */
bookingId: string;
/** Format: date-time */
createdAt: string;
customerEmail: string;
customerName: string;
customerPhone?: string;
reference: string;
status: string;
waitlistPosition?: number;
};
StaffMember: {
active: boolean;
/** Format: date-time */
@@ -1334,6 +1396,8 @@ export interface components {
siteId?: string;
};
UpcomingBooking: {
/** Format: uuid */
classSessionId?: string;
customerEmail?: string;
customerName?: string;
/** Format: date-time */
@@ -1348,6 +1412,8 @@ export interface components {
/** Format: date-time */
startsAt?: string;
status?: string;
/** @description 1-based queue spot for waitlisted class bookings; absent otherwise */
waitlistPosition?: number;
};
UpdateBrandRequest: {
logoUrl?: string;
@@ -2084,6 +2150,51 @@ export interface operations {
};
};
};
promoteDashboardBooking: {
parameters: {
query?: never;
header?: never;
path: {
bookingID: string;
};
cookie?: never;
};
requestBody?: {
content: {
"application/json": components["schemas"]["PromoteBookingRequest"];
};
};
responses: {
/** @description Waitlisted booking confirmed; customer notified */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Booking is not on a waitlist */
400: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Booking not found */
404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Session at capacity; retry with allowOverCapacity to override */
409: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
listClassSessions: {
parameters: {
query?: never;
@@ -2169,6 +2280,35 @@ export interface operations {
};
};
};
getSessionRoster: {
parameters: {
query?: never;
header?: never;
path: {
sessionID: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Confirmed attendees and FIFO waitlist for the session */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["SessionRoster"];
};
};
/** @description Class session not found */
404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
listClassTemplates: {
parameters: {
query?: never;