+
+
+
+
diff --git a/packages/api-client/src/generated/types.ts b/packages/api-client/src/generated/types.ts
index 0f6b11b..7645620 100644
--- a/packages/api-client/src/generated/types.ts
+++ b/packages/api-client/src/generated/types.ts
@@ -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;
diff --git a/project.md b/project.md
index b7a5e84..f253a84 100644
--- a/project.md
+++ b/project.md
@@ -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