package v1 import ( "net/http" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/service" "github.com/gochat/gochat/internal/worker" "github.com/gochat/gochat/pkg/response" ) // BulkActionHandler handles generic bulk actions for Conversations and Contacts. // Reference: Chatwoot app/controllers/api/v1/accounts/bulk_actions_controller.rb type BulkActionHandler struct { conversationSvc *service.ConversationService contactSvc *service.ContactService worker *worker.WorkerPool } // NewBulkActionHandler creates a new BulkActionHandler. func NewBulkActionHandler(conversationSvc *service.ConversationService, contactSvc *service.ContactService) *BulkActionHandler { return &BulkActionHandler{conversationSvc: conversationSvc, contactSvc: contactSvc} } func (h *BulkActionHandler) WithWorkerPool(wp *worker.WorkerPool) *BulkActionHandler { h.worker = wp return h } // BulkActionRequest is the DTO for generic bulk actions. // Reference: Chatwoot bulk_actions_controller#create — params[:type], params[:action_name], params[:ids] type BulkActionRequest struct { Type string `json:"type" binding:"required"` // "Conversation" or "Contact" ActionName string `json:"action_name,omitempty"` // legacy local action names IDs []uint `json:"ids" binding:"required,min=1"` // Chatwoot sends conversation display IDs Fields service.ConversationBulkActionFields `json:"fields,omitempty"` Labels service.ConversationBulkActionLabels `json:"labels,omitempty"` AssigneeID *uint `json:"assignee_id,omitempty"` // legacy local assign field TeamID *uint `json:"team_id,omitempty"` // legacy local team field SnoozedUntil string `json:"snoozed_until,omitempty"` // for conversation snooze } // Create processes a bulk action request. // POST /api/v1/accounts/:account_id/bulk_actions // Reference: Chatwoot bulk_actions_controller#create — returns 200 OK on success func (h *BulkActionHandler) Create(c *gin.Context) { accountID, err := parseUintParam(c, "account_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") return } var req BulkActionRequest if bindErr := c.ShouldBindJSON(&req); bindErr != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, bindErr.Error()) return } switch req.Type { case "Conversation": h.handleConversationBulk(c, accountID, req) case "Contact": h.handleContactBulk(c, accountID, req) default: c.JSON(http.StatusUnprocessableEntity, gin.H{"success": false}) } } // handleConversationBulk processes bulk actions on conversations. func (h *BulkActionHandler) handleConversationBulk(c *gin.Context, accountID uint, req BulkActionRequest) { if h.worker != nil { params := service.ConversationBulkActionParams{ Type: req.Type, ActionName: req.ActionName, IDs: req.IDs, Fields: req.Fields, Labels: req.Labels, SnoozedUntil: req.SnoozedUntil, } if req.AssigneeID != nil && params.Fields.AssigneeID == nil { params.Fields.AssigneeID = req.AssigneeID } if req.TeamID != nil && params.Fields.TeamID == nil { params.Fields.TeamID = req.TeamID } if _, err := service.EnqueueConversationBulkAction(c.Request.Context(), h.worker, accountID, getUserID(c), params); err != nil { response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to enqueue bulk action") return } c.Status(http.StatusOK) return } successCount := 0 failCount := 0 for _, convID := range req.IDs { var svcErr error switch req.ActionName { case "resolve": _, svcErr = h.conversationSvc.ToggleStatus(c.Request.Context(), accountID, convID, service.ToggleStatusRequest{Status: string(model.ConversationStatusResolved)}) case "open": _, svcErr = h.conversationSvc.ToggleStatus(c.Request.Context(), accountID, convID, service.ToggleStatusRequest{Status: string(model.ConversationStatusOpen)}) case "snooze": _, svcErr = h.conversationSvc.ToggleStatus(c.Request.Context(), accountID, convID, service.ToggleStatusRequest{Status: string(model.ConversationStatusSnoozed)}) case "assign": if req.AssigneeID != nil { _, svcErr = h.conversationSvc.AssignAgent(c.Request.Context(), accountID, convID, *req.AssigneeID) } case "unassign": _, svcErr = h.conversationSvc.UnassignAgent(c.Request.Context(), accountID, convID) case "assign_team": _, svcErr = h.conversationSvc.AssignTeam(c.Request.Context(), accountID, convID, req.AssigneeID, req.TeamID) case "delete": svcErr = h.conversationSvc.Delete(c.Request.Context(), accountID, convID) case "label_add": if len(req.Labels.Add) > 0 { _, svcErr = h.conversationSvc.UpdateLabels(c.Request.Context(), accountID, convID, req.Labels.Add) } default: failCount++ continue } if svcErr != nil { failCount++ } else { successCount++ } } response.OK(c, gin.H{ "success_count": successCount, "fail_count": failCount, }) } // handleContactBulk processes bulk actions on contacts. // Reference: Chatwoot only supports "delete" and label operations for contacts in bulk_actions. func (h *BulkActionHandler) handleContactBulk(c *gin.Context, accountID uint, req BulkActionRequest) { successCount := 0 failCount := 0 for _, contactID := range req.IDs { var svcErr error switch req.ActionName { case "delete": svcErr = h.contactSvc.Delete(c.Request.Context(), accountID, contactID) default: // Chatwoot: other contact actions (labels) require separate async jobs // For now, return error for unsupported actions failCount++ continue } if svcErr != nil { failCount++ } else { successCount++ } } response.OK(c, gin.H{ "success_count": successCount, "fail_count": failCount, }) }