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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user