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

@@ -848,6 +848,26 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/metrics/services": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* 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`.
*/
get: operations["getServiceMetrics"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/metrics/staff": {
parameters: {
query?: never;
@@ -1482,6 +1502,10 @@ export interface components {
intake?: {
[key: string]: string;
};
/** @description Maps intake answer keys to the service's declared field labels; absent for services without intake */
intakeLabels?: {
[key: string]: string;
};
location?: string;
notes?: string;
reference?: string;
@@ -1670,6 +1694,28 @@ export interface components {
/** Format: uuid */
tenantId: string;
};
ServiceMetric: {
bookings: number;
cancelled: number;
completed: number;
name: string;
/**
* Format: int64
* @description Sum of the service's price for non-cancelled bookings
*/
revenueCents: number;
/** Format: uuid */
serviceId: string;
};
ServiceMetricsResponse: {
/** @description Session bookings in the window; reported separately since they carry no service or price */
classBookings: number;
/** Format: date-time */
from: string;
services: components["schemas"]["ServiceMetric"][];
/** Format: date-time */
to: string;
};
SessionMoveTarget: {
capacity?: number;
confirmedCount?: number;
@@ -1845,6 +1891,10 @@ export interface components {
intake?: {
[key: string]: string;
};
/** @description Maps intake answer keys to the service's declared field labels; absent for services without intake */
intakeLabels?: {
[key: string]: string;
};
label?: string;
location?: string;
notes?: string;
@@ -3910,6 +3960,35 @@ export interface operations {
};
};
};
getServiceMetrics: {
parameters: {
query?: {
days?: number;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Per-service metrics */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ServiceMetricsResponse"];
};
};
/** @description Tenant membership not found */
404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
getStaffMetrics: {
parameters: {
query?: {