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

+25
View File
@@ -27,6 +27,8 @@ type UpcomingBooking struct {
StaffName string `json:"staffName,omitempty"`
Notes string `json:"notes,omitempty"`
Intake map[string]string `json:"intake,omitempty"`
// IntakeLabels maps Intake keys to the service's declared field labels.
IntakeLabels map[string]string `json:"intakeLabels,omitempty"`
StartsAt time.Time `json:"startsAt"`
EndsAt time.Time `json:"endsAt"`
Status string `json:"status"`
@@ -545,6 +547,8 @@ type CustomerBookingView struct {
Status string `json:"status"`
Notes string `json:"notes,omitempty"`
Intake map[string]string `json:"intake,omitempty"`
// IntakeLabels maps Intake keys to the service's declared field labels.
IntakeLabels map[string]string `json:"intakeLabels,omitempty"`
// WaitlistPosition is the 1-based queue spot for waitlisted class
// bookings; zero for any other status.
WaitlistPosition int `json:"waitlistPosition,omitempty"`
@@ -624,6 +628,27 @@ type StaffMetricsResponse struct {
Staff []StaffMetric `json:"staff"`
}
// ServiceMetric aggregates appointment bookings for one catalog service over
// a trailing window. Revenue counts non-cancelled bookings at list price.
type ServiceMetric struct {
ServiceID string `json:"serviceId"`
Name string `json:"name"`
Bookings int `json:"bookings"`
Completed int `json:"completed"`
Cancelled int `json:"cancelled"`
RevenueCents int64 `json:"revenueCents"`
}
type ServiceMetricsResponse struct {
From time.Time `json:"from"`
To time.Time `json:"to"`
// Services lists every catalog service, including zero rows.
Services []ServiceMetric `json:"services"`
// ClassBookings counts session bookings in the window; they carry no
// service or price so they are reported separately.
ClassBookings int `json:"classBookings"`
}
// ============================================
// ADMIN MODELS
// ============================================