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

+14
View File
@@ -24,6 +24,20 @@ func ParseIntakeFields(raw string) []domain.IntakeField {
return fields
}
// IntakeLabels maps a service's declared intake field keys to their display
// labels, so booking views can render questions rather than slugs.
func IntakeLabels(raw string) map[string]string {
fields := ParseIntakeFields(raw)
if len(fields) == 0 {
return nil
}
labels := make(map[string]string, len(fields))
for _, field := range fields {
labels[field.Key] = field.Label
}
return labels
}
// NormalizeIntakeFields validates a service's declared intake fields and
// returns the canonical JSON string for storage. Keys are slugified from the
// provided key or the label, must be unique, and fields are capped at 8.