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/pagination" "github.com/gochat/gochat/pkg/response" ) // TeamHandler handles Team CRUD + members. // Reference: Chatwoot app/controllers/api/v1/teams_controller.rb type TeamHandler struct { svc *service.TeamService } // NewTeamHandler creates a new Team handler. func NewTeamHandler(svc *service.TeamService) *TeamHandler { return &TeamHandler{svc: svc} } // List returns all teams for an account. // GET /api/v1/accounts/:account_id/teams func (h *TeamHandler) List(c *gin.Context) { accountID := getAccountID(c) if accountID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified") return } pg := pagination.Parse(c) teams, total, err := h.svc.List(c.Request.Context(), accountID, pg.Offset, pg.PerPage) if err != nil { applogger.L().Errorf("List teams for account %d: %v", accountID, err) handleServiceError(c, err) return } _ = total c.JSON(http.StatusOK, serializeTeams(c, h.svc, teams)) } // Get returns a single team by ID. // GET /api/v1/accounts/:account_id/teams/:id func (h *TeamHandler) Get(c *gin.Context) { accountID := getAccountID(c) if accountID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified") return } id, err := parseTeamIDParam(c) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid team ID") return } team, svcErr := h.svc.Get(c.Request.Context(), uint(id), accountID) if svcErr != nil { applogger.L().Errorf("Get team %d for account %d: %v", id, accountID, svcErr) handleServiceError(c, svcErr) return } c.JSON(http.StatusOK, serializeTeamForAccount(c, h.svc, team)) } // Create creates a new team within an account. // POST /api/v1/accounts/:account_id/teams func (h *TeamHandler) Create(c *gin.Context) { accountID := getAccountID(c) if accountID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified") return } var req service.CreateTeamRequest if err := bindJSONWrappedOrRaw(c, "team", &req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error()) return } team, svcErr := h.svc.Create(c.Request.Context(), accountID, req) if svcErr != nil { applogger.L().Errorf("Create team for account %d: %v", accountID, svcErr) handleServiceError(c, svcErr) return } c.JSON(http.StatusOK, serializeTeamForAccount(c, h.svc, team)) } // Update updates an existing team. // PUT /api/v1/accounts/:account_id/teams/:id func (h *TeamHandler) Update(c *gin.Context) { accountID := getAccountID(c) if accountID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified") return } id, err := parseTeamIDParam(c) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid team ID") return } var req service.UpdateTeamRequest if err := bindJSONWrappedOrRaw(c, "team", &req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error()) return } team, svcErr := h.svc.Update(c.Request.Context(), uint(id), accountID, req) if svcErr != nil { applogger.L().Errorf("Update team %d for account %d: %v", id, accountID, svcErr) handleServiceError(c, svcErr) return } c.JSON(http.StatusOK, serializeTeamForAccount(c, h.svc, team)) } // Delete soft-deletes a team. // DELETE /api/v1/accounts/:account_id/teams/:id func (h *TeamHandler) Delete(c *gin.Context) { accountID := getAccountID(c) if accountID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified") return } id, err := parseTeamIDParam(c) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid team ID") return } if svcErr := h.svc.Delete(c.Request.Context(), uint(id), accountID); svcErr != nil { applogger.L().Errorf("Delete team %d for account %d: %v", id, accountID, svcErr) handleServiceError(c, svcErr) return } c.Status(http.StatusOK) } // AddMembers adds users to a team. // POST /api/v1/accounts/:account_id/teams/:id/members func (h *TeamHandler) AddMembers(c *gin.Context) { accountID := getAccountID(c) if accountID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified") return } id, err := parseTeamIDParam(c) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid team ID") return } var req service.TeamMemberRequest if err := c.ShouldBindJSON(&req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error()) return } _, svcErr := h.svc.AddMembers(c.Request.Context(), uint(id), accountID, req.UserIDs) if svcErr != nil { applogger.L().Errorf("Add members to team %d for account %d: %v", id, accountID, svcErr) handleServiceError(c, svcErr) return } members, svcErr := h.svc.ListMembers(c.Request.Context(), uint(id), accountID) if svcErr != nil { handleServiceError(c, svcErr) return } c.JSON(http.StatusOK, serializeTeamMemberAgents(accountID, members)) } // RemoveMembers removes a user from a team. // DELETE /api/v1/accounts/:account_id/teams/:id/members/:user_id func (h *TeamHandler) RemoveMembers(c *gin.Context) { accountID := getAccountID(c) if accountID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified") return } id, err := parseTeamIDParam(c) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid team ID") return } userID, err := strconv.ParseUint(c.Param("user_id"), 10, 32) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid user ID") return } if svcErr := h.svc.RemoveMember(c.Request.Context(), uint(id), uint(userID), accountID); svcErr != nil { applogger.L().Errorf("Remove member %d from team %d for account %d: %v", userID, id, accountID, svcErr) handleServiceError(c, svcErr) return } c.Status(http.StatusOK) } // ListMembers retrieves all members of a team. // GET /api/v1/accounts/:account_id/teams/:id/members func (h *TeamHandler) ListMembers(c *gin.Context) { accountID := getAccountID(c) if accountID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified") return } id, err := parseTeamIDParam(c) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid team ID") return } members, svcErr := h.svc.ListMembers(c.Request.Context(), uint(id), accountID) if svcErr != nil { applogger.L().Errorf("List members for team %d for account %d: %v", id, accountID, svcErr) handleServiceError(c, svcErr) return } c.JSON(http.StatusOK, serializeTeamMemberAgents(accountID, members)) } // UpdateMembers adds/removes members to match the provided user_ids list. // PATCH /api/v1/accounts/:account_id/teams/:team_id/team_members // Reference: Chatwoot team_members#update — calculates add/remove diffs from user_ids func (h *TeamHandler) UpdateMembers(c *gin.Context) { accountID := getAccountID(c) if accountID == 0 { response.AbortWithStatusError(c, http.StatusUnauthorized, response.ErrUnauthorized, "account not identified") return } teamID, err := parseTeamIDParam(c) if err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid team ID") return } var req service.TeamMemberRequest if err := c.ShouldBindJSON(&req); err != nil { response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error()) return } members, svcErr := h.svc.UpdateMembers(c.Request.Context(), uint(teamID), accountID, req.UserIDs) if svcErr != nil { applogger.L().Errorf("Update members for team %d for account %d: %v", teamID, accountID, svcErr) handleServiceError(c, svcErr) return } c.JSON(http.StatusOK, serializeTeamMemberAgents(accountID, members)) } func parseTeamIDParam(c *gin.Context) (uint64, error) { value := c.Param("team_id") if value == "" { value = c.Param("id") } return strconv.ParseUint(value, 10, 32) } func serializeTeams(c *gin.Context, svc *service.TeamService, teams []model.Team) []map[string]any { payload := make([]map[string]any, 0, len(teams)) for i := range teams { payload = append(payload, serializeTeamForAccount(c, svc, &teams[i])) } return payload } func serializeTeamForAccount(c *gin.Context, svc *service.TeamService, team *model.Team) map[string]any { if team == nil { return map[string]any{} } payload := map[string]any{ "id": team.ID, "name": team.Name, "description": team.Description, "allow_auto_assign": team.AllowAutoAssignment, "account_id": team.AccountID, "is_member": false, } userID := getUserID(c) if userID != 0 && svc != nil && svc.DB() != nil { var count int64 svc.DB().WithContext(c.Request.Context()).Model(&model.TeamMember{}).Where("team_id = ? AND user_id = ?", team.ID, userID).Count(&count) payload["is_member"] = count > 0 } return payload } func serializeTeamMemberAgents(accountID uint, members []model.TeamMember) []map[string]any { payload := make([]map[string]any, 0, len(members)) for i := range members { member := members[i] payload = append(payload, serializeAgentUser(&member.User, accountID, "", member.AvailabilityStatus, false, 0)) } return payload }