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:
Krispire Admin
2026-10-18 13:47:09 +05:30
co-authored by Claude Opus 5.5
parent fa0255e9cd
commit d76505772a
13 changed files with 610 additions and 40 deletions
+12 -1
View File
@@ -76,6 +76,7 @@ type Server struct {
pushH *handler.PushHandler
reminderH *handler.ReminderHandler
feedbackH *handler.FeedbackHandler
feedbackCatH *handler.FeedbackCategoryHandler
statsH *handler.StatsHandler
adminH *handler.AdminHandler
broadcastH *handler.BroadcastHandler
@@ -343,8 +344,11 @@ func (s *Server) initAuth(cfg *config.Config, logger *zap.Logger) {
// the existing mailer instance.
feedbackRepo := repository.NewFeedbackRepo(s.pg)
statsRepo := repository.NewStatsRepo(s.pg)
feedbackSvc := service.NewFeedbackService(feedbackRepo, mail, cfg.FeedbackAdminEmails, logger)
// Categories are admin-managed; submissions are checked against them.
feedbackCatRepo := repository.NewFeedbackCategoryRepo(s.pg)
feedbackSvc := service.NewFeedbackService(feedbackRepo, feedbackCatRepo, mail, cfg.FeedbackAdminEmails, logger)
s.feedbackH = handler.NewFeedbackHandler(feedbackSvc)
s.feedbackCatH = handler.NewFeedbackCategoryHandler(service.NewFeedbackCategoryService(feedbackCatRepo))
s.statsH = handler.NewStatsHandler(statsRepo, feedbackRepo)
// Admin panel — read-mostly listing + notify/reply/resolve actions.
@@ -1112,6 +1116,8 @@ func (s *Server) setupRouter() {
r.HandleFunc("/feedback/mine", s.authUnavailable)
return
}
// The form's categories — public, like the form itself.
r.Get("/feedback/categories", s.feedbackCatH.Public)
// Submit — OptionalAuth (anonymous callers allowed).
r.Group(func(r chi.Router) {
r.Use(middleware.OptionalAuth(s.authIssuer, s.userRepo))
@@ -1248,6 +1254,11 @@ func (s *Server) setupRouter() {
can("collections:read").Get("/collections", s.adminH.ListCollections)
can("feedback:read").Get("/feedback", s.adminH.ListFeedback)
can("feedback:read").Get("/feedback/categories", s.feedbackCatH.List)
can("feedback:categories").Post("/feedback/categories", s.feedbackCatH.Create)
can("feedback:categories").Put("/feedback/categories/order", s.feedbackCatH.Reorder)
can("feedback:categories").Put("/feedback/categories/{key}", s.feedbackCatH.Update)
can("feedback:categories").Delete("/feedback/categories/{key}", s.feedbackCatH.Delete)
can("feedback:reply").Post("/feedback/{id}/reply", s.adminH.ReplyFeedback)
can("feedback:resolve").Post("/feedback/{id}/acknowledge", s.adminH.AcknowledgeFeedback)
can("feedback:resolve").Post("/feedback/{id}/resolve", s.adminH.ResolveFeedback)