feat(reports): align inbox label matrix filters

This commit is contained in:
2026-06-07 01:32:57 +08:00
parent 1232e974ac
commit f3fda02a9a
6 changed files with 166 additions and 15 deletions
+50 -1
View File
@@ -4,6 +4,7 @@ import (
"encoding/csv"
"net/http"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
@@ -498,8 +499,12 @@ func (h *AnalyticsHandler) InboxLabelMatrix(c *gin.Context) {
if !ok {
return
}
filter, ok := parseInboxLabelMatrixFilter(c)
if !ok {
return
}
result, err := h.svc.GetInboxLabelMatrix(c.Request.Context(), accountID)
result, err := h.svc.GetInboxLabelMatrix(c.Request.Context(), accountID, filter)
if err != nil {
applogger.L().Errorf("Inbox label matrix report: %v", err)
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to generate inbox label matrix")
@@ -509,6 +514,50 @@ func (h *AnalyticsHandler) InboxLabelMatrix(c *gin.Context) {
c.JSON(http.StatusOK, result)
}
func parseInboxLabelMatrixFilter(c *gin.Context) (service.InboxLabelMatrixFilter, bool) {
filter := service.InboxLabelMatrixFilter{
InboxIDs: parseReportUintList(c, "inbox_ids"),
LabelIDs: parseReportUintList(c, "label_ids"),
}
sinceRaw := c.Query("since")
untilRaw := c.Query("until")
if sinceRaw == "" || untilRaw == "" {
return filter, true
}
since, err := parseChatwootReportTime(sinceRaw)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid since date format")
return service.InboxLabelMatrixFilter{}, false
}
until, err := parseChatwootReportTime(untilRaw)
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid until date format")
return service.InboxLabelMatrixFilter{}, false
}
filter.Since = since
filter.Until = until
return filter, true
}
func parseReportUintList(c *gin.Context, key string) []uint {
values := append([]string{}, c.QueryArray(key)...)
values = append(values, c.QueryArray(key+"[]")...)
ids := make([]uint, 0, len(values))
for _, value := range values {
for _, part := range strings.Split(value, ",") {
part = strings.TrimSpace(part)
if part == "" {
continue
}
parsed, err := strconv.ParseUint(part, 10, 64)
if err == nil && parsed > 0 {
ids = append(ids, uint(parsed))
}
}
}
return ids
}
// FirstResponseTimeDistribution returns first response time distribution.
// GET /api/v1/accounts/:account_id/reports/first_response_time_distribution
// Reference: Chatwoot reports#first_response_time_distribution
@@ -5,6 +5,7 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"
@@ -73,6 +74,7 @@ func (s *AnalyticsHandlerTestSuite) SetupSuite() {
accounts.GET("/reports/conversation_traffic", s.handler.ConversationTraffic)
accounts.GET("/reports/conversations", s.handler.Conversations)
accounts.GET("/reports/conversations_summary", s.handler.ConversationsSummary)
accounts.GET("/reports/inbox_label_matrix", s.handler.InboxLabelMatrix)
s.router = r
}
@@ -469,6 +471,39 @@ func (s *AnalyticsHandlerTestSuite) TestConversations_AgentTypeReturnsChatwootAg
s.Equal("https://example.com/low.png", payload[1]["thumbnail"])
}
func (s *AnalyticsHandlerTestSuite) TestInboxLabelMatrix_ParsesFrontendFilters() {
since := parseTime("2025-01-01T00:00:00Z")
inbox := model.Inbox{AccountID: s.accountID, Name: "Web", ChannelType: "web_widget", ChannelID: 1, Enabled: true}
otherInbox := model.Inbox{AccountID: s.accountID, Name: "Other", ChannelType: "web_widget", ChannelID: 2, Enabled: true}
s.Require().NoError(s.db.Create(&inbox).Error)
s.Require().NoError(s.db.Create(&otherInbox).Error)
label := model.Tag{AccountID: s.accountID, Name: "vip"}
otherLabel := model.Tag{AccountID: s.accountID, Name: "bug"}
s.Require().NoError(s.db.Create(&label).Error)
s.Require().NoError(s.db.Create(&otherLabel).Error)
conv := model.Conversation{AccountID: s.accountID, InboxID: inbox.ID, ContactID: 1, Status: string(model.ConversationStatusOpen), ChannelType: "web_widget", Channel: "web_widget", Base: model.Base{CreatedAt: since.Add(time.Hour)}}
oldConv := model.Conversation{AccountID: s.accountID, InboxID: inbox.ID, ContactID: 1, Status: string(model.ConversationStatusOpen), ChannelType: "web_widget", Channel: "web_widget", Base: model.Base{CreatedAt: since.Add(-time.Hour)}}
otherConv := model.Conversation{AccountID: s.accountID, InboxID: otherInbox.ID, ContactID: 1, Status: string(model.ConversationStatusOpen), ChannelType: "web_widget", Channel: "web_widget", Base: model.Base{CreatedAt: since.Add(time.Hour)}}
s.Require().NoError(s.db.Create(&conv).Error)
s.Require().NoError(s.db.Create(&oldConv).Error)
s.Require().NoError(s.db.Create(&otherConv).Error)
s.Require().NoError(s.db.Create(&model.ConversationLabel{AccountID: s.accountID, ConversationID: conv.ID, TagID: label.ID}).Error)
s.Require().NoError(s.db.Create(&model.ConversationLabel{AccountID: s.accountID, ConversationID: conv.ID, TagID: otherLabel.ID}).Error)
s.Require().NoError(s.db.Create(&model.ConversationLabel{AccountID: s.accountID, ConversationID: oldConv.ID, TagID: label.ID}).Error)
s.Require().NoError(s.db.Create(&model.ConversationLabel{AccountID: s.accountID, ConversationID: otherConv.ID, TagID: label.ID}).Error)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/reports/inbox_label_matrix?since=1735689600&until=1735776000&inbox_ids[]="+strconv.FormatUint(uint64(inbox.ID), 10)+"&label_ids="+strconv.FormatUint(uint64(label.ID), 10), nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
var payload map[string]interface{}
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &payload))
s.Equal([]interface{}{[]interface{}{float64(1)}}, payload["matrix"])
s.Require().Len(payload["inboxes"], 1)
s.Require().Len(payload["labels"], 1)
}
// ========== Nil service guard ==========
func (s *AnalyticsHandlerTestSuite) TestNilService() {