package handler import ( "database/sql" "errors" "strconv" "strings" "github.com/gin-gonic/gin" "github.com/shopspring/decimal" "github.com/wmms/wmms/internal/middleware" "github.com/wmms/wmms/internal/model" "github.com/wmms/wmms/internal/repository" "github.com/wmms/wmms/internal/service" "github.com/wmms/wmms/pkg/response" ) type Handler struct { materialSvc *service.MaterialService inboundSvc *service.InboundService outboundSvc *service.OutboundService stockSvc *service.StockService logSvc *service.MaterialLogService authSvc *service.AuthService appSvc *service.ApplicationService organizationSvc *service.OrganizationService masterDataSvc *service.MasterDataService partnerSvc *service.PartnerService warehouseSvc *service.WarehouseService } func NewHandler() *Handler { return &Handler{ materialSvc: service.NewMaterialService(), inboundSvc: service.NewInboundService(), outboundSvc: service.NewOutboundService(), stockSvc: service.NewStockService(), logSvc: service.NewMaterialLogService(), authSvc: service.NewAuthService(), appSvc: service.NewApplicationService(), organizationSvc: service.NewOrganizationService(), masterDataSvc: service.NewMasterDataService(), partnerSvc: service.NewPartnerService(), warehouseSvc: service.NewWarehouseService(), } } func (h *Handler) RegisterRoutes(r *gin.Engine) { // Health check (public) r.GET("/api/health", func(c *gin.Context) { c.JSON(200, gin.H{"status": "ok"}) }) api := r.Group("/api") { // Protected upload route upload := api.Group("/upload") upload.Use(middleware.AuthMiddleware()) { upload.POST("", h.UploadImage) } // Auth routes (public, with rate limiting) auth := api.Group("/auth") auth.Use(middleware.RateLimitMiddleware()) { auth.POST("/login", h.Login) } // Protected routes logs := api.Group("/logs") logs.Use(middleware.AuthMiddleware()) { logs.GET("", h.ListLogs) logs.GET("/materials/:materialId", h.GetLogsByMaterial) } materials := api.Group("/materials") materials.Use(middleware.AuthMiddleware()) { materials.GET("", h.ListMaterials) materials.GET("/:id", h.GetMaterial) materials.POST("", h.CreateMaterial) materials.PUT("/:id", h.UpdateMaterial) materials.POST("/:id/obsolete", h.ObsoleteMaterial) materials.POST("/:id/restore", h.RestoreMaterial) } inbound := api.Group("/inbound") inbound.Use(middleware.AuthMiddleware()) { inbound.GET("", h.ListInbound) inbound.GET("/:id", h.GetInbound) inbound.POST("", h.CreateInbound) inbound.PUT("/:id", h.UpdateInbound) inbound.DELETE("/:id", h.DeleteInbound) inbound.GET("/materials/:materialId", h.GetInboundByMaterial) } outbound := api.Group("/outbound") outbound.Use(middleware.AuthMiddleware()) { outbound.GET("", h.ListOutbound) outbound.GET("/:id", h.GetOutbound) outbound.POST("", h.CreateOutbound) outbound.PUT("/:id", h.UpdateOutbound) outbound.DELETE("/:id", h.DeleteOutbound) outbound.GET("/materials/:materialId", h.GetOutboundByMaterial) } stock := api.Group("/stock") stock.Use(middleware.AuthMiddleware()) { stock.GET("", h.ListStock) stock.GET("/warning", h.GetWarningStock) stock.GET("/logs", h.GetAllStockLogs) stock.GET("/logs/:materialId", h.GetStockLogs) stock.PUT("/:materialId/warning", h.UpdateWarningStock) stock.GET("/:materialId/details", h.GetInventoryBalanceDetails) stock.GET("/:materialId", h.GetStock) } // Admin only routes users := api.Group("/users") users.Use(middleware.AuthMiddleware(), middleware.AdminOnly()) { users.GET("", h.ListUsers) users.POST("", h.CreateUser) users.PUT("/:id", h.UpdateUser) users.DELETE("/:id", h.DeleteUser) } organizations := api.Group("/organizations") organizations.Use(middleware.AuthMiddleware(), middleware.AdminOnly()) { organizations.GET("", h.ListOrganizations) organizations.POST("", h.CreateOrganization) organizations.PUT("/:id", h.UpdateOrganization) } roles := api.Group("/roles") roles.Use(middleware.AuthMiddleware(), middleware.AdminOnly()) { roles.GET("", h.ListRoles) roles.PUT("/:id", h.UpdateRole) } permissions := api.Group("/permissions") permissions.Use(middleware.AuthMiddleware(), middleware.AdminOnly()) { permissions.GET("", h.ListPermissions) } categories := api.Group("/material-categories") categories.Use(middleware.AuthMiddleware(), middleware.AdminOnly()) { categories.GET("", h.ListMaterialCategories) categories.POST("", h.CreateMaterialCategory) categories.PUT("/:id", h.UpdateMaterialCategory) } units := api.Group("/units") units.Use(middleware.AuthMiddleware(), middleware.AdminOnly()) { units.GET("", h.ListUnits) units.POST("", h.CreateUnit) units.PUT("/:id", h.UpdateUnit) } codeRules := api.Group("/code-rules") codeRules.Use(middleware.AuthMiddleware(), middleware.AdminOnly()) { codeRules.GET("", h.ListCodeRules) codeRules.POST("", h.CreateCodeRule) codeRules.PUT("/:id", h.UpdateCodeRule) } partners := api.Group("/partners") partners.Use(middleware.AuthMiddleware(), middleware.AdminOnly()) { partners.GET("", h.ListPartners) partners.POST("", h.CreatePartner) partners.PUT("/:id", h.UpdatePartner) } warehouses := api.Group("/warehouses") warehouses.Use(middleware.AuthMiddleware(), middleware.AdminOnly()) { warehouses.GET("", h.ListWarehouses) warehouses.POST("", h.CreateWarehouse) warehouses.PUT("/:id", h.UpdateWarehouse) warehouses.POST("/:id/locations", h.CreateWarehouseLocation) warehouses.PUT("/:id/locations/:locationId", h.UpdateWarehouseLocation) } // Application routes (user: create/cancel own, list own; admin: list all, approve/reject) applications := api.Group("/applications") applications.Use(middleware.AuthMiddleware()) { applications.POST("", h.CreateApplication) applications.GET("/my", h.ListMyApplications) applications.POST("/:id/cancel", h.CancelApplication) applications.GET("", h.ListApplications) // admin only applications.POST("/:id/approve", h.ApproveApplication) // admin only applications.POST("/:id/reject", h.RejectApplication) // admin only } // Current user route me := api.Group("/auth") me.Use(middleware.AuthMiddleware()) { me.GET("/me", h.GetCurrentUser) me.POST("/logout", h.Logout) } } } // Material handlers func (h *Handler) ListMaterials(c *gin.Context) { search := c.Query("search") category := c.Query("category") limit := 20 offset := 0 if l := c.Query("limit"); l != "" { if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 { limit = parsed if limit > 100 { limit = 100 } } } if o := c.Query("offset"); o != "" { if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 { offset = parsed } } materials, total, err := h.materialSvc.List(search, category, limit, offset) if err != nil { response.InternalError(c, err.Error()) return } if materials == nil { materials = []model.Material{} } response.PaginatedSuccess(c, materials, limit, offset, total) } func (h *Handler) GetMaterial(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "invalid material id") return } material, err := h.materialSvc.GetByID(id) if err != nil { if errors.Is(err, sql.ErrNoRows) { response.NotFound(c, "Material not found") return } response.InternalError(c, err.Error()) return } response.Success(c, material) } func (h *Handler) CreateMaterial(c *gin.Context) { var req model.CreateMaterialRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, err.Error()) return } orgID, ok := organizationID(c) if !ok { return } req.OrganizationID = orgID operator := middleware.GetCurrentUsername(c) if operator == "" { operator = "系统" } material, err := h.materialSvc.Create(&req, operator) if err != nil { if errors.Is(err, service.ErrInvalidMaterialCategory) { response.BadRequest(c, err.Error()) return } if err.Error() == "material code already exists" { response.BadRequest(c, err.Error()) return } response.InternalError(c, err.Error()) return } response.Created(c, "Material created successfully", material) } func (h *Handler) UpdateMaterial(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "invalid material id") return } var req model.UpdateMaterialRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, err.Error()) return } operator := middleware.GetCurrentUsername(c) if operator == "" { operator = "系统" } material, err := h.materialSvc.Update(id, &req, operator) if err != nil { if errors.Is(err, service.ErrInvalidMaterialCategory) { response.BadRequest(c, err.Error()) return } if errors.Is(err, sql.ErrNoRows) { response.NotFound(c, "Material not found") return } response.InternalError(c, err.Error()) return } response.SuccessWithMessage(c, "Material updated successfully", material) } func (h *Handler) DeleteMaterial(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "invalid material id") return } operator := middleware.GetCurrentUsername(c) if operator == "" { operator = "系统" } err = h.materialSvc.Delete(id, operator) if err != nil { response.InternalError(c, err.Error()) return } response.SuccessWithMessage(c, "Material deleted successfully", nil) } func (h *Handler) ObsoleteMaterial(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "invalid material id") return } operator := middleware.GetCurrentUsername(c) if operator == "" { operator = "系统" } if err := h.materialSvc.Obsolete(id, operator); err != nil { if strings.Contains(err.Error(), "仍有库存") { response.Error(c, 409, err.Error()) return } if strings.Contains(err.Error(), "物料不存在") { response.Error(c, 404, err.Error()) return } response.InternalError(c, err.Error()) return } response.SuccessWithMessage(c, "Material obsoleted successfully", nil) } func (h *Handler) RestoreMaterial(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "invalid material id") return } operator := middleware.GetCurrentUsername(c) if operator == "" { operator = "系统" } if err := h.materialSvc.Restore(id, operator); err != nil { if strings.Contains(err.Error(), "物料不存在") { response.Error(c, 404, err.Error()) return } response.InternalError(c, err.Error()) return } response.SuccessWithMessage(c, "Material restored successfully", nil) } // Inbound handlers func (h *Handler) ListInbound(c *gin.Context) { search := c.Query("search") limit := 20 offset := 0 if l := c.Query("limit"); l != "" { if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 { limit = parsed if limit > 100 { limit = 100 } } } if o := c.Query("offset"); o != "" { if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 { offset = parsed } } orders, total, err := h.inboundSvc.List(search, limit, offset) if err != nil { response.InternalError(c, err.Error()) return } if orders == nil { orders = []model.InboundOrder{} } response.PaginatedSuccess(c, orders, limit, offset, total) } func (h *Handler) GetInbound(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "invalid inbound order id") return } order, err := h.inboundSvc.GetByID(id) if err != nil { if errors.Is(err, sql.ErrNoRows) { response.NotFound(c, "Inbound order not found") return } response.InternalError(c, err.Error()) return } response.Success(c, order) } func (h *Handler) CreateInbound(c *gin.Context) { var req model.CreateInboundRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, err.Error()) return } operator := middleware.GetCurrentUsername(c) if operator == "" { operator = "系统" } order, err := h.inboundSvc.Create(&req, operator) if err != nil { if errors.Is(err, service.ErrMaterialNotFound) { response.BadRequest(c, "material not found") return } response.InternalError(c, err.Error()) return } // Broadcast stock update to all connected clients GetHub().BroadcastStockUpdate("inbound", order.ID, order) response.Created(c, "Inbound order created successfully", order) } func (h *Handler) DeleteInbound(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "invalid inbound order id") return } err = h.inboundSvc.Delete(id) if err != nil { response.InternalError(c, err.Error()) return } response.SuccessWithMessage(c, "Inbound order deleted successfully", nil) } func (h *Handler) UpdateInbound(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "invalid inbound order id") return } var req model.UpdateInboundRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, err.Error()) return } order, err := h.inboundSvc.Update(id, &req) if err != nil { if errors.Is(err, service.ErrMaterialNotFound) { response.BadRequest(c, "material not found") return } response.InternalError(c, err.Error()) return } GetHub().BroadcastStockUpdate("inbound", order.ID, order) response.Success(c, order) } func (h *Handler) GetInboundByMaterial(c *gin.Context) { materialID, err := strconv.ParseInt(c.Param("materialId"), 10, 64) if err != nil { response.BadRequest(c, "invalid material id") return } records, err := h.inboundSvc.GetByMaterialID(materialID) if err != nil { response.InternalError(c, err.Error()) return } if records == nil { records = []model.InboundRecord{} } response.Success(c, records) } // Outbound handlers func (h *Handler) ListOutbound(c *gin.Context) { search := c.Query("search") limit := 20 offset := 0 if l := c.Query("limit"); l != "" { if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 { limit = parsed if limit > 100 { limit = 100 } } } if o := c.Query("offset"); o != "" { if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 { offset = parsed } } orders, total, err := h.outboundSvc.List(search, limit, offset) if err != nil { response.InternalError(c, err.Error()) return } if orders == nil { orders = []model.OutboundOrder{} } response.PaginatedSuccess(c, orders, limit, offset, total) } func (h *Handler) GetOutbound(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "invalid outbound order id") return } order, err := h.outboundSvc.GetByID(id) if err != nil { if errors.Is(err, sql.ErrNoRows) { response.NotFound(c, "Outbound order not found") return } response.InternalError(c, err.Error()) return } response.Success(c, order) } func (h *Handler) CreateOutbound(c *gin.Context) { var req model.CreateOutboundRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, err.Error()) return } operator := middleware.GetCurrentUsername(c) if operator == "" { operator = "系统" } order, err := h.outboundSvc.Create(&req, operator) if err != nil { if errors.Is(err, service.ErrMaterialNotFound) { response.BadRequest(c, "material not found") return } if errors.Is(err, service.ErrInsufficientStock) { response.BadRequest(c, "insufficient stock") return } response.InternalError(c, err.Error()) return } // Broadcast stock update to all connected clients GetHub().BroadcastStockUpdate("outbound", order.ID, order) response.Created(c, "Outbound order created successfully", order) } func (h *Handler) DeleteOutbound(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "invalid outbound order id") return } err = h.outboundSvc.Delete(id) if err != nil { response.InternalError(c, err.Error()) return } response.SuccessWithMessage(c, "Outbound order deleted successfully", nil) } func (h *Handler) UpdateOutbound(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "invalid outbound order id") return } var req model.UpdateOutboundRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, err.Error()) return } operator := middleware.GetCurrentUsername(c) if operator == "" { operator = "系统" } order, err := h.outboundSvc.Update(id, &req, operator) if err != nil { if errors.Is(err, service.ErrMaterialNotFound) { response.BadRequest(c, "material not found") return } if errors.Is(err, service.ErrInsufficientStock) { response.BadRequest(c, "insufficient stock") return } response.InternalError(c, err.Error()) return } GetHub().BroadcastStockUpdate("outbound", order.ID, order) response.Success(c, order) } func (h *Handler) GetOutboundByMaterial(c *gin.Context) { materialID, err := strconv.ParseInt(c.Param("materialId"), 10, 64) if err != nil { response.BadRequest(c, "invalid material id") return } records, err := h.outboundSvc.GetByMaterialID(materialID) if err != nil { response.InternalError(c, err.Error()) return } if records == nil { records = []model.OutboundRecord{} } response.Success(c, records) } // Stock handlers func (h *Handler) ListStock(c *gin.Context) { limit := 20 offset := 0 if l := c.Query("limit"); l != "" { if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 { limit = parsed if limit > 100 { limit = 100 } } } if o := c.Query("offset"); o != "" { if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 { offset = parsed } } search := c.Query("search") stocks, total, err := h.stockSvc.GetAllStock(search, limit, offset) if err != nil { response.InternalError(c, err.Error()) return } if stocks == nil { stocks = []model.Stock{} } response.PaginatedSuccess(c, stocks, limit, offset, total) } func (h *Handler) GetStock(c *gin.Context) { materialID, err := strconv.ParseInt(c.Param("materialId"), 10, 64) if err != nil { response.BadRequest(c, "invalid material id") return } stock, err := h.stockSvc.GetByMaterialID(materialID) if err != nil { if errors.Is(err, sql.ErrNoRows) { response.NotFound(c, "Material not found") return } response.InternalError(c, err.Error()) return } response.Success(c, stock) } // GetInventoryBalanceDetails 返回某物料在各组织/仓库/库位上的库存明细 func (h *Handler) GetInventoryBalanceDetails(c *gin.Context) { materialID, err := strconv.ParseInt(c.Param("materialId"), 10, 64) if err != nil { response.BadRequest(c, "invalid material id") return } exists, err := h.stockSvc.MaterialExists(materialID) if err != nil { response.InternalError(c, err.Error()) return } if !exists { response.NotFound(c, "Material not found") return } details, err := h.stockSvc.GetInventoryBalanceDetails(materialID) if err != nil { response.InternalError(c, err.Error()) return } if details == nil { details = []model.InventoryBalanceDetail{} } response.Success(c, details) } func (h *Handler) GetWarningStock(c *gin.Context) { limit := 20 offset := 0 if l := c.Query("limit"); l != "" { if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 { limit = parsed if limit > 100 { limit = 100 } } } if o := c.Query("offset"); o != "" { if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 { offset = parsed } } stocks, total, err := h.stockSvc.GetWarningStock(limit, offset) if err != nil { response.InternalError(c, err.Error()) return } if stocks == nil { stocks = []model.Stock{} } response.PaginatedSuccess(c, stocks, limit, offset, total) } func (h *Handler) UpdateWarningStock(c *gin.Context) { materialID, err := strconv.ParseInt(c.Param("materialId"), 10, 64) if err != nil { response.BadRequest(c, "invalid material id") return } var req struct { WarningStock decimal.Decimal `json:"warning_stock"` // Enabled 用指针接收:不传表示沿用旧行为(启用),只有显式传 false 才是关闭 // 该物料的预警。 Enabled *bool `json:"enabled"` } if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, err.Error()) return } if req.WarningStock.IsNegative() { response.BadRequest(c, "warning_stock must be greater than or equal to 0") return } enabled := true if req.Enabled != nil { enabled = *req.Enabled } if err := h.stockSvc.UpdateWarningStock(materialID, req.WarningStock, enabled); err != nil { response.InternalError(c, err.Error()) return } response.SuccessWithMessage(c, "Stock warning updated successfully", nil) } func (h *Handler) GetStockLogs(c *gin.Context) { materialID, err := strconv.ParseInt(c.Param("materialId"), 10, 64) if err != nil { response.BadRequest(c, "invalid material id") return } exists, err := h.stockSvc.MaterialExists(materialID) if err != nil { response.InternalError(c, err.Error()) return } if !exists { response.NotFound(c, "Material not found") return } limit := 20 offset := 0 if l := c.Query("limit"); l != "" { if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 { limit = parsed if limit > 100 { limit = 100 } } } if o := c.Query("offset"); o != "" { if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 { offset = parsed } } logs, total, err := h.stockSvc.GetLogsByMaterialID(materialID, limit, offset) if err != nil { response.InternalError(c, err.Error()) return } if logs == nil { logs = []model.StockLogResponse{} } response.PaginatedSuccess(c, logs, limit, offset, total) } func (h *Handler) GetAllStockLogs(c *gin.Context) { limit := 20 offset := 0 if l := c.Query("limit"); l != "" { if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 { limit = parsed if limit > 100 { limit = 100 } } } if o := c.Query("offset"); o != "" { if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 { offset = parsed } } logs, total, err := h.stockSvc.GetAllLogs(limit, offset) if err != nil { response.InternalError(c, err.Error()) return } if logs == nil { logs = []model.StockLogResponse{} } response.PaginatedSuccess(c, logs, limit, offset, total) } // Material Log handlers func (h *Handler) ListLogs(c *gin.Context) { operationType := c.Query("operation_type") materialId := c.Query("material_id") startDate := c.Query("start_date") endDate := c.Query("end_date") limit := 20 offset := 0 if l := c.Query("limit"); l != "" { if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 { limit = parsed if limit > 100 { limit = 100 } } } if o := c.Query("offset"); o != "" { if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 { offset = parsed } } logs, total, err := h.logSvc.List(operationType, materialId, startDate, endDate, limit, offset) if err != nil { response.InternalError(c, err.Error()) return } if logs == nil { logs = []model.MaterialLog{} } response.PaginatedSuccess(c, logs, limit, offset, total) } func (h *Handler) GetLogsByMaterial(c *gin.Context) { materialID, err := strconv.ParseInt(c.Param("materialId"), 10, 64) if err != nil { response.BadRequest(c, "invalid material id") return } logs, err := h.logSvc.GetByMaterialID(materialID) if err != nil { response.InternalError(c, err.Error()) return } if logs == nil { logs = []model.MaterialLog{} } response.Success(c, logs) } // Auth handlers func (h *Handler) Login(c *gin.Context) { ip := c.ClientIP() // Check if IP is rate limited before processing if middleware.IsRateLimited(ip) { response.Error(c, 429, "too many login attempts, please try again later") return } var req model.LoginRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, err.Error()) return } result, err := h.authSvc.Login(req.Username, req.Password) if err != nil { // Record failed attempt middleware.RecordFailedAttempt(ip) response.Unauthorized(c, "invalid username or password") return } // Reset attempts on successful login middleware.ResetAttempts(ip) response.Success(c, result) } func (h *Handler) Logout(c *gin.Context) { // For simple token auth, logout is handled client-side by removing the token response.SuccessWithMessage(c, "logout successful", nil) } func (h *Handler) GetCurrentUser(c *gin.Context) { userID := middleware.GetCurrentUserID(c) user, err := h.authSvc.GetUserByID(userID) if err != nil { response.NotFound(c, "user not found") return } response.Success(c, user) } // User management handlers (Admin only) func (h *Handler) ListUsers(c *gin.Context) { limit := 20 offset := 0 if l := c.Query("limit"); l != "" { if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 { limit = parsed if limit > 100 { limit = 100 } } } if o := c.Query("offset"); o != "" { if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 { offset = parsed } } search := c.Query("search") users, total, err := h.authSvc.ListUsers(search, limit, offset) if err != nil { response.InternalError(c, err.Error()) return } if users == nil { users = []model.User{} } response.PaginatedSuccess(c, users, limit, offset, total) } func (h *Handler) CreateUser(c *gin.Context) { var req struct { Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required"` RoleIDs []int64 `json:"role_ids" binding:"required,min=1"` Description string `json:"description"` } if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, err.Error()) return } user, err := h.authSvc.CreateUser(req.Username, req.Password, req.Description, req.RoleIDs) if err != nil { if errors.Is(err, repository.ErrInvalidRoles) { response.BadRequest(c, err.Error()) return } response.InternalError(c, err.Error()) return } response.Created(c, "user created successfully", user) } func (h *Handler) UpdateUser(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "invalid user id") return } var req struct { Password string `json:"password"` RoleIDs []int64 `json:"role_ids" binding:"required,min=1"` Description string `json:"description"` } if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, err.Error()) return } err = h.authSvc.UpdateUser(id, req.Description, req.RoleIDs, req.Password) if err != nil { if errors.Is(err, repository.ErrInvalidRoles) || errors.Is(err, repository.ErrLastSuperAdmin) { response.BadRequest(c, err.Error()) return } response.InternalError(c, err.Error()) return } response.SuccessWithMessage(c, "user updated successfully", nil) } func (h *Handler) DeleteUser(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "invalid user id") return } // Prevent deleting yourself currentUserID := middleware.GetCurrentUserID(c) if currentUserID == id { response.BadRequest(c, "cannot delete yourself") return } if err := h.authSvc.DeleteUser(id); err != nil { if errors.Is(err, repository.ErrLastSuperAdmin) { response.BadRequest(c, err.Error()) return } response.InternalError(c, err.Error()) return } response.SuccessWithMessage(c, "user deleted successfully", nil) } // Application handlers func (h *Handler) CreateApplication(c *gin.Context) { userID := middleware.GetCurrentUserID(c) var req model.CreateApplicationRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, err.Error()) return } app, err := h.appSvc.Create(userID, &req) if err != nil { response.InternalError(c, err.Error()) return } response.Created(c, "application created successfully", app) } func (h *Handler) ListMyApplications(c *gin.Context) { userID := middleware.GetCurrentUserID(c) status := c.Query("status") limit := 20 offset := 0 if l := c.Query("limit"); l != "" { if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 { limit = parsed if limit > 100 { limit = 100 } } } if o := c.Query("offset"); o != "" { if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 { offset = parsed } } apps, total, err := h.appSvc.ListByUser(userID, status, limit, offset) if err != nil { response.InternalError(c, err.Error()) return } response.PaginatedSuccess(c, apps, limit, offset, int64(total)) } func (h *Handler) ListApplications(c *gin.Context) { if !middleware.IsSuperAdmin(c) { response.Unauthorized(c, "admin only") return } status := c.Query("status") limit := 20 offset := 0 if l := c.Query("limit"); l != "" { if parsed, err := strconv.Atoi(l); err == nil && parsed > 0 { limit = parsed if limit > 100 { limit = 100 } } } if o := c.Query("offset"); o != "" { if parsed, err := strconv.Atoi(o); err == nil && parsed >= 0 { offset = parsed } } apps, total, err := h.appSvc.ListAll(status, limit, offset) if err != nil { response.InternalError(c, err.Error()) return } response.PaginatedSuccess(c, apps, limit, offset, int64(total)) } func (h *Handler) CancelApplication(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "invalid application id") return } userID := middleware.GetCurrentUserID(c) if err := h.appSvc.Cancel(id, userID); err != nil { if err.Error() == "cannot cancel others' applications" || err.Error() == "cannot cancel application" { response.BadRequest(c, err.Error()) return } response.InternalError(c, err.Error()) return } response.SuccessWithMessage(c, "application cancelled successfully", nil) } func (h *Handler) ApproveApplication(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "invalid application id") return } if !middleware.IsSuperAdmin(c) { response.Unauthorized(c, "admin only") return } var req model.ApproveApplicationRequest c.ShouldBindJSON(&req) // optional approverID := middleware.GetCurrentUserID(c) if err := h.appSvc.Approve(id, approverID, req.AutoOutbound); err != nil { if err == service.ErrApplicationNotFound { response.NotFound(c, "application not found") return } if err == service.ErrApplicationNotPending { response.BadRequest(c, "application is not pending") return } if err == service.ErrInsufficientStock { response.BadRequest(c, "insufficient stock") return } response.InternalError(c, err.Error()) return } response.SuccessWithMessage(c, "application approved successfully", nil) } func (h *Handler) RejectApplication(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { response.BadRequest(c, "invalid application id") return } if !middleware.IsSuperAdmin(c) { response.Unauthorized(c, "admin only") return } var req model.RejectApplicationRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "rejection reason is required") return } if err := h.appSvc.Reject(id, req.Reason); err != nil { if err == service.ErrApplicationNotFound { response.NotFound(c, "application not found") return } if err == service.ErrApplicationNotPending { response.BadRequest(c, "application is not pending") return } response.InternalError(c, err.Error()) return } response.SuccessWithMessage(c, "application rejected successfully", nil) }