feat: data surfacing — intake labels, service metrics, honest capacity

- booking views gain intakeLabels (declared key→label join); dashboard
  modal + manage view render real question labels with slug fallback
- GET /v1/metrics/services aggregates bookings/completed/cancelled/
  revenue per service + classBookings; service-report.csv delegates to
  the same ServiceMetrics aggregation and gains a completed column
- services page gains a Service performance card (per-service stat grid,
  locale CZK revenue, class-bookings footer); demo rows match the
  fabricated service list
- staff capacity utilization subtracts blocked days — tenant-wide and
  staff-scoped blocks remove that day's rule minutes via blockedAt
- tests: blocked-day capacity (tenant + staff scope), service aggregation

Authored By: TDvorak <[email protected]>

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-22 20:25:32 +02:00
1 parent 34becc1bf4
commit 5c860a1264
16 files changed
+560 -69

No files matched your search

+73
View File
@@ -1545,6 +1545,33 @@ paths:
$ref: "#/components/schemas/StaffMetricsResponse"
"404":
description: Tenant membership not found
/v1/metrics/services:
get:
tags: [Metrics]
operationId: getServiceMetrics
security:
- bearerAuth: []
summary: Per-service appointment metrics over a trailing window
description: >-
Bookings, completions, cancellations, and revenue per catalog service.
Every service appears, including zero rows. Class bookings carry no
service or price and are counted separately in `classBookings`.
parameters:
- name: days
in: query
schema:
type: integer
default: 30
maximum: 365
responses:
"200":
description: Per-service metrics
content:
application/json:
schema:
$ref: "#/components/schemas/ServiceMetricsResponse"
"404":
description: Tenant membership not found
/v1/export/bookings.csv:
get:
tags: [Export]
@@ -2219,6 +2246,11 @@ components:
additionalProperties:
type: string
description: Structured intake answers keyed by the service's declared intakeFields keys
intakeLabels:
type: object
additionalProperties:
type: string
description: Maps intake answer keys to the service's declared field labels; absent for services without intake
classSessionId:
type: string
format: uuid
@@ -2266,6 +2298,11 @@ components:
type: object
additionalProperties:
type: string
intakeLabels:
type: object
additionalProperties:
type: string
description: Maps intake answer keys to the service's declared field labels; absent for services without intake
waitlistPosition:
type: integer
description: 1-based queue spot for waitlisted class bookings; absent otherwise
@@ -2408,6 +2445,42 @@ components:
type: array
items:
$ref: "#/components/schemas/StaffMetric"
ServiceMetric:
type: object
required: [serviceId, name, bookings, completed, cancelled, revenueCents]
properties:
serviceId:
type: string
format: uuid
name:
type: string
bookings:
type: integer
completed:
type: integer
cancelled:
type: integer
revenueCents:
type: integer
format: int64
description: Sum of the service's price for non-cancelled bookings
ServiceMetricsResponse:
type: object
required: [from, to, services, classBookings]
properties:
from:
type: string
format: date-time
to:
type: string
format: date-time
services:
type: array
items:
$ref: "#/components/schemas/ServiceMetric"
classBookings:
type: integer
description: Session bookings in the window; reported separately since they carry no service or price
DashboardBookingRequest:
type: object
required: [customerName, customerEmail, startsAt]