mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 15:51:15 +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
@@ -672,6 +672,51 @@ func (s *Service) DashboardSummary(ctx context.Context, principal domain.Princip
|
||||
}, nil
|
||||
}
|
||||
|
||||
// StaffMetrics returns per-staff appointment workload over a trailing
|
||||
// window — every active staff member appears, even with zero bookings.
|
||||
func (s *Service) StaffMetrics(ctx context.Context, principal domain.Principal, days int) (domain.StaffMetricsResponse, error) {
|
||||
membership, err := s.repo.GetTenantMembershipBySubject(ctx, principal.Subject)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return domain.StaffMetricsResponse{}, ErrTenantMembership
|
||||
}
|
||||
return domain.StaffMetricsResponse{}, err
|
||||
}
|
||||
if days <= 0 || days > 365 {
|
||||
days = 30
|
||||
}
|
||||
to := time.Now().UTC()
|
||||
from := to.AddDate(0, 0, -days)
|
||||
|
||||
records, err := s.repo.GetStaffMetrics(ctx, membership.Tenant.ID, from, to)
|
||||
if err != nil {
|
||||
return domain.StaffMetricsResponse{}, err
|
||||
}
|
||||
byStaff := make(map[string]db.StaffMetricsRecord, len(records))
|
||||
for _, rec := range records {
|
||||
byStaff[rec.StaffID] = rec
|
||||
}
|
||||
|
||||
staff, err := s.repo.ListStaffByTenant(ctx, membership.Tenant.ID)
|
||||
if err != nil {
|
||||
return domain.StaffMetricsResponse{}, err
|
||||
}
|
||||
metrics := make([]domain.StaffMetric, 0, len(staff))
|
||||
for _, member := range staff {
|
||||
rec := byStaff[member.ID]
|
||||
metrics = append(metrics, domain.StaffMetric{
|
||||
StaffID: member.ID,
|
||||
DisplayName: member.DisplayName,
|
||||
Bookings: rec.BookingsCount,
|
||||
Completed: rec.CompletedCount,
|
||||
Cancelled: rec.CancellationsCount,
|
||||
UtilizationPercent: rec.UtilizationPercent,
|
||||
RevenueCents: rec.RevenueCents,
|
||||
})
|
||||
}
|
||||
return domain.StaffMetricsResponse{From: from, To: to, Staff: metrics}, nil
|
||||
}
|
||||
|
||||
// setupCompletion scores onboarding readiness across five equal-weight checks:
|
||||
// brand profile, location, service catalog, staff roster, and schedule rules.
|
||||
func setupCompletion(brandName string, locationCount, serviceCount, staffCount, scheduleCount int) int {
|
||||
|
||||
Reference in new issue
Block a user