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

+27 -4
View File
@@ -81,7 +81,8 @@
- shared `bookings.PromoteWaitlist` helper drives both cancel paths; waitlist ledger entries are marked resolved on promotion
- bookings surface the `waitlisted` status: stats card, filter option, warning-tone badge, demo seed entry; `dashboard.{status}` interpolation resolves the new i18n key automatically
- `BookingRecord` now carries `created_at` (needed for fair waitlist ordering); `GetBookingByID` added to the repository surface
- remaining: live staging validation; waitlist position display and manual promotion controls are not yet exposed
- waitlist position display landed in Milestone 16; manual promotion controls landed in Milestone 20
- remaining: live staging validation
- `[done/partial]` Milestone 12: Real dashboard business metrics
- `DashboardMetrics` gained `CompletedCount` (confirmed bookings already ended within the window) in both pgx and in-memory repositories
- `DashboardSummary` now queries three trailing windows (next 7d, last 7d, 7-14d ago) and emits four KPIs with real trend deltas: `bookings_this_week`, `completed`, `cancellations`, `utilization` (delta in percentage points)
@@ -111,7 +112,8 @@
- `GetWaitlistPosition` repository method (pgx + memory) returns the 1-based queue spot: count of earlier-created waitlisted bookings on the same session + 1
- `CustomerBookingView` carries `waitlistPosition` for waitlisted class bookings; the manage page now shows a proper status badge for `waitlisted`/`cancelled` (previously both rendered as "Pending") plus a notice with the queue position and the auto-promotion explanation, in CS and EN
- fixed invalid token names along the way (`--danger-soft`/`--warning-soft` do not exist; `--error-subtle`/`--warning-subtle` do)
- remaining: live staging validation; manual waitlist promotion controls are still not exposed
- manual promotion controls landed in Milestone 20
- remaining: live staging validation
- `[done/partial]` Milestone 17: Class template editing
- `PUT /v1/catalog/class-templates/{id}` end-to-end: `UpdateClassTemplate` in both repositories, catalog service method sharing `validateClassTemplateInput` with create, route, OpenAPI path, regenerated client
- classes tab template rows gained a pencil edit action that loads the template into the form; the submit button switches to save-changes with a cancel affordance
@@ -127,6 +129,15 @@
- validated end-to-end against a local `postgres:17` container: SQL-level matrix (staff/location/unassigned overlap blocked, abutting allowed, cancel frees the slot), repo-level 23P01 detection, and a 10-request concurrent burst on one slot producing exactly one 201 + 409s for the rest; down/up migration roundtrip clean
- local test container: `docker run -d --name bookra-pg-local -p 5439:5432 -e POSTGRES_PASSWORD=bookra-local postgres:17-alpine`, migrations via `npm run db:migrate:up` with `BOOKRA_DATABASE_DIRECT_URL` pointed at it
- remaining: live staging validation on Neon
- `[done/partial]` Milestone 20: Class roster + manual waitlist promotion
- `GET /v1/catalog/class-sessions/{id}/roster` returns session metadata plus `confirmed` and `waitlist` entry lists (booking id, reference, customer name/email, status, `waitlistPosition`, `createdAt`); `ClassSession` now carries `waitlistCount` so session rows can badge queue length without an extra request
- `POST /v1/catalog/bookings/{id}/promote` manually confirms a waitlisted booking: `409` when the session is full unless the caller passes `allowOverCapacity`, `400` for non-waitlisted bookings, `404` for missing or foreign bookings; promotion reuses the shared confirm path so waitlist ledger rows resolve and reminders re-arm automatically
- `ListBookingsBySession` repository method (pgx + memory) backs the roster and simplifies `PromoteWaitlist`; `UpcomingBooking` carries `classSessionId` + `waitlistPosition`, computed per session in the dashboard summary
- classes tab session rows expand into a two-column roster panel (attendees vs waitlist with `#N` position badges) with per-entry promote buttons; a full session arms an explicit "confirm anyway" over-capacity step instead of silently overriding
- the booking detail modal shows the waitlist position and offers the same promote action for waitlisted bookings; the bookings list badges waitlisted rows with `#N`
- demo mode ships a seeded session with a populated roster; mutation attempts surface the demo notice instead of failing
- verified live in demo mode: seeded a full session via the public booking API, roster listed 4 confirmed + 1 waitlisted, promote rejected with 409, `allowOverCapacity` promoted and drained the queue; browser-checked CS/EN rendering with a clean console
- remaining: live staging validation on Neon + pgx repository path
## Overview
Bookra is a Czech-first booking SaaS for small local service businesses. It targets salons, massage therapists, small clinics, repair shops, and small studios/clubs that need a simple public booking flow, internal schedule management, reminders, and basic business reporting without becoming a full ERP.
@@ -361,7 +372,7 @@ Key entities:
## Current Gaps
- Neon Auth token extraction is wired structurally, but still needs live browser validation against a real Neon Auth environment.
- Tenant membership bootstrap now syncs identities structurally, but staging still needs seeded membership records and onboarding flows for first-time tenants.
- Post-onboarding CRUD now covers services, staff, locations, blocked days, working hours, and customers; class/session template management and per-staff availability rules still have no management UI.
- Post-onboarding CRUD now covers services, staff, locations, blocked days, working hours, customers, and class templates/sessions with rosters; per-staff availability rules still have no management UI.
- Stripe lifecycle sync is implemented; still needs live environment validation against real products, price IDs, and webhook signatures.
- Real email/SMS provider integrations are not implemented yet; current delivery providers are safe noops.
- The internal reminder dispatch route exists, but Railway scheduling and secret rotation still need production setup.
@@ -371,7 +382,19 @@ Key entities:
- sync Neon Auth identities into tenant membership records and onboarding bootstrap
- validate Stripe checkout/webhooks/portal and reminder dispatch against live staging infrastructure
- replace noop notification providers with the first production email transport and a gated SMS transport
- add management UI for class/session templates and per-staff availability rules so service duration, capacity, and staff schedules are editable after onboarding
- add management UI for per-staff availability rules so staff schedules are editable after onboarding
## Future Phases
Shortlist of worthwhile directions once v1 stabilizes — kept deliberately small, in line with the "no feature sprawl" principle:
- **Activity/audit log**: a per-tenant feed of who changed what (booking created/cancelled/promoted, settings edits). Natural fit for the notification dropdown and useful for multi-staff accountability.
- **Customer self-service rebooking**: let customers move to a different free slot from the manage page instead of cancel-and-rebook; the reschedule path already validates targets, so this is mostly UI plus a picker.
- **Calendar integrations**: ICS feed per tenant/staff and optional Google Calendar sync; high demand for salons, moderate complexity. Start with a read-only `.ics` subscription URL — zero third-party auth needed.
- **Recurring class schedules**: templates currently schedule sessions one at a time; a weekly recurrence rule with per-session overrides would cut setup time for studios.
- **Booking notes intake**: structured intake fields per service (e.g. "first visit?", "injuries?") instead of the single free-text notes field — still schema-light.
- **Persistent notification center**: the dropdown currently derives notifications from summary data; a `notifications` table would enable read/unread state and delivery history in-app.
- **Staff performance view**: bookings/completion/utilization per staff member — the metrics primitives already exist; this is a filtered presentation layer.
- **CSV export**: bookings and customers export for accounting/backup; cheap to build, frequently requested by small businesses.
## Product Principles
- calm, premium, structured UX