feat: add feedback resolver

This commit is contained in:
Krispire Admin
2026-06-03 23:43:17 +05:30
parent 61b91c473b
commit e9a96f1ff9
7 changed files with 211 additions and 16 deletions
+40 -4
View File
@@ -1,11 +1,14 @@
package handler
import (
"errors"
"html/template"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/prabhupadasays/spsays-bff/internal/repository"
"github.com/prabhupadasays/spsays-bff/internal/service"
)
// StatsHandler renders a single-page HTML snapshot of the system —
@@ -13,13 +16,18 @@ import (
// (outside /api/v1, since it returns HTML not JSON) and intentionally
// public per current product call. If that ever needs to change, wrap
// the route in middleware in server.go.
//
// It also handles the "Mark resolved" form POST per feedback row, which
// stamps resolved_at (one-way) and fires the author thank-you email via
// the feedback service.
type StatsHandler struct {
stats *repository.StatsRepo
feedback *repository.FeedbackRepo
svc *service.FeedbackService
}
func NewStatsHandler(stats *repository.StatsRepo, feedback *repository.FeedbackRepo) *StatsHandler {
return &StatsHandler{stats: stats, feedback: feedback}
func NewStatsHandler(stats *repository.StatsRepo, feedback *repository.FeedbackRepo, svc *service.FeedbackService) *StatsHandler {
return &StatsHandler{stats: stats, feedback: feedback, svc: svc}
}
func (h *StatsHandler) Render(w http.ResponseWriter, r *http.Request) {
@@ -47,6 +55,20 @@ func (h *StatsHandler) Render(w http.ResponseWriter, r *http.Request) {
}
}
// ResolveFeedback handles the per-row "Mark resolved" form POST. It marks
// the row resolved (one-way) and fires the author thank-you email, then
// 303-redirects back to /stats so a refresh doesn't re-submit. Already-
// resolved or unknown ids redirect silently — the desired end state is
// already true.
func (h *StatsHandler) ResolveFeedback(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
if err := h.svc.Resolve(r.Context(), id); err != nil && !errors.Is(err, service.ErrAlreadyResolved) {
http.Error(w, "resolve failed: "+err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/stats", http.StatusSeeOther)
}
var statsTpl = template.Must(template.New("stats").Funcs(template.FuncMap{
"fmtTime": func(t time.Time) string { return t.Format("2006-01-02 15:04") },
}).Parse(statsHTML))
@@ -72,10 +94,15 @@ h2 { font-size: 1.05rem; margin-top: 2rem; margin-bottom: 0.5rem; border-bottom:
.row:last-child { border-bottom: 0 }
.row .cat { font-weight: 600; text-transform: capitalize }
.fb { background: #fafafa; border: 1px solid #ececec; border-radius: 8px; padding: 0.9rem 1rem; margin-bottom: 0.6rem }
.fb .head { display: flex; justify-content: space-between; gap: 1rem; font-size: 0.82rem; color: #666; margin-bottom: 0.4rem; flex-wrap: wrap }
.fb.resolved { background: #f3f8f4; border-color: #d6e9da }
.fb .head { display: flex; justify-content: space-between; gap: 1rem; font-size: 0.82rem; color: #666; margin-bottom: 0.4rem; flex-wrap: wrap; align-items: center }
.fb .cat-pill { font-weight: 700; text-transform: capitalize; color: #1a1a1a }
.fb .msg { white-space: pre-wrap; font-size: 0.95rem }
.fb .author { font-size: 0.78rem; color: #888; margin-top: 0.5rem }
.fb .foot { display: flex; justify-content: flex-end; margin-top: 0.6rem }
.resolved-badge { display: inline-flex; align-items: center; gap: 0.3rem; font-size: 0.78rem; font-weight: 600; color: #1e7a4a }
.resolve-btn { font: inherit; font-size: 0.8rem; font-weight: 600; color: #fff; background: #1e7a4a; border: 0; border-radius: 6px; padding: 0.4rem 0.85rem; cursor: pointer }
.resolve-btn:hover { background: #196b40 }
code { background: #f4f4f4; padding: 0.05em 0.3em; border-radius: 3px; font-size: 0.85em }
.empty { color: #888; font-style: italic }
</style>
@@ -106,7 +133,7 @@ code { background: #f4f4f4; padding: 0.05em 0.3em; border-radius: 3px; font-size
<h2>Recent feedback</h2>
{{if .Feedback}}
{{range .Feedback}}
<div class="fb">
<div class="fb{{if .ResolvedAt}} resolved{{end}}">
<div class="head">
<span class="cat-pill">{{.Category}}</span>
<span>{{fmtTime .CreatedAt}}</span>
@@ -121,6 +148,15 @@ code { background: #f4f4f4; padding: 0.05em 0.3em; border-radius: 3px; font-size
anonymous
{{end}}
</div>
<div class="foot">
{{if .ResolvedAt}}
<span class="resolved-badge">✓ Resolved {{fmtTime .ResolvedAt}}</span>
{{else}}
<form method="post" action="/stats/feedback/{{.ID}}/resolve" onsubmit="return confirm('Mark this feedback as resolved? This cannot be undone, and the author will be emailed.');">
<button type="submit" class="resolve-btn">Mark resolved</button>
</form>
{{end}}
</div>
</div>
{{end}}
{{else}}