package v1 import ( "encoding/csv" "fmt" "net/http" "time" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/service" "github.com/gochat/gochat/pkg/response" ) // CsatMetricsHandler handles dedicated CSAT metrics and CSV download endpoints. // Reference: Chatwoot csat_report controller — enterprise metrics dashboard + export type CsatMetricsHandler struct { svc *service.CsatMetricsService } // NewCsatMetricsHandler creates a new CsatMetricsHandler. func NewCsatMetricsHandler(svc *service.CsatMetricsService) *CsatMetricsHandler { return &CsatMetricsHandler{svc: svc} } // Metrics returns aggregated CSAT metrics (average rating, response rate, trend). // GET /api/v1/accounts/:account_id/csat_survey_responses/metrics?since=...&until=... func (h *CsatMetricsHandler) Metrics(c *gin.Context) { accountID := getAccountID(c) if accountID == 0 { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "account_id is required") return } since, until := parseSinceUntilQuery(c) report, err := h.svc.GetMetrics(c.Request.Context(), accountID, since, until) if err != nil { handleServiceError(c, err) return } response.OK(c, report) } // Download generates a CSV file of CSAT responses and streams it back. // GET /api/v1/accounts/:account_id/csat_survey_responses/download?since=...&until=... func (h *CsatMetricsHandler) Download(c *gin.Context) { accountID := getAccountID(c) if accountID == 0 { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "account_id is required") return } since, until := parseSinceUntilQuery(c) rows, err := h.svc.ExportCSV(c.Request.Context(), accountID, since, until) if err != nil { handleServiceError(c, err) return } // Set CSV response headers filename := fmt.Sprintf("csat_metrics_account_%d.csv", accountID) c.Header("Content-Type", "text/csv") c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) // Write CSV header + rows directly to the response writer writer := csv.NewWriter(c.Writer) // CSV header row header := []string{"conversation_id", "contact_id", "assigned_agent_id", "rating", "feedback_message", "created_at"} if err := writer.Write(header); err != nil { response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to write CSV header") return } // CSV data rows for _, row := range rows { record := []string{ row.ConversationID, row.ContactID, row.AssignedAgentID, row.Rating, row.FeedbackMessage, row.CreatedAt, } if err := writer.Write(record); err != nil { response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to write CSV row") return } } writer.Flush() if err := writer.Error(); err != nil { response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "CSV flush error") return } } // parseSinceUntilQuery extracts optional since/until time.Time pointers from query params. func parseSinceUntilQuery(c *gin.Context) (*time.Time, *time.Time) { var since, until *time.Time if v := c.Query("since"); v != "" { t, err := time.Parse(time.RFC3339, v) if err == nil { since = &t } } if v := c.Query("until"); v != "" { t, err := time.Parse(time.RFC3339, v) if err == nil { until = &t } } return since, until } // RegisterCsatMetricsRoutes registers CSAT metrics routes on the given router group. // Reference: Chatwoot GET /csat_survey_responses/metrics + /csat_survey_responses/download func RegisterCsatMetricsRoutes(g *gin.RouterGroup, h *CsatMetricsHandler) { responses := g.Group("/csat_survey_responses") { responses.GET("/metrics", h.Metrics) responses.GET("/download", h.Download) } }