Files
wotra/internal/handler/week_handler.go
Andreas Schneider 8ca838fa6e feat: overall overtime balance on history page
Backend:
- ClosedWeekStore.SumDelta: single SQL aggregate returning total delta_ms and
  row count across all closed_weeks
- WeekService.Balance: thin passthrough returning BalanceResult{TotalDeltaMs, ClosedWeekCount}
- GET /api/weeks/balance handler; route registered alongside /weeks list/close/reopen
- Tests: store-level SumDelta (empty + populated), service-level Balance (empty + 2 weeks)

Frontend:
- weeks.balance() added to API client
- History page: balance card at top, fetched in parallel with existing data
- Loading state shows '—'; once loaded shows formatDelta value in green/red/gray
- Shows 'across N closed weeks' count alongside the value
2026-04-30 20:01:24 +02:00

93 lines
2.4 KiB
Go

package handler
import (
"errors"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/wotra/wotra/internal/service"
)
// WeekHandler serves /api/weeks routes.
type WeekHandler struct {
svc *service.WeekService
}
func NewWeekHandler(svc *service.WeekService) *WeekHandler {
return &WeekHandler{svc: svc}
}
func (h *WeekHandler) Routes(r chi.Router) {
r.Get("/weeks", h.List)
r.Get("/weeks/balance", h.Balance)
r.Post("/weeks/{week_key}/close", h.Close)
r.Delete("/weeks/{week_key}/close", h.Reopen)
}
// Balance GET /api/weeks/balance
func (h *WeekHandler) Balance(w http.ResponseWriter, r *http.Request) {
bal, err := h.svc.Balance(r.Context())
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
writeJSON(w, http.StatusOK, bal)
}
// List GET /api/weeks?from=YYYY-Www&to=YYYY-Www
func (h *WeekHandler) List(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"
}
weeks, err := h.svc.ListWeeks(r.Context(), from, to)
if err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
}
if weeks == nil {
writeJSON(w, http.StatusOK, []struct{}{})
return
}
writeJSON(w, http.StatusOK, weeks)
}
// Close POST /api/weeks/{week_key}/close
func (h *WeekHandler) Close(w http.ResponseWriter, r *http.Request) {
weekKey := chi.URLParam(r, "week_key")
cw, err := h.svc.CloseWeek(r.Context(), weekKey)
if err != nil {
switch {
case errors.Is(err, service.ErrWeekAlreadyClosed):
writeError(w, http.StatusConflict, err.Error())
case errors.Is(err, service.ErrWeekHasUnclosedDays):
writeError(w, http.StatusUnprocessableEntity, err.Error())
case errors.Is(err, service.ErrNoSettings):
writeError(w, http.StatusUnprocessableEntity, err.Error())
default:
writeError(w, http.StatusInternalServerError, err.Error())
}
return
}
writeJSON(w, http.StatusOK, cw)
}
// Reopen DELETE /api/weeks/{week_key}/close
func (h *WeekHandler) Reopen(w http.ResponseWriter, r *http.Request) {
weekKey := chi.URLParam(r, "week_key")
if err := h.svc.ReopenWeek(r.Context(), weekKey); err != nil {
switch {
case errors.Is(err, service.ErrWeekNotClosed):
writeError(w, http.StatusNotFound, err.Error())
default:
writeError(w, http.StatusInternalServerError, err.Error())
}
return
}
w.WriteHeader(http.StatusNoContent)
}