844 lines
28 KiB
Go
844 lines
28 KiB
Go
package handlers
|
|
|
|
import (
|
|
"Yimaru-Backend/internal/domain"
|
|
jwtutil "Yimaru-Backend/internal/web_server/jwt"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// teamMemberLoginRes represents the response body for team member login
|
|
type teamMemberLoginRes struct {
|
|
AccessToken string `json:"access_token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."`
|
|
RefreshToken string `json:"refresh_token" example:""`
|
|
MemberID int64 `json:"member_id" example:"1"`
|
|
TeamRole string `json:"team_role" example:"admin"`
|
|
}
|
|
|
|
// changePasswordReq represents the request body for changing password
|
|
type changePasswordReq struct {
|
|
CurrentPassword string `json:"current_password" validate:"required" example:"oldpassword123"`
|
|
NewPassword string `json:"new_password" validate:"required,min=8" example:"newpassword123"`
|
|
}
|
|
|
|
func mapTeamRoleToRole(teamRole domain.TeamRole) domain.Role {
|
|
switch teamRole {
|
|
case domain.TeamRoleSuperAdmin:
|
|
return domain.RoleSuperAdmin
|
|
case domain.TeamRoleAdmin:
|
|
return domain.RoleAdmin
|
|
case domain.TeamRoleInstructor:
|
|
return domain.RoleInstructor
|
|
case domain.TeamRoleSupportAgent:
|
|
return domain.RoleSupport
|
|
default:
|
|
return domain.RoleAdmin
|
|
}
|
|
}
|
|
|
|
// TeamMemberLogin godoc
|
|
// @Summary Login team member
|
|
// @Description Authenticate a team member (internal staff) with email and password
|
|
// @Tags team
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param body body domain.TeamMemberLoginReq true "Team member login credentials"
|
|
// @Success 200 {object} domain.Response{data=teamMemberLoginRes}
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 401 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/team/login [post]
|
|
func (h *Handler) TeamMemberLogin(c *fiber.Ctx) error {
|
|
var req domain.TeamMemberLoginReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
h.mongoLoggerSvc.Info("Failed to parse TeamMemberLogin request",
|
|
zap.Int("status_code", fiber.StatusBadRequest),
|
|
zap.Error(err),
|
|
zap.Time("timestamp", time.Now()),
|
|
)
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Failed to login",
|
|
Error: "Invalid request body: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
if valErrs, ok := h.validator.Validate(c, req); !ok {
|
|
var errMsg string
|
|
for field, msg := range valErrs {
|
|
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
|
|
}
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Failed to login",
|
|
Error: errMsg,
|
|
})
|
|
}
|
|
|
|
loginRes, err := h.teamSvc.Login(c.Context(), req)
|
|
if err != nil {
|
|
switch {
|
|
case errors.Is(err, domain.ErrTeamMemberNotFound):
|
|
h.mongoLoggerSvc.Info("Team member login failed: member not found",
|
|
zap.Int("status_code", fiber.StatusUnauthorized),
|
|
zap.String("email", req.Email),
|
|
zap.Error(err),
|
|
zap.Time("timestamp", time.Now()),
|
|
)
|
|
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
|
|
Message: "Failed to login",
|
|
Error: "Invalid credentials",
|
|
})
|
|
case errors.Is(err, domain.ErrInvalidTeamMemberStatus):
|
|
h.mongoLoggerSvc.Info("Team member login failed: account suspended or inactive",
|
|
zap.Int("status_code", fiber.StatusUnauthorized),
|
|
zap.String("email", req.Email),
|
|
zap.Error(err),
|
|
zap.Time("timestamp", time.Now()),
|
|
)
|
|
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
|
|
Message: "Failed to login",
|
|
Error: "Account is not active",
|
|
})
|
|
default:
|
|
h.mongoLoggerSvc.Error("Team member login failed",
|
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
|
zap.String("email", req.Email),
|
|
zap.Error(err),
|
|
zap.Time("timestamp", time.Now()),
|
|
)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to login",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
}
|
|
|
|
role := mapTeamRoleToRole(loginRes.TeamRole)
|
|
accessToken, err := jwtutil.CreateJwt(
|
|
loginRes.ID,
|
|
role,
|
|
h.jwtConfig.JwtAccessKey,
|
|
h.jwtConfig.JwtAccessExpiry,
|
|
)
|
|
if err != nil {
|
|
h.mongoLoggerSvc.Error("Failed to create access token for team member",
|
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
|
zap.Int64("member_id", loginRes.ID),
|
|
zap.Error(err),
|
|
zap.Time("timestamp", time.Now()),
|
|
)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to login",
|
|
Error: "Failed to generate access token",
|
|
})
|
|
}
|
|
|
|
h.mongoLoggerSvc.Info("Team member login successful",
|
|
zap.Int("status_code", fiber.StatusOK),
|
|
zap.Int64("member_id", loginRes.ID),
|
|
zap.String("team_role", string(loginRes.TeamRole)),
|
|
zap.Time("timestamp", time.Now()),
|
|
)
|
|
|
|
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
|
Message: "Login successful",
|
|
Data: teamMemberLoginRes{
|
|
AccessToken: accessToken,
|
|
RefreshToken: "",
|
|
MemberID: loginRes.ID,
|
|
TeamRole: string(loginRes.TeamRole),
|
|
},
|
|
Success: true,
|
|
StatusCode: fiber.StatusOK,
|
|
})
|
|
}
|
|
|
|
// CreateTeamMember godoc
|
|
// @Summary Create a new team member
|
|
// @Description Create a new internal team member (admin only)
|
|
// @Tags team
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param body body domain.CreateTeamMemberReq true "Team member creation payload"
|
|
// @Success 201 {object} domain.Response{data=domain.TeamMemberResponse}
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 401 {object} domain.ErrorResponse
|
|
// @Failure 403 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/team/members [post]
|
|
func (h *Handler) CreateTeamMember(c *fiber.Ctx) error {
|
|
var req domain.CreateTeamMemberReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
h.mongoLoggerSvc.Info("Failed to parse CreateTeamMember request",
|
|
zap.Int("status_code", fiber.StatusBadRequest),
|
|
zap.Error(err),
|
|
zap.Time("timestamp", time.Now()),
|
|
)
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Failed to create team member",
|
|
Error: "Invalid request body: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
if valErrs, ok := h.validator.Validate(c, req); !ok {
|
|
var errMsg string
|
|
for field, msg := range valErrs {
|
|
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
|
|
}
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Failed to create team member",
|
|
Error: errMsg,
|
|
})
|
|
}
|
|
|
|
creatorID, ok := c.Locals("user_id").(int64)
|
|
if !ok {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Failed to create team member",
|
|
Error: "User ID not found in request context",
|
|
})
|
|
}
|
|
|
|
member, err := h.teamSvc.CreateTeamMember(c.Context(), req, &creatorID)
|
|
if err != nil {
|
|
switch {
|
|
case errors.Is(err, domain.ErrTeamMemberEmailExists):
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Failed to create team member",
|
|
Error: "Email already exists",
|
|
})
|
|
case errors.Is(err, domain.ErrInvalidTeamRole):
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Failed to create team member",
|
|
Error: "Invalid team role",
|
|
})
|
|
default:
|
|
h.mongoLoggerSvc.Error("Failed to create team member",
|
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
|
zap.Error(err),
|
|
zap.Time("timestamp", time.Now()),
|
|
)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to create team member",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
}
|
|
|
|
h.mongoLoggerSvc.Info("Team member created successfully",
|
|
zap.Int("status_code", fiber.StatusCreated),
|
|
zap.Int64("member_id", member.ID),
|
|
zap.Int64("created_by", creatorID),
|
|
zap.Time("timestamp", time.Now()),
|
|
)
|
|
|
|
actorRole := string(c.Locals("role").(domain.Role))
|
|
ip := c.IP()
|
|
ua := c.Get("User-Agent")
|
|
meta, _ := json.Marshal(map[string]interface{}{"email": member.Email, "team_role": string(member.TeamRole)})
|
|
go h.activityLogSvc.RecordAction(context.Background(), &creatorID, &actorRole, domain.ActionTeamMemberCreated, domain.ResourceTeamMember, &member.ID, "Created team member: "+member.Email, meta, &ip, &ua)
|
|
|
|
h.sendInAppNotification(member.ID, domain.NOTIFICATION_TYPE_TEAM_MEMBER_CREATED, "Welcome to the Team", "You have been added as a team member.")
|
|
|
|
return c.Status(fiber.StatusCreated).JSON(domain.Response{
|
|
Message: "Team member created successfully",
|
|
Data: toTeamMemberResponse(&member),
|
|
Success: true,
|
|
StatusCode: fiber.StatusCreated,
|
|
})
|
|
}
|
|
|
|
// GetTeamMember godoc
|
|
// @Summary Get team member by ID
|
|
// @Description Retrieve a team member's details by their ID
|
|
// @Tags team
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param id path int true "Team Member ID"
|
|
// @Success 200 {object} domain.Response{data=domain.TeamMemberResponse}
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 401 {object} domain.ErrorResponse
|
|
// @Failure 404 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/team/members/{id} [get]
|
|
func (h *Handler) GetTeamMember(c *fiber.Ctx) error {
|
|
memberIDStr := c.Params("id")
|
|
memberID, err := strconv.ParseInt(memberIDStr, 10, 64)
|
|
if err != nil || memberID <= 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid member ID",
|
|
Error: "Member ID must be a valid positive integer",
|
|
})
|
|
}
|
|
|
|
member, err := h.teamSvc.GetTeamMemberByID(c.Context(), memberID)
|
|
if err != nil {
|
|
if errors.Is(err, domain.ErrTeamMemberNotFound) {
|
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
Message: "Team member not found",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
h.mongoLoggerSvc.Error("Failed to get team member",
|
|
zap.Int64("member_id", memberID),
|
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
|
zap.Error(err),
|
|
zap.Time("timestamp", time.Now()),
|
|
)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to get team member",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
|
Message: "Team member retrieved successfully",
|
|
Data: toTeamMemberResponse(&member),
|
|
Success: true,
|
|
StatusCode: fiber.StatusOK,
|
|
})
|
|
}
|
|
|
|
// GetAllTeamMembers godoc
|
|
// @Summary List all team members
|
|
// @Description Get a paginated list of team members with optional filtering
|
|
// @Tags team
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param team_role query string false "Filter by team role (super_admin, admin, content_manager, support_agent, instructor, finance, hr, analyst)"
|
|
// @Param department query string false "Filter by department"
|
|
// @Param status query string false "Filter by status (active, inactive, suspended, terminated)"
|
|
// @Param search query string false "Search by name, email, or phone number"
|
|
// @Param page query int false "Page number (default: 1)"
|
|
// @Param page_size query int false "Items per page (default: 10, max: 100)"
|
|
// @Success 200 {object} domain.Response{data=[]domain.TeamMemberResponse,metadata=domain.Pagination}
|
|
// @Failure 401 {object} domain.ErrorResponse
|
|
// @Failure 403 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/team/members [get]
|
|
func (h *Handler) GetAllTeamMembers(c *fiber.Ctx) error {
|
|
filter := domain.TeamMemberFilter{}
|
|
|
|
if teamRole := c.Query("team_role"); teamRole != "" {
|
|
filter.TeamRole = &teamRole
|
|
}
|
|
if department := c.Query("department"); department != "" {
|
|
filter.Department = &department
|
|
}
|
|
if status := c.Query("status"); status != "" {
|
|
filter.Status = &status
|
|
}
|
|
filter.Search = c.Query("search")
|
|
|
|
page, _ := strconv.ParseInt(c.Query("page", "1"), 10, 64)
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
filter.Page = page
|
|
|
|
pageSize, _ := strconv.ParseInt(c.Query("page_size", "10"), 10, 64)
|
|
if pageSize < 1 || pageSize > 100 {
|
|
pageSize = 10
|
|
}
|
|
filter.PageSize = pageSize
|
|
|
|
var members []domain.TeamMember
|
|
var total int64
|
|
var err error
|
|
|
|
if filter.Search != "" {
|
|
members, err = h.teamSvc.SearchTeamMembers(c.Context(), filter.Search, filter.TeamRole, filter.Status)
|
|
if err == nil {
|
|
total = int64(len(members))
|
|
}
|
|
} else {
|
|
members, total, err = h.teamSvc.GetAllTeamMembers(c.Context(), filter)
|
|
}
|
|
|
|
if err != nil {
|
|
h.mongoLoggerSvc.Error("Failed to get team members",
|
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
|
zap.Error(err),
|
|
zap.Time("timestamp", time.Now()),
|
|
)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to get team members",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
memberResponses := make([]domain.TeamMemberResponse, 0, len(members))
|
|
for i := range members {
|
|
memberResponses = append(memberResponses, toTeamMemberResponse(&members[i]))
|
|
}
|
|
|
|
totalPages := int((total + filter.PageSize - 1) / filter.PageSize)
|
|
|
|
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
|
Message: "Team members retrieved successfully",
|
|
Data: memberResponses,
|
|
Success: true,
|
|
MetaData: domain.Pagination{
|
|
Total: int(total),
|
|
TotalPages: totalPages,
|
|
CurrentPage: int(filter.Page),
|
|
Limit: int(filter.PageSize),
|
|
},
|
|
StatusCode: fiber.StatusOK,
|
|
})
|
|
}
|
|
|
|
// UpdateTeamMember godoc
|
|
// @Summary Update team member
|
|
// @Description Update an existing team member's details (admin only)
|
|
// @Tags team
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param id path int true "Team Member ID"
|
|
// @Param body body domain.UpdateTeamMemberReq true "Team member update payload"
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 401 {object} domain.ErrorResponse
|
|
// @Failure 403 {object} domain.ErrorResponse
|
|
// @Failure 404 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/team/members/{id} [put]
|
|
func (h *Handler) UpdateTeamMember(c *fiber.Ctx) error {
|
|
memberIDStr := c.Params("id")
|
|
memberID, err := strconv.ParseInt(memberIDStr, 10, 64)
|
|
if err != nil || memberID <= 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid member ID",
|
|
Error: "Member ID must be a valid positive integer",
|
|
})
|
|
}
|
|
|
|
var req domain.UpdateTeamMemberReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Failed to update team member",
|
|
Error: "Invalid request body: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
updaterID, ok := c.Locals("user_id").(int64)
|
|
if !ok {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Failed to update team member",
|
|
Error: "User ID not found in request context",
|
|
})
|
|
}
|
|
|
|
req.TeamMemberID = memberID
|
|
req.UpdatedBy = updaterID
|
|
|
|
if err := h.teamSvc.UpdateTeamMember(c.Context(), req); err != nil {
|
|
switch {
|
|
case errors.Is(err, domain.ErrTeamMemberNotFound):
|
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
Message: "Team member not found",
|
|
Error: err.Error(),
|
|
})
|
|
case errors.Is(err, domain.ErrInvalidTeamRole):
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Failed to update team member",
|
|
Error: "Invalid team role",
|
|
})
|
|
default:
|
|
h.mongoLoggerSvc.Error("Failed to update team member",
|
|
zap.Int64("member_id", memberID),
|
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
|
zap.Error(err),
|
|
zap.Time("timestamp", time.Now()),
|
|
)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to update team member",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
}
|
|
|
|
h.mongoLoggerSvc.Info("Team member updated successfully",
|
|
zap.Int64("member_id", memberID),
|
|
zap.Int64("updated_by", updaterID),
|
|
zap.Time("timestamp", time.Now()),
|
|
)
|
|
|
|
actorRole := string(c.Locals("role").(domain.Role))
|
|
ip := c.IP()
|
|
ua := c.Get("User-Agent")
|
|
meta, _ := json.Marshal(map[string]interface{}{"member_id": memberID})
|
|
go h.activityLogSvc.RecordAction(context.Background(), &updaterID, &actorRole, domain.ActionTeamMemberUpdated, domain.ResourceTeamMember, &memberID, fmt.Sprintf("Updated team member ID: %d", memberID), meta, &ip, &ua)
|
|
|
|
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
|
Message: "Team member updated successfully",
|
|
Success: true,
|
|
StatusCode: fiber.StatusOK,
|
|
})
|
|
}
|
|
|
|
// UpdateTeamMemberStatus godoc
|
|
// @Summary Update team member status
|
|
// @Description Update a team member's status (active, inactive, suspended, terminated)
|
|
// @Tags team
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param id path int true "Team Member ID"
|
|
// @Param body body domain.UpdateTeamMemberStatusReq true "Status update payload"
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 401 {object} domain.ErrorResponse
|
|
// @Failure 403 {object} domain.ErrorResponse
|
|
// @Failure 404 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/team/members/{id}/status [patch]
|
|
func (h *Handler) UpdateTeamMemberStatus(c *fiber.Ctx) error {
|
|
memberIDStr := c.Params("id")
|
|
memberID, err := strconv.ParseInt(memberIDStr, 10, 64)
|
|
if err != nil || memberID <= 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid member ID",
|
|
Error: "Member ID must be a valid positive integer",
|
|
})
|
|
}
|
|
|
|
var req domain.UpdateTeamMemberStatusReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Failed to update team member status",
|
|
Error: "Invalid request body: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
if valErrs, ok := h.validator.Validate(c, req); !ok {
|
|
var errMsg string
|
|
for field, msg := range valErrs {
|
|
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
|
|
}
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Failed to update team member status",
|
|
Error: errMsg,
|
|
})
|
|
}
|
|
|
|
updaterID, ok := c.Locals("user_id").(int64)
|
|
if !ok {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Failed to update team member status",
|
|
Error: "User ID not found in request context",
|
|
})
|
|
}
|
|
|
|
req.TeamMemberID = memberID
|
|
req.UpdatedBy = updaterID
|
|
|
|
if err := h.teamSvc.UpdateTeamMemberStatus(c.Context(), req); err != nil {
|
|
switch {
|
|
case errors.Is(err, domain.ErrTeamMemberNotFound):
|
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
Message: "Team member not found",
|
|
Error: err.Error(),
|
|
})
|
|
case errors.Is(err, domain.ErrInvalidTeamMemberStatus):
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Failed to update team member status",
|
|
Error: "Invalid status value",
|
|
})
|
|
default:
|
|
h.mongoLoggerSvc.Error("Failed to update team member status",
|
|
zap.Int64("member_id", memberID),
|
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
|
zap.Error(err),
|
|
zap.Time("timestamp", time.Now()),
|
|
)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to update team member status",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
}
|
|
|
|
h.mongoLoggerSvc.Info("Team member status updated successfully",
|
|
zap.Int64("member_id", memberID),
|
|
zap.String("new_status", req.Status),
|
|
zap.Int64("updated_by", updaterID),
|
|
zap.Time("timestamp", time.Now()),
|
|
)
|
|
|
|
actorRole := string(c.Locals("role").(domain.Role))
|
|
ip := c.IP()
|
|
ua := c.Get("User-Agent")
|
|
meta, _ := json.Marshal(map[string]interface{}{"member_id": memberID, "new_status": req.Status})
|
|
go h.activityLogSvc.RecordAction(context.Background(), &updaterID, &actorRole, domain.ActionTeamMemberUpdated, domain.ResourceTeamMember, &memberID, fmt.Sprintf("Updated team member status ID: %d to %s", memberID, req.Status), meta, &ip, &ua)
|
|
|
|
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
|
Message: "Team member status updated successfully",
|
|
Success: true,
|
|
StatusCode: fiber.StatusOK,
|
|
})
|
|
}
|
|
|
|
// DeleteTeamMember godoc
|
|
// @Summary Delete team member
|
|
// @Description Delete a team member (super admin only)
|
|
// @Tags team
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param id path int true "Team Member ID"
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 401 {object} domain.ErrorResponse
|
|
// @Failure 403 {object} domain.ErrorResponse
|
|
// @Failure 404 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/team/members/{id} [delete]
|
|
func (h *Handler) DeleteTeamMember(c *fiber.Ctx) error {
|
|
memberIDStr := c.Params("id")
|
|
memberID, err := strconv.ParseInt(memberIDStr, 10, 64)
|
|
if err != nil || memberID <= 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid member ID",
|
|
Error: "Member ID must be a valid positive integer",
|
|
})
|
|
}
|
|
|
|
if err := h.teamSvc.DeleteTeamMember(c.Context(), memberID); err != nil {
|
|
if errors.Is(err, domain.ErrTeamMemberNotFound) {
|
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
Message: "Team member not found",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
h.mongoLoggerSvc.Error("Failed to delete team member",
|
|
zap.Int64("member_id", memberID),
|
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
|
zap.Error(err),
|
|
zap.Time("timestamp", time.Now()),
|
|
)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to delete team member",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
h.mongoLoggerSvc.Info("Team member deleted successfully",
|
|
zap.Int64("member_id", memberID),
|
|
zap.Time("timestamp", time.Now()),
|
|
)
|
|
|
|
actorID := c.Locals("user_id").(int64)
|
|
actorRole := string(c.Locals("role").(domain.Role))
|
|
ip := c.IP()
|
|
ua := c.Get("User-Agent")
|
|
meta, _ := json.Marshal(map[string]interface{}{"member_id": memberID})
|
|
go h.activityLogSvc.RecordAction(context.Background(), &actorID, &actorRole, domain.ActionTeamMemberDeleted, domain.ResourceTeamMember, &memberID, fmt.Sprintf("Deleted team member ID: %d", memberID), meta, &ip, &ua)
|
|
|
|
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
|
Message: "Team member deleted successfully",
|
|
Success: true,
|
|
StatusCode: fiber.StatusOK,
|
|
})
|
|
}
|
|
|
|
// GetTeamMemberStats godoc
|
|
// @Summary Get team member statistics
|
|
// @Description Get statistics about team members by status
|
|
// @Tags team
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Success 200 {object} domain.Response{data=domain.TeamMemberStats}
|
|
// @Failure 401 {object} domain.ErrorResponse
|
|
// @Failure 403 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/team/stats [get]
|
|
func (h *Handler) GetTeamMemberStats(c *fiber.Ctx) error {
|
|
stats, err := h.teamSvc.GetTeamMemberStats(c.Context())
|
|
if err != nil {
|
|
h.mongoLoggerSvc.Error("Failed to get team member stats",
|
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
|
zap.Error(err),
|
|
zap.Time("timestamp", time.Now()),
|
|
)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to get team member stats",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
|
Message: "Team member stats retrieved successfully",
|
|
Data: stats,
|
|
Success: true,
|
|
StatusCode: fiber.StatusOK,
|
|
})
|
|
}
|
|
|
|
// ChangeTeamMemberPassword godoc
|
|
// @Summary Change team member password
|
|
// @Description Change a team member's password (requires current password)
|
|
// @Tags team
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Param id path int true "Team Member ID"
|
|
// @Param body body changePasswordReq true "Password change payload"
|
|
// @Success 200 {object} domain.Response
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 401 {object} domain.ErrorResponse
|
|
// @Failure 404 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/team/members/{id}/change-password [post]
|
|
func (h *Handler) ChangeTeamMemberPassword(c *fiber.Ctx) error {
|
|
memberIDStr := c.Params("id")
|
|
memberID, err := strconv.ParseInt(memberIDStr, 10, 64)
|
|
if err != nil || memberID <= 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid member ID",
|
|
Error: "Member ID must be a valid positive integer",
|
|
})
|
|
}
|
|
|
|
var req changePasswordReq
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Failed to change password",
|
|
Error: "Invalid request body: " + err.Error(),
|
|
})
|
|
}
|
|
|
|
if valErrs, ok := h.validator.Validate(c, req); !ok {
|
|
var errMsg string
|
|
for field, msg := range valErrs {
|
|
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
|
|
}
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Failed to change password",
|
|
Error: errMsg,
|
|
})
|
|
}
|
|
|
|
if err := h.teamSvc.ChangePassword(c.Context(), memberID, req.CurrentPassword, req.NewPassword); err != nil {
|
|
switch {
|
|
case errors.Is(err, domain.ErrTeamMemberNotFound):
|
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
Message: "Team member not found",
|
|
Error: err.Error(),
|
|
})
|
|
default:
|
|
h.mongoLoggerSvc.Error("Failed to change team member password",
|
|
zap.Int64("member_id", memberID),
|
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
|
zap.Error(err),
|
|
zap.Time("timestamp", time.Now()),
|
|
)
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Failed to change password",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
}
|
|
|
|
h.mongoLoggerSvc.Info("Team member password changed successfully",
|
|
zap.Int64("member_id", memberID),
|
|
zap.Time("timestamp", time.Now()),
|
|
)
|
|
|
|
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
|
Message: "Password changed successfully",
|
|
Success: true,
|
|
StatusCode: fiber.StatusOK,
|
|
})
|
|
}
|
|
|
|
// GetMyTeamProfile godoc
|
|
// @Summary Get my team profile
|
|
// @Description Get the authenticated team member's own profile
|
|
// @Tags team
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security Bearer
|
|
// @Success 200 {object} domain.Response{data=domain.TeamMemberResponse}
|
|
// @Failure 400 {object} domain.ErrorResponse
|
|
// @Failure 401 {object} domain.ErrorResponse
|
|
// @Failure 404 {object} domain.ErrorResponse
|
|
// @Failure 500 {object} domain.ErrorResponse
|
|
// @Router /api/v1/team/me [get]
|
|
func (h *Handler) GetMyTeamProfile(c *fiber.Ctx) error {
|
|
memberID, ok := c.Locals("user_id").(int64)
|
|
if !ok {
|
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
|
Message: "Invalid user context",
|
|
Error: "User ID not found in request context",
|
|
})
|
|
}
|
|
|
|
member, err := h.teamSvc.GetTeamMemberByID(c.Context(), memberID)
|
|
if err != nil {
|
|
if errors.Is(err, domain.ErrTeamMemberNotFound) {
|
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
|
Message: "Team member not found",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
h.mongoLoggerSvc.Error("Failed to get team member profile",
|
|
zap.Int64("member_id", memberID),
|
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
|
zap.Error(err),
|
|
zap.Time("timestamp", time.Now()),
|
|
)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
|
Message: "Failed to get profile",
|
|
Error: err.Error(),
|
|
})
|
|
}
|
|
|
|
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
|
Message: "Profile retrieved successfully",
|
|
Data: toTeamMemberResponse(&member),
|
|
Success: true,
|
|
StatusCode: fiber.StatusOK,
|
|
})
|
|
}
|
|
|
|
func toTeamMemberResponse(m *domain.TeamMember) domain.TeamMemberResponse {
|
|
resp := domain.TeamMemberResponse{
|
|
ID: m.ID,
|
|
FirstName: m.FirstName,
|
|
LastName: m.LastName,
|
|
Email: m.Email,
|
|
PhoneNumber: m.PhoneNumber,
|
|
TeamRole: m.TeamRole,
|
|
Department: m.Department,
|
|
JobTitle: m.JobTitle,
|
|
EmploymentType: m.EmploymentType,
|
|
ProfilePictureURL: m.ProfilePictureURL,
|
|
Bio: m.Bio,
|
|
WorkPhone: m.WorkPhone,
|
|
Status: m.Status,
|
|
EmailVerified: m.EmailVerified,
|
|
Permissions: m.Permissions,
|
|
LastLogin: m.LastLogin,
|
|
CreatedAt: m.CreatedAt,
|
|
UpdatedAt: m.UpdatedAt,
|
|
}
|
|
|
|
if m.HireDate != nil {
|
|
resp.HireDate = m.HireDate.Format("2006-01-02")
|
|
}
|
|
|
|
return resp
|
|
}
|