diff --git a/internal/modules/purchases/controllers/purchase.controller.go b/internal/modules/purchases/controllers/purchase.controller.go index c4291619..a2cf6fad 100644 --- a/internal/modules/purchases/controllers/purchase.controller.go +++ b/internal/modules/purchases/controllers/purchase.controller.go @@ -30,6 +30,11 @@ func (ctrl *PurchaseController) GetAll(c *fiber.Ctx) error { query := &validation.Query{ Page: c.QueryInt("page", 1), Limit: c.QueryInt("limit", 10), + Search: strings.TrimSpace(c.Query("search")), + ApprovalStatus: strings.TrimSpace(c.Query("approval_status")), + PoDate: strings.TrimSpace(c.Query("po_date")), + PoDateFrom: strings.TrimSpace(c.Query("po_date_from")), + PoDateTo: strings.TrimSpace(c.Query("po_date_to")), CreatedFrom: strings.TrimSpace(c.Query("created_from")), CreatedTo: strings.TrimSpace(c.Query("created_to")), SupplierID: uint(c.QueryInt("supplier_id", 0)), diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index 5b419482..1935d9da 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -146,6 +146,27 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti return nil, 0, utils.BadRequest(err.Error()) } + var poDateStart *time.Time + var poDateEnd *time.Time + + if strings.TrimSpace(params.PoDate) != "" { + poDate, parseErr := utils.ParseDateString(strings.TrimSpace(params.PoDate)) + if parseErr != nil { + return nil, 0, utils.BadRequest("po_date must use format YYYY-MM-DD") + } + poDateStart = &poDate + poDateEndValue := poDate.AddDate(0, 0, 1) + poDateEnd = &poDateEndValue + } else { + poDateStart, poDateEnd, err = parsePoDateRangeForQuery(params.PoDateFrom, params.PoDateTo) + if err != nil { + return nil, 0, utils.BadRequest(err.Error()) + } + } + + search := strings.ToLower(strings.TrimSpace(params.Search)) + approvalStatus := normalizeApprovalStatusFilter(params.ApprovalStatus) + purchases, total, err := s.PurchaseRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) db = db.Where("purchases.deleted_at IS NULL") @@ -162,6 +183,14 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti db = db.Where("created_at < ?", *createdTo) } + if poDateStart != nil { + db = db.Where("purchases.po_date >= ?", *poDateStart) + } + + if poDateEnd != nil { + db = db.Where("purchases.po_date < ?", *poDateEnd) + } + if scope.Restrict { if len(scope.IDs) == 0 { return db.Where("1 = 0") @@ -213,6 +242,70 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti ) } + 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 search != "" { + like := "%" + search + "%" + db = db.Where( + `( + LOWER(COALESCE(purchases.pr_number, '')) LIKE ? + OR LOWER(COALESCE(purchases.po_number, '')) LIKE ? + OR EXISTS ( + SELECT 1 + FROM suppliers s + WHERE s.id = purchases.supplier_id + AND LOWER(COALESCE(s.name, '')) LIKE ? + ) + OR EXISTS ( + SELECT 1 + FROM users u + WHERE u.id = purchases.created_by + AND LOWER(COALESCE(u.name, '')) LIKE ? + ) + OR EXISTS ( + SELECT 1 + FROM purchase_items pi + JOIN products p ON p.id = pi.product_id + WHERE pi.purchase_id = purchases.id + AND LOWER(COALESCE(p.name, '')) LIKE ? + ) + )`, + like, + like, + like, + like, + like, + ) + } + return db.Order("created_at DESC").Order("purchases.id DESC") }) @@ -221,10 +314,8 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti return nil, 0, utils.Internal("Failed to get purchases") } - for i := range purchases { - if err := s.attachLatestApproval(c.Context(), &purchases[i]); err != nil { - s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", purchases[i].Id, err) - } + if err := s.attachLatestApprovals(c.Context(), purchases); err != nil { + s.Log.Warnf("Unable to attach latest approvals for purchases: %+v", err) } return purchases, total, nil @@ -2028,6 +2119,78 @@ func (s *purchaseService) attachLatestApproval(ctx context.Context, item *entity return nil } +func (s *purchaseService) attachLatestApprovals(ctx context.Context, items []entity.Purchase) error { + if len(items) == 0 || s.ApprovalSvc == nil { + return nil + } + + ids := make([]uint, 0, len(items)) + for i := range items { + if items[i].Id == 0 { + continue + } + ids = append(ids, items[i].Id) + } + + if len(ids) == 0 { + return nil + } + + latestMap, err := s.ApprovalSvc.LatestByTargets(ctx, utils.ApprovalWorkflowPurchase, ids, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err != nil { + return err + } + + for i := range items { + items[i].LatestApproval = latestMap[items[i].Id] + } + + return nil +} + +func parsePoDateRangeForQuery(fromStr, toStr string) (*time.Time, *time.Time, error) { + var fromPtr *time.Time + var toPtr *time.Time + + if strings.TrimSpace(fromStr) != "" { + parsed, err := utils.ParseDateString(strings.TrimSpace(fromStr)) + if err != nil { + return nil, nil, errors.New("po_date_from must use format YYYY-MM-DD") + } + fromValue := parsed + fromPtr = &fromValue + } + + if strings.TrimSpace(toStr) != "" { + parsed, err := utils.ParseDateString(strings.TrimSpace(toStr)) + if err != nil { + return nil, nil, errors.New("po_date_to must use format YYYY-MM-DD") + } + nextDay := parsed.AddDate(0, 0, 1) + toPtr = &nextDay + } + + if fromPtr != nil && toPtr != nil && fromPtr.After(*toPtr) { + return nil, nil, errors.New("po_date_from must be earlier than po_date_to") + } + + return fromPtr, toPtr, nil +} + +func normalizeApprovalStatusFilter(raw string) string { + value := strings.ToLower(strings.TrimSpace(raw)) + switch value { + case "disetujui": + return "approved" + case "ditolak": + return "rejected" + default: + return value + } +} + 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 6f5d3013..dcd55499 100644 --- a/internal/modules/purchases/validations/purchase.validation.go +++ b/internal/modules/purchases/validations/purchase.validation.go @@ -68,6 +68,10 @@ type Query struct { LocationID uint `query:"location_id" validate:"omitempty,gt=0"` ProductCategoryID uint `query:"product_category_id" validate:"omitempty,gt=0"` Search string `query:"search" validate:"omitempty,max=100"` + ApprovalStatus string `query:"approval_status" validate:"omitempty,max=100"` + 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"` CreatedFrom string `query:"created_from" validate:"omitempty,datetime=2006-01-02"` CreatedTo string `query:"created_to" validate:"omitempty,datetime=2006-01-02"` }