feat(reports): capacity-based utilization + report CSVs

Utilization now measures booked minutes against each member's
availability-rule capacity in the tenant timezone, replacing the
confirmed-share proxy. StaffMetric exposes booked/capacity minutes.

New endpoints: GET /v1/export/staff-report.csv (reuses StaffMetrics)
and GET /v1/export/service-report.csv (per-service bookings/cancelled/
revenue, class bookings rolled up). Export buttons on the staff
performance panel and services page.

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 17:10:15 +02:00
1 parent 2464aa45fa
commit 7b9080eb1c
13 files changed
+541 -33

No files matched your search

@@ -833,8 +833,15 @@ func TestStaffMetricsAggregatesPerStaff(t *testing.T) {
if row.Cancelled != 1 {
t.Fatalf("expected 1 cancellation, got %d", row.Cancelled)
}
if row.UtilizationPercent != 66 {
t.Fatalf("expected 66%% utilization, got %d", row.UtilizationPercent)
if row.BookedMinutes != 120 {
t.Fatalf("expected 120 booked minutes (2×60min confirmed), got %d", row.BookedMinutes)
}
if row.CapacityMinutes <= 0 {
t.Fatalf("expected positive capacity from Mon–Fri 9–17 rules, got %d", row.CapacityMinutes)
}
expectedUtilization := row.BookedMinutes * 100 / row.CapacityMinutes
if row.UtilizationPercent != expectedUtilization {
t.Fatalf("expected utilization %d (booked/capacity), got %d", expectedUtilization, row.UtilizationPercent)
}
if row.RevenueCents != 240000 {
t.Fatalf("expected 240000 revenue cents (2×1200 Kč), got %d", row.RevenueCents)