feat(booking): stepped public booking flow with service/staff selection

The public booking page showed a capped flat preview of slots with no way
to pick a service, day, or staff member. Replaces it with a stepped flow:
pick service, optional staff preference, pick a day, pick a time.

Backend: availability accepts serviceId/staffId/date filters (default
response unchanged for the embed widget), a new public catalog endpoint
exposes bookable services and active staff, and slots carry staffName.
Slot generation and booking creation now honor tenant-wide and per-staff
blocked days; appointment requests must fit inside a matching
availability rule and reference tenant-owned, active staff and locations.

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 10:18:03 +02:00
1 parent 09f780e7a6
commit d9aa29dbcb
9 files changed
+940 -118

No files matched your search

+64 -1
View File
@@ -607,6 +607,22 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/public/tenants/{tenantSlug}/catalog": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["getPublicCatalog"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/public/widget.js": {
parameters: {
query?: never;
@@ -1033,6 +1049,11 @@ export interface components {
tenantSlug: string;
timezone: string;
};
PublicCatalogResponse: {
services: components["schemas"]["PublicService"][];
staff: components["schemas"]["PublicStaff"][];
tenantSlug: string;
};
PublicConfig: {
/** Format: uri */
apiUrl: string;
@@ -1040,6 +1061,18 @@ export interface components {
environment: string;
neonAuthEnabled: boolean;
};
PublicService: {
durationMinutes: number;
/** Format: uuid */
id: string;
name: string;
priceCents: number;
};
PublicStaff: {
displayName: string;
/** Format: uuid */
id: string;
};
Service: {
/** @description Blocked time after the appointment (0-480) */
bufferAfterMinutes: number;
@@ -1136,6 +1169,7 @@ export interface components {
serviceId?: string | null;
/** Format: uuid */
staffId?: string | null;
staffName?: string | null;
/** Format: date-time */
startsAt: string;
};
@@ -2674,7 +2708,14 @@ export interface operations {
};
getPublicAvailability: {
parameters: {
query?: never;
query?: {
/** @description Return all slots for a single tenant-local day (YYYY-MM-DD) instead of the capped preview */
date?: string;
/** @description Restrict appointment slots to a single service */
serviceId?: string;
/** @description Restrict appointment slots to a single staff member */
staffId?: string;
};
header?: never;
path: {
tenantSlug: string;
@@ -2694,6 +2735,28 @@ export interface operations {
};
};
};
getPublicCatalog: {
parameters: {
query?: never;
header?: never;
path: {
tenantSlug: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Bookable services and active staff for a tenant booking page */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["PublicCatalogResponse"];
};
};
};
};
getWidgetScript: {
parameters: {
query?: never;