mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-29 07:54:44 +00:00
feat(metrics): per-staff performance view in dashboard
GET /v1/metrics/staff returns bookings, completed, cancelled, utilization, and revenue per staff member over a trailing window (default 30d, max 365d). Appointment bookings only — class sessions have no staff assignment. All active staff appear, including zero-booking rows. Staff page gains a localized Performance panel; demo mode seeds trailing-window bookings and matching metric rows. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
4674203502
commit
76e45d4178
11 files changed
+496
-3
No files matched your search
@@ -766,3 +766,77 @@ func TestWaitlistPositionReflectsQueueOrder(t *testing.T) {
|
||||
t.Fatalf("expected promoted booking to report no position, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaffMetricsAggregatesPerStaff(t *testing.T) {
|
||||
repo := db.NewMemoryRepository()
|
||||
service := NewService(repo, nil, "http://localhost:3000")
|
||||
ctx := context.Background()
|
||||
|
||||
tenantID := "5d6b3551-0a3e-4b86-bdf0-e9df20a47148"
|
||||
staffID := "6936c444-c7d0-4a7d-b596-a9b72d2f4fc0" // Demo Stylist
|
||||
serviceID := "d5d76a61-3d49-467c-8dd4-bf61ee754e39" // Signature treatment, 1200 Kč
|
||||
now := time.Now().UTC()
|
||||
|
||||
seed := func(offsetDays int, status string, withService bool) {
|
||||
t.Helper()
|
||||
params := db.CreateBookingParams{
|
||||
TenantID: tenantID,
|
||||
StaffID: &staffID,
|
||||
CustomerName: "Client",
|
||||
CustomerEmail: "[email protected]",
|
||||
StartsAt: now.AddDate(0, 0, offsetDays),
|
||||
EndsAt: now.AddDate(0, 0, offsetDays).Add(time.Hour),
|
||||
Status: status,
|
||||
Reference: "BK-SM",
|
||||
}
|
||||
if withService {
|
||||
params.ServiceID = &serviceID
|
||||
}
|
||||
if _, err := repo.CreateBooking(ctx, params); err != nil {
|
||||
t.Fatalf("seed booking: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
seed(-5, "confirmed", true)
|
||||
seed(-3, "confirmed", true) // already ended → completed too
|
||||
seed(-2, "cancelled", false) // cancelled → no revenue
|
||||
seed(-40, "confirmed", true) // outside the 30-day window
|
||||
// Class booking (no staff) must not appear in per-staff metrics.
|
||||
sessionID := "4bf74c12-44dd-45ca-86bb-b104f16f2435"
|
||||
if _, err := repo.CreateBooking(ctx, db.CreateBookingParams{
|
||||
TenantID: tenantID,
|
||||
ClassSessionID: &sessionID,
|
||||
CustomerName: "Class Client",
|
||||
CustomerEmail: "[email protected]",
|
||||
StartsAt: now.AddDate(0, 0, -1),
|
||||
EndsAt: now.AddDate(0, 0, -1).Add(time.Hour),
|
||||
Status: "confirmed",
|
||||
Reference: "BK-SM-CLASS",
|
||||
}); err != nil {
|
||||
t.Fatalf("seed class booking: %v", err)
|
||||
}
|
||||
|
||||
response, err := service.StaffMetrics(ctx, domain.Principal{Subject: "demo-owner", Role: "owner"}, 30)
|
||||
if err != nil {
|
||||
t.Fatalf("staff metrics: %v", err)
|
||||
}
|
||||
if len(response.Staff) != 1 {
|
||||
t.Fatalf("expected 1 staff row, got %d", len(response.Staff))
|
||||
}
|
||||
row := response.Staff[0]
|
||||
if row.StaffID != staffID || row.DisplayName != "Demo Stylist" {
|
||||
t.Fatalf("unexpected staff row identity: %+v", row)
|
||||
}
|
||||
if row.Bookings != 3 {
|
||||
t.Fatalf("expected 3 bookings in window, got %d", row.Bookings)
|
||||
}
|
||||
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.RevenueCents != 240000 {
|
||||
t.Fatalf("expected 240000 revenue cents (2×1200 Kč), got %d", row.RevenueCents)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user