mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 15:51:15 +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
@@ -262,9 +262,9 @@ func (r *PGRepository) GetStaffMetrics(ctx context.Context, tenantID string, sta
|
||||
COUNT(*) FILTER (WHERE b.status = 'cancelled') AS cancellations_count,
|
||||
COUNT(*) FILTER (WHERE b.status = 'confirmed' AND b.ends_at < now()) AS completed_count,
|
||||
COALESCE(
|
||||
ROUND(100.0 * COUNT(*) FILTER (WHERE b.status = 'confirmed') / NULLIF(COUNT(*), 0)),
|
||||
SUM(EXTRACT(EPOCH FROM (b.ends_at - b.starts_at)) / 60) FILTER (WHERE b.status <> 'cancelled'),
|
||||
0
|
||||
)::integer AS utilization_percent,
|
||||
)::integer AS booked_minutes,
|
||||
COALESCE(SUM(s.price_cents) FILTER (WHERE b.status <> 'cancelled'), 0) AS revenue_cents
|
||||
FROM bookings b
|
||||
LEFT JOIN services s ON s.id = b.service_id
|
||||
@@ -281,7 +281,7 @@ func (r *PGRepository) GetStaffMetrics(ctx context.Context, tenantID string, sta
|
||||
for rows.Next() {
|
||||
var rec StaffMetricsRecord
|
||||
if err := rows.Scan(&rec.StaffID, &rec.BookingsCount, &rec.CancellationsCount,
|
||||
&rec.CompletedCount, &rec.UtilizationPercent, &rec.RevenueCents); err != nil {
|
||||
&rec.CompletedCount, &rec.BookedMinutes, &rec.RevenueCents); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
records = append(records, rec)
|
||||
|
||||
@@ -315,7 +315,7 @@ type StaffMetricsRecord struct {
|
||||
BookingsCount int
|
||||
CancellationsCount int
|
||||
CompletedCount int
|
||||
UtilizationPercent int
|
||||
BookedMinutes int
|
||||
RevenueCents int64
|
||||
}
|
||||
|
||||
@@ -1806,22 +1806,19 @@ func (r *MemoryRepository) GetStaffMetrics(_ context.Context, tenantID string, s
|
||||
case booking.Status == "cancelled":
|
||||
rec.CancellationsCount++
|
||||
case booking.Status == "confirmed":
|
||||
rec.UtilizationPercent++
|
||||
if booking.EndsAt.Before(time.Now().UTC()) {
|
||||
rec.CompletedCount++
|
||||
}
|
||||
}
|
||||
if booking.Status != "cancelled" && booking.ServiceID != nil {
|
||||
rec.RevenueCents += int64(servicePrices[*booking.ServiceID])
|
||||
if booking.Status != "cancelled" {
|
||||
rec.BookedMinutes += int(booking.EndsAt.Sub(booking.StartsAt).Minutes())
|
||||
if booking.ServiceID != nil {
|
||||
rec.RevenueCents += int64(servicePrices[*booking.ServiceID])
|
||||
}
|
||||
}
|
||||
}
|
||||
records := make([]StaffMetricsRecord, 0, len(byStaff))
|
||||
for _, rec := range byStaff {
|
||||
if rec.BookingsCount == 0 {
|
||||
rec.UtilizationPercent = 0
|
||||
} else {
|
||||
rec.UtilizationPercent = int(float64(rec.UtilizationPercent) / float64(rec.BookingsCount) * 100)
|
||||
}
|
||||
records = append(records, *rec)
|
||||
}
|
||||
sort.SliceStable(records, func(i, j int) bool { return records[i].BookingsCount > records[j].BookingsCount })
|
||||
|
||||
Reference in new issue
Block a user