package v1 import ( "net/http" "strconv" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/model" "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 } var req service.CreateTagRequest if err := bindJSONWrappedOrRaw(c, "label", &req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } tag, err := h.tagSvc.Create(c.Request.Context(), accountID, &req) if err != nil { applogger.L().Errorf("Create tag: %v", err) handleServiceError(c, err) return } c.JSON(http.StatusOK, serializeLabel(tag)) } // GetTag retrieves a tag by ID. // GET /api/v1/accounts/:account_id/tags/:tag_id func (h *LabelHandler) GetTag(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 } tag, err := h.tagSvc.GetByIDAndAccountID(c.Request.Context(), accountID, tagID) if err != nil { applogger.L().Errorf("Get tag: %v", err) handleServiceError(c, err) return } c.JSON(http.StatusOK, serializeLabel(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 } c.JSON(http.StatusOK, gin.H{"payload": serializeLabels(tags)}) } // UpdateTag modifies an existing tag. // PUT /api/v1/accounts/:account_id/tags/:tag_id func (h *LabelHandler) UpdateTag(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 } if _, err := h.tagSvc.GetByIDAndAccountID(c.Request.Context(), accountID, tagID); err != nil { applogger.L().Errorf("Get tag for update: %v", err) handleServiceError(c, err) return } var req service.UpdateTagRequest if err := bindJSONWrappedOrRaw(c, "label", &req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error()) return } tag, err := h.tagSvc.Update(c.Request.Context(), tagID, &req) if err != nil { applogger.L().Errorf("Update tag: %v", err) handleServiceError(c, err) return } c.JSON(http.StatusOK, serializeLabel(tag)) } // DeleteTag soft-deletes a tag. // DELETE /api/v1/accounts/:account_id/tags/:tag_id func (h *LabelHandler) DeleteTag(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 } if _, err := h.tagSvc.GetByIDAndAccountID(c.Request.Context(), accountID, tagID); err != nil { applogger.L().Errorf("Get tag for delete: %v", err) handleServiceError(c, err) return } if err := h.tagSvc.Delete(c.Request.Context(), tagID); err != nil { applogger.L().Errorf("Delete tag: %v", err) handleServiceError(c, err) return } c.Status(http.StatusOK) } func serializeLabels(tags []model.Tag) []gin.H { payload := make([]gin.H, 0, len(tags)) for i := range tags { payload = append(payload, serializeLabel(&tags[i])) } return payload } func serializeLabel(tag *model.Tag) gin.H { return gin.H{ "id": tag.ID, "title": tag.Name, "description": tag.Description, "color": tag.Color, "show_on_sidebar": labelShowOnSidebar(tag), } } func labelShowOnSidebar(tag *model.Tag) bool { if tag.ShowOnSidebar == nil { return false } return *tag.ShowOnSidebar } // ========== 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, }) }