package v1 import ( "net/http" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/service" ) // InboxMemberHandler handles inbox member (seat assignment) API endpoints. // Reference: Chatwoot app/controllers/api/v1/inbox_members_controller.rb // Routes manage which agents are assigned to which inboxes (seat assignment). type InboxMemberHandler struct { svc *service.InboxMemberService } // NewInboxMemberHandler creates a new InboxMemberHandler. func NewInboxMemberHandler(svc *service.InboxMemberService) *InboxMemberHandler { return &InboxMemberHandler{svc: svc} } // ListMembers retrieves all agents assigned to an inbox. // GET /api/v1/accounts/:id/inboxes/:inbox_id/members // Reference: Chatwoot inbox_members#index func (h *InboxMemberHandler) ListMembers(c *gin.Context) { inboxID, err := parseUintParam(c, "inbox_id") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid inbox id"}) return } members, svcErr := h.svc.ListByInbox(c.Request.Context(), inboxID) if svcErr != nil { c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to list inbox members"}) return } c.JSON(http.StatusOK, inboxMembersPayload(members)) } // AddMember assigns an agent to an inbox (seat assignment). // POST /api/v1/accounts/:id/inboxes/:inbox_id/members // Reference: Chatwoot inbox_members#create func (h *InboxMemberHandler) AddMember(c *gin.Context) { inboxID, err := parseUintParam(c, "inbox_id") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid inbox id"}) return } var req service.AddMemberRequest if bindErr := c.ShouldBindJSON(&req); bindErr != nil { c.JSON(http.StatusBadRequest, gin.H{"error": bindErr.Error()}) return } // Override inbox_id from URL path parameter req.InboxID = inboxID member, svcErr := h.svc.AddMember(c.Request.Context(), req) if svcErr != nil { c.JSON(http.StatusBadRequest, gin.H{"error": svcErr.Error()}) return } c.JSON(http.StatusCreated, inboxMembersPayload([]model.InboxMember{*member})) } // RemoveMember removes an agent from an inbox (unassign seat). // DELETE /api/v1/accounts/:id/inboxes/:inbox_id/members/:user_id // Reference: Chatwoot inbox_members#destroy func (h *InboxMemberHandler) RemoveMember(c *gin.Context) { inboxID, err := parseUintParam(c, "inbox_id") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid inbox id"}) return } userID, err := parseUintParam(c, "user_id") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"}) return } if svcErr := h.svc.RemoveMember(c.Request.Context(), inboxID, userID); svcErr != nil { c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to remove member"}) return } c.JSON(http.StatusOK, gin.H{"inbox_id": inboxID, "user_id": userID, "removed": true}) } // UpdateMember updates a member's role or availability status. // PATCH /api/v1/accounts/:id/inboxes/:inbox_id/members/:user_id // Reference: Chatwoot inbox_members#update func (h *InboxMemberHandler) UpdateMember(c *gin.Context) { inboxID, err := parseUintParam(c, "inbox_id") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid inbox id"}) return } userID, err := parseUintParam(c, "user_id") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"}) return } var req service.UpdateMemberRequest if bindErr := c.ShouldBindJSON(&req); bindErr != nil { c.JSON(http.StatusBadRequest, gin.H{"error": bindErr.Error()}) return } member, svcErr := h.svc.UpdateMember(c.Request.Context(), inboxID, userID, req) if svcErr != nil { c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to update member"}) return } c.JSON(http.StatusOK, inboxMembersPayload([]model.InboxMember{*member})) } // UpdateMultiple replaces all members of an inbox with a new set of user IDs (batch seat assignment). // PATCH /api/v1/accounts/:id/inboxes/:inbox_id/members/update_multiple // Reference: Chatwoot inbox_members#update (batch replace) func (h *InboxMemberHandler) UpdateMultiple(c *gin.Context) { inboxID, err := parseUintParam(c, "inbox_id") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid inbox id"}) return } var req service.UpdateMultipleRequest if bindErr := c.ShouldBindJSON(&req); bindErr != nil { c.JSON(http.StatusBadRequest, gin.H{"error": bindErr.Error()}) return } req.InboxID = inboxID members, svcErr := h.svc.UpdateMultiple(c.Request.Context(), req) if svcErr != nil { c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to update members"}) return } c.JSON(http.StatusOK, inboxMembersPayload(members)) } // ShowAccountScoped retrieves all agents assigned to an inbox using Chatwoot's account-level route. // GET /api/v1/accounts/:account_id/inbox_members/:inbox_id func (h *InboxMemberHandler) ShowAccountScoped(c *gin.Context) { inboxID, err := parseUintParam(c, "inbox_id") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid inbox id"}) return } members, svcErr := h.svc.ListByInbox(c.Request.Context(), inboxID) if svcErr != nil { c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to list inbox members"}) return } c.JSON(http.StatusOK, inboxMembersPayload(members)) } // CreateAccountScoped adds or replaces inbox members using Chatwoot's account-level create route. // POST /api/v1/accounts/:account_id/inbox_members func (h *InboxMemberHandler) CreateAccountScoped(c *gin.Context) { var req service.UpdateMultipleRequest if bindErr := c.ShouldBindJSON(&req); bindErr != nil { c.JSON(http.StatusBadRequest, gin.H{"error": bindErr.Error()}) return } members, svcErr := h.svc.AddMembers(c.Request.Context(), req) if svcErr != nil { c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to update members"}) return } c.JSON(http.StatusOK, inboxMembersPayload(members)) } // UpdateAccountScoped replaces all members of an inbox using Chatwoot's account-level update route. // PATCH /api/v1/accounts/:account_id/inbox_members func (h *InboxMemberHandler) UpdateAccountScoped(c *gin.Context) { h.updateAccountScoped(c) } func (h *InboxMemberHandler) updateAccountScoped(c *gin.Context) { var req service.UpdateMultipleRequest if bindErr := c.ShouldBindJSON(&req); bindErr != nil { c.JSON(http.StatusBadRequest, gin.H{"error": bindErr.Error()}) return } members, svcErr := h.svc.UpdateMultiple(c.Request.Context(), req) if svcErr != nil { c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to update members"}) return } c.JSON(http.StatusOK, inboxMembersPayload(members)) } // DestroyAccountScoped removes selected users from an inbox using Chatwoot's account-level route. // DELETE /api/v1/accounts/:account_id/inbox_members func (h *InboxMemberHandler) DestroyAccountScoped(c *gin.Context) { var req struct { InboxID uint `json:"inbox_id" binding:"required"` UserIDs []uint `json:"user_ids" binding:"required"` } if bindErr := c.ShouldBindJSON(&req); bindErr != nil { c.JSON(http.StatusBadRequest, gin.H{"error": bindErr.Error()}) return } for _, userID := range req.UserIDs { if svcErr := h.svc.RemoveMember(c.Request.Context(), req.InboxID, userID); svcErr != nil { c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to remove member"}) return } } c.Status(http.StatusOK) } func inboxMembersPayload(members []model.InboxMember) gin.H { payload := make([]gin.H, 0, len(members)) for _, member := range members { payload = append(payload, serializeInboxMemberAgent(member)) } return gin.H{"payload": payload} } func serializeInboxMemberAgent(member model.InboxMember) gin.H { user := member.User availableName := user.DisplayName if availableName == "" { availableName = user.Name } if availableName == "" { availableName = user.Email } availabilityStatus := member.AvailabilityStatus if availabilityStatus == "" { if user.Available { availabilityStatus = "online" } else { availabilityStatus = "offline" } } role := user.Role if role == "" { role = member.Role } if role == "" { role = "agent" } return gin.H{ "id": user.ID, "account_id": member.Inbox.AccountID, "availability_status": availabilityStatus, "auto_offline": true, "confirmed": user.ConfirmedAt != nil, "email": user.Email, "provider": user.Provider, "available_name": availableName, "name": user.Name, "role": role, "thumbnail": user.AvatarURL, "custom_role_id": user.CustomRoleID, } }