feat(reports): align v2 csv downloads
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -100,6 +101,93 @@ func parseChatwootReportTime(value string) (time.Time, error) {
|
||||
return time.Parse(time.RFC3339, value)
|
||||
}
|
||||
|
||||
func parseReportBusinessHours(c *gin.Context) bool {
|
||||
return c.Query("business_hours") == "true" || c.Query("business_hours") == "1"
|
||||
}
|
||||
|
||||
func parseConversationTrafficRange(c *gin.Context) (time.Time, time.Time, float64, bool) {
|
||||
timezoneOffset := 0.0
|
||||
if raw := c.Query("timezone_offset"); raw != "" {
|
||||
parsed, err := strconv.ParseFloat(raw, 64)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid timezone_offset")
|
||||
return time.Time{}, time.Time{}, 0, false
|
||||
}
|
||||
timezoneOffset = parsed
|
||||
}
|
||||
|
||||
if c.Query("since") != "" || c.Query("until") != "" {
|
||||
since, until, ok := parseDateRange(c)
|
||||
return since, until, timezoneOffset, ok
|
||||
}
|
||||
|
||||
daysBefore := 6
|
||||
if raw := c.Query("days_before"); raw != "" {
|
||||
parsed, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid days_before")
|
||||
return time.Time{}, time.Time{}, 0, false
|
||||
}
|
||||
daysBefore = parsed
|
||||
}
|
||||
|
||||
loc := fixedOffsetLocation(timezoneOffset)
|
||||
now := time.Now().In(loc)
|
||||
untilLocal := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
|
||||
sinceLocal := untilLocal.AddDate(0, 0, -daysBefore)
|
||||
return sinceLocal.UTC(), untilLocal.UTC(), timezoneOffset, true
|
||||
}
|
||||
|
||||
func fixedOffsetLocation(offsetHours float64) *time.Location {
|
||||
seconds := int(offsetHours * 3600)
|
||||
return time.FixedZone(reportTimezoneName(offsetHours), seconds)
|
||||
}
|
||||
|
||||
func reportTimezoneName(offsetHours float64) string {
|
||||
if offsetHours == 0 {
|
||||
return "UTC"
|
||||
}
|
||||
sign := "+"
|
||||
if offsetHours < 0 {
|
||||
sign = "-"
|
||||
offsetHours = -offsetHours
|
||||
}
|
||||
totalMinutes := int(offsetHours*60 + 0.5)
|
||||
return "UTC" + sign + twoDigit(totalMinutes/60) + ":" + twoDigit(totalMinutes%60)
|
||||
}
|
||||
|
||||
func twoDigit(value int) string {
|
||||
if value < 10 {
|
||||
return "0" + strconv.Itoa(value)
|
||||
}
|
||||
return strconv.Itoa(value)
|
||||
}
|
||||
|
||||
func writeReportCSV(c *gin.Context, filename string, since, until time.Time, headers []string, rows [][]string) {
|
||||
c.Header("Content-Type", "text/csv")
|
||||
c.Header("Content-Disposition", "attachment; filename="+filename+".csv")
|
||||
writer := csv.NewWriter(c.Writer)
|
||||
_ = writer.Write([]string{"Reporting period " + since.Format("2006-01-02") + " to " + until.Format("2006-01-02")})
|
||||
_ = writer.Write([]string{})
|
||||
_ = writer.Write(headers)
|
||||
for _, row := range rows {
|
||||
_ = writer.Write(row)
|
||||
}
|
||||
writer.Flush()
|
||||
}
|
||||
|
||||
func writeConversationTrafficCSV(c *gin.Context, timezoneOffset float64, rows [][]string) {
|
||||
c.Header("Content-Type", "text/csv")
|
||||
c.Header("Content-Disposition", "attachment; filename=conversation_traffic_reports.csv")
|
||||
writer := csv.NewWriter(c.Writer)
|
||||
_ = writer.Write([]string{"Timezone", reportTimezoneName(timezoneOffset)})
|
||||
_ = writer.Write([]string{})
|
||||
for _, row := range rows {
|
||||
_ = writer.Write(row)
|
||||
}
|
||||
writer.Flush()
|
||||
}
|
||||
|
||||
// Summary returns account-level aggregated metrics.
|
||||
// GET /api/v1/accounts/:account_id/reports/summary
|
||||
func (h *AnalyticsHandler) Summary(c *gin.Context) {
|
||||
@@ -134,14 +222,21 @@ func (h *AnalyticsHandler) AgentMetrics(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.svc.GetAgentMetrics(c.Request.Context(), accountID, since, until)
|
||||
result, err := h.svc.GetAgentReportCSVRows(c.Request.Context(), accountID, since, until, parseReportBusinessHours(c))
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Agent metrics report: %v", err)
|
||||
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to generate agent metrics")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
writeReportCSV(c, "agents_report", since, until, []string{
|
||||
"Agent name",
|
||||
"Assigned conversations",
|
||||
"Avg first response time",
|
||||
"Avg resolution time",
|
||||
"Avg customer waiting time",
|
||||
"Resolution Count",
|
||||
}, result)
|
||||
}
|
||||
|
||||
// InboxMetrics returns metrics grouped by inbox.
|
||||
@@ -156,14 +251,20 @@ func (h *AnalyticsHandler) InboxMetrics(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.svc.GetInboxMetrics(c.Request.Context(), accountID, since, until)
|
||||
result, err := h.svc.GetInboxReportCSVRows(c.Request.Context(), accountID, since, until, parseReportBusinessHours(c))
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Inbox metrics report: %v", err)
|
||||
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to generate inbox metrics")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
writeReportCSV(c, "inboxes_report", since, until, []string{
|
||||
"Inbox name",
|
||||
"Inbox type",
|
||||
"No. of conversations",
|
||||
"Avg first response time",
|
||||
"Avg resolution time",
|
||||
}, result)
|
||||
}
|
||||
|
||||
// LabelMetrics returns metrics grouped by label.
|
||||
@@ -178,14 +279,21 @@ func (h *AnalyticsHandler) LabelMetrics(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.svc.GetLabelMetrics(c.Request.Context(), accountID, since, until)
|
||||
result, err := h.svc.GetLabelReportCSVRows(c.Request.Context(), accountID, since, until, parseReportBusinessHours(c))
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Label metrics report: %v", err)
|
||||
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to generate label metrics")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
writeReportCSV(c, "labels_report", since, until, []string{
|
||||
"Label",
|
||||
"No. of conversations",
|
||||
"Avg first response time",
|
||||
"Avg resolution time",
|
||||
"Avg reply time",
|
||||
"Resolution Count",
|
||||
}, result)
|
||||
}
|
||||
|
||||
// TeamMetrics returns metrics grouped by team.
|
||||
@@ -200,14 +308,21 @@ func (h *AnalyticsHandler) TeamMetrics(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.svc.GetTeamMetrics(c.Request.Context(), accountID, since, until)
|
||||
result, err := h.svc.GetTeamReportCSVRows(c.Request.Context(), accountID, since, until, parseReportBusinessHours(c))
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Team metrics report: %v", err)
|
||||
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to generate team metrics")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
writeReportCSV(c, "teams_report", since, until, []string{
|
||||
"Team name",
|
||||
"Conversations count",
|
||||
"Avg first response time",
|
||||
"Avg resolution time",
|
||||
"Avg customer waiting time",
|
||||
"Resolution Count",
|
||||
}, result)
|
||||
}
|
||||
|
||||
// ConversationTraffic returns daily conversation traffic time-series.
|
||||
@@ -217,19 +332,19 @@ func (h *AnalyticsHandler) ConversationTraffic(c *gin.Context) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
since, until, ok := parseDateRange(c)
|
||||
since, until, timezoneOffset, ok := parseConversationTrafficRange(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.svc.GetConversationTraffic(c.Request.Context(), accountID, since, until)
|
||||
result, err := h.svc.GetConversationTrafficCSVRows(c.Request.Context(), accountID, since, until, timezoneOffset)
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Conversation traffic report: %v", err)
|
||||
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to generate conversation traffic")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
writeConversationTrafficCSV(c, timezoneOffset, result)
|
||||
}
|
||||
|
||||
// BotSummary returns bot-level summary metrics.
|
||||
@@ -294,14 +409,22 @@ func (h *AnalyticsHandler) ConversationsSummary(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.svc.GetConversationsSummary(c.Request.Context(), accountID, since, until)
|
||||
result, err := h.svc.GetConversationsSummaryCSVRows(c.Request.Context(), accountID, since, until, parseReportBusinessHours(c))
|
||||
if err != nil {
|
||||
applogger.L().Errorf("Conversations summary report: %v", err)
|
||||
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to generate conversations summary")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
writeReportCSV(c, "conversations_summary_report", since, until, []string{
|
||||
"Conversations",
|
||||
"Messages received",
|
||||
"Messages sent",
|
||||
"Avg first response time",
|
||||
"Avg resolution time",
|
||||
"Resolution count",
|
||||
"Avg customer waiting time",
|
||||
}, result)
|
||||
}
|
||||
|
||||
// BotMetrics returns bot metrics.
|
||||
|
||||
Reference in New Issue
Block a user