feat(csat): align report download csv
This commit is contained in:
@@ -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"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user