mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-10-28 07:41:16 +00:00
feat(export): CSV export for bookings and customers
GET /v1/export/bookings.csv (optional from/to bounds) and /v1/export/customers.csv — UTF-8 with BOM for Excel, labels resolved at export time, buffered responses so auth failures stay JSON. Export buttons on the bookings and customers pages. 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
d86cba61e3
commit
4674203502
12 files changed
+583
-5
No files matched your search
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
"bookra/apps/backend/internal/config"
|
||||
"bookra/apps/backend/internal/db"
|
||||
"bookra/apps/backend/internal/domain"
|
||||
"bookra/apps/backend/internal/export"
|
||||
"bookra/apps/backend/internal/httpx"
|
||||
"bookra/apps/backend/internal/notifications"
|
||||
"bookra/apps/backend/internal/sms"
|
||||
@@ -59,6 +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)
|
||||
authService := auth.NewService(repository, cfg.AuthJWTSecret)
|
||||
adminService := admin.NewService(repository, cfg.AdminEmail, cfg.AdminKey)
|
||||
smsService := sms.NewService(cfg, repository)
|
||||
@@ -1293,6 +1296,37 @@ func NewServer(cfg config.Config, pools *db.Pools) (*Server, error) {
|
||||
c.JSON(http.StatusOK, response)
|
||||
})
|
||||
|
||||
protected.GET("/export/bookings.csv", func(c *gin.Context) {
|
||||
from, to := exportRange(c)
|
||||
var buf bytes.Buffer
|
||||
err := exportService.WriteBookingsCSV(c.Request.Context(), auth.PrincipalFromContext(c), &buf, from, to)
|
||||
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-bookings.csv"`)
|
||||
c.Data(http.StatusOK, "text/csv; charset=utf-8", buf.Bytes())
|
||||
})
|
||||
|
||||
protected.GET("/export/customers.csv", func(c *gin.Context) {
|
||||
var buf bytes.Buffer
|
||||
err := exportService.WriteCustomersCSV(c.Request.Context(), auth.PrincipalFromContext(c), &buf)
|
||||
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-customers.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 {
|
||||
@@ -1578,6 +1612,24 @@ func (s *Server) runTrialEndingCheck() {
|
||||
}
|
||||
}
|
||||
|
||||
// exportRange parses optional from/to=YYYY-MM-DD bounds for CSV export;
|
||||
// the default is effectively all-time.
|
||||
func exportRange(c *gin.Context) (time.Time, time.Time) {
|
||||
from := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
to := time.Date(2100, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
if value := c.Query("from"); value != "" {
|
||||
if parsed, err := time.Parse("2006-01-02", value); err == nil {
|
||||
from = parsed
|
||||
}
|
||||
}
|
||||
if value := c.Query("to"); value != "" {
|
||||
if parsed, err := time.Parse("2006-01-02", value); err == nil {
|
||||
to = parsed.AddDate(0, 0, 1) // inclusive day bound
|
||||
}
|
||||
}
|
||||
return from, to
|
||||
}
|
||||
|
||||
func authorizeJobRunner(c *gin.Context, cfg config.Config) bool {
|
||||
if cfg.JobRunnerKey == "" {
|
||||
return false
|
||||
|
||||
Reference in new issue
Block a user