From e24e2ff123183cf6b5d0332459fb52fa5c95782e Mon Sep 17 00:00:00 2001 From: Adnan Zahir Date: Thu, 23 Apr 2026 00:17:24 +0700 Subject: [PATCH] feat: filter improvement --- .../controllers/expense.controller.go | 15 +- .../expenses/services/expense.service.go | 193 +++++++++- .../validations/expense.validation.go | 15 +- .../controllers/deliveryorder.controller.go | 16 +- .../services/deliveryorder.service.go | 134 +++++-- .../validations/deliveryorder.validation.go | 16 +- .../controllers/recording.controller.go | 20 +- .../repositories/recording.repository.go | 97 ++++- .../recordings/services/recording.service.go | 179 +++++----- .../validations/recording.validation.go | 8 +- .../controllers/purchase.controller.go | 28 +- .../purchases/services/purchase.service.go | 331 ++++++++---------- .../validations/purchase.validation.go | 28 +- 13 files changed, 702 insertions(+), 378 deletions(-) diff --git a/internal/modules/expenses/controllers/expense.controller.go b/internal/modules/expenses/controllers/expense.controller.go index 013b97c6..ac1e66ee 100644 --- a/internal/modules/expenses/controllers/expense.controller.go +++ b/internal/modules/expenses/controllers/expense.controller.go @@ -51,9 +51,18 @@ func (u *ExpenseController) GetAll(c *fiber.Ctx) error { } query := &validation.Query{ - Page: c.QueryInt("page", 1), - Limit: c.QueryInt("limit", 10), - Search: c.Query("search", ""), + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + Search: strings.TrimSpace(c.Query("search", "")), + TransactionDate: strings.TrimSpace(c.Query("transaction_date", "")), + RealizationDate: strings.TrimSpace(c.Query("realization_date", "")), + LocationID: uint64(c.QueryInt("location_id", 0)), + VendorID: uint64(c.QueryInt("vendor_id", 0)), + Category: strings.TrimSpace(c.Query("category", "")), + ApprovalStatus: strings.TrimSpace(c.Query("approval_status", "")), + RealizationStatus: strings.TrimSpace(c.Query("realization_status", "")), + ProjectFlockID: uint64(c.QueryInt("project_flock_id", 0)), + ProjectFlockKandangID: uint64(c.QueryInt("project_flock_kandang_id", 0)), } if query.Page < 1 || query.Limit < 1 { diff --git a/internal/modules/expenses/services/expense.service.go b/internal/modules/expenses/services/expense.service.go index c410fbd0..860c9212 100644 --- a/internal/modules/expenses/services/expense.service.go +++ b/internal/modules/expenses/services/expense.service.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "strings" "time" "gitlab.com/mbugroup/lti-api.git/internal/common/exportprogress" @@ -86,6 +87,25 @@ func (s expenseService) withRelations(db *gorm.DB) *gorm.DB { }) } +func normalizeExpenseApprovalStatusFilter(raw string) string { + switch strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(raw), " ", "_")) { + case "HEAD_AREA", "APPROVAL_HEAD_AREA": + return "Approval Head Area" + case "UNIT_VICE_PRESIDENT", "APPROVAL_UNIT_VICE_PRESIDENT", "BUSINESS_UNIT_VICE_PRESIDENT", "APPROVAL_BUSINESS_UNIT_VICE_PRESIDENT": + return "Approval Unit Vice President" + case "FINANCE", "APPROVAL_FINANCE": + return "Approval Finance" + case "REALISASI": + return "Realisasi" + case "SELESAI": + return "Selesai" + case "DITOLAK", "REJECTED": + return "REJECTED" + default: + return "" + } +} + func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expenseDto.ExpenseListDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err @@ -98,10 +118,177 @@ func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]expens expenses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) db, scopeErr = middleware.ApplyLocationScope(c, db, "expenses.location_id") - if params.Search != "" { - return db.Where("category ILIKE ?", "%"+params.Search+"%") + db = db.Where("expenses.deleted_at IS NULL") + + if params.TransactionDate != "" { + db = db.Where("DATE(expenses.transaction_date) = DATE(?)", params.TransactionDate) } - return db.Order("created_at DESC").Order("updated_at DESC") + if params.RealizationDate != "" { + db = db.Where("DATE(expenses.realization_date) = DATE(?)", params.RealizationDate) + } + if params.LocationID > 0 { + db = db.Where("expenses.location_id = ?", params.LocationID) + } + if params.VendorID > 0 { + db = db.Where("expenses.supplier_id = ?", params.VendorID) + } + if params.Category != "" { + db = db.Where("expenses.category = ?", params.Category) + } + if params.ProjectFlockID > 0 { + projectFlockJSON := fmt.Sprintf("[%d]", params.ProjectFlockID) + db = db.Where(`( + EXISTS ( + SELECT 1 + FROM expense_nonstocks en + LEFT JOIN project_flock_kandangs pfk ON pfk.id = en.project_flock_kandang_id + WHERE en.expense_id = expenses.id + AND ( + pfk.project_flock_id = ? OR + en.kandang_id IN ( + SELECT kandang_id + FROM project_flock_kandangs + WHERE project_flock_id = ? + ) + ) + ) OR + (expenses.project_flock_id IS NOT NULL AND expenses.project_flock_id::jsonb @> ?::jsonb) + )`, params.ProjectFlockID, params.ProjectFlockID, projectFlockJSON) + } + if params.ProjectFlockKandangID > 0 { + db = db.Where(`EXISTS ( + SELECT 1 + FROM expense_nonstocks en + LEFT JOIN project_flock_kandangs selected_pfk ON selected_pfk.id = ? + WHERE en.expense_id = expenses.id + AND ( + en.project_flock_kandang_id = ? OR + (selected_pfk.kandang_id IS NOT NULL AND en.kandang_id = selected_pfk.kandang_id) + ) + )`, params.ProjectFlockKandangID, params.ProjectFlockKandangID) + } + + latestApprovalSubQuery := s.Repository.DB(). + WithContext(c.Context()). + Table("approvals"). + Select("DISTINCT ON (approvable_id) approvable_id, step_name, action, step_number"). + Where("approvable_type = ?", utils.ApprovalWorkflowExpense.String()). + Order("approvable_id, action_at DESC, id DESC") + + if approvalStatus := normalizeExpenseApprovalStatusFilter(params.ApprovalStatus); approvalStatus != "" { + if approvalStatus == "REJECTED" { + db = db.Where(`EXISTS ( + SELECT 1 + FROM (?) AS latest_approval + WHERE latest_approval.approvable_id = expenses.id + AND latest_approval.action = ? + )`, latestApprovalSubQuery, string(entity.ApprovalActionRejected)) + } else { + db = db.Where(`EXISTS ( + SELECT 1 + FROM (?) AS latest_approval + WHERE latest_approval.approvable_id = expenses.id + AND LOWER(latest_approval.step_name) = LOWER(?) + AND (latest_approval.action IS NULL OR latest_approval.action <> ?) + )`, latestApprovalSubQuery, approvalStatus, string(entity.ApprovalActionRejected)) + } + } + + switch strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(params.RealizationStatus), " ", "_")) { + case "REALIZED", "SUDAH_REALISASI": + db = db.Where(`EXISTS ( + SELECT 1 + FROM (?) AS latest_approval + WHERE latest_approval.approvable_id = expenses.id + AND (latest_approval.action IS NULL OR latest_approval.action <> ?) + AND latest_approval.step_number >= 5 + )`, latestApprovalSubQuery, string(entity.ApprovalActionRejected)) + case "NOT_REALIZED", "BELUM_REALISASI": + db = db.Where(`NOT EXISTS ( + SELECT 1 + FROM (?) AS latest_approval + WHERE latest_approval.approvable_id = expenses.id + AND (latest_approval.action IS NULL OR latest_approval.action <> ?) + AND latest_approval.step_number >= 5 + )`, latestApprovalSubQuery, string(entity.ApprovalActionRejected)) + db = db.Where(`NOT EXISTS ( + SELECT 1 + FROM (?) AS latest_approval + WHERE latest_approval.approvable_id = expenses.id + AND latest_approval.action = ? + )`, latestApprovalSubQuery, string(entity.ApprovalActionRejected)) + case "REJECTED", "DITOLAK": + db = db.Where(`EXISTS ( + SELECT 1 + FROM (?) AS latest_approval + WHERE latest_approval.approvable_id = expenses.id + AND latest_approval.action = ? + )`, latestApprovalSubQuery, string(entity.ApprovalActionRejected)) + } + + if search := strings.ToLower(strings.TrimSpace(params.Search)); search != "" { + like := "%" + search + "%" + db = db.Where(`( + LOWER(COALESCE(expenses.reference_number, '')) LIKE ? + OR LOWER(COALESCE(expenses.po_number, '')) LIKE ? + OR LOWER(COALESCE(expenses.category, '')) LIKE ? + OR EXISTS ( + SELECT 1 + FROM suppliers s + WHERE s.id = expenses.supplier_id + AND LOWER(COALESCE(s.name, '')) LIKE ? + ) + OR EXISTS ( + SELECT 1 + FROM locations l + WHERE l.id = expenses.location_id + AND LOWER(COALESCE(l.name, '')) LIKE ? + ) + OR EXISTS ( + SELECT 1 + FROM users u + WHERE u.id = expenses.created_by + AND LOWER(COALESCE(u.name, '')) LIKE ? + ) + OR EXISTS ( + SELECT 1 + FROM expense_nonstocks en + LEFT JOIN project_flock_kandangs pfk ON pfk.id = en.project_flock_kandang_id + LEFT JOIN project_flocks pf ON pf.id = pfk.project_flock_id + LEFT JOIN kandangs k ON k.id = COALESCE(en.kandang_id, pfk.kandang_id) + WHERE en.expense_id = expenses.id + AND ( + LOWER(COALESCE(pf.flock_name, '')) LIKE ? OR + LOWER(COALESCE(k.name, '')) LIKE ? + ) + ) + OR EXISTS ( + SELECT 1 + FROM approvals a + WHERE a.approvable_type = ? + AND a.approvable_id = expenses.id + AND ( + LOWER(COALESCE(a.step_name, '')) LIKE ? OR + LOWER(COALESCE(CAST(a.action AS TEXT), '')) LIKE ? OR + LOWER(COALESCE(a.notes, '')) LIKE ? + ) + ) + )`, + like, + like, + like, + like, + like, + like, + like, + like, + utils.ApprovalWorkflowExpense.String(), + like, + like, + like, + ) + } + return db.Order("expenses.created_at DESC").Order("expenses.updated_at DESC") }) if scopeErr != nil { diff --git a/internal/modules/expenses/validations/expense.validation.go b/internal/modules/expenses/validations/expense.validation.go index d8107e7c..0046ce7f 100644 --- a/internal/modules/expenses/validations/expense.validation.go +++ b/internal/modules/expenses/validations/expense.validation.go @@ -42,9 +42,18 @@ type Update struct { } type Query struct { - Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` - Search string `query:"search" validate:"omitempty,max=50"` + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + Search string `query:"search" validate:"omitempty,max=100"` + TransactionDate string `query:"transaction_date" validate:"omitempty,datetime=2006-01-02"` + RealizationDate string `query:"realization_date" validate:"omitempty,datetime=2006-01-02"` + LocationID uint64 `query:"location_id" validate:"omitempty,gt=0"` + VendorID uint64 `query:"vendor_id" validate:"omitempty,gt=0"` + Category string `query:"category" validate:"omitempty,oneof=BOP NON-BOP"` + ApprovalStatus string `query:"approval_status" validate:"omitempty,max=100"` + RealizationStatus string `query:"realization_status" validate:"omitempty,max=100"` + ProjectFlockID uint64 `query:"project_flock_id" validate:"omitempty,gt=0"` + ProjectFlockKandangID uint64 `query:"project_flock_kandang_id" validate:"omitempty,gt=0"` } type CreateRealization struct { diff --git a/internal/modules/marketing/controllers/deliveryorder.controller.go b/internal/modules/marketing/controllers/deliveryorder.controller.go index 39ab38eb..bb285294 100644 --- a/internal/modules/marketing/controllers/deliveryorder.controller.go +++ b/internal/modules/marketing/controllers/deliveryorder.controller.go @@ -57,13 +57,15 @@ func (u *DeliveryOrdersController) GetAll(c *fiber.Ctx) error { } query := &validation.DeliveryOrderQuery{ - Page: c.QueryInt("page", 1), - Limit: c.QueryInt("limit", 10), - Search: strings.TrimSpace(c.Query("search", "")), - ProductIDs: productIDs, - Status: strings.ReplaceAll(strings.TrimSpace(c.Query("status", "")), "_", " "), - CustomerId: uint(c.QueryInt("customer_id", 0)), - MarketingId: uint(c.QueryInt("marketing_id", 0)), + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + Search: strings.TrimSpace(c.Query("search", "")), + ProductIDs: productIDs, + Status: strings.ReplaceAll(strings.TrimSpace(c.Query("status", "")), "_", " "), + CustomerId: uint(c.QueryInt("customer_id", 0)), + MarketingId: uint(c.QueryInt("marketing_id", 0)), + ProjectFlockID: uint(c.QueryInt("project_flock_id", 0)), + ProjectFlockKandangID: uint(c.QueryInt("project_flock_kandang_id", 0)), } if isAllExcelExportRequest(c) { diff --git a/internal/modules/marketing/services/deliveryorder.service.go b/internal/modules/marketing/services/deliveryorder.service.go index 7a1c4cdc..c06ff3de 100644 --- a/internal/modules/marketing/services/deliveryorder.service.go +++ b/internal/modules/marketing/services/deliveryorder.service.go @@ -85,6 +85,102 @@ func (s deliveryOrdersService) withRelations(db *gorm.DB) *gorm.DB { Preload("Products.DeliveryProduct") } +func (s deliveryOrdersService) marketingOwnerRelationQuery(ctx context.Context) *gorm.DB { + return s.MarketingRepo.DB(). + WithContext(ctx). + Table("marketing_products mp"). + Select("1"). + Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id"). + Joins("LEFT JOIN project_flock_kandangs pfk ON pfk.id = pw.project_flock_kandang_id"). + Joins("LEFT JOIN project_flocks pf ON pf.id = pfk.project_flock_id"). + Joins("LEFT JOIN warehouses w ON w.id = pw.warehouse_id"). + Joins("LEFT JOIN kandangs k ON k.id = COALESCE(pfk.kandang_id, w.kandang_id)"). + Where("mp.marketing_id = marketings.id") +} + +func (s deliveryOrdersService) marketingAttributionRelationQuery(ctx context.Context) *gorm.DB { + baseDB := s.MarketingRepo.DB().WithContext(ctx) + return baseDB. + Table("marketing_delivery_products mdp"). + Select("1"). + Joins("JOIN marketing_products mp ON mp.id = mdp.marketing_product_id"). + Joins("JOIN (?) AS mda ON mda.marketing_delivery_product_id = mdp.id", commonRepo.MarketingDeliveryAttributionRowsQuery(baseDB)). + Joins("JOIN project_flock_kandangs pfk_attr ON pfk_attr.id = mda.project_flock_kandang_id"). + Joins("JOIN project_flocks pf_attr ON pf_attr.id = pfk_attr.project_flock_id"). + Joins("JOIN kandangs k_attr ON k_attr.id = pfk_attr.kandang_id"). + Where("mp.marketing_id = marketings.id") +} + +func (s deliveryOrdersService) applyMarketingProjectFlockFilter(ctx context.Context, db *gorm.DB, projectFlockID, projectFlockKandangID uint) *gorm.DB { + if projectFlockID > 0 { + db = db.Where( + "(EXISTS (?) OR EXISTS (?))", + s.marketingOwnerRelationQuery(ctx).Where("pfk.project_flock_id = ?", projectFlockID), + s.marketingAttributionRelationQuery(ctx).Where("mda.project_flock_id = ?", projectFlockID), + ) + } + + if projectFlockKandangID > 0 { + db = db.Where( + "(EXISTS (?) OR EXISTS (?))", + s.marketingOwnerRelationQuery(ctx).Where("pw.project_flock_kandang_id = ?", projectFlockKandangID), + s.marketingAttributionRelationQuery(ctx).Where("mda.project_flock_kandang_id = ?", projectFlockKandangID), + ) + } + + return db +} + +func (s deliveryOrdersService) applyMarketingSearchFilter(ctx context.Context, db *gorm.DB, rawSearch string) *gorm.DB { + searchPattern := "%" + strings.TrimSpace(rawSearch) + "%" + if searchPattern == "%%" { + return db + } + + return db.Where( + `( + marketings.so_number ILIKE ? OR + EXISTS ( + SELECT 1 + FROM customers c + WHERE c.id = marketings.customer_id + AND c.name ILIKE ? + ) OR + EXISTS ( + SELECT 1 + FROM users su + WHERE su.id = marketings.sales_person_id + AND su.name ILIKE ? + ) OR + EXISTS ( + SELECT 1 + FROM marketing_products mp + JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id + JOIN products p ON p.id = pw.product_id + WHERE mp.marketing_id = marketings.id + AND p.name ILIKE ? + ) OR + EXISTS ( + SELECT 1 + FROM marketing_products mp + JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id + JOIN warehouses w ON w.id = pw.warehouse_id + WHERE mp.marketing_id = marketings.id + AND w.name ILIKE ? + ) OR + EXISTS (?) OR + EXISTS (?) + )`, + searchPattern, + searchPattern, + searchPattern, + searchPattern, + searchPattern, + s.marketingOwnerRelationQuery(ctx).Where("pf.flock_name ILIKE ? OR k.name ILIKE ?", searchPattern, searchPattern), + s.marketingAttributionRelationQuery(ctx).Where("pf_attr.flock_name ILIKE ? OR k_attr.name ILIKE ?", searchPattern, searchPattern), + ) +} + func (s deliveryOrdersService) getMarketingWithDeliveries(c *fiber.Ctx, marketingId uint) (*dto.MarketingDetailDTO, error) { if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), marketingId); err != nil { return nil, err @@ -158,41 +254,6 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO } } - if params.Search != "" { - searchPattern := "%" + params.Search + "%" - db = db.Where(`( - marketings.so_number ILIKE ? OR - EXISTS ( - SELECT 1 - FROM customers c - WHERE c.id = marketings.customer_id - AND c.name ILIKE ? - ) OR - EXISTS ( - SELECT 1 - FROM users su - WHERE su.id = marketings.sales_person_id - AND su.name ILIKE ? - ) OR - EXISTS ( - SELECT 1 - FROM marketing_products mp - JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id - JOIN products p ON p.id = pw.product_id - WHERE mp.marketing_id = marketings.id - AND p.name ILIKE ? - ) OR - EXISTS ( - SELECT 1 - FROM marketing_products mp - JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id - JOIN warehouses w ON w.id = pw.warehouse_id - WHERE mp.marketing_id = marketings.id - AND w.name ILIKE ? - ) - )`, searchPattern, searchPattern, searchPattern, searchPattern, searchPattern) - } - if len(params.ProductIDs) > 0 { db = db.Where(`EXISTS ( SELECT 1 @@ -208,6 +269,9 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryO db = db.Where("marketings.customer_id = ?", params.CustomerId) } + db = s.applyMarketingProjectFlockFilter(c.Context(), db, params.ProjectFlockID, params.ProjectFlockKandangID) + db = s.applyMarketingSearchFilter(c.Context(), db, params.Search) + if scope.Restrict { if len(scope.IDs) == 0 { return db.Where("1 = 0") diff --git a/internal/modules/marketing/validations/deliveryorder.validation.go b/internal/modules/marketing/validations/deliveryorder.validation.go index 20719d55..5a53c174 100644 --- a/internal/modules/marketing/validations/deliveryorder.validation.go +++ b/internal/modules/marketing/validations/deliveryorder.validation.go @@ -22,13 +22,15 @@ type DeliveryOrderUpdate struct { } type DeliveryOrderQuery struct { - Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` - Search string `query:"search" validate:"omitempty,max=100"` - ProductIDs []uint `query:"product_ids" validate:"omitempty,dive,gt=0"` - Status string `query:"status" validate:"omitempty,max=50"` - CustomerId uint `query:"customer_id" validate:"omitempty,gt=0"` - MarketingId uint `query:"marketing_id" validate:"omitempty,gt=0"` + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + Search string `query:"search" validate:"omitempty,max=100"` + ProductIDs []uint `query:"product_ids" validate:"omitempty,dive,gt=0"` + Status string `query:"status" validate:"omitempty,max=50"` + CustomerId uint `query:"customer_id" validate:"omitempty,gt=0"` + MarketingId uint `query:"marketing_id" validate:"omitempty,gt=0"` + ProjectFlockID uint `query:"project_flock_id" validate:"omitempty,gt=0"` + ProjectFlockKandangID uint `query:"project_flock_kandang_id" validate:"omitempty,gt=0"` } type DeliveryOrderApprove struct { diff --git a/internal/modules/production/recordings/controllers/recording.controller.go b/internal/modules/production/recordings/controllers/recording.controller.go index 795874da..3bf55546 100644 --- a/internal/modules/production/recordings/controllers/recording.controller.go +++ b/internal/modules/production/recordings/controllers/recording.controller.go @@ -27,7 +27,7 @@ func NewRecordingController(recordingService service.RecordingService) *Recordin } func (u *RecordingController) GetAll(c *fiber.Ctx) error { - projectFlockID := c.QueryInt("project_flock_kandang_id", 0) + projectFlockKandangID := c.QueryInt("project_flock_kandang_id", 0) exportType := strings.TrimSpace(c.Query("export")) if exportprogress.IsProgressExportRequest(c) { @@ -54,13 +54,19 @@ func (u *RecordingController) GetAll(c *fiber.Ctx) error { offset := (page - 1) * limit query := &validation.Query{ - Page: page, - Limit: limit, - Offset: offset, - Search: c.Query("search"), + Page: page, + Limit: limit, + Offset: offset, + Search: strings.TrimSpace(c.Query("search")), + ProjectFlockId: uint(c.QueryInt("project_flock_id", 0)), + AreaId: uint(c.QueryInt("area_id", 0)), + LocationId: uint(c.QueryInt("location_id", 0)), + KandangId: uint(c.QueryInt("kandang_id", 0)), + ProjectFlockCategory: strings.TrimSpace(c.Query("project_flock_category")), + ApprovalStatus: strings.TrimSpace(c.Query("approval_status")), } - if projectFlockID > 0 { - query.ProjectFlockKandangId = uint(projectFlockID) + if projectFlockKandangID > 0 { + query.ProjectFlockKandangId = uint(projectFlockKandangID) } result, totalResults, err := u.RecordingService.GetAll(c, query) diff --git a/internal/modules/production/recordings/repositories/recording.repository.go b/internal/modules/production/recordings/repositories/recording.repository.go index bddddfda..8b58b8a3 100644 --- a/internal/modules/production/recordings/repositories/recording.repository.go +++ b/internal/modules/production/recordings/repositories/recording.repository.go @@ -11,6 +11,8 @@ import ( "gitlab.com/mbugroup/lti-api.git/internal/common/exportprogress" "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations" + "gitlab.com/mbugroup/lti-api.git/internal/utils" "gorm.io/gorm" ) @@ -19,10 +21,10 @@ type RecordingRepository interface { WithRelations(db *gorm.DB) *gorm.DB WithRelationsList(db *gorm.DB) *gorm.DB - ApplyListFilters(db *gorm.DB, search string, projectFlockKandangId uint) *gorm.DB - ApplyListCountFilters(db *gorm.DB, search string, projectFlockKandangId uint) *gorm.DB + ApplyListFilters(db *gorm.DB, params *validation.Query) *gorm.DB + ApplyListCountFilters(db *gorm.DB, params *validation.Query) *gorm.DB ApplySearchFilters(db *gorm.DB, rawSearch string) *gorm.DB - GetAllWithFilters(ctx context.Context, offset, limit int, search string, projectFlockKandangId uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.Recording, int64, error) + GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query, modifier func(*gorm.DB) *gorm.DB) ([]entity.Recording, int64, error) GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error) ListByProjectFlockKandangID(ctx context.Context, tx *gorm.DB, projectFlockKandangId uint, from *time.Time) ([]entity.Recording, error) GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error) @@ -147,36 +149,89 @@ func (r *RecordingRepositoryImpl) WithRelationsList(db *gorm.DB) *gorm.DB { Preload("ProjectFlockKandang.ProjectFlock.ProductionStandard") } -func (r *RecordingRepositoryImpl) ApplyListFilters(db *gorm.DB, search string, projectFlockKandangId uint) *gorm.DB { +func (r *RecordingRepositoryImpl) latestApprovalSubQuery(db *gorm.DB) *gorm.DB { + return db.Session(&gorm.Session{NewDB: true}). + Table("approvals"). + Select("DISTINCT ON (approvable_id) approvable_id, action, step_name, notes"). + Where("approvable_type = ?", utils.ApprovalWorkflowRecording.String()). + Order("approvable_id, action_at DESC, id DESC") +} + +func (r *RecordingRepositoryImpl) applyStructuredListFilters(db *gorm.DB, params *validation.Query) *gorm.DB { + if params == nil { + return db + } + + if params.ProjectFlockKandangId != 0 { + db = db.Where("recordings.project_flock_kandangs_id = ?", params.ProjectFlockKandangId) + } + if params.ProjectFlockId != 0 { + db = db.Where("pfk.project_flock_id = ?", params.ProjectFlockId) + } + if params.KandangId != 0 { + db = db.Where("pfk.kandang_id = ?", params.KandangId) + } + if params.LocationId != 0 { + db = db.Where("pf.location_id = ?", params.LocationId) + } + if params.AreaId != 0 { + db = db.Where("pf.area_id = ?", params.AreaId) + } + if params.ProjectFlockCategory != "" { + db = db.Where("UPPER(COALESCE(pf.category, '')) = ?", strings.ToUpper(strings.TrimSpace(params.ProjectFlockCategory))) + } + if params.ApprovalStatus != "" { + db = db.Where( + `EXISTS ( + SELECT 1 + FROM (?) AS latest_approval + WHERE latest_approval.approvable_id = recordings.id + AND UPPER(COALESCE(CAST(latest_approval.action AS TEXT), '')) = ? + )`, + r.latestApprovalSubQuery(db), + strings.ToUpper(strings.TrimSpace(params.ApprovalStatus)), + ) + } + + return db +} + +func (r *RecordingRepositoryImpl) ApplyListFilters(db *gorm.DB, params *validation.Query) *gorm.DB { + search := "" + if params != nil { + search = params.Search + } db = r.WithRelationsList(db) db = db. Joins("JOIN project_flock_kandangs pfk ON pfk.id = recordings.project_flock_kandangs_id"). - Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id") - if projectFlockKandangId != 0 { - db = db.Where("recordings.project_flock_kandangs_id = ?", projectFlockKandangId) - } + Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id"). + Joins("LEFT JOIN kandangs k ON k.id = pfk.kandang_id") + db = r.applyStructuredListFilters(db, params) db = r.ApplySearchFilters(db, search) return db.Order("recordings.record_datetime DESC").Order("recordings.created_at DESC") } -func (r *RecordingRepositoryImpl) ApplyListCountFilters(db *gorm.DB, search string, projectFlockKandangId uint) *gorm.DB { +func (r *RecordingRepositoryImpl) ApplyListCountFilters(db *gorm.DB, params *validation.Query) *gorm.DB { + search := "" + if params != nil { + search = params.Search + } db = db. Joins("JOIN project_flock_kandangs pfk ON pfk.id = recordings.project_flock_kandangs_id"). - Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id") - if projectFlockKandangId != 0 { - db = db.Where("recordings.project_flock_kandangs_id = ?", projectFlockKandangId) - } + Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id"). + Joins("LEFT JOIN kandangs k ON k.id = pfk.kandang_id") + db = r.applyStructuredListFilters(db, params) db = r.ApplySearchFilters(db, search) return db } -func (r *RecordingRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, search string, projectFlockKandangId uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.Recording, int64, error) { +func (r *RecordingRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query, modifier func(*gorm.DB) *gorm.DB) ([]entity.Recording, int64, error) { var ( records []entity.Recording total int64 ) - countQ := r.ApplyListCountFilters(r.DB().WithContext(ctx).Model(&entity.Recording{}), search, projectFlockKandangId) + countQ := r.ApplyListCountFilters(r.DB().WithContext(ctx).Model(&entity.Recording{}), params) if modifier != nil { countQ = modifier(countQ) } @@ -184,7 +239,7 @@ func (r *RecordingRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, return nil, 0, err } - listQ := r.ApplyListFilters(r.DB().WithContext(ctx).Model(&entity.Recording{}), search, projectFlockKandangId) + listQ := r.ApplyListFilters(r.DB().WithContext(ctx).Model(&entity.Recording{}), params) if modifier != nil { listQ = modifier(listQ) } @@ -218,6 +273,8 @@ func (r *RecordingRepositoryImpl) ApplySearchFilters(db *gorm.DB, rawSearch stri Joins("LEFT JOIN warehouses ws ON ws.id = pws.warehouse_id"). Joins("LEFT JOIN warehouses wd ON wd.id = pwd.warehouse_id"). Joins("LEFT JOIN warehouses we ON we.id = pwe.warehouse_id"). + Joins("LEFT JOIN users cu ON cu.id = recordings.created_by"). + Joins("LEFT JOIN (?) AS latest_approval ON latest_approval.approvable_id = recordings.id", r.latestApprovalSubQuery(db)). Where(` LOWER(pf.flock_name) LIKE ? OR LOWER(k.name) LIKE ? @@ -225,8 +282,12 @@ func (r *RecordingRepositoryImpl) ApplySearchFilters(db *gorm.DB, rawSearch stri OR LOWER(l.address) LIKE ? OR LOWER(ws.name) LIKE ? OR LOWER(wd.name) LIKE ? - OR LOWER(we.name) LIKE ?`, - likeQuery, likeQuery, likeQuery, likeQuery, likeQuery, likeQuery, likeQuery, + OR LOWER(we.name) LIKE ? + OR LOWER(COALESCE(cu.name, '')) LIKE ? + OR LOWER(COALESCE(latest_approval.step_name, '')) LIKE ? + OR LOWER(COALESCE(CAST(latest_approval.action AS TEXT), '')) LIKE ? + OR LOWER(COALESCE(latest_approval.notes, '')) LIKE ?`, + likeQuery, likeQuery, likeQuery, likeQuery, likeQuery, likeQuery, likeQuery, likeQuery, likeQuery, likeQuery, likeQuery, ) return db.Where("recordings.id IN (?)", subQuery) } diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index 18bf2a01..55315a16 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -116,8 +116,7 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti c.Context(), params.Offset, params.Limit, - params.Search, - params.ProjectFlockKandangId, + params, func(db *gorm.DB) *gorm.DB { db, scopeErr = m.ApplyLocationScope(c, db, "pf.location_id") return db @@ -450,12 +449,12 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent return err } - mappedStocks := recordingutil.MapStocks(createdRecording.Id, stockOwnerProjectFlockKandangID, req.Stocks) - stockDesired := resetStockQuantitiesForFIFO(mappedStocks) - if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil { - s.Log.Errorf("Failed to persist stocks: %+v", err) - return err - } + mappedStocks := recordingutil.MapStocks(createdRecording.Id, stockOwnerProjectFlockKandangID, req.Stocks) + stockDesired := resetStockQuantitiesForFIFO(mappedStocks) + if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil { + s.Log.Errorf("Failed to persist stocks: %+v", err) + return err + } for i := range mappedStocks { if i >= len(stockDesired) { @@ -519,14 +518,14 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent s.Log.Errorf("Failed to compute recording metrics: %+v", err) return err } - if err := s.recalculateFrom(ctx, tx, createdRecording.ProjectFlockKandangId, createdRecording.RecordDatetime); err != nil { - s.Log.Errorf("Failed to recalculate recordings after create: %+v", err) - return err - } - if err := s.syncFarmDepreciationManualInputFromRecordingStocks(ctx, tx, createdRecording.ProjectFlockKandangId, createdRecording.RecordDatetime); err != nil { - s.Log.Errorf("Failed to sync farm depreciation manual input after create: %+v", err) - return err - } + if err := s.recalculateFrom(ctx, tx, createdRecording.ProjectFlockKandangId, createdRecording.RecordDatetime); err != nil { + s.Log.Errorf("Failed to recalculate recordings after create: %+v", err) + return err + } + if err := s.syncFarmDepreciationManualInputFromRecordingStocks(ctx, tx, createdRecording.ProjectFlockKandangId, createdRecording.RecordDatetime); err != nil { + s.Log.Errorf("Failed to sync farm depreciation manual input after create: %+v", err) + return err + } action := entity.ApprovalActionCreated if err := s.createRecordingApproval(ctx, tx, createdRecording.Id, utils.RecordingStepPengajuan, action, createdRecording.CreatedBy, nil); err != nil { @@ -587,8 +586,8 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin return err } - recordingEntity = recording - pfkForRoute := recordingEntity.ProjectFlockKandang + recordingEntity = recording + pfkForRoute := recordingEntity.ProjectFlockKandang if pfkForRoute == nil || pfkForRoute.Id == 0 { fetchedPfk, fetchErr := s.ProjectFlockKandangRepo.GetByIDLight(ctx, recordingEntity.ProjectFlockKandangId) if fetchErr != nil { @@ -599,43 +598,43 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin return fetchErr } pfkForRoute = fetchedPfk - } - if err := s.tryAutoExecuteTransferForRecordingCreate(c, pfkForRoute, recordingEntity.RecordDatetime); err != nil { - return err - } - routePayload := buildRecordingRoutePayloadFromUpdate(req) - if err := s.enforceTransferRecordingRoute(ctx, pfkForRoute, recordingEntity.RecordDatetime, routePayload); err != nil { - return err - } + } + if err := s.tryAutoExecuteTransferForRecordingCreate(c, pfkForRoute, recordingEntity.RecordDatetime); err != nil { + return err + } + routePayload := buildRecordingRoutePayloadFromUpdate(req) + if err := s.enforceTransferRecordingRoute(ctx, pfkForRoute, recordingEntity.RecordDatetime, routePayload); err != nil { + return err + } hasStockChanges := req.Stocks != nil hasDepletionChanges := req.Depletions != nil hasEggChanges := req.Eggs != nil - var existingStocks []entity.RecordingStock - var existingDepletions []entity.RecordingDepletion - var existingEggs []entity.RecordingEgg - var mappedDepletions []entity.RecordingDepletion - var stockOwnerProjectFlockKandangID *uint + var existingStocks []entity.RecordingStock + var existingDepletions []entity.RecordingDepletion + var existingEggs []entity.RecordingEgg + var mappedDepletions []entity.RecordingDepletion + var stockOwnerProjectFlockKandangID *uint note := recordingutil.RecordingNote("Edit", recordingEntity.Id) - if hasStockChanges { - stockOwnerProjectFlockKandangID, err = s.resolveRecordingStockOwnerProjectFlockKandangID(ctx, pfkForRoute, recordingEntity.RecordDatetime) - if err != nil { - return err - } - existingStocks, err = s.Repository.ListStocks(tx, recordingEntity.Id) - if err != nil { - s.Log.Errorf("Failed to list existing stocks: %+v", err) - return err - } + if hasStockChanges { + stockOwnerProjectFlockKandangID, err = s.resolveRecordingStockOwnerProjectFlockKandangID(ctx, pfkForRoute, recordingEntity.RecordDatetime) + if err != nil { + return err + } + existingStocks, err = s.Repository.ListStocks(tx, recordingEntity.Id) + if err != nil { + s.Log.Errorf("Failed to list existing stocks: %+v", err) + return err + } existingUsage := recordingutil.StockUsageByWarehouse(existingStocks) incomingUsage := recordingutil.StockUsageByWarehouseReq(req.Stocks) - match := recordingutil.FloatMapsEqual(existingUsage, incomingUsage) && recordingStocksAllOwnedBy(existingStocks, stockOwnerProjectFlockKandangID) - if match { - hasStockChanges = false - } else { + match := recordingutil.FloatMapsEqual(existingUsage, incomingUsage) && recordingStocksAllOwnedBy(existingStocks, stockOwnerProjectFlockKandangID) + if match { + hasStockChanges = false + } else { if err := s.ensureProductWarehousesExist(c, req.Stocks, nil, nil); err != nil { return err } @@ -643,11 +642,11 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin if err := s.ensureProductWarehousesByFlags(ctx, feedIDs, []string{"PAKAN", "OVK"}, "feed"); err != nil { return err } - if err := s.reflowSyncRecordingStocks(ctx, tx, recordingEntity.Id, existingStocks, req.Stocks, stockOwnerProjectFlockKandangID, note, actorID); err != nil { - return err - } + if err := s.reflowSyncRecordingStocks(ctx, tx, recordingEntity.Id, existingStocks, req.Stocks, stockOwnerProjectFlockKandangID, note, actorID); err != nil { + return err } } + } if hasDepletionChanges { existingDepletions, err = s.Repository.ListDepletions(tx, recordingEntity.Id) @@ -809,22 +808,22 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin return err } - if hasStockChanges || hasDepletionChanges || hasEggChanges { + if hasStockChanges || hasDepletionChanges || hasEggChanges { if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil { s.Log.Errorf("Failed to recompute recording metrics: %+v", err) return err } - if err := s.recalculateFrom(ctx, tx, recordingEntity.ProjectFlockKandangId, recordingEntity.RecordDatetime); err != nil { - s.Log.Errorf("Failed to recalculate recordings after update: %+v", err) - return err - } + if err := s.recalculateFrom(ctx, tx, recordingEntity.ProjectFlockKandangId, recordingEntity.RecordDatetime); err != nil { + s.Log.Errorf("Failed to recalculate recordings after update: %+v", err) + return err } - if hasStockChanges { - if err := s.syncFarmDepreciationManualInputFromRecordingStocks(ctx, tx, recordingEntity.ProjectFlockKandangId, recordingEntity.RecordDatetime); err != nil { - s.Log.Errorf("Failed to sync farm depreciation manual input after update: %+v", err) - return err - } + } + if hasStockChanges { + if err := s.syncFarmDepreciationManualInputFromRecordingStocks(ctx, tx, recordingEntity.ProjectFlockKandangId, recordingEntity.RecordDatetime); err != nil { + s.Log.Errorf("Failed to sync farm depreciation manual input after update: %+v", err) + return err } + } action := entity.ApprovalActionUpdated actorID := recordingEntity.CreatedBy @@ -1082,15 +1081,15 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error { return err } - if err := s.recalculateFrom(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime); err != nil { - s.Log.Errorf("Failed to recalculate recordings after delete: %+v", err) - return err - } - if err := s.syncFarmDepreciationManualInputFromRecordingStocks(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime); err != nil { - s.Log.Errorf("Failed to sync farm depreciation manual input after delete: %+v", err) - return err - } - s.invalidateDepreciationSnapshots(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime) + if err := s.recalculateFrom(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime); err != nil { + s.Log.Errorf("Failed to recalculate recordings after delete: %+v", err) + return err + } + if err := s.syncFarmDepreciationManualInputFromRecordingStocks(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime); err != nil { + s.Log.Errorf("Failed to sync farm depreciation manual input after delete: %+v", err) + return err + } + s.invalidateDepreciationSnapshots(ctx, tx, recording.ProjectFlockKandangId, recording.RecordDatetime) return nil }) @@ -2905,32 +2904,32 @@ func (s *recordingService) reflowSyncRecordingStocks( if len(list) > 0 { stock = list[0] existingByWarehouse[item.ProductWarehouseId] = list[1:] - } else { - zero := 0.0 - stock = entity.RecordingStock{ - RecordingId: recordingID, - ProductWarehouseId: item.ProductWarehouseId, - ProjectFlockKandangId: ownerProjectFlockKandangID, - UsageQty: &zero, - PendingQty: &zero, - } - if err := s.Repository.CreateStock(tx, &stock); err != nil { - return err - } + } else { + zero := 0.0 + stock = entity.RecordingStock{ + RecordingId: recordingID, + ProductWarehouseId: item.ProductWarehouseId, + ProjectFlockKandangId: ownerProjectFlockKandangID, + UsageQty: &zero, + PendingQty: &zero, } - stock.ProjectFlockKandangId = ownerProjectFlockKandangID - if stock.Id != 0 { - if err := tx.Model(&entity.RecordingStock{}). - Where("id = ?", stock.Id). - Updates(map[string]any{ - "project_flock_kandang_id": ownerProjectFlockKandangID, - }).Error; err != nil { - return err - } + if err := s.Repository.CreateStock(tx, &stock); err != nil { + return err } + } + stock.ProjectFlockKandangId = ownerProjectFlockKandangID + if stock.Id != 0 { + if err := tx.Model(&entity.RecordingStock{}). + Where("id = ?", stock.Id). + Updates(map[string]any{ + "project_flock_kandang_id": ownerProjectFlockKandangID, + }).Error; err != nil { + return err + } + } - desired := item.Qty - stock.UsageQty = &desired + desired := item.Qty + stock.UsageQty = &desired zero := 0.0 stock.PendingQty = &zero stocksToApply = append(stocksToApply, stock) diff --git a/internal/modules/production/recordings/validations/recording.validation.go b/internal/modules/production/recordings/validations/recording.validation.go index da2ca38f..73d4fdef 100644 --- a/internal/modules/production/recordings/validations/recording.validation.go +++ b/internal/modules/production/recordings/validations/recording.validation.go @@ -39,8 +39,14 @@ type Query struct { Page int `query:"page" validate:"omitempty,number,min=1"` Limit int `query:"limit" validate:"omitempty,number,min=1"` Offset int `query:"-" validate:"omitempty,number,min=0"` + ProjectFlockId uint `query:"project_flock_id" validate:"omitempty,number,min=1"` ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"` - Search string `query:"search" validate:"omitempty,max=50"` + AreaId uint `query:"area_id" validate:"omitempty,number,min=1"` + LocationId uint `query:"location_id" validate:"omitempty,number,min=1"` + KandangId uint `query:"kandang_id" validate:"omitempty,number,min=1"` + ProjectFlockCategory string `query:"project_flock_category" validate:"omitempty,oneof=GROWING LAYING"` + ApprovalStatus string `query:"approval_status" validate:"omitempty,max=50"` + Search string `query:"search" validate:"omitempty,max=100"` } type Approve struct { diff --git a/internal/modules/purchases/controllers/purchase.controller.go b/internal/modules/purchases/controllers/purchase.controller.go index 1616acbf..6c627cb2 100644 --- a/internal/modules/purchases/controllers/purchase.controller.go +++ b/internal/modules/purchases/controllers/purchase.controller.go @@ -87,19 +87,21 @@ func (ctrl *PurchaseController) GetAll(c *fiber.Ctx) error { func buildPurchaseQuery(c *fiber.Ctx) *validation.Query { return &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)), - AreaID: uint(c.QueryInt("area_id", 0)), - LocationID: uint(c.QueryInt("location_id", 0)), - ProductCategoryID: strings.TrimSpace(c.Query("product_category_id")), + 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)), + AreaID: uint(c.QueryInt("area_id", 0)), + LocationID: uint(c.QueryInt("location_id", 0)), + ProjectFlockID: uint(c.QueryInt("project_flock_id", 0)), + ProjectFlockKandangID: uint(c.QueryInt("project_flock_kandang_id", 0)), + ProductCategoryID: strings.TrimSpace(c.Query("product_category_id")), } } diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index 486144c5..5703cb8e 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -247,7 +247,7 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti if len(productCategoryIDs) > 0 { db = db.Where( `EXISTS ( - SELECT 1 + 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 IN ? @@ -256,183 +256,9 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti ) } - 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 != "" { - 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 ? - ) - OR EXISTS ( - SELECT 1 - FROM purchase_items pi - JOIN warehouses w ON w.id = pi.warehouse_id - JOIN locations l ON l.id = w.location_id - WHERE pi.purchase_id = purchases.id - AND LOWER(COALESCE(l.name, '')) LIKE ? - ) - OR EXISTS ( - SELECT 1 - FROM purchase_items pi - JOIN expense_nonstocks en ON en.id = pi.expense_nonstock_id - JOIN expenses e ON e.id = en.expense_id - WHERE pi.purchase_id = purchases.id - AND LOWER(COALESCE(e.reference_number, '')) LIKE ? - ) - )`, - like, - like, - like, - like, - like, - like, - like, - ) - } - - 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 != "" { - 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 ? - ) - OR EXISTS ( - SELECT 1 - FROM purchase_items pi - JOIN warehouses w ON w.id = pi.warehouse_id - JOIN locations l ON l.id = w.location_id - WHERE pi.purchase_id = purchases.id - AND LOWER(COALESCE(l.name, '')) LIKE ? - ) - OR EXISTS ( - SELECT 1 - FROM purchase_items pi - JOIN expense_nonstocks en ON en.id = pi.expense_nonstock_id - JOIN expenses e ON e.id = en.expense_id - WHERE pi.purchase_id = purchases.id - AND LOWER(COALESCE(e.reference_number, '')) LIKE ? - ) - )`, - like, - like, - like, - like, - like, - like, - like, - ) - } + db = applyPurchaseProjectFlockFilter(db, params.ProjectFlockID, params.ProjectFlockKandangID) + db = applyPurchaseApprovalStatusFilter(db, approvalStatuses) + db = applyPurchaseSearchFilter(db, search) return db.Order("created_at DESC").Order("purchases.id DESC") }) @@ -2361,6 +2187,155 @@ func parsePoDateRangeForQuery(fromStr, toStr string) (*time.Time, *time.Time, er return fromPtr, toPtr, nil } +func applyPurchaseProjectFlockFilter(db *gorm.DB, projectFlockID, projectFlockKandangID uint) *gorm.DB { + if projectFlockID > 0 { + db = db.Where( + `EXISTS ( + SELECT 1 + FROM purchase_items pi + LEFT JOIN project_flock_kandangs pfk_explicit ON pfk_explicit.id = pi.project_flock_kandang_id + LEFT JOIN warehouses w ON w.id = pi.warehouse_id + LEFT JOIN project_flock_kandangs pfk_active ON pfk_active.kandang_id = w.kandang_id AND pfk_active.closed_at IS NULL + WHERE pi.purchase_id = purchases.id + AND COALESCE(pfk_explicit.project_flock_id, pfk_active.project_flock_id) = ? + )`, + projectFlockID, + ) + } + + if projectFlockKandangID > 0 { + db = db.Where( + `EXISTS ( + SELECT 1 + FROM purchase_items pi + LEFT JOIN warehouses w ON w.id = pi.warehouse_id + LEFT JOIN project_flock_kandangs pfk_active ON pfk_active.kandang_id = w.kandang_id AND pfk_active.closed_at IS NULL + WHERE pi.purchase_id = purchases.id + AND COALESCE(pi.project_flock_kandang_id, pfk_active.id) = ? + )`, + projectFlockKandangID, + ) + } + + return db +} + +func applyPurchaseApprovalStatusFilter(db *gorm.DB, approvalStatuses []string) *gorm.DB { + if len(approvalStatuses) == 0 { + return db + } + + 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 { + return db + } + + 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, + ) + + return db.Where(approvalQuery, approvalArgs...) +} + +func applyPurchaseSearchFilter(db *gorm.DB, search string) *gorm.DB { + if search == "" { + return db + } + + like := "%" + search + "%" + return 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 ? + ) + OR EXISTS ( + SELECT 1 + FROM purchase_items pi + JOIN warehouses w ON w.id = pi.warehouse_id + JOIN locations l ON l.id = w.location_id + WHERE pi.purchase_id = purchases.id + AND LOWER(COALESCE(l.name, '')) LIKE ? + ) + OR EXISTS ( + SELECT 1 + FROM purchase_items pi + LEFT JOIN project_flock_kandangs pfk_explicit ON pfk_explicit.id = pi.project_flock_kandang_id + LEFT JOIN warehouses w ON w.id = pi.warehouse_id + LEFT JOIN project_flock_kandangs pfk_active ON pfk_active.kandang_id = w.kandang_id AND pfk_active.closed_at IS NULL + LEFT JOIN project_flocks pf ON pf.id = COALESCE(pfk_explicit.project_flock_id, pfk_active.project_flock_id) + LEFT JOIN kandangs k ON k.id = COALESCE(pfk_explicit.kandang_id, pfk_active.kandang_id, w.kandang_id) + WHERE pi.purchase_id = purchases.id + AND ( + LOWER(COALESCE(pf.flock_name, '')) LIKE ? OR + LOWER(COALESCE(k.name, '')) LIKE ? + ) + ) + OR EXISTS ( + SELECT 1 + FROM purchase_items pi + JOIN expense_nonstocks en ON en.id = pi.expense_nonstock_id + JOIN expenses e ON e.id = en.expense_id + WHERE pi.purchase_id = purchases.id + AND LOWER(COALESCE(e.reference_number, '')) LIKE ? + ) + )`, + like, + like, + like, + like, + like, + like, + like, + like, + like, + ) +} + func normalizeApprovalStatusFilter(raw string) string { value := strings.ToLower(strings.TrimSpace(raw)) switch value { diff --git a/internal/modules/purchases/validations/purchase.validation.go b/internal/modules/purchases/validations/purchase.validation.go index b643501c..a16390ef 100644 --- a/internal/modules/purchases/validations/purchase.validation.go +++ b/internal/modules/purchases/validations/purchase.validation.go @@ -61,17 +61,19 @@ type DeletePurchaseItemsRequest struct { } type Query struct { - Page int `query:"page" validate:"omitempty,number,min=1"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` - 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 string `query:"product_category_id" validate:"omitempty,max=500"` - 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"` - Search string `query:"search" validate:"omitempty,max=100"` - CreatedFrom string `query:"created_from" validate:"omitempty,datetime=2006-01-02"` - CreatedTo string `query:"created_to" validate:"omitempty,datetime=2006-01-02"` + Page int `query:"page" validate:"omitempty,number,min=1"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` + 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"` + ProjectFlockID uint `query:"project_flock_id" validate:"omitempty,gt=0"` + ProjectFlockKandangID uint `query:"project_flock_kandang_id" validate:"omitempty,gt=0"` + ProductCategoryID string `query:"product_category_id" validate:"omitempty,max=500"` + 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"` + Search string `query:"search" validate:"omitempty,max=100"` + CreatedFrom string `query:"created_from" validate:"omitempty,datetime=2006-01-02"` + CreatedTo string `query:"created_to" validate:"omitempty,datetime=2006-01-02"` }