diff --git a/internal/modules/purchases/controllers/purchase.controller.go b/internal/modules/purchases/controllers/purchase.controller.go index a2cf6fad..985349f0 100644 --- a/internal/modules/purchases/controllers/purchase.controller.go +++ b/internal/modules/purchases/controllers/purchase.controller.go @@ -40,7 +40,7 @@ func (ctrl *PurchaseController) GetAll(c *fiber.Ctx) error { SupplierID: uint(c.QueryInt("supplier_id", 0)), AreaID: uint(c.QueryInt("area_id", 0)), LocationID: uint(c.QueryInt("location_id", 0)), - ProductCategoryID: uint(c.QueryInt("product_category_id", 0)), + ProductCategoryID: strings.TrimSpace(c.Query("product_category_id")), } if query.Page < 1 || query.Limit < 1 { diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index 1935d9da..ff8ef614 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -7,6 +7,7 @@ import ( "math" "mime/multipart" "sort" + "strconv" "strings" "time" @@ -146,6 +147,11 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti return nil, 0, utils.BadRequest(err.Error()) } + productCategoryIDs, err := parseUintCSVFilter(params.ProductCategoryID, "product_category_id") + if err != nil { + return nil, 0, utils.BadRequest(err.Error()) + } + var poDateStart *time.Time var poDateEnd *time.Time @@ -165,7 +171,10 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti } search := strings.ToLower(strings.TrimSpace(params.Search)) - approvalStatus := normalizeApprovalStatusFilter(params.ApprovalStatus) + approvalStatuses := parseStringCSVFilter(params.ApprovalStatus) + for i := range approvalStatuses { + approvalStatuses[i] = normalizeApprovalStatusFilter(approvalStatuses[i]) + } purchases, total, err := s.PurchaseRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) @@ -230,46 +239,53 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti ) } - if params.ProductCategoryID > 0 { + if len(productCategoryIDs) > 0 { db = db.Where( `EXISTS ( SELECT 1 FROM purchase_items pi JOIN products p ON p.id = pi.product_id - WHERE pi.purchase_id = purchases.id AND p.product_category_id = ? + WHERE pi.purchase_id = purchases.id AND p.product_category_id IN ? )`, - params.ProductCategoryID, + productCategoryIDs, ) } - if approvalStatus != "" { - approvalLike := "%" + approvalStatus + "%" - db = db.Where( - `EXISTS ( - SELECT 1 - FROM approvals a - WHERE a.approvable_type = ? - AND a.approvable_id = purchases.id - AND a.id = ( - SELECT a2.id - FROM approvals a2 - WHERE a2.approvable_type = ? - AND a2.approvable_id = purchases.id - ORDER BY a2.action_at DESC, a2.id DESC - LIMIT 1 - ) - AND ( - LOWER(COALESCE(a.step_name, '')) LIKE ? - OR LOWER(COALESCE(CAST(a.action AS TEXT), '')) LIKE ? - OR CAST(a.step_number AS TEXT) = ? - ) - )`, - utils.ApprovalWorkflowPurchase.String(), - utils.ApprovalWorkflowPurchase.String(), - approvalLike, - approvalLike, - approvalStatus, - ) + if len(approvalStatuses) > 0 { + approvalConditions := make([]string, 0, len(approvalStatuses)) + approvalArgs := make([]any, 0, 2+(len(approvalStatuses)*3)) + approvalArgs = append(approvalArgs, utils.ApprovalWorkflowPurchase.String(), utils.ApprovalWorkflowPurchase.String()) + for _, status := range approvalStatuses { + if status == "" { + continue + } + like := "%" + status + "%" + approvalConditions = append(approvalConditions, `(LOWER(COALESCE(a.step_name, '')) LIKE ? OR LOWER(COALESCE(CAST(a.action AS TEXT), '')) LIKE ? OR CAST(a.step_number AS TEXT) = ?)`) + approvalArgs = append(approvalArgs, like, like, status) + } + + if len(approvalConditions) > 0 { + approvalClause := strings.Join(approvalConditions, " OR ") + approvalQuery := fmt.Sprintf( + `EXISTS ( + SELECT 1 + FROM approvals a + WHERE a.approvable_type = ? + AND a.approvable_id = purchases.id + AND a.id = ( + SELECT a2.id + FROM approvals a2 + WHERE a2.approvable_type = ? + AND a2.approvable_id = purchases.id + ORDER BY a2.action_at DESC, a2.id DESC + LIMIT 1 + ) + AND (%s) + )`, + approvalClause, + ) + db = db.Where(approvalQuery, approvalArgs...) + } } if search != "" { @@ -2191,6 +2207,51 @@ func normalizeApprovalStatusFilter(raw string) string { } } +func parseUintCSVFilter(raw, fieldName string) ([]uint, error) { + parts := strings.Split(raw, ",") + result := make([]uint, 0, len(parts)) + seen := make(map[uint]struct{}, len(parts)) + + for _, part := range parts { + value := strings.TrimSpace(part) + if value == "" { + continue + } + num, err := strconv.ParseUint(value, 10, 64) + if err != nil || num == 0 { + return nil, fmt.Errorf("%s contains invalid value: %s", fieldName, value) + } + u := uint(num) + if _, exists := seen[u]; exists { + continue + } + seen[u] = struct{}{} + result = append(result, u) + } + + return result, nil +} + +func parseStringCSVFilter(raw string) []string { + parts := strings.Split(raw, ",") + result := make([]string, 0, len(parts)) + seen := make(map[string]struct{}, len(parts)) + + for _, part := range parts { + value := strings.ToLower(strings.TrimSpace(part)) + if value == "" { + continue + } + if _, exists := seen[value]; exists { + continue + } + seen[value] = struct{}{} + result = append(result, value) + } + + return result +} + func parseApprovalActionInput(raw string) (entity.ApprovalAction, error) { value := strings.ToUpper(strings.TrimSpace(raw)) switch value { diff --git a/internal/modules/purchases/validations/purchase.validation.go b/internal/modules/purchases/validations/purchase.validation.go index dcd55499..2e21f0d9 100644 --- a/internal/modules/purchases/validations/purchase.validation.go +++ b/internal/modules/purchases/validations/purchase.validation.go @@ -66,9 +66,9 @@ type Query struct { SupplierID uint `query:"supplier_id" validate:"omitempty,gt=0"` AreaID uint `query:"area_id" validate:"omitempty,gt=0"` LocationID uint `query:"location_id" validate:"omitempty,gt=0"` - ProductCategoryID uint `query:"product_category_id" validate:"omitempty,gt=0"` + ProductCategoryID string `query:"product_category_id" validate:"omitempty,max=500"` Search string `query:"search" validate:"omitempty,max=100"` - ApprovalStatus string `query:"approval_status" validate:"omitempty,max=100"` + ApprovalStatus string `query:"approval_status" validate:"omitempty,max=500"` PoDate string `query:"po_date" validate:"omitempty,datetime=2006-01-02"` PoDateFrom string `query:"po_date_from" validate:"omitempty,datetime=2006-01-02"` PoDateTo string `query:"po_date_to" validate:"omitempty,datetime=2006-01-02"`