Admin-managed feedback categories
Categories move from a hard-coded list to a feedback_categories table (migration 0062), seeded with the four existing keys, labels and descriptions so production feedback is untouched; any other key already in use gets an archived category. Keys are fixed, so renames relabel past feedback. Archive hides a category from the form; only unused categories can be deleted. Submissions are validated against active categories, and feedback responses carry category_label. New: GET /feedback/categories (public); /admin/feedback/categories CRUD + reorder behind the new feedback:categories permission (granted to Admin). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fa0255e9cd
commit
d76505772a
@@ -0,0 +1,114 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/prabhupadasays/spsays-bff/internal/repository"
|
||||
"github.com/prabhupadasays/spsays-bff/internal/service"
|
||||
"github.com/prabhupadasays/spsays-bff/pkg/response"
|
||||
)
|
||||
|
||||
// FeedbackCategoryHandler serves the /feedback form's categories: the
|
||||
// public list, and their management in the admin panel.
|
||||
type FeedbackCategoryHandler struct {
|
||||
svc *service.FeedbackCategoryService
|
||||
}
|
||||
|
||||
func NewFeedbackCategoryHandler(svc *service.FeedbackCategoryService) *FeedbackCategoryHandler {
|
||||
return &FeedbackCategoryHandler{svc: svc}
|
||||
}
|
||||
|
||||
func writeCategoryError(w http.ResponseWriter, err error) {
|
||||
switch {
|
||||
case errors.Is(err, repository.ErrFeedbackCategoryNotFound):
|
||||
response.Error(w, http.StatusNotFound, err.Error())
|
||||
case errors.Is(err, repository.ErrFeedbackCategoryInUse),
|
||||
errors.Is(err, repository.ErrFeedbackCategoryExists),
|
||||
errors.Is(err, service.ErrCategoryLastActive):
|
||||
response.Error(w, http.StatusConflict, err.Error())
|
||||
case errors.Is(err, service.ErrCategoryLabel), errors.Is(err, service.ErrCategoryDescription):
|
||||
response.Error(w, http.StatusBadRequest, err.Error())
|
||||
default:
|
||||
response.Error(w, http.StatusInternalServerError, "feedback category request failed")
|
||||
}
|
||||
}
|
||||
|
||||
// Public — GET /feedback/categories: the form's options, in order.
|
||||
func (h *FeedbackCategoryHandler) Public(w http.ResponseWriter, r *http.Request) {
|
||||
list, err := h.svc.Active(r.Context())
|
||||
if err != nil {
|
||||
writeCategoryError(w, err)
|
||||
return
|
||||
}
|
||||
response.JSON(w, http.StatusOK, list)
|
||||
}
|
||||
|
||||
// List — GET /admin/feedback/categories: all, with usage counts.
|
||||
func (h *FeedbackCategoryHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
list, err := h.svc.All(r.Context())
|
||||
if err != nil {
|
||||
writeCategoryError(w, err)
|
||||
return
|
||||
}
|
||||
response.JSON(w, http.StatusOK, list)
|
||||
}
|
||||
|
||||
type categoryReq struct {
|
||||
Label string `json:"label"`
|
||||
Description string `json:"description"`
|
||||
Archived bool `json:"archived"`
|
||||
}
|
||||
|
||||
func (h *FeedbackCategoryHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
var req categoryReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
response.Error(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
c, err := h.svc.Create(r.Context(), req.Label, req.Description)
|
||||
if err != nil {
|
||||
writeCategoryError(w, err)
|
||||
return
|
||||
}
|
||||
response.JSON(w, http.StatusCreated, c)
|
||||
}
|
||||
|
||||
func (h *FeedbackCategoryHandler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
var req categoryReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
response.Error(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
if err := h.svc.Update(r.Context(), chi.URLParam(r, "key"), req.Label, req.Description, req.Archived); err != nil {
|
||||
writeCategoryError(w, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *FeedbackCategoryHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.svc.Delete(r.Context(), chi.URLParam(r, "key")); err != nil {
|
||||
writeCategoryError(w, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// Reorder — PUT /admin/feedback/categories/order {"keys": [...]}.
|
||||
func (h *FeedbackCategoryHandler) Reorder(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Keys []string `json:"keys"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || len(req.Keys) == 0 {
|
||||
response.Error(w, http.StatusBadRequest, "keys are required")
|
||||
return
|
||||
}
|
||||
if err := h.svc.Reorder(r.Context(), req.Keys); err != nil {
|
||||
writeCategoryError(w, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
Reference in New Issue
Block a user