feat(m2): settings history, close day, holiday/vacation/sick marking
This commit is contained in:
101
internal/handler/day_handler.go
Normal file
101
internal/handler/day_handler.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/wotra/wotra/internal/domain"
|
||||
"github.com/wotra/wotra/internal/service"
|
||||
)
|
||||
|
||||
// DayHandler serves /api/days routes.
|
||||
type DayHandler struct {
|
||||
svc *service.DayService
|
||||
}
|
||||
|
||||
func NewDayHandler(svc *service.DayService) *DayHandler {
|
||||
return &DayHandler{svc: svc}
|
||||
}
|
||||
|
||||
func (h *DayHandler) Routes(r chi.Router) {
|
||||
r.Get("/days", h.List)
|
||||
r.Post("/days/{day_key}/close", h.Close)
|
||||
r.Post("/days/{day_key}/mark", h.Mark)
|
||||
r.Delete("/days/{day_key}/close", h.Reopen)
|
||||
}
|
||||
|
||||
// List GET /api/days?from=YYYY-MM-DD&to=YYYY-MM-DD
|
||||
func (h *DayHandler) List(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"
|
||||
}
|
||||
days, err := h.svc.ListDays(r.Context(), from, to)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if days == nil {
|
||||
writeJSON(w, http.StatusOK, []struct{}{})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, days)
|
||||
}
|
||||
|
||||
// Close POST /api/days/{day_key}/close
|
||||
func (h *DayHandler) Close(w http.ResponseWriter, r *http.Request) {
|
||||
dayKey := chi.URLParam(r, "day_key")
|
||||
cd, err := h.svc.CloseDay(r.Context(), dayKey)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, service.ErrDayAlreadyClosed):
|
||||
writeError(w, http.StatusConflict, err.Error())
|
||||
case errors.Is(err, service.ErrRunningEntryOnDay):
|
||||
writeError(w, http.StatusConflict, err.Error())
|
||||
case errors.Is(err, service.ErrDayHasNoEntries):
|
||||
writeError(w, http.StatusUnprocessableEntity, err.Error())
|
||||
default:
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, cd)
|
||||
}
|
||||
|
||||
// Mark POST /api/days/{day_key}/mark
|
||||
func (h *DayHandler) Mark(w http.ResponseWriter, r *http.Request) {
|
||||
dayKey := chi.URLParam(r, "day_key")
|
||||
var body struct {
|
||||
Kind domain.DayKind `json:"kind"`
|
||||
}
|
||||
if err := decodeJSON(r, &body); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid JSON")
|
||||
return
|
||||
}
|
||||
cd, err := h.svc.MarkDay(r.Context(), dayKey, body.Kind)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, cd)
|
||||
}
|
||||
|
||||
// Reopen DELETE /api/days/{day_key}/close
|
||||
func (h *DayHandler) Reopen(w http.ResponseWriter, r *http.Request) {
|
||||
dayKey := chi.URLParam(r, "day_key")
|
||||
if err := h.svc.ReopenDay(r.Context(), dayKey); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, service.ErrDayNotClosed):
|
||||
writeError(w, http.StatusNotFound, err.Error())
|
||||
default:
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
// NewRouter builds the full HTTP router.
|
||||
func NewRouter(authToken string, entrySvc *service.EntryService) http.Handler {
|
||||
func NewRouter(authToken string, entrySvc *service.EntryService, daySvc *service.DayService, settingsSvc *service.SettingsService) http.Handler {
|
||||
r := chi.NewRouter()
|
||||
r.Use(middleware.RequestID)
|
||||
r.Use(middleware.RealIP)
|
||||
@@ -27,6 +27,12 @@ func NewRouter(authToken string, entrySvc *service.EntryService) http.Handler {
|
||||
|
||||
entryH := NewEntryHandler(entrySvc)
|
||||
entryH.Routes(r)
|
||||
|
||||
dayH := NewDayHandler(daySvc)
|
||||
dayH.Routes(r)
|
||||
|
||||
settingsH := NewSettingsHandler(settingsSvc)
|
||||
settingsH.Routes(r)
|
||||
})
|
||||
|
||||
return r
|
||||
|
||||
78
internal/handler/settings_handler.go
Normal file
78
internal/handler/settings_handler.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/wotra/wotra/internal/service"
|
||||
)
|
||||
|
||||
// SettingsHandler serves /api/settings routes.
|
||||
type SettingsHandler struct {
|
||||
svc *service.SettingsService
|
||||
}
|
||||
|
||||
func NewSettingsHandler(svc *service.SettingsService) *SettingsHandler {
|
||||
return &SettingsHandler{svc: svc}
|
||||
}
|
||||
|
||||
func (h *SettingsHandler) Routes(r chi.Router) {
|
||||
r.Get("/settings", h.Current)
|
||||
r.Put("/settings", h.Upsert)
|
||||
r.Get("/settings/history", h.History)
|
||||
}
|
||||
|
||||
// Current GET /api/settings
|
||||
func (h *SettingsHandler) Current(w http.ResponseWriter, r *http.Request) {
|
||||
set, err := h.svc.Current(r.Context())
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, set)
|
||||
}
|
||||
|
||||
// Upsert PUT /api/settings
|
||||
func (h *SettingsHandler) Upsert(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
EffectiveFrom string `json:"effective_from"`
|
||||
HoursPerWeek float64 `json:"hours_per_week"`
|
||||
WorkdaysMask int `json:"workdays_mask"`
|
||||
Timezone string `json:"timezone"`
|
||||
}
|
||||
if err := decodeJSON(r, &body); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid JSON")
|
||||
return
|
||||
}
|
||||
set, err := h.svc.Upsert(r.Context(), service.UpsertSettingsInput{
|
||||
EffectiveFrom: body.EffectiveFrom,
|
||||
HoursPerWeek: body.HoursPerWeek,
|
||||
WorkdaysMask: body.WorkdaysMask,
|
||||
Timezone: body.Timezone,
|
||||
})
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, service.ErrInvalidHours), errors.Is(err, service.ErrInvalidWorkdaysMask):
|
||||
writeError(w, http.StatusUnprocessableEntity, err.Error())
|
||||
default:
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, set)
|
||||
}
|
||||
|
||||
// History GET /api/settings/history
|
||||
func (h *SettingsHandler) History(w http.ResponseWriter, r *http.Request) {
|
||||
history, err := h.svc.History(r.Context())
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if history == nil {
|
||||
writeJSON(w, http.StatusOK, []struct{}{})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, history)
|
||||
}
|
||||
Reference in New Issue
Block a user