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
93 lines
2.4 KiB
Go
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)
|
|
}
|