feat(availability): per-staff working hours and class/session management

- Working-hours repo accepts nullable staff scope (pgx + memory); staff
  rules live in availability_rules and drive public slot generation
- Protected routes for staff working hours, class templates, and class
  sessions with tenant-ownership checks and input validation
- Sessions derive endsAt/capacity from templates; list resolves location
  names and live booked counts; template delete cascades sessions,
  session delete preserves bookings via SET NULL (mirrored in memory)
- Zones page: staff-scope selector on working hours, new Classes tab
  with template/session CRUD, CS+EN strings, mobile tab wrapping
- Normalize availability_rules times to HH:MM in the service layer

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 11:01:01 +02:00
1 parent d9aa29dbcb
commit 0405106587
12 files changed
+2069 -58

No files matched your search

+370
View File
@@ -287,6 +287,70 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/catalog/class-sessions": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["listClassSessions"];
put?: never;
post: operations["createClassSession"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/catalog/class-sessions/{sessionID}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
delete: operations["deleteClassSession"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/catalog/class-templates": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["listClassTemplates"];
put?: never;
post: operations["createClassTemplate"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/catalog/class-templates/{templateID}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
delete: operations["deleteClassTemplate"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/catalog/customers": {
parameters: {
query?: never;
@@ -415,6 +479,38 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/catalog/staff/{staffID}/working-hours": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["listStaffWorkingHours"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/catalog/staff/{staffID}/working-hours/{dayOfWeek}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put: operations["updateStaffWorkingHours"];
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/catalog/working-hours": {
parameters: {
query?: never;
@@ -895,6 +991,35 @@ export interface components {
*/
planCode: "starter" | "pro" | "business";
};
ClassSession: {
bookedCount: number;
capacity: number;
/** Format: date-time */
endsAt: string;
/** Format: uuid */
id: string;
/** Format: uuid */
locationId?: string;
locationName?: string;
/** Format: date-time */
startsAt: string;
/** Format: uuid */
templateId: string;
templateTitle: string;
/** Format: uuid */
tenantId: string;
};
ClassTemplate: {
capacity: number;
/** Format: date-time */
createdAt: string;
durationMinutes: number;
/** Format: uuid */
id: string;
/** Format: uuid */
tenantId: string;
title: string;
};
CreateBookingRequest: {
/** @enum {string} */
bookingMode: "appointment" | "class";
@@ -925,6 +1050,15 @@ export interface components {
/** @enum {string} */
status: "confirmed" | "waitlisted";
};
CreateClassSessionRequest: {
capacity?: number;
/** Format: uuid */
locationId?: string;
/** Format: date-time */
startsAt: string;
/** Format: uuid */
templateId: string;
};
Customer: {
bookingsCount?: number;
/** Format: email */
@@ -1222,6 +1356,11 @@ export interface components {
/** @enum {string} */
type: "closure" | "holiday" | "maintenance";
};
UpsertClassTemplateRequest: {
capacity: number;
durationMinutes: number;
title: string;
};
UpsertCustomerRequest: {
/** Format: email */
email: string;
@@ -1891,6 +2030,169 @@ export interface operations {
};
};
};
listClassSessions: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Upcoming class sessions */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ClassSession"][];
};
};
};
};
createClassSession: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["CreateClassSessionRequest"];
};
};
responses: {
/** @description Class session created */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ClassSession"];
};
};
/** @description Invalid request */
400: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Template or location not found */
404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
deleteClassSession: {
parameters: {
query?: never;
header?: never;
path: {
sessionID: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Class session deleted */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Class session not found */
404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
listClassTemplates: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Class templates */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ClassTemplate"][];
};
};
};
};
createClassTemplate: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["UpsertClassTemplateRequest"];
};
};
responses: {
/** @description Class template created */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ClassTemplate"];
};
};
/** @description Invalid request */
400: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
deleteClassTemplate: {
parameters: {
query?: never;
header?: never;
path: {
templateID: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Class template deleted */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Class template not found */
404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
listCustomers: {
parameters: {
query?: {
@@ -2380,6 +2682,74 @@ export interface operations {
};
};
};
listStaffWorkingHours: {
parameters: {
query?: never;
header?: never;
path: {
staffID: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Staff member's weekly working hours */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["WorkingHours"][];
};
};
/** @description Staff member not found */
404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
updateStaffWorkingHours: {
parameters: {
query?: never;
header?: never;
path: {
dayOfWeek: number;
staffID: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["UpdateWorkingHoursRequest"];
};
};
responses: {
/** @description Staff working hours updated */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Invalid request */
400: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Staff member not found */
404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
listWorkingHours: {
parameters: {
query?: never;