package v1 import ( "net/http" "strconv" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/service" applogger "github.com/gochat/gochat/pkg/logger" "github.com/gochat/gochat/pkg/response" ) // LabelHandler handles Tag CRUD and conversation-label association endpoints. type LabelHandler struct { tagSvc *service.TagService labelSvc *service.LabelService } func NewLabelHandler(tagSvc *service.TagService, labelSvc *service.LabelService) *LabelHandler { return &LabelHandler{tagSvc: tagSvc, labelSvc: labelSvc} } // ========== Tag CRUD ========== // CreateTag creates a new tag/label in an account. // POST /api/v1/accounts/:account_id/tags func (h *LabelHandler) CreateTag(c *gin.Context) { accountID, err := parseUintParam(c, "account_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") return } // Chatwoot: params.require(:label) → {"label": {...}} var wrapper struct { Label service.CreateTagRequest `json:"label"` } if err := c.ShouldBindJSON(&wrapper); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } req := wrapper.Label tag, err := h.tagSvc.Create(c.Request.Context(), accountID, &req) if err != nil { applogger.L().Errorf("Create tag: %v", err) handleServiceError(c, err) return } response.Created(c, tag) } // GetTag retrieves a tag by ID. // GET /api/v1/accounts/:account_id/tags/:tag_id func (h *LabelHandler) GetTag(c *gin.Context) { tagID, err := parseUintParam(c, "tag_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid tag_id") return } tag, err := h.tagSvc.GetByID(c.Request.Context(), tagID) if err != nil { applogger.L().Errorf("Get tag: %v", err) handleServiceError(c, err) return } response.OK(c, tag) } // ListTags returns all tags in an account. // GET /api/v1/accounts/:account_id/tags func (h *LabelHandler) ListTags(c *gin.Context) { accountID, err := parseUintParam(c, "account_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") return } tags, err := h.tagSvc.List(c.Request.Context(), accountID) if err != nil { applogger.L().Errorf("List tags: %v", err) handleServiceError(c, err) return } response.OK(c, tags) } // UpdateTag modifies an existing tag. // PUT /api/v1/accounts/:account_id/tags/:tag_id func (h *LabelHandler) UpdateTag(c *gin.Context) { tagID, err := parseUintParam(c, "tag_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid tag_id") return } // Chatwoot: params.require(:label) → {"label": {...}} var wrapper struct { Label service.UpdateTagRequest `json:"label"` } if err := c.ShouldBindJSON(&wrapper); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } req := wrapper.Label tag, err := h.tagSvc.Update(c.Request.Context(), tagID, &req) if err != nil { applogger.L().Errorf("Update tag: %v", err) handleServiceError(c, err) return } response.OK(c, tag) } // DeleteTag soft-deletes a tag. // DELETE /api/v1/accounts/:account_id/tags/:tag_id func (h *LabelHandler) DeleteTag(c *gin.Context) { tagID, err := parseUintParam(c, "tag_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid tag_id") return } if err := h.tagSvc.Delete(c.Request.Context(), tagID); err != nil { applogger.L().Errorf("Delete tag: %v", err) handleServiceError(c, err) return } response.NoContent(c) } // ========== Conversation-Label Association ========== // AddLabelToConversation attaches a label to a conversation. // POST /api/v1/accounts/:account_id/conversations/:id/labels func (h *LabelHandler) AddLabelToConversation(c *gin.Context) { accountID, err := parseUintParam(c, "account_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") return } conversationID, err := parseUintParam(c, "id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid conversation id") return } var req struct { TagID uint `json:"tag_id" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } cl, err := h.labelSvc.AddLabelToConversation(c.Request.Context(), accountID, conversationID, req.TagID) if err != nil { applogger.L().Errorf("Add label to conversation: %v", err) handleServiceError(c, err) return } response.Created(c, cl) } // RemoveLabelFromConversation detaches a label from a conversation. // DELETE /api/v1/accounts/:account_id/conversations/:id/labels/:tag_id func (h *LabelHandler) RemoveLabelFromConversation(c *gin.Context) { conversationID, err := parseUintParam(c, "id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid conversation id") return } tagID, err := parseUintParam(c, "tag_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid tag_id") return } if err := h.labelSvc.RemoveLabelFromConversation(c.Request.Context(), conversationID, tagID); err != nil { applogger.L().Errorf("Remove label from conversation: %v", err) handleServiceError(c, err) return } response.NoContent(c) } // GetConversationLabels returns all labels on a conversation. // GET /api/v1/accounts/:account_id/conversations/:id/labels func (h *LabelHandler) GetConversationLabels(c *gin.Context) { conversationID, err := parseUintParam(c, "id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid conversation id") return } labels, err := h.labelSvc.GetConversationLabels(c.Request.Context(), conversationID) if err != nil { applogger.L().Errorf("Get conversation labels: %v", err) handleServiceError(c, err) return } response.OK(c, labels) } // ReplaceConversationLabels replaces all labels on a conversation (Chatwoot-style). // PATCH /api/v1/accounts/:account_id/conversations/:id/labels func (h *LabelHandler) ReplaceConversationLabels(c *gin.Context) { accountID, err := parseUintParam(c, "account_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") return } conversationID, err := parseUintParam(c, "id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid conversation id") return } var req struct { TagIDs []uint `json:"tag_ids"` } if err := c.ShouldBindJSON(&req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } labels, err := h.labelSvc.ReplaceConversationLabels(c.Request.Context(), accountID, conversationID, req.TagIDs) if err != nil { applogger.L().Errorf("Replace conversation labels: %v", err) handleServiceError(c, err) return } response.OK(c, labels) } // ========== Batch Label Operations ========== // BatchAddLabel attaches a label to multiple conversations. // POST /api/v1/accounts/:account_id/labels/batch_add func (h *LabelHandler) BatchAddLabel(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 service.BatchAddLabelRequest if err := c.ShouldBindJSON(&req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } if err := h.labelSvc.BatchAddLabel(c.Request.Context(), accountID, &req); err != nil { applogger.L().Errorf("Batch add label: %v", err) handleServiceError(c, err) return } response.NoContent(c) } // BatchRemoveLabel detaches a label from multiple conversations. // POST /api/v1/accounts/:account_id/labels/batch_remove func (h *LabelHandler) BatchRemoveLabel(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 service.BatchRemoveLabelRequest if err := c.ShouldBindJSON(&req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } if err := h.labelSvc.BatchRemoveLabel(c.Request.Context(), accountID, &req); err != nil { applogger.L().Errorf("Batch remove label: %v", err) handleServiceError(c, err) return } response.NoContent(c) } // ========== Conversations by Tag ========== // GetConversationsByTag returns all conversations that have a specific tag. // GET /api/v1/accounts/:account_id/tags/:tag_id/conversations func (h *LabelHandler) GetConversationsByTag(c *gin.Context) { accountID, err := parseUintParam(c, "account_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id") return } tagID, err := parseUintParam(c, "tag_id") if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid tag_id") return } page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) perPage, _ := strconv.Atoi(c.DefaultQuery("per_page", "25")) if page < 1 { page = 1 } if perPage < 1 || perPage > 100 { perPage = 25 } labels, count, err := h.labelSvc.GetConversationsByTag(c.Request.Context(), accountID, tagID, page, perPage) if err != nil { applogger.L().Errorf("Get conversations by tag: %v", err) handleServiceError(c, err) return } response.OK(c, gin.H{ "labels": labels, "count": count, "page": page, "per_page": perPage, }) }