package handler import ( "encoding/csv" "fmt" "net/http" "time" "github.com/go-chi/chi/v5" "github.com/wotra/wotra/internal/service" ) // ExportHandler serves /api/export routes. type ExportHandler struct { entrySvc *service.EntryService daySvc *service.DayService weekSvc *service.WeekService } func NewExportHandler(e *service.EntryService, d *service.DayService, w *service.WeekService) *ExportHandler { return &ExportHandler{entrySvc: e, daySvc: d, weekSvc: w} } func (h *ExportHandler) Routes(r chi.Router) { r.Get("/export/entries.csv", h.EntriesCSV) r.Get("/export/days.csv", h.DaysCSV) r.Get("/export/weeks.csv", h.WeeksCSV) } // EntriesCSV GET /api/export/entries.csv?from=YYYY-MM-DD&to=YYYY-MM-DD func (h *ExportHandler) EntriesCSV(w http.ResponseWriter, r *http.Request) { from := r.URL.Query().Get("from") to := r.URL.Query().Get("to") if from == "" { from = "0000-01-01" } if to == "" { to = "9999-12-31" } es, err := h.entrySvc.List(r.Context(), from, to) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } w.Header().Set("Content-Type", "text/csv") w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="entries-%s.csv"`, time.Now().Format("20060102"))) cw := csv.NewWriter(w) cw.Write([]string{"id", "day_key", "start_time", "end_time", "duration_ms", "note", "auto_stopped"}) for _, e := range es { endStr := "" durStr := "0" if e.EndTime != nil { endStr = time.UnixMilli(*e.EndTime).UTC().Format(time.RFC3339) durStr = fmt.Sprintf("%d", *e.EndTime - e.StartTime) } cw.Write([]string{ e.ID, e.DayKey, time.UnixMilli(e.StartTime).UTC().Format(time.RFC3339), endStr, durStr, e.Note, fmt.Sprintf("%v", e.AutoStopped), }) } cw.Flush() } // DaysCSV GET /api/export/days.csv?from=YYYY-MM-DD&to=YYYY-MM-DD func (h *ExportHandler) DaysCSV(w http.ResponseWriter, r *http.Request) { from := r.URL.Query().Get("from") to := r.URL.Query().Get("to") if from == "" { from = "0000-01-01" } if to == "" { to = "9999-12-31" } ds, err := h.daySvc.ListDays(r.Context(), from, to) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } w.Header().Set("Content-Type", "text/csv") w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="days-%s.csv"`, time.Now().Format("20060102"))) cw := csv.NewWriter(w) cw.Write([]string{"day_key", "kind", "worked_ms", "worked_hours"}) for _, d := range ds { workedH := fmt.Sprintf("%.2f", float64(d.WorkedMs)/3_600_000) cw.Write([]string{d.DayKey, string(d.Kind), fmt.Sprintf("%d", d.WorkedMs), workedH}) } cw.Flush() } // WeeksCSV GET /api/export/weeks.csv func (h *ExportHandler) WeeksCSV(w http.ResponseWriter, r *http.Request) { from := r.URL.Query().Get("from") to := r.URL.Query().Get("to") if from == "" { from = "0000-W01" } if to == "" { to = "9999-W53" } ws, err := h.weekSvc.ListWeeks(r.Context(), from, to) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } w.Header().Set("Content-Type", "text/csv") w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="weeks-%s.csv"`, time.Now().Format("20060102"))) cw := csv.NewWriter(w) cw.Write([]string{"week_key", "expected_ms", "worked_ms", "delta_ms", "expected_h", "worked_h", "delta_h"}) for _, wk := range ws { cw.Write([]string{ wk.WeekKey, fmt.Sprintf("%d", wk.ExpectedMs), fmt.Sprintf("%d", wk.WorkedMs), fmt.Sprintf("%d", wk.DeltaMs), fmt.Sprintf("%.2f", float64(wk.ExpectedMs)/3_600_000), fmt.Sprintf("%.2f", float64(wk.WorkedMs)/3_600_000), fmt.Sprintf("%.2f", float64(wk.DeltaMs)/3_600_000), }) } cw.Flush() }