feat(#17): CRUD de categorias — API Go + tela Vue + testes unitários
API REST /api/categories (GET/POST/PUT/DELETE) com regras de negócio: categorias padrão não podem ser excluídas; categorias com transações vinculadas também bloqueadas. Tela Vue com formulário inline de criação/edição, indicador visual de categorias padrão e botão de exclusão condicional. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"financeiro-carvalho/internal/model"
|
||||
"financeiro-carvalho/internal/repository"
|
||||
"financeiro-carvalho/internal/service"
|
||||
)
|
||||
|
||||
type CategoryHandler struct {
|
||||
svc *service.CategoryService
|
||||
}
|
||||
|
||||
func NewCategoryHandler(svc *service.CategoryService) *CategoryHandler {
|
||||
return &CategoryHandler{svc: svc}
|
||||
}
|
||||
|
||||
func (h *CategoryHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
cats, err := h.svc.List(r.Context())
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, "failed to list categories")
|
||||
return
|
||||
}
|
||||
if cats == nil {
|
||||
cats = []model.Category{}
|
||||
}
|
||||
respondJSON(w, http.StatusOK, cats)
|
||||
}
|
||||
|
||||
func (h *CategoryHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
var in model.CategoryInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "invalid JSON")
|
||||
return
|
||||
}
|
||||
cat, err := h.svc.Create(r.Context(), in)
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
respondJSON(w, http.StatusCreated, cat)
|
||||
}
|
||||
|
||||
func (h *CategoryHandler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.Atoi(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
var in model.CategoryInput
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
respondError(w, http.StatusBadRequest, "invalid JSON")
|
||||
return
|
||||
}
|
||||
cat, err := h.svc.Update(r.Context(), id, in)
|
||||
if errors.Is(err, repository.ErrNotFound) {
|
||||
respondError(w, http.StatusNotFound, "category not found")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
respondJSON(w, http.StatusOK, cat)
|
||||
}
|
||||
|
||||
func (h *CategoryHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := strconv.Atoi(chi.URLParam(r, "id"))
|
||||
if err != nil {
|
||||
respondError(w, http.StatusBadRequest, "invalid id")
|
||||
return
|
||||
}
|
||||
err = h.svc.Delete(r.Context(), id)
|
||||
switch {
|
||||
case errors.Is(err, repository.ErrNotFound):
|
||||
respondError(w, http.StatusNotFound, "category not found")
|
||||
case errors.Is(err, service.ErrDefaultCategory):
|
||||
respondError(w, http.StatusConflict, err.Error())
|
||||
case errors.Is(err, service.ErrHasTransactions):
|
||||
respondError(w, http.StatusConflict, err.Error())
|
||||
case err != nil:
|
||||
respondError(w, http.StatusInternalServerError, "failed to delete")
|
||||
default:
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func respondJSON(w http.ResponseWriter, status int, data any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
|
||||
func respondError(w http.ResponseWriter, status int, msg string) {
|
||||
respondJSON(w, status, map[string]string{"error": msg})
|
||||
}
|
||||
Reference in New Issue
Block a user