mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 07:41:16 +00:00
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:
1 parent
2464aa45fa
commit
7b9080eb1c
13 files changed
+541
-33
No files matched your search
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -701,22 +702,83 @@ func (s *Service) StaffMetrics(ctx context.Context, principal domain.Principal,
|
||||
if err != nil {
|
||||
return domain.StaffMetricsResponse{}, err
|
||||
}
|
||||
rules, err := s.repo.ListAvailabilityRulesByTenant(ctx, membership.Tenant.ID)
|
||||
if err != nil {
|
||||
return domain.StaffMetricsResponse{}, err
|
||||
}
|
||||
loc, err := time.LoadLocation(membership.Tenant.Timezone)
|
||||
if err != nil {
|
||||
loc = time.UTC
|
||||
}
|
||||
capacity := staffCapacityMinutes(rules, from, to, loc)
|
||||
|
||||
metrics := make([]domain.StaffMetric, 0, len(staff))
|
||||
for _, member := range staff {
|
||||
rec := byStaff[member.ID]
|
||||
capMinutes := capacity[member.ID]
|
||||
utilization := 0
|
||||
if capMinutes > 0 {
|
||||
utilization = int(float64(rec.BookedMinutes) / float64(capMinutes) * 100)
|
||||
}
|
||||
metrics = append(metrics, domain.StaffMetric{
|
||||
StaffID: member.ID,
|
||||
DisplayName: member.DisplayName,
|
||||
Bookings: rec.BookingsCount,
|
||||
Completed: rec.CompletedCount,
|
||||
Cancelled: rec.CancellationsCount,
|
||||
UtilizationPercent: rec.UtilizationPercent,
|
||||
BookedMinutes: rec.BookedMinutes,
|
||||
CapacityMinutes: capMinutes,
|
||||
UtilizationPercent: utilization,
|
||||
RevenueCents: rec.RevenueCents,
|
||||
})
|
||||
}
|
||||
return domain.StaffMetricsResponse{From: from, To: to, Staff: metrics}, nil
|
||||
}
|
||||
|
||||
// staffCapacityMinutes sums each member's availability-rule minutes across the
|
||||
// window, evaluated on local calendar dates in the tenant timezone. Rules
|
||||
// without a staff assignment are tenant-wide and don't map to a member.
|
||||
func staffCapacityMinutes(rules []db.AvailabilityRuleRecord, from, to time.Time, loc *time.Location) map[string]int {
|
||||
byStaff := map[string]int{}
|
||||
for day := from.In(loc); day.Before(to.In(loc)); day = day.AddDate(0, 0, 1) {
|
||||
weekday := int(day.Weekday())
|
||||
for _, rule := range rules {
|
||||
if rule.StaffID == nil || rule.DayOfWeek != weekday {
|
||||
continue
|
||||
}
|
||||
byStaff[*rule.StaffID] += localMinutesBetween(rule.StartsLocal, rule.EndsLocal)
|
||||
}
|
||||
}
|
||||
return byStaff
|
||||
}
|
||||
|
||||
// localMinutesBetween returns the minutes between two "HH:MM[:SS]" local times,
|
||||
// or zero when the range is empty or malformed.
|
||||
func localMinutesBetween(starts, ends string) int {
|
||||
start := parseLocalMinutes(starts)
|
||||
end := parseLocalMinutes(ends)
|
||||
if end <= start {
|
||||
return 0
|
||||
}
|
||||
return end - start
|
||||
}
|
||||
|
||||
func parseLocalMinutes(hhmm string) int {
|
||||
parts := strings.SplitN(hhmm, ":", 3)
|
||||
if len(parts) < 2 {
|
||||
return 0
|
||||
}
|
||||
hours, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
minutes, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return hours*60 + minutes
|
||||
}
|
||||
|
||||
// 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