material_manage_system/internal/handler/api.go
2026-09-20 11:18:00 +08:00

1279 lines
31 KiB
Go

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)
}