feat: edit and delete settings history rows
Backend:
- SettingsStore: Add GetByID, Update, Delete, Count methods
- SettingsService: Add UpdateSettings (validates same rules as Upsert),
DeleteSettings (guards against deleting the last row → 409)
- New sentinels: ErrSettingsNotFound, ErrLastSettingsRow
- Handler: PUT /api/settings/history/{id} → 200 updated row
DELETE /api/settings/history/{id} → 204 / 404 / 409
Frontend:
- API client: settings.update(id, body) and settings.delete(id)
- Settings page: history table gains edit (pencil) and delete (×) buttons
- Inline edit form expands in place within the table row
- Delete button disabled and hint shown when only one row remains
- maskLabel() helper shows workday names instead of raw bitmask
- After save/delete: full reload to reflect changes in 'current' section
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/wotra/wotra/internal/service"
|
||||
@@ -21,6 +23,8 @@ func (h *SettingsHandler) Routes(r chi.Router) {
|
||||
r.Get("/settings", h.Current)
|
||||
r.Put("/settings", h.Upsert)
|
||||
r.Get("/settings/history", h.History)
|
||||
r.Put("/settings/history/{id}", h.UpdateHistoryRow)
|
||||
r.Delete("/settings/history/{id}", h.DeleteHistoryRow)
|
||||
}
|
||||
|
||||
// Current GET /api/settings
|
||||
@@ -76,3 +80,62 @@ func (h *SettingsHandler) History(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
writeJSON(w, http.StatusOK, history)
|
||||
}
|
||||
|
||||
// UpdateHistoryRow PUT /api/settings/history/{id}
|
||||
func (h *SettingsHandler) UpdateHistoryRow(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
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.UpdateSettings(r.Context(), id, service.UpdateSettingsInput{
|
||||
EffectiveFrom: body.EffectiveFrom,
|
||||
HoursPerWeek: body.HoursPerWeek,
|
||||
WorkdaysMask: body.WorkdaysMask,
|
||||
Timezone: body.Timezone,
|
||||
})
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, service.ErrSettingsNotFound), errors.Is(err, sql.ErrNoRows):
|
||||
writeError(w, http.StatusNotFound, err.Error())
|
||||
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)
|
||||
}
|
||||
|
||||
// DeleteHistoryRow DELETE /api/settings/history/{id}
|
||||
func (h *SettingsHandler) DeleteHistoryRow(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
if err := h.svc.DeleteSettings(r.Context(), id); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, service.ErrSettingsNotFound):
|
||||
writeError(w, http.StatusNotFound, err.Error())
|
||||
case errors.Is(err, service.ErrLastSettingsRow):
|
||||
writeError(w, http.StatusConflict, err.Error())
|
||||
default:
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user