Files
gochat/backend/internal/handler/api/v1/csat_metrics_handler.go
T
rogee aeddedf2a3 Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories:
- backend/: Go module root (cmd, internal, pkg, configs, migrations,
  docs/swagger, scripts, tests, go.mod, Makefile, .air.toml)
- deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd
- docs/: project documentation + reports/ (moved from repo root)
- AGENTS.md: new AI coding-agent guide at repo root

Update all references to the new layout:
- Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root)
- docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile,
  env_file ../../.env, volume mounts ../../backend:/app
- deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile
- CI: working-directory: backend for go commands, file deploy/docker/Dockerfile,
  coverage path backend/coverage.out, health_check backend/scripts/
- backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../
- README: architecture tree, quickstart, config paths updated

Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh,
gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to
preserve history. Build, vet, SQLite tests, and docker compose config verified.
2026-07-07 14:44:12 +08:00

129 lines
3.7 KiB
Go

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)
}
}