391 lines
12 KiB
Go
391 lines
12 KiB
Go
package v1
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"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}
|
|
}
|
|
|
|
// Index returns metric time-series for the overview charts.
|
|
// GET /api/v2/accounts/:account_id/reports?metric=...&since=...&until=...
|
|
func (h *AnalyticsHandler) Index(c *gin.Context) {
|
|
accountID, ok := parseAccountID(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
since, until, ok := parseDateRange(c)
|
|
if !ok {
|
|
return
|
|
}
|
|
metric := c.Query("metric")
|
|
if metric == "" {
|
|
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrBadRequest, "metric parameter is required")
|
|
return
|
|
}
|
|
id := uint(0)
|
|
if rawID := c.Query("id"); rawID != "" {
|
|
parsed, err := strconv.ParseUint(rawID, 10, 64)
|
|
if err != nil {
|
|
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid id")
|
|
return
|
|
}
|
|
id = uint(parsed)
|
|
}
|
|
businessHours := c.Query("business_hours") == "true" || c.Query("business_hours") == "1"
|
|
result, err := h.svc.GetTimeseries(c.Request.Context(), accountID, metric, since, until, c.DefaultQuery("type", "account"), id, c.Query("group_by"), businessHours)
|
|
if err != nil {
|
|
applogger.L().Errorf("Timeseries report: %v", err)
|
|
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to generate report")
|
|
return
|
|
}
|
|
response.OK(c, result)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
result, err := h.svc.GetConversationsByType(c.Request.Context(), accountID, reportType, time.Time{}, time.Time{})
|
|
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
|
|
}
|
|
groupBy := c.Query("group_by")
|
|
if groupBy != "agent" && groupBy != "team" && groupBy != "inbox" && groupBy != "label" {
|
|
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrBadRequest, "invalid group_by")
|
|
return
|
|
}
|
|
|
|
result, err := h.svc.GetOutgoingMessagesCountGrouped(c.Request.Context(), accountID, since, until, groupBy)
|
|
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)
|
|
}
|