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
@@ -251,6 +251,44 @@ func (r *PGRepository) GetDashboardMetrics(ctx context.Context, tenantID string,
|
||||
return metrics, err
|
||||
}
|
||||
|
||||
// GetStaffMetrics mirrors GetDashboardMetrics grouped by staff_id. Class
|
||||
// bookings have no staff_id and fall out naturally; revenue sums the linked
|
||||
// service price for non-cancelled bookings.
|
||||
func (r *PGRepository) GetStaffMetrics(ctx context.Context, tenantID string, startsAt time.Time, endsAt time.Time) ([]StaffMetricsRecord, error) {
|
||||
rows, err := r.pool.Query(ctx, `
|
||||
SELECT
|
||||
b.staff_id,
|
||||
COUNT(*) AS bookings_count,
|
||||
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)),
|
||||
0
|
||||
)::integer AS utilization_percent,
|
||||
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
|
||||
WHERE b.tenant_id = $1 AND b.staff_id IS NOT NULL AND b.starts_at >= $2 AND b.starts_at < $3
|
||||
GROUP BY b.staff_id
|
||||
ORDER BY bookings_count DESC
|
||||
`, tenantID, startsAt, endsAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var records []StaffMetricsRecord
|
||||
for rows.Next() {
|
||||
var rec StaffMetricsRecord
|
||||
if err := rows.Scan(&rec.StaffID, &rec.BookingsCount, &rec.CancellationsCount,
|
||||
&rec.CompletedCount, &rec.UtilizationPercent, &rec.RevenueCents); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
records = append(records, rec)
|
||||
}
|
||||
return records, rows.Err()
|
||||
}
|
||||
|
||||
func (r *PGRepository) GetWaitlistPosition(ctx context.Context, sessionID, bookingID string) (int, error) {
|
||||
var position int
|
||||
err := r.pool.QueryRow(ctx, `
|
||||
|
||||
Reference in new issue
Block a user