package web import ( "encoding/csv" "encoding/json" "errors" "net/http" "strconv" "strings" "ymhut-box/server/unified-management/internal/db" ) func (r *router) handleAdminFeedbacks(w http.ResponseWriter, req *http.Request) { path := cleanPath(req.URL.Path) if req.Method == http.MethodGet && path == "/api/admin/feedbacks" { if req.URL.Query().Get("page") != "" { page, _ := strconv.Atoi(req.URL.Query().Get("page")) perPage, _ := strconv.Atoi(req.URL.Query().Get("perPage")) items, total, err := r.store.ListFeedbacksFiltered(page, perPage, db.FeedbackFilters{ Status: req.URL.Query().Get("status"), Category: req.URL.Query().Get("category"), Priority: req.URL.Query().Get("priority"), Query: req.URL.Query().Get("q"), Assignee: req.URL.Query().Get("assignee"), Sort: req.URL.Query().Get("sort"), }) if err != nil { writeError(w, http.StatusInternalServerError, "FEEDBACK_LIST_FAILED", err) return } if page <= 0 { page = 1 } if perPage <= 0 { perPage = 20 } writeJSON(w, http.StatusOK, map[string]any{"ok": true, "page": map[string]any{"items": items, "total": total, "page": page, "perPage": perPage}}) return } limit, _ := strconv.Atoi(req.URL.Query().Get("limit")) items, err := r.store.ListFeedbacks(limit) if err != nil { writeError(w, http.StatusInternalServerError, "FEEDBACK_LIST_FAILED", err) return } writeJSON(w, http.StatusOK, map[string]any{"ok": true, "items": items}) return } if req.Method == http.MethodGet && path == "/api/admin/feedbacks/export" { items, _, err := r.store.ListFeedbacksFiltered(1, 100, db.FeedbackFilters{ Status: req.URL.Query().Get("status"), Category: req.URL.Query().Get("category"), Priority: req.URL.Query().Get("priority"), Query: req.URL.Query().Get("q"), }) if err != nil { writeError(w, http.StatusInternalServerError, "EXPORT_FAILED", err) return } w.Header().Set("Content-Type", "text/csv; charset=utf-8") w.Header().Set("Content-Disposition", `attachment; filename="feedbacks.csv"`) writer := csv.NewWriter(w) _ = writer.Write([]string{"code", "created_at", "title", "status", "category", "priority", "contact", "status_detail", "public_reply"}) for _, item := range items { _ = writer.Write([]string{item.Code, item.CreatedAt, item.Title, item.Status, item.Category, item.Priority, item.Contact, item.StatusDetail, item.PublicReply}) } writer.Flush() return } if req.Method == http.MethodGet && strings.HasPrefix(path, "/api/admin/feedbacks/") { code := strings.TrimPrefix(path, "/api/admin/feedbacks/") detail, err := r.store.GetFeedbackDetail(code) if err != nil { writeError(w, http.StatusNotFound, "NOT_FOUND", err) return } writeJSON(w, http.StatusOK, map[string]any{"ok": true, "feedback": detail}) return } if req.Method == http.MethodPatch && path == "/api/admin/feedbacks/bulk" { var body struct { Codes []string `json:"codes"` Status string `json:"status"` StatusDetail string `json:"statusDetail"` PublicReply string `json:"publicReply"` Assignee string `json:"assignee"` Tags []string `json:"tags"` } if err := json.NewDecoder(req.Body).Decode(&body); err != nil || len(body.Codes) == 0 { writeError(w, http.StatusBadRequest, "INVALID_PAYLOAD", errors.New("codes are required")) return } if err := r.store.BulkUpdateFeedback(body.Codes, db.FeedbackUpdate{Status: body.Status, StatusDetail: body.StatusDetail, PublicReply: body.PublicReply, Assignee: body.Assignee, Actor: "admin", Tags: body.Tags}); err != nil { writeError(w, http.StatusInternalServerError, "BULK_UPDATE_FAILED", err) return } writeJSON(w, http.StatusOK, map[string]any{"ok": true, "updated": len(body.Codes)}) return } if req.Method == http.MethodPost && strings.HasPrefix(path, "/api/admin/feedbacks/") && strings.HasSuffix(path, "/comments") { code := strings.TrimSuffix(strings.TrimPrefix(path, "/api/admin/feedbacks/"), "/comments") var body struct { Author string `json:"author"` Body string `json:"body"` Internal bool `json:"internal"` } if err := json.NewDecoder(req.Body).Decode(&body); err != nil { writeError(w, http.StatusBadRequest, "INVALID_PAYLOAD", err) return } comment, err := r.store.InsertFeedbackComment(db.FeedbackComment{Code: code, Author: firstNonEmpty(body.Author, "admin"), Body: body.Body, Internal: body.Internal}) if err != nil { writeError(w, http.StatusInternalServerError, "COMMENT_FAILED", err) return } writeJSON(w, http.StatusOK, map[string]any{"ok": true, "comment": comment}) return } if req.Method == http.MethodPatch && strings.HasPrefix(path, "/api/admin/feedbacks/") { code := strings.TrimPrefix(path, "/api/admin/feedbacks/") var body struct { Status string `json:"status"` StatusDetail string `json:"statusDetail"` PublicReply string `json:"publicReply"` } if err := json.NewDecoder(req.Body).Decode(&body); err != nil { writeError(w, http.StatusBadRequest, "INVALID_PAYLOAD", err) return } if err := r.store.UpdateFeedbackTicket(code, db.FeedbackUpdate{Status: firstNonEmpty(body.Status, "new"), StatusDetail: body.StatusDetail, PublicReply: body.PublicReply, Actor: "admin"}); err != nil { writeError(w, http.StatusInternalServerError, "FEEDBACK_UPDATE_FAILED", err) return } writeJSON(w, http.StatusOK, map[string]any{"ok": true}) return } http.NotFound(w, req) }