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

+239
View File
@@ -986,6 +986,171 @@ paths:
description: Working hours updated
"400":
description: Invalid request
/v1/catalog/staff/{staffID}/working-hours:
get:
tags: [Catalog]
operationId: listStaffWorkingHours
security:
- bearerAuth: []
parameters:
- in: path
name: staffID
required: true
schema:
type: string
format: uuid
responses:
"200":
description: Staff member's weekly working hours
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/WorkingHours"
"404":
description: Staff member not found
/v1/catalog/staff/{staffID}/working-hours/{dayOfWeek}:
put:
tags: [Catalog]
operationId: updateStaffWorkingHours
security:
- bearerAuth: []
parameters:
- in: path
name: staffID
required: true
schema:
type: string
format: uuid
- in: path
name: dayOfWeek
required: true
schema:
type: integer
minimum: 0
maximum: 6
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateWorkingHoursRequest"
responses:
"200":
description: Staff working hours updated
"400":
description: Invalid request
"404":
description: Staff member not found
/v1/catalog/class-templates:
get:
tags: [Catalog]
operationId: listClassTemplates
security:
- bearerAuth: []
responses:
"200":
description: Class templates
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ClassTemplate"
post:
tags: [Catalog]
operationId: createClassTemplate
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/UpsertClassTemplateRequest"
responses:
"201":
description: Class template created
content:
application/json:
schema:
$ref: "#/components/schemas/ClassTemplate"
"400":
description: Invalid request
/v1/catalog/class-templates/{templateID}:
delete:
tags: [Catalog]
operationId: deleteClassTemplate
security:
- bearerAuth: []
parameters:
- in: path
name: templateID
required: true
schema:
type: string
format: uuid
responses:
"200":
description: Class template deleted
"404":
description: Class template not found
/v1/catalog/class-sessions:
get:
tags: [Catalog]
operationId: listClassSessions
security:
- bearerAuth: []
responses:
"200":
description: Upcoming class sessions
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/ClassSession"
post:
tags: [Catalog]
operationId: createClassSession
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateClassSessionRequest"
responses:
"201":
description: Class session created
content:
application/json:
schema:
$ref: "#/components/schemas/ClassSession"
"400":
description: Invalid request
"404":
description: Template or location not found
/v1/catalog/class-sessions/{sessionID}:
delete:
tags: [Catalog]
operationId: deleteClassSession
security:
- bearerAuth: []
parameters:
- in: path
name: sessionID
required: true
schema:
type: string
format: uuid
responses:
"200":
description: Class session deleted
"404":
description: Class session not found
# ---------------- Billing ----------------
/v1/billing/subscription:
get:
@@ -1880,6 +2045,80 @@ components:
example: "18:00"
isOpen:
type: boolean
ClassTemplate:
type: object
required: [id, tenantId, title, durationMinutes, capacity, createdAt]
properties:
id:
type: string
format: uuid
tenantId:
type: string
format: uuid
title:
type: string
durationMinutes:
type: integer
capacity:
type: integer
createdAt:
type: string
format: date-time
UpsertClassTemplateRequest:
type: object
required: [title, durationMinutes, capacity]
properties:
title:
type: string
durationMinutes:
type: integer
capacity:
type: integer
ClassSession:
type: object
required: [id, tenantId, templateId, templateTitle, startsAt, endsAt, capacity, bookedCount]
properties:
id:
type: string
format: uuid
tenantId:
type: string
format: uuid
templateId:
type: string
format: uuid
templateTitle:
type: string
locationId:
type: string
format: uuid
locationName:
type: string
startsAt:
type: string
format: date-time
endsAt:
type: string
format: date-time
capacity:
type: integer
bookedCount:
type: integer
CreateClassSessionRequest:
type: object
required: [templateId, startsAt]
properties:
templateId:
type: string
format: uuid
locationId:
type: string
format: uuid
startsAt:
type: string
format: date-time
capacity:
type: integer
PlanEntitlements:
type: object
required: [maxLocations, maxStaff, emailReminders, advancedReporting, widgetEmbedding, umamiTracking]