feat: admin view approval needed
This commit is contained in:
parent
992b75e32b
commit
094a147489
|
@ -0,0 +1,144 @@
|
|||
package admin
|
||||
|
||||
import "time"
|
||||
|
||||
// Request DTOs
|
||||
type GetPendingUsersRequest struct {
|
||||
Role string `query:"role" validate:"omitempty,oneof=pengelola pengepul"`
|
||||
Status string `query:"status" validate:"omitempty,oneof=awaiting_approval pending"`
|
||||
Page int `query:"page" validate:"min=1"`
|
||||
Limit int `query:"limit" validate:"min=1,max=100"`
|
||||
}
|
||||
|
||||
type ApprovalActionRequest struct {
|
||||
UserID string `json:"user_id" validate:"required,uuid"`
|
||||
Action string `json:"action" validate:"required,oneof=approve reject"`
|
||||
Notes string `json:"notes" validate:"omitempty,max=500"`
|
||||
}
|
||||
|
||||
type BulkApprovalRequest struct {
|
||||
UserIDs []string `json:"user_ids" validate:"required,min=1,max=50,dive,uuid"`
|
||||
Action string `json:"action" validate:"required,oneof=approve reject"`
|
||||
Notes string `json:"notes" validate:"omitempty,max=500"`
|
||||
}
|
||||
|
||||
// Response DTOs
|
||||
type PendingUserResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Phone string `json:"phone"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Role RoleInfo `json:"role"`
|
||||
RegistrationStatus string `json:"registration_status"`
|
||||
RegistrationProgress int8 `json:"registration_progress"`
|
||||
SubmittedAt time.Time `json:"submitted_at"`
|
||||
IdentityCard *IdentityCardInfo `json:"identity_card,omitempty"`
|
||||
CompanyProfile *CompanyProfileInfo `json:"company_profile,omitempty"`
|
||||
RegistrationStepInfo *RegistrationStepResponse `json:"step_info"`
|
||||
}
|
||||
|
||||
type RoleInfo struct {
|
||||
ID string `json:"id"`
|
||||
RoleName string `json:"role_name"`
|
||||
}
|
||||
|
||||
type IdentityCardInfo struct {
|
||||
ID string `json:"id"`
|
||||
IdentificationNumber string `json:"identification_number"`
|
||||
Fullname string `json:"fullname"`
|
||||
Placeofbirth string `json:"place_of_birth"`
|
||||
Dateofbirth string `json:"date_of_birth"`
|
||||
Gender string `json:"gender"`
|
||||
BloodType string `json:"blood_type"`
|
||||
Province string `json:"province"`
|
||||
District string `json:"district"`
|
||||
SubDistrict string `json:"sub_district"`
|
||||
Village string `json:"village"`
|
||||
PostalCode string `json:"postal_code"`
|
||||
Religion string `json:"religion"`
|
||||
Maritalstatus string `json:"marital_status"`
|
||||
Job string `json:"job"`
|
||||
Citizenship string `json:"citizenship"`
|
||||
Validuntil string `json:"valid_until"`
|
||||
Cardphoto string `json:"card_photo"`
|
||||
}
|
||||
|
||||
type CompanyProfileInfo struct {
|
||||
ID string `json:"id"`
|
||||
CompanyName string `json:"company_name"`
|
||||
CompanyAddress string `json:"company_address"`
|
||||
CompanyPhone string `json:"company_phone"`
|
||||
CompanyEmail string `json:"company_email"`
|
||||
CompanyLogo string `json:"company_logo"`
|
||||
CompanyWebsite string `json:"company_website"`
|
||||
TaxID string `json:"tax_id"`
|
||||
FoundedDate string `json:"founded_date"`
|
||||
CompanyType string `json:"company_type"`
|
||||
CompanyDescription string `json:"company_description"`
|
||||
}
|
||||
|
||||
type RegistrationStepResponse struct {
|
||||
Step int `json:"step"`
|
||||
Status string `json:"status"`
|
||||
Description string `json:"description"`
|
||||
RequiresAdminApproval bool `json:"requires_admin_approval"`
|
||||
IsAccessible bool `json:"is_accessible"`
|
||||
IsCompleted bool `json:"is_completed"`
|
||||
}
|
||||
|
||||
type PendingUsersListResponse struct {
|
||||
Users []PendingUserResponse `json:"users"`
|
||||
Pagination PaginationInfo `json:"pagination"`
|
||||
Summary ApprovalSummary `json:"summary"`
|
||||
}
|
||||
|
||||
type PaginationInfo struct {
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
TotalRecords int64 `json:"total_records"`
|
||||
HasNext bool `json:"has_next"`
|
||||
HasPrev bool `json:"has_prev"`
|
||||
}
|
||||
|
||||
type ApprovalSummary struct {
|
||||
TotalPending int64 `json:"total_pending"`
|
||||
PengelolaPending int64 `json:"pengelola_pending"`
|
||||
PengepulPending int64 `json:"pengepul_pending"`
|
||||
}
|
||||
|
||||
type ApprovalActionResponse struct {
|
||||
UserID string `json:"user_id"`
|
||||
Action string `json:"action"`
|
||||
PreviousStatus string `json:"previous_status"`
|
||||
NewStatus string `json:"new_status"`
|
||||
ProcessedAt time.Time `json:"processed_at"`
|
||||
ProcessedBy string `json:"processed_by"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
}
|
||||
|
||||
type BulkApprovalResponse struct {
|
||||
SuccessCount int `json:"success_count"`
|
||||
FailureCount int `json:"failure_count"`
|
||||
Results []ApprovalActionResponse `json:"results"`
|
||||
Failures []ApprovalFailure `json:"failures,omitempty"`
|
||||
}
|
||||
|
||||
type ApprovalFailure struct {
|
||||
UserID string `json:"user_id"`
|
||||
Error string `json:"error"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
// Validation helper
|
||||
func (r *GetPendingUsersRequest) SetDefaults() {
|
||||
if r.Page <= 0 {
|
||||
r.Page = 1
|
||||
}
|
||||
if r.Limit <= 0 {
|
||||
r.Limit = 20
|
||||
}
|
||||
if r.Status == "" {
|
||||
r.Status = "awaiting_approval"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,293 @@
|
|||
// internal/admin/approval_handler.go
|
||||
package admin
|
||||
|
||||
import (
|
||||
"rijig/middleware"
|
||||
"rijig/utils"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type ApprovalHandler struct {
|
||||
service ApprovalService
|
||||
}
|
||||
|
||||
func NewApprovalHandler(service ApprovalService) *ApprovalHandler {
|
||||
return &ApprovalHandler{
|
||||
service: service,
|
||||
}
|
||||
}
|
||||
|
||||
// GetPendingUsers menampilkan daftar pengguna yang menunggu persetujuan
|
||||
// @Summary Get pending users for approval
|
||||
// @Description Retrieve list of users (pengelola/pengepul) waiting for admin approval with filtering and pagination
|
||||
// @Tags Admin - User Approval
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param role query string false "Filter by role" Enums(pengelola, pengepul)
|
||||
// @Param status query string false "Filter by status" Enums(awaiting_approval, pending) default(awaiting_approval)
|
||||
// @Param page query int false "Page number" default(1) minimum(1)
|
||||
// @Param limit query int false "Items per page" default(20) minimum(1) maximum(100)
|
||||
// @Success 200 {object} utils.Response{data=PendingUsersListResponse} "List of pending users"
|
||||
// @Failure 400 {object} utils.Response "Bad request"
|
||||
// @Failure 401 {object} utils.Response "Unauthorized"
|
||||
// @Failure 403 {object} utils.Response "Forbidden - Admin role required"
|
||||
// @Failure 500 {object} utils.Response "Internal server error"
|
||||
// @Security Bearer
|
||||
// @Router /admin/users/pending [get]
|
||||
func (h *ApprovalHandler) GetPendingUsers(c *fiber.Ctx) error {
|
||||
// Parse query parameters
|
||||
var req GetPendingUsersRequest
|
||||
if err := c.QueryParser(&req); err != nil {
|
||||
return utils.BadRequest(c, "Invalid query parameters: "+err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
// if err := h.validator.Struct(&req); err != nil {
|
||||
// return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", err.Error())
|
||||
// }
|
||||
|
||||
// Call service
|
||||
result, err := h.service.GetPendingUsers(c.Context(), &req)
|
||||
if err != nil {
|
||||
return utils.InternalServerError(c, "Failed to get pending users: "+err.Error())
|
||||
}
|
||||
|
||||
return utils.SuccessWithData(c, "Pending users retrieved successfully", result)
|
||||
}
|
||||
|
||||
// GetUserApprovalDetails menampilkan detail lengkap pengguna untuk approval
|
||||
// @Summary Get user approval details
|
||||
// @Description Get detailed information of a specific user for approval decision
|
||||
// @Tags Admin - User Approval
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param user_id path string true "User ID" format(uuid)
|
||||
// @Success 200 {object} utils.Response{data=PendingUserResponse} "User approval details"
|
||||
// @Failure 400 {object} utils.Response "Bad request"
|
||||
// @Failure 401 {object} utils.Response "Unauthorized"
|
||||
// @Failure 403 {object} utils.Response "Forbidden - Admin role required"
|
||||
// @Failure 404 {object} utils.Response "User not found"
|
||||
// @Failure 500 {object} utils.Response "Internal server error"
|
||||
// @Security Bearer
|
||||
// @Router /admin/users/{user_id}/approval-details [get]
|
||||
func (h *ApprovalHandler) GetUserApprovalDetails(c *fiber.Ctx) error {
|
||||
userID := c.Params("user_id")
|
||||
if userID == "" {
|
||||
return utils.BadRequest(c, "User ID is required")
|
||||
}
|
||||
|
||||
// Validate UUID format
|
||||
// if err := h.validator.Var(userID, "uuid"); err != nil {
|
||||
// return utils.BadRequest(c, "Invalid user ID format")
|
||||
// }
|
||||
|
||||
// Call service
|
||||
result, err := h.service.GetUserApprovalDetails(c.Context(), userID)
|
||||
if err != nil {
|
||||
if err.Error() == "user not found" {
|
||||
return utils.NotFound(c, "User not found")
|
||||
}
|
||||
return utils.InternalServerError(c, "Failed to get user details: "+err.Error())
|
||||
}
|
||||
|
||||
return utils.SuccessWithData(c, "User approval details retrieved successfully", result)
|
||||
}
|
||||
|
||||
// ProcessApprovalAction memproses aksi approval (approve/reject) untuk satu user
|
||||
// @Summary Process approval action
|
||||
// @Description Approve or reject a user registration
|
||||
// @Tags Admin - User Approval
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body ApprovalActionRequest true "Approval action request"
|
||||
// @Success 200 {object} utils.Response{data=ApprovalActionResponse} "Approval processed successfully"
|
||||
// @Failure 400 {object} utils.Response "Bad request"
|
||||
// @Failure 401 {object} utils.Response "Unauthorized"
|
||||
// @Failure 403 {object} utils.Response "Forbidden - Admin role required"
|
||||
// @Failure 404 {object} utils.Response "User not found"
|
||||
// @Failure 500 {object} utils.Response "Internal server error"
|
||||
// @Security Bearer
|
||||
// @Router /admin/users/approval-action [post]
|
||||
func (h *ApprovalHandler) ProcessApprovalAction(c *fiber.Ctx) error {
|
||||
// Get admin ID from context
|
||||
adminClaims, err := middleware.GetUserFromContext(c)
|
||||
if err != nil {
|
||||
return utils.Unauthorized(c, "Admin authentication required")
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
var req ApprovalActionRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return utils.BadRequest(c, "Invalid request body: "+err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
// if err := h.validator.Struct(&req); err != nil {
|
||||
// return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", err.Error())
|
||||
// }
|
||||
|
||||
// Call service
|
||||
result, err := h.service.ProcessApprovalAction(c.Context(), &req, adminClaims.UserID)
|
||||
if err != nil {
|
||||
if err.Error() == "user not found" {
|
||||
return utils.NotFound(c, "User not found")
|
||||
}
|
||||
return utils.InternalServerError(c, "Failed to process approval: "+err.Error())
|
||||
}
|
||||
|
||||
actionMessage := "User approved successfully"
|
||||
if req.Action == "reject" {
|
||||
actionMessage = "User rejected successfully"
|
||||
}
|
||||
|
||||
return utils.SuccessWithData(c, actionMessage, result)
|
||||
}
|
||||
|
||||
// BulkProcessApproval memproses aksi approval untuk multiple users sekaligus
|
||||
// @Summary Bulk process approval actions
|
||||
// @Description Approve or reject multiple users at once
|
||||
// @Tags Admin - User Approval
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body BulkApprovalRequest true "Bulk approval request"
|
||||
// @Success 200 {object} utils.Response{data=BulkApprovalResponse} "Bulk approval processed"
|
||||
// @Failure 400 {object} utils.Response "Bad request"
|
||||
// @Failure 401 {object} utils.Response "Unauthorized"
|
||||
// @Failure 403 {object} utils.Response "Forbidden - Admin role required"
|
||||
// @Failure 500 {object} utils.Response "Internal server error"
|
||||
// @Security Bearer
|
||||
// @Router /admin/users/bulk-approval [post]
|
||||
func (h *ApprovalHandler) BulkProcessApproval(c *fiber.Ctx) error {
|
||||
// Get admin ID from context
|
||||
adminClaims, err := middleware.GetUserFromContext(c)
|
||||
if err != nil {
|
||||
return utils.Unauthorized(c, "Admin authentication required")
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
var req BulkApprovalRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return utils.BadRequest(c, "Invalid request body: "+err.Error())
|
||||
}
|
||||
|
||||
// Validate request
|
||||
// if err := h.validator.Struct(&req); err != nil {
|
||||
// return utils.ResponseErrorData(c, fiber.StatusBadRequest, "Validation failed", err.Error())
|
||||
// }
|
||||
|
||||
// Call service
|
||||
result, err := h.service.BulkProcessApproval(c.Context(), &req, adminClaims.UserID)
|
||||
if err != nil {
|
||||
return utils.InternalServerError(c, "Failed to process bulk approval: "+err.Error())
|
||||
}
|
||||
|
||||
actionMessage := "Bulk approval processed successfully"
|
||||
if req.Action == "reject" {
|
||||
actionMessage = "Bulk rejection processed successfully"
|
||||
}
|
||||
|
||||
return utils.SuccessWithData(c, actionMessage, result)
|
||||
}
|
||||
|
||||
// ApproveUser endpoint khusus untuk approve satu user (shortcut)
|
||||
// @Summary Approve user
|
||||
// @Description Approve a user registration (shortcut endpoint)
|
||||
// @Tags Admin - User Approval
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param user_id path string true "User ID" format(uuid)
|
||||
// @Param notes body string false "Optional approval notes"
|
||||
// @Success 200 {object} utils.Response{data=ApprovalActionResponse} "User approved successfully"
|
||||
// @Failure 400 {object} utils.Response "Bad request"
|
||||
// @Failure 401 {object} utils.Response "Unauthorized"
|
||||
// @Failure 403 {object} utils.Response "Forbidden - Admin role required"
|
||||
// @Failure 404 {object} utils.Response "User not found"
|
||||
// @Failure 500 {object} utils.Response "Internal server error"
|
||||
// @Security Bearer
|
||||
// @Router /admin/users/{user_id}/approve [post]
|
||||
func (h *ApprovalHandler) ApproveUser(c *fiber.Ctx) error {
|
||||
userID := c.Params("user_id")
|
||||
if userID == "" {
|
||||
return utils.BadRequest(c, "User ID is required")
|
||||
}
|
||||
|
||||
// Validate UUID format
|
||||
// if err := h.validator.Var(userID, "uuid"); err != nil {
|
||||
// return utils.BadRequest(c, "Invalid user ID format")
|
||||
// }
|
||||
|
||||
// Get admin ID from context
|
||||
adminClaims, err := middleware.GetUserFromContext(c)
|
||||
if err != nil {
|
||||
return utils.Unauthorized(c, "Admin authentication required")
|
||||
}
|
||||
|
||||
// Parse optional notes from body
|
||||
var body struct {
|
||||
Notes string `json:"notes"`
|
||||
}
|
||||
c.BodyParser(&body) // Ignore error as notes are optional
|
||||
|
||||
// Call service
|
||||
result, err := h.service.ApproveUser(c.Context(), userID, adminClaims.UserID, body.Notes)
|
||||
if err != nil {
|
||||
if err.Error() == "user not found" {
|
||||
return utils.NotFound(c, "User not found")
|
||||
}
|
||||
return utils.InternalServerError(c, "Failed to approve user: "+err.Error())
|
||||
}
|
||||
|
||||
return utils.SuccessWithData(c, "User approved successfully", result)
|
||||
}
|
||||
|
||||
// RejectUser endpoint khusus untuk reject satu user (shortcut)
|
||||
// @Summary Reject user
|
||||
// @Description Reject a user registration (shortcut endpoint)
|
||||
// @Tags Admin - User Approval
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param user_id path string true "User ID" format(uuid)
|
||||
// @Param notes body string false "Optional rejection notes"
|
||||
// @Success 200 {object} utils.Response{data=ApprovalActionResponse} "User rejected successfully"
|
||||
// @Failure 400 {object} utils.Response "Bad request"
|
||||
// @Failure 401 {object} utils.Response "Unauthorized"
|
||||
// @Failure 403 {object} utils.Response "Forbidden - Admin role required"
|
||||
// @Failure 404 {object} utils.Response "User not found"
|
||||
// @Failure 500 {object} utils.Response "Internal server error"
|
||||
// @Security Bearer
|
||||
// @Router /admin/users/{user_id}/reject [post]
|
||||
func (h *ApprovalHandler) RejectUser(c *fiber.Ctx) error {
|
||||
userID := c.Params("user_id")
|
||||
if userID == "" {
|
||||
return utils.BadRequest(c, "User ID is required")
|
||||
}
|
||||
|
||||
// Validate UUID format
|
||||
// if err := h.validator.Var(userID, "uuid"); err != nil {
|
||||
// return utils.BadRequest(c, "Invalid user ID format")
|
||||
// }
|
||||
|
||||
// Get admin ID from context
|
||||
adminClaims, err := middleware.GetUserFromContext(c)
|
||||
if err != nil {
|
||||
return utils.Unauthorized(c, "Admin authentication required")
|
||||
}
|
||||
|
||||
// Parse optional notes from body
|
||||
var body struct {
|
||||
Notes string `json:"notes"`
|
||||
}
|
||||
c.BodyParser(&body) // Ignore error as notes are optional
|
||||
|
||||
// Call service
|
||||
result, err := h.service.RejectUser(c.Context(), userID, adminClaims.UserID, body.Notes)
|
||||
if err != nil {
|
||||
if err.Error() == "user not found" {
|
||||
return utils.NotFound(c, "User not found")
|
||||
}
|
||||
return utils.InternalServerError(c, "Failed to reject user: "+err.Error())
|
||||
}
|
||||
|
||||
return utils.SuccessWithData(c, "User rejected successfully", result)
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"rijig/model"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type ApprovalRepository interface {
|
||||
GetPendingUsers(ctx context.Context, req *GetPendingUsersRequest) ([]model.User, int64, error)
|
||||
GetUserByID(ctx context.Context, userID string) (*model.User, error)
|
||||
UpdateUserRegistrationStatus(ctx context.Context, userID, status string, progress int8) error
|
||||
GetApprovalSummary(ctx context.Context) (*ApprovalSummary, error)
|
||||
GetUsersByIDs(ctx context.Context, userIDs []string) ([]model.User, error)
|
||||
BulkUpdateRegistrationStatus(ctx context.Context, userIDs []string, status string, progress int8) error
|
||||
}
|
||||
|
||||
type approvalRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewApprovalRepository(db *gorm.DB) ApprovalRepository {
|
||||
return &approvalRepository{
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *approvalRepository) GetPendingUsers(ctx context.Context, req *GetPendingUsersRequest) ([]model.User, int64, error) {
|
||||
var users []model.User
|
||||
var totalRecords int64
|
||||
|
||||
query := r.db.WithContext(ctx).Model(&model.User{}).
|
||||
Preload("Role").
|
||||
Preload("IdentityCard").
|
||||
Preload("CompanyProfile")
|
||||
|
||||
query = r.applyFilters(query, req)
|
||||
|
||||
if err := query.Count(&totalRecords).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to count pending users: %w", err)
|
||||
}
|
||||
|
||||
offset := (req.Page - 1) * req.Limit
|
||||
if err := query.
|
||||
Order("created_at DESC").
|
||||
Limit(req.Limit).
|
||||
Offset(offset).
|
||||
Find(&users).Error; err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to fetch pending users: %w", err)
|
||||
}
|
||||
|
||||
return users, totalRecords, nil
|
||||
}
|
||||
|
||||
func (r *approvalRepository) applyFilters(query *gorm.DB, req *GetPendingUsersRequest) *gorm.DB {
|
||||
|
||||
if req.Status != "" {
|
||||
|
||||
if req.Status == "pending" {
|
||||
req.Status = "awaiting_approval"
|
||||
}
|
||||
query = query.Where("registration_status = ?", req.Status)
|
||||
}
|
||||
|
||||
if req.Role != "" {
|
||||
|
||||
query = query.Joins("JOIN roles ON roles.id = users.role_id").
|
||||
Where("LOWER(roles.role_name) = ?", strings.ToLower(req.Role))
|
||||
}
|
||||
|
||||
query = query.Where("registration_progress >= ?", 2)
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
func (r *approvalRepository) GetUserByID(ctx context.Context, userID string) (*model.User, error) {
|
||||
var user model.User
|
||||
|
||||
if err := r.db.WithContext(ctx).
|
||||
Preload("Role").
|
||||
Preload("IdentityCard").
|
||||
Preload("CompanyProfile").
|
||||
Where("id = ?", userID).
|
||||
First(&user).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, fmt.Errorf("user not found")
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (r *approvalRepository) UpdateUserRegistrationStatus(ctx context.Context, userID, status string, progress int8) error {
|
||||
result := r.db.WithContext(ctx).
|
||||
Model(&model.User{}).
|
||||
Where("id = ?", userID).
|
||||
Updates(map[string]interface{}{
|
||||
"registration_status": status,
|
||||
"registration_progress": progress,
|
||||
"updated_at": "NOW()",
|
||||
})
|
||||
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("failed to update user registration status: %w", result.Error)
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *approvalRepository) GetApprovalSummary(ctx context.Context) (*ApprovalSummary, error) {
|
||||
var summary ApprovalSummary
|
||||
|
||||
if err := r.db.WithContext(ctx).
|
||||
Model(&model.User{}).
|
||||
Where("registration_status = ? AND registration_progress >= ?", "awaiting_approval", 2).
|
||||
Count(&summary.TotalPending).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to count total pending users: %w", err)
|
||||
}
|
||||
|
||||
if err := r.db.WithContext(ctx).
|
||||
Model(&model.User{}).
|
||||
Joins("JOIN roles ON roles.id = users.role_id").
|
||||
Where("users.registration_status = ? AND users.registration_progress >= ? AND LOWER(roles.role_name) = ?",
|
||||
"awaiting_approval", 2, "pengelola").
|
||||
Count(&summary.PengelolaPending).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to count pengelola pending: %w", err)
|
||||
}
|
||||
|
||||
if err := r.db.WithContext(ctx).
|
||||
Model(&model.User{}).
|
||||
Joins("JOIN roles ON roles.id = users.role_id").
|
||||
Where("users.registration_status = ? AND users.registration_progress >= ? AND LOWER(roles.role_name) = ?",
|
||||
"awaiting_approval", 2, "pengepul").
|
||||
Count(&summary.PengepulPending).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to count pengepul pending: %w", err)
|
||||
}
|
||||
|
||||
return &summary, nil
|
||||
}
|
||||
|
||||
func (r *approvalRepository) GetUsersByIDs(ctx context.Context, userIDs []string) ([]model.User, error) {
|
||||
var users []model.User
|
||||
|
||||
if err := r.db.WithContext(ctx).
|
||||
Preload("Role").
|
||||
Preload("IdentityCard").
|
||||
Preload("CompanyProfile").
|
||||
Where("id IN ?", userIDs).
|
||||
Find(&users).Error; err != nil {
|
||||
return nil, fmt.Errorf("failed to get users by IDs: %w", err)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (r *approvalRepository) BulkUpdateRegistrationStatus(ctx context.Context, userIDs []string, status string, progress int8) error {
|
||||
result := r.db.WithContext(ctx).
|
||||
Model(&model.User{}).
|
||||
Where("id IN ?", userIDs).
|
||||
Updates(map[string]interface{}{
|
||||
"registration_status": status,
|
||||
"registration_progress": progress,
|
||||
"updated_at": "NOW()",
|
||||
})
|
||||
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("failed to bulk update registration status: %w", result.Error)
|
||||
}
|
||||
|
||||
if result.RowsAffected == 0 {
|
||||
return fmt.Errorf("no users found to update")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"rijig/config"
|
||||
"rijig/middleware"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func ApprovalRoutes(api fiber.Router) {
|
||||
baseRepo := NewApprovalRepository(config.DB)
|
||||
baseService := NewApprovalService(baseRepo)
|
||||
baseHandler := NewApprovalHandler(baseService)
|
||||
|
||||
adminGroup := api.Group("/needapprove")
|
||||
adminGroup.Use(middleware.RequireAdminRole(), middleware.AuthMiddleware())
|
||||
|
||||
adminGroup.Get("/pending", baseHandler.GetPendingUsers)
|
||||
|
||||
adminGroup.Get("/:user_id/approval-details", baseHandler.GetUserApprovalDetails)
|
||||
adminGroup.Post("/approval-action", baseHandler.ProcessApprovalAction)
|
||||
adminGroup.Post("/bulk-approval", baseHandler.BulkProcessApproval)
|
||||
adminGroup.Post("/:user_id/approve", baseHandler.ApproveUser)
|
||||
adminGroup.Post("/:user_id/reject", baseHandler.RejectUser)
|
||||
}
|
|
@ -0,0 +1,320 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"rijig/model"
|
||||
"rijig/utils"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ApprovalService interface {
|
||||
GetPendingUsers(ctx context.Context, req *GetPendingUsersRequest) (*PendingUsersListResponse, error)
|
||||
ApproveUser(ctx context.Context, userID, adminID string, notes string) (*ApprovalActionResponse, error)
|
||||
RejectUser(ctx context.Context, userID, adminID string, notes string) (*ApprovalActionResponse, error)
|
||||
ProcessApprovalAction(ctx context.Context, req *ApprovalActionRequest, adminID string) (*ApprovalActionResponse, error)
|
||||
BulkProcessApproval(ctx context.Context, req *BulkApprovalRequest, adminID string) (*BulkApprovalResponse, error)
|
||||
GetUserApprovalDetails(ctx context.Context, userID string) (*PendingUserResponse, error)
|
||||
}
|
||||
|
||||
type approvalService struct {
|
||||
repo ApprovalRepository
|
||||
}
|
||||
|
||||
func NewApprovalService(repo ApprovalRepository) ApprovalService {
|
||||
return &approvalService{
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *approvalService) GetPendingUsers(ctx context.Context, req *GetPendingUsersRequest) (*PendingUsersListResponse, error) {
|
||||
|
||||
req.SetDefaults()
|
||||
|
||||
users, totalRecords, err := s.repo.GetPendingUsers(ctx, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get pending users: %w", err)
|
||||
}
|
||||
|
||||
summary, err := s.repo.GetApprovalSummary(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get approval summary: %w", err)
|
||||
}
|
||||
|
||||
totalPages := int(math.Ceil(float64(totalRecords) / float64(req.Limit)))
|
||||
pagination := PaginationInfo{
|
||||
Page: req.Page,
|
||||
Limit: req.Limit,
|
||||
TotalPages: totalPages,
|
||||
TotalRecords: totalRecords,
|
||||
HasNext: req.Page < totalPages,
|
||||
HasPrev: req.Page > 1,
|
||||
}
|
||||
|
||||
userResponses := make([]PendingUserResponse, len(users))
|
||||
for i, user := range users {
|
||||
userResponses[i] = s.convertToUserResponse(user)
|
||||
}
|
||||
|
||||
return &PendingUsersListResponse{
|
||||
Users: userResponses,
|
||||
Pagination: pagination,
|
||||
Summary: *summary,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *approvalService) ApproveUser(ctx context.Context, userID, adminID string, notes string) (*ApprovalActionResponse, error) {
|
||||
|
||||
user, err := s.repo.GetUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
|
||||
if err := s.validateUserForApproval(user, "approved"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
previousStatus := user.RegistrationStatus
|
||||
|
||||
newStatus := utils.RegStatusConfirmed
|
||||
newProgress := utils.ProgressDataSubmitted
|
||||
|
||||
if err := s.repo.UpdateUserRegistrationStatus(ctx, userID, newStatus, int8(newProgress)); err != nil {
|
||||
return nil, fmt.Errorf("failed to approved user: %w", err)
|
||||
}
|
||||
|
||||
if err := s.revokeUserTokens(userID); err != nil {
|
||||
|
||||
fmt.Printf("Warning: failed to revoke tokens for user %s: %v\n", userID, err)
|
||||
}
|
||||
|
||||
return &ApprovalActionResponse{
|
||||
UserID: userID,
|
||||
Action: "approved",
|
||||
PreviousStatus: previousStatus,
|
||||
NewStatus: newStatus,
|
||||
ProcessedAt: time.Now(),
|
||||
ProcessedBy: adminID,
|
||||
Notes: notes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *approvalService) RejectUser(ctx context.Context, userID, adminID string, notes string) (*ApprovalActionResponse, error) {
|
||||
|
||||
user, err := s.repo.GetUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
|
||||
if err := s.validateUserForApproval(user, "rejected"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
previousStatus := user.RegistrationStatus
|
||||
|
||||
newStatus := utils.RegStatusRejected
|
||||
newProgress := utils.ProgressOTPVerified
|
||||
|
||||
if err := s.repo.UpdateUserRegistrationStatus(ctx, userID, newStatus, int8(newProgress)); err != nil {
|
||||
return nil, fmt.Errorf("failed to rejected user: %w", err)
|
||||
}
|
||||
|
||||
if err := s.revokeUserTokens(userID); err != nil {
|
||||
|
||||
fmt.Printf("Warning: failed to revoke tokens for user %s: %v\n", userID, err)
|
||||
}
|
||||
|
||||
return &ApprovalActionResponse{
|
||||
UserID: userID,
|
||||
Action: "rejected",
|
||||
PreviousStatus: previousStatus,
|
||||
NewStatus: newStatus,
|
||||
ProcessedAt: time.Now(),
|
||||
ProcessedBy: adminID,
|
||||
Notes: notes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *approvalService) ProcessApprovalAction(ctx context.Context, req *ApprovalActionRequest, adminID string) (*ApprovalActionResponse, error) {
|
||||
switch req.Action {
|
||||
case "approved":
|
||||
return s.ApproveUser(ctx, req.UserID, adminID, req.Notes)
|
||||
case "rejected":
|
||||
return s.RejectUser(ctx, req.UserID, adminID, req.Notes)
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid action: %s", req.Action)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *approvalService) BulkProcessApproval(ctx context.Context, req *BulkApprovalRequest, adminID string) (*BulkApprovalResponse, error) {
|
||||
|
||||
users, err := s.repo.GetUsersByIDs(ctx, req.UserIDs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get users: %w", err)
|
||||
}
|
||||
|
||||
var results []ApprovalActionResponse
|
||||
var failures []ApprovalFailure
|
||||
successCount := 0
|
||||
|
||||
for _, user := range users {
|
||||
|
||||
if err := s.validateUserForApproval(&user, req.Action); err != nil {
|
||||
failures = append(failures, ApprovalFailure{
|
||||
UserID: user.ID,
|
||||
Error: "validation_failed",
|
||||
Reason: err.Error(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
actionReq := &ApprovalActionRequest{
|
||||
UserID: user.ID,
|
||||
Action: req.Action,
|
||||
Notes: req.Notes,
|
||||
}
|
||||
|
||||
result, err := s.ProcessApprovalAction(ctx, actionReq, adminID)
|
||||
if err != nil {
|
||||
failures = append(failures, ApprovalFailure{
|
||||
UserID: user.ID,
|
||||
Error: "processing_failed",
|
||||
Reason: err.Error(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
results = append(results, *result)
|
||||
successCount++
|
||||
}
|
||||
|
||||
return &BulkApprovalResponse{
|
||||
SuccessCount: successCount,
|
||||
FailureCount: len(failures),
|
||||
Results: results,
|
||||
Failures: failures,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *approvalService) GetUserApprovalDetails(ctx context.Context, userID string) (*PendingUserResponse, error) {
|
||||
user, err := s.repo.GetUserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user details: %w", err)
|
||||
}
|
||||
|
||||
userResponse := s.convertToUserResponse(*user)
|
||||
return &userResponse, nil
|
||||
}
|
||||
|
||||
func (s *approvalService) validateUserForApproval(user *model.User, action string) error {
|
||||
|
||||
if user.Role == nil {
|
||||
return fmt.Errorf("user role not found")
|
||||
}
|
||||
|
||||
roleName := strings.ToLower(user.Role.RoleName)
|
||||
if roleName != "pengelola" && roleName != "pengepul" {
|
||||
return fmt.Errorf("only pengelola and pengepul can be approved/rejected")
|
||||
}
|
||||
|
||||
if user.RegistrationStatus != utils.RegStatusPending {
|
||||
return fmt.Errorf("user is not in awaiting_approval status")
|
||||
}
|
||||
|
||||
if user.RegistrationProgress < utils.ProgressDataSubmitted {
|
||||
return fmt.Errorf("user has not submitted required data yet")
|
||||
}
|
||||
|
||||
if roleName == "pengepul" && user.IdentityCard == nil {
|
||||
return fmt.Errorf("pengepul must have identity card data")
|
||||
}
|
||||
|
||||
if roleName == "pengelola" && user.CompanyProfile == nil {
|
||||
return fmt.Errorf("pengelola must have company profile data")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *approvalService) revokeUserTokens(userID string) error {
|
||||
|
||||
return utils.RevokeAllRefreshTokens(userID)
|
||||
}
|
||||
|
||||
func (s *approvalService) convertToUserResponse(user model.User) PendingUserResponse {
|
||||
response := PendingUserResponse{
|
||||
ID: user.ID,
|
||||
Name: user.Name,
|
||||
Phone: user.Phone,
|
||||
Email: user.Email,
|
||||
RegistrationStatus: user.RegistrationStatus,
|
||||
RegistrationProgress: user.RegistrationProgress,
|
||||
SubmittedAt: user.UpdatedAt,
|
||||
}
|
||||
|
||||
if user.Role != nil {
|
||||
response.Role = RoleInfo{
|
||||
ID: user.Role.ID,
|
||||
RoleName: user.Role.RoleName,
|
||||
}
|
||||
}
|
||||
|
||||
stepInfo := utils.GetRegistrationStepInfo(
|
||||
user.Role.RoleName,
|
||||
int(user.RegistrationProgress),
|
||||
user.RegistrationStatus,
|
||||
)
|
||||
if stepInfo != nil {
|
||||
response.RegistrationStepInfo = &RegistrationStepResponse{
|
||||
Step: stepInfo.Step,
|
||||
Status: stepInfo.Status,
|
||||
Description: stepInfo.Description,
|
||||
RequiresAdminApproval: stepInfo.RequiresAdminApproval,
|
||||
IsAccessible: stepInfo.IsAccessible,
|
||||
IsCompleted: stepInfo.IsCompleted,
|
||||
}
|
||||
}
|
||||
|
||||
if user.IdentityCard != nil {
|
||||
response.IdentityCard = &IdentityCardInfo{
|
||||
ID: user.IdentityCard.ID,
|
||||
IdentificationNumber: user.IdentityCard.Identificationumber,
|
||||
Fullname: user.IdentityCard.Fullname,
|
||||
Placeofbirth: user.IdentityCard.Placeofbirth,
|
||||
Dateofbirth: user.IdentityCard.Dateofbirth,
|
||||
Gender: user.IdentityCard.Gender,
|
||||
BloodType: user.IdentityCard.BloodType,
|
||||
Province: user.IdentityCard.Province,
|
||||
District: user.IdentityCard.District,
|
||||
SubDistrict: user.IdentityCard.SubDistrict,
|
||||
Village: user.IdentityCard.Village,
|
||||
PostalCode: user.IdentityCard.PostalCode,
|
||||
Religion: user.IdentityCard.Religion,
|
||||
Maritalstatus: user.IdentityCard.Maritalstatus,
|
||||
Job: user.IdentityCard.Job,
|
||||
Citizenship: user.IdentityCard.Citizenship,
|
||||
Validuntil: user.IdentityCard.Validuntil,
|
||||
Cardphoto: user.IdentityCard.Cardphoto,
|
||||
}
|
||||
}
|
||||
|
||||
if user.CompanyProfile != nil {
|
||||
response.CompanyProfile = &CompanyProfileInfo{
|
||||
ID: user.CompanyProfile.ID,
|
||||
CompanyName: user.CompanyProfile.CompanyName,
|
||||
CompanyAddress: user.CompanyProfile.CompanyAddress,
|
||||
CompanyPhone: user.CompanyProfile.CompanyPhone,
|
||||
CompanyEmail: user.CompanyProfile.CompanyEmail,
|
||||
CompanyLogo: user.CompanyProfile.CompanyLogo,
|
||||
CompanyWebsite: user.CompanyProfile.CompanyWebsite,
|
||||
TaxID: user.CompanyProfile.TaxID,
|
||||
FoundedDate: user.CompanyProfile.FoundedDate,
|
||||
CompanyType: user.CompanyProfile.CompanyType,
|
||||
CompanyDescription: user.CompanyProfile.CompanyDescription,
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
|
@ -147,7 +147,7 @@ func (s *authenticationService) LoginAdmin(ctx context.Context, req *LoginAdminR
|
|||
return nil, fmt.Errorf("invalid credentials")
|
||||
}
|
||||
|
||||
if user.RegistrationStatus != "completed" {
|
||||
if user.RegistrationStatus != "complete" {
|
||||
return nil, fmt.Errorf("account not activated")
|
||||
}
|
||||
|
||||
|
@ -470,7 +470,7 @@ func (s *authenticationService) RegisterAdmin(ctx context.Context, req *Register
|
|||
Password: hashedPassword,
|
||||
RoleID: role.ID,
|
||||
RegistrationStatus: "pending_email_verification",
|
||||
RegistrationProgress: 1,
|
||||
// RegistrationProgress: 1,
|
||||
EmailVerified: false,
|
||||
}
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
package model
|
|
@ -105,8 +105,6 @@ func (s *userProfileService) UpdateRegistUserProfile(ctx context.Context, userID
|
|||
NextStep: nextStep,
|
||||
SessionID: tokenResponse.SessionID,
|
||||
}, nil
|
||||
|
||||
// return s.mapToResponseDTO(updatedUser), nil
|
||||
}
|
||||
|
||||
func (s *userProfileService) mapToResponseDTO(user *model.User) *UserProfileResponseDTO {
|
||||
|
|
|
@ -18,6 +18,8 @@ type User struct {
|
|||
Role *Role `gorm:"foreignKey:RoleID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"role"`
|
||||
RegistrationStatus string `json:"registrationstatus"`
|
||||
RegistrationProgress int8 `json:"registration_progress"`
|
||||
IdentityCard *IdentityCard `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"identity_card,omitempty"`
|
||||
CompanyProfile *CompanyProfile `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"company_profile,omitempty"`
|
||||
CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"`
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"os"
|
||||
|
||||
"rijig/internal/about"
|
||||
"rijig/internal/admin"
|
||||
"rijig/internal/article"
|
||||
"rijig/internal/authentication"
|
||||
"rijig/internal/company"
|
||||
|
@ -42,6 +43,7 @@ func SetupRoutes(app *fiber.App) {
|
|||
trash.TrashRouter(api)
|
||||
about.AboutRouter(api)
|
||||
whatsapp.WhatsAppRouter(api)
|
||||
admin.ApprovalRoutes(api)
|
||||
|
||||
// || auth router || //
|
||||
// presentation.AuthRouter(api)
|
||||
|
|
Loading…
Reference in New Issue