Merge branch 'codex/filter-improvement' into 'development'

feat: filter improvement

See merge request mbugroup/lti-api!442
This commit is contained in:
Adnan Zahir
2026-04-23 00:19:08 +07:00
13 changed files with 702 additions and 378 deletions
@@ -53,9 +53,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 isAllExpenseExcelExportRequest(c) {
@@ -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 {
@@ -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 {
@@ -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) {
@@ -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")
@@ -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 {
@@ -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)
@@ -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)
}
@@ -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)
@@ -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 {
@@ -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")),
}
}
@@ -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 {
@@ -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"`
}