package v1 import ( "net/http" "time" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/service" applogger "github.com/gochat/gochat/pkg/logger" "github.com/gochat/gochat/pkg/response" ) // AnalyticsHandler handles reporting/analytics API endpoints. // Reference: Chatwoot app/controllers/api/v1/reports_controller.rb type AnalyticsHandler struct { svc *service.AnalyticsService } func NewAnalyticsHandler(svc *service.AnalyticsService) *AnalyticsHandler { return &AnalyticsHandler{svc: svc} } // parseAccountID extracts account_id from URL params. func parseAccountID(c *gin.Context) (uint, bool) { id := getAccountID(c) if id == 0 { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") return 0, false } return id, true } // parseDateRange extracts since/unsince from query params. func parseDateRange(c *gin.Context) (since, until time.Time, ok bool) { sinceStr := c.Query("since") untilStr := c.Query("until") if sinceStr == "" || untilStr == "" { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "since and until query params are required") return time.Time{}, time.Time{}, false } since, err := time.Parse(time.RFC3339, sinceStr) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid since date format") return time.Time{}, time.Time{}, false } until, err = time.Parse(time.RFC3339, untilStr) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid until date format") return time.Time{}, time.Time{}, false } return since, until, true } // Summary returns account-level aggregated metrics. // GET /api/v1/accounts/:account_id/reports/summary func (h *AnalyticsHandler) Summary(c *gin.Context) { accountID, ok := parseAccountID(c) if !ok { return } since, until, ok := parseDateRange(c) if !ok { return } result, err := h.svc.GetSummary(c.Request.Context(), accountID, since, until) if err != nil { applogger.L().Errorf("Summary report: %v", err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to generate summary report") return } response.OK(c, result) } // AgentMetrics returns metrics grouped by agent. // GET /api/v1/accounts/:account_id/reports/agents func (h *AnalyticsHandler) AgentMetrics(c *gin.Context) { accountID, ok := parseAccountID(c) if !ok { return } since, until, ok := parseDateRange(c) if !ok { return } result, err := h.svc.GetAgentMetrics(c.Request.Context(), accountID, since, until) if err != nil { applogger.L().Errorf("Agent metrics report: %v", err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to generate agent metrics") return } response.OK(c, result) } // InboxMetrics returns metrics grouped by inbox. // GET /api/v1/accounts/:account_id/reports/inboxes func (h *AnalyticsHandler) InboxMetrics(c *gin.Context) { accountID, ok := parseAccountID(c) if !ok { return } since, until, ok := parseDateRange(c) if !ok { return } result, err := h.svc.GetInboxMetrics(c.Request.Context(), accountID, since, until) if err != nil { applogger.L().Errorf("Inbox metrics report: %v", err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to generate inbox metrics") return } response.OK(c, result) } // LabelMetrics returns metrics grouped by label. // GET /api/v1/accounts/:account_id/reports/labels func (h *AnalyticsHandler) LabelMetrics(c *gin.Context) { accountID, ok := parseAccountID(c) if !ok { return } since, until, ok := parseDateRange(c) if !ok { return } result, err := h.svc.GetLabelMetrics(c.Request.Context(), accountID, since, until) if err != nil { applogger.L().Errorf("Label metrics report: %v", err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to generate label metrics") return } response.OK(c, result) } // TeamMetrics returns metrics grouped by team. // GET /api/v1/accounts/:account_id/reports/teams func (h *AnalyticsHandler) TeamMetrics(c *gin.Context) { accountID, ok := parseAccountID(c) if !ok { return } since, until, ok := parseDateRange(c) if !ok { return } result, err := h.svc.GetTeamMetrics(c.Request.Context(), accountID, since, until) if err != nil { applogger.L().Errorf("Team metrics report: %v", err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to generate team metrics") return } response.OK(c, result) } // ConversationTraffic returns daily conversation traffic time-series. // GET /api/v1/accounts/:account_id/reports/conversation_traffic func (h *AnalyticsHandler) ConversationTraffic(c *gin.Context) { accountID, ok := parseAccountID(c) if !ok { return } since, until, ok := parseDateRange(c) if !ok { return } result, err := h.svc.GetConversationTraffic(c.Request.Context(), accountID, since, until) if err != nil { applogger.L().Errorf("Conversation traffic report: %v", err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to generate conversation traffic") return } response.OK(c, result) } // BotSummary returns bot-level summary metrics. // GET /api/v1/accounts/:account_id/reports/bot_summary // Reference: Chatwoot reports#bot_summary func (h *AnalyticsHandler) BotSummary(c *gin.Context) { accountID, ok := parseAccountID(c) if !ok { return } since, until, ok := parseDateRange(c) if !ok { return } result, err := h.svc.GetBotSummary(c.Request.Context(), accountID, since, until) if err != nil { applogger.L().Errorf("Bot summary report: %v", err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to generate bot summary") return } response.OK(c, result) } // Conversations returns conversation metrics filtered by type. // GET /api/v1/accounts/:account_id/reports/conversations // Reference: Chatwoot reports#conversations — requires params[:type], returns 422 if missing func (h *AnalyticsHandler) Conversations(c *gin.Context) { accountID, ok := parseAccountID(c) if !ok { return } reportType := c.Query("type") if reportType == "" { // Reference: Chatwoot returns head :unprocessable_entity if type is blank response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrBadRequest, "type parameter is required") return } since, until, ok := parseDateRange(c) if !ok { return } result, err := h.svc.GetConversationsByType(c.Request.Context(), accountID, reportType, since, until) if err != nil { applogger.L().Errorf("Conversations report: %v", err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to generate conversations report") return } response.OK(c, result) } // ConversationsSummary returns conversation summary report. // GET /api/v1/accounts/:account_id/reports/conversations_summary // Reference: Chatwoot reports#conversations_summary func (h *AnalyticsHandler) ConversationsSummary(c *gin.Context) { accountID, ok := parseAccountID(c) if !ok { return } since, until, ok := parseDateRange(c) if !ok { return } result, err := h.svc.GetConversationsSummary(c.Request.Context(), accountID, since, until) if err != nil { applogger.L().Errorf("Conversations summary report: %v", err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to generate conversations summary") return } response.OK(c, result) } // BotMetrics returns bot metrics. // GET /api/v1/accounts/:account_id/reports/bot_metrics // Reference: Chatwoot reports#bot_metrics func (h *AnalyticsHandler) BotMetrics(c *gin.Context) { accountID, ok := parseAccountID(c) if !ok { return } since, until, ok := parseDateRange(c) if !ok { return } result, err := h.svc.GetBotMetrics(c.Request.Context(), accountID, since, until) if err != nil { applogger.L().Errorf("Bot metrics report: %v", err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to generate bot metrics") return } response.OK(c, result) } // InboxLabelMatrix returns inbox-label matrix data. // GET /api/v1/accounts/:account_id/reports/inbox_label_matrix // Reference: Chatwoot reports#inbox_label_matrix func (h *AnalyticsHandler) InboxLabelMatrix(c *gin.Context) { accountID, ok := parseAccountID(c) if !ok { return } result, err := h.svc.GetInboxLabelMatrix(c.Request.Context(), accountID) 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") return } response.OK(c, result) } // FirstResponseTimeDistribution returns first response time distribution. // GET /api/v1/accounts/:account_id/reports/first_response_time_distribution // Reference: Chatwoot reports#first_response_time_distribution func (h *AnalyticsHandler) FirstResponseTimeDistribution(c *gin.Context) { accountID, ok := parseAccountID(c) if !ok { return } since, until, ok := parseDateRange(c) if !ok { return } result, err := h.svc.GetFirstResponseTimeDistribution(c.Request.Context(), accountID, since, until) if err != nil { applogger.L().Errorf("First response time distribution report: %v", err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to generate first response time distribution") return } response.OK(c, result) } // OutgoingMessagesCount returns outgoing message count metrics. // GET /api/v1/accounts/:account_id/reports/outgoing_messages_count // Reference: Chatwoot reports#outgoing_messages_count func (h *AnalyticsHandler) OutgoingMessagesCount(c *gin.Context) { accountID, ok := parseAccountID(c) if !ok { return } since, until, ok := parseDateRange(c) if !ok { return } result, err := h.svc.GetOutgoingMessagesCount(c.Request.Context(), accountID, since, until) if err != nil { applogger.L().Errorf("Outgoing messages count report: %v", err) response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to generate outgoing messages count") return } response.OK(c, result) }