feat(csat): align report download csv

This commit is contained in:
2026-06-05 08:40:21 +08:00
parent 5c771d8c2c
commit b36cf079db
2 changed files with 127 additions and 23 deletions
+84 -23
View File
@@ -2,6 +2,7 @@ package v1
import (
"context"
"encoding/csv"
"errors"
"fmt"
"net/http"
@@ -411,58 +412,118 @@ func (h *CsatSurveyHandler) Download(c *gin.Context) {
return
}
// Chatwoot CSV columns: agent_name, rating, feedback, contact_name, contact_email,
// contact_phone, conversation_link, recorded_at (+ review_notes for enterprise)
csv := "Agent Name,Rating,Feedback Message,Contact Name,Contact Email,Contact Phone Number,Conversation Link,Recorded At\n"
c.Header("Content-Type", "text/csv")
c.Header("Content-Disposition", "attachment; filename=csat_report.csv")
writer := csv.NewWriter(c.Writer)
header := []string{
"Agent Name",
"Rating",
"Feedback Comment",
"Contact Name",
"Contact Email Address",
"Contact Phone Number",
"Link to the conversation",
"Recorded date",
"Review Notes",
}
if err := writer.Write(header); err != nil {
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to write CSV header")
return
}
for _, r := range responses {
agentName := ""
if r.AssignedAgentID != nil {
// Lookup agent name via DB
var agent model.User
if err := h.svc.DB().Where("id = ?", *r.AssignedAgentID).First(&agent).Error; err == nil {
if err := h.svc.DB().WithContext(c.Request.Context()).Where("id = ?", *r.AssignedAgentID).First(&agent).Error; err == nil {
agentName = fmt.Sprintf("%s (%s)", agent.Name, agent.Email)
}
}
// Lookup contact details
var contact model.Contact
contactName := ""
contactEmail := ""
contactPhone := ""
if err := h.svc.DB().Where("id = ?", r.ContactID).First(&contact).Error; err == nil {
if err := h.svc.DB().WithContext(c.Request.Context()).Where("id = ?", r.ContactID).First(&contact).Error; err == nil {
contactName = contact.Name
contactEmail = contact.Email
contactPhone = contact.PhoneNumber
}
// Lookup conversation display_id for link
var conv model.Conversation
conversationLink := ""
if err := h.svc.DB().Where("id = ?", r.ConversationID).First(&conv).Error; err == nil {
// Chatwoot format: /app/accounts/{account_id}/conversations/{display_id}
conversationLink = fmt.Sprintf("/app/accounts/%d/conversations/%d", accountID, conv.DisplayID)
if err := h.svc.DB().WithContext(c.Request.Context()).Where("id = ?", r.ConversationID).First(&conv).Error; err == nil {
conversationLink = csatConversationURL(c.Request, accountID, &conv)
}
csv += fmt.Sprintf("\"%s\",%d,\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"\n",
record := []string{
agentName,
r.Rating,
sanitizeCSV(r.FeedbackMessage),
strconv.Itoa(r.Rating),
r.FeedbackMessage,
contactName,
contactEmail,
contactPhone,
conversationLink,
r.CreatedAt.Format(time.RFC3339),
)
r.CsatReviewNotes,
}
if err := writer.Write(record); err != nil {
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to write CSV row")
return
}
}
c.Header("Content-Type", "text/csv")
c.Header("Content-Disposition", "attachment; filename=csat_report.csv")
c.String(http.StatusOK, csv)
if period := csatReportPeriod(c); period != "" {
if err := writer.Write([]string{period}); err != nil {
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to write CSV period")
return
}
}
writer.Flush()
if err := writer.Error(); err != nil {
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to write CSV")
return
}
}
// sanitizeCSV escapes double quotes and newlines for CSV safety.
func sanitizeCSV(s string) string {
s = strings.ReplaceAll(s, "\"", "\"\"")
s = strings.ReplaceAll(s, "\n", " ")
return s
func csatConversationURL(req *http.Request, accountID uint, conversation *model.Conversation) string {
if conversation == nil {
return ""
}
displayID := conversation.ID
if conversation.DisplayID != nil && *conversation.DisplayID != 0 {
displayID = *conversation.DisplayID
}
path := fmt.Sprintf("/app/accounts/%d/conversations/%d", accountID, displayID)
if req == nil || req.Host == "" {
return path
}
scheme := req.Header.Get("X-Forwarded-Proto")
if scheme == "" {
if req.TLS != nil {
scheme = "https"
} else {
scheme = "http"
}
}
return fmt.Sprintf("%s://%s%s", scheme, req.Host, path)
}
func csatReportPeriod(c *gin.Context) string {
sinceRaw := c.Query("since")
untilRaw := c.Query("until")
if sinceRaw == "" || untilRaw == "" {
return ""
}
since, err := parseCsatQueryTime(sinceRaw)
if err != nil {
return ""
}
until, err := parseCsatQueryTime(untilRaw)
if err != nil {
return ""
}
return fmt.Sprintf("Reporting period %s to %s", since.Format("2006-01-02"), until.Format("2006-01-02"))
}