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:
Tomas DvorakandDevin committed 2026-09-18 17:10:15 +02:00
1 parent 2464aa45fa
commit 7b9080eb1c
13 files changed
+541 -33

No files matched your search

+33 -1
View File
@@ -61,7 +61,7 @@ func NewServer(cfg config.Config, pools *db.Pools) (*Server, error) {
catalogService := catalog.NewService(repository, billingService, notificationService)
calendarService := calendar.NewService(repository)
activityService := activity.NewService(repository)
exportService := export.NewService(repository)
exportService := export.NewService(repository, bookingService)
authService := auth.NewService(repository, cfg.AuthJWTSecret)
adminService := admin.NewService(repository, cfg.AdminEmail, cfg.AdminKey)
smsService := sms.NewService(cfg, repository)
@@ -1354,6 +1354,38 @@ func NewServer(cfg config.Config, pools *db.Pools) (*Server, error) {
c.Data(http.StatusOK, "text/csv; charset=utf-8", buf.Bytes())
})
protected.GET("/export/staff-report.csv", func(c *gin.Context) {
days, _ := strconv.Atoi(c.DefaultQuery("days", "30"))
var buf bytes.Buffer
err := exportService.WriteStaffReportCSV(c.Request.Context(), auth.PrincipalFromContext(c), &buf, days)
if err != nil {
status := http.StatusInternalServerError
if errors.Is(err, export.ErrTenantMembership) {
status = http.StatusNotFound
}
c.JSON(status, gin.H{"error": err.Error()})
return
}
c.Header("Content-Disposition", `attachment; filename="bookra-staff-report.csv"`)
c.Data(http.StatusOK, "text/csv; charset=utf-8", buf.Bytes())
})
protected.GET("/export/service-report.csv", func(c *gin.Context) {
days, _ := strconv.Atoi(c.DefaultQuery("days", "30"))
var buf bytes.Buffer
err := exportService.WriteServiceReportCSV(c.Request.Context(), auth.PrincipalFromContext(c), &buf, days)
if err != nil {
status := http.StatusInternalServerError
if errors.Is(err, export.ErrTenantMembership) {
status = http.StatusNotFound
}
c.JSON(status, gin.H{"error": err.Error()})
return
}
c.Header("Content-Disposition", `attachment; filename="bookra-service-report.csv"`)
c.Data(http.StatusOK, "text/csv; charset=utf-8", buf.Bytes())
})
protected.GET("/billing/subscription", func(c *gin.Context) {
response, err := billingService.GetSubscription(c.Request.Context(), auth.PrincipalFromContext(c))
if err != nil {