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.
129 lines
3.7 KiB
Go
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)
|
|
}
|
|
} |