From afe4b2ffe395ae05d42c436a7a139a632213aaf8 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 16 Dec 2025 21:10:48 +0700 Subject: [PATCH] feat[BE}: change get penjualan repport dto an add more params --- .../controllers/closing.controller.go | 22 ++ .../closings/dto/closingKeuangan.dto.go | 186 +++++++++++++ internal/modules/closings/route.go | 2 + .../closings/services/closing.service.go | 235 ++++++++++++++++ .../expense_realization.repository.go | 6 +- .../salesorder_delivery_product.repository.go | 119 ++++++-- .../controllers/repport.controller.go | 24 +- .../repports/dto/repportMarketing.dto.go | 260 ++++++------------ .../repports/services/repport.service.go | 112 ++++++-- .../validations/repport.validation.go | 22 +- 10 files changed, 741 insertions(+), 247 deletions(-) create mode 100644 internal/modules/closings/dto/closingKeuangan.dto.go diff --git a/internal/modules/closings/controllers/closing.controller.go b/internal/modules/closings/controllers/closing.controller.go index a04fc5f9..bca2f9cb 100644 --- a/internal/modules/closings/controllers/closing.controller.go +++ b/internal/modules/closings/controllers/closing.controller.go @@ -245,3 +245,25 @@ func (u *ClosingController) GetSapronakByKandang(c *fiber.Ctx) error { Data: payload, }) } + +func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error { + param := c.Params("project_flock_id") + + projectFlockID, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id") + } + + result, err := u.ClosingService.GetClosingKeuangan(c, uint(projectFlockID)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get closing keuangan successfully", + Data: result, + }) +} diff --git a/internal/modules/closings/dto/closingKeuangan.dto.go b/internal/modules/closings/dto/closingKeuangan.dto.go new file mode 100644 index 00000000..d380dc3d --- /dev/null +++ b/internal/modules/closings/dto/closingKeuangan.dto.go @@ -0,0 +1,186 @@ +package dto + +// === BASE METRICS === +type FinancialMetrics struct { + RpPerBird float64 `json:"rp_per_bird"` + RpPerKg float64 `json:"rp_per_kg"` + Amount float64 `json:"amount"` +} + +type Comparison struct { + Budgeting FinancialMetrics `json:"budgeting"` + Realization FinancialMetrics `json:"realization"` +} + +// === HPP PURCHASES PACKAGE === +type HppItem struct { + Type string `json:"type"` + Comparison +} + +type HppGroup struct { + GroupName string `json:"group_name"` + Data []HppItem `json:"data"` +} + +type SummaryHpp struct { + Label string `json:"label"` + Comparison +} + +// Ini adalah struct mandiri untuk bagian HPP Purchases +type HppPurchasesSection struct { + Title string `json:"title"` + Hpp []HppGroup `json:"hpp"` + SummaryHpp SummaryHpp `json:"summary_hpp"` +} + +// === PROFIT LOSS PACKAGE === +type PLItem struct { + Type string `json:"type"` + FinancialMetrics +} + +type PLSummaryItem struct { + Label string `json:"label"` + FinancialMetrics +} + +type PLSummaryGroup struct { + GrossProfit PLSummaryItem `json:"gross_profit"` + SubTotal PLSummaryItem `json:"sub_total"` + NetProfit PLSummaryItem `json:"net_profit"` +} + +type ProfitLossData struct { + Penjualan []PLItem `json:"penjualan"` + Pembelian []PLItem `json:"pembelian"` + Summary PLSummaryGroup `json:"summary"` +} + +// Ini adalah struct mandiri untuk bagian Profit Loss +type ProfitLossSection struct { + Title string `json:"title"` + Data ProfitLossData `json:"data"` +} + +// === RESPONSE DTO (ROOT) === +// Sekarang Root-nya terlihat sangat bersih dan tidak "janggal" lagi +type ReportResponse struct { + HppPurchases HppPurchasesSection `json:"hpp_purchases"` + ProfitLoss ProfitLossSection `json:"profit_loss"` +} + +// === MAPPER FUNCTIONS === + +// FinancialMetrics Mappers +func ToFinancialMetrics(rpPerBird, rpPerKg, amount float64) FinancialMetrics { + return FinancialMetrics{ + RpPerBird: rpPerBird, + RpPerKg: rpPerKg, + Amount: amount, + } +} + +// Comparison Mappers +func ToComparison(budgeting, realization FinancialMetrics) Comparison { + return Comparison{ + Budgeting: budgeting, + Realization: realization, + } +} + +// HppItem Mappers +func ToHppItem(itemType string, comparison Comparison) HppItem { + return HppItem{ + Type: itemType, + Comparison: comparison, + } +} + +// HppGroup Mappers +func ToHppGroup(groupName string, items []HppItem) HppGroup { + return HppGroup{ + GroupName: groupName, + Data: items, + } +} + +// SummaryHpp Mappers +func ToSummaryHpp(label string, comparison Comparison) SummaryHpp { + return SummaryHpp{ + Label: label, + Comparison: comparison, + } +} + +// HppPurchasesSection Mappers +func ToHppPurchasesSection(title string, hppGroups []HppGroup, summaryHpp SummaryHpp) HppPurchasesSection { + return HppPurchasesSection{ + Title: title, + Hpp: hppGroups, + SummaryHpp: summaryHpp, + } +} + +// PLItem Mappers +func ToPLItem(itemType string, metrics FinancialMetrics) PLItem { + return PLItem{ + Type: itemType, + FinancialMetrics: metrics, + } +} + +// PLSummaryItem Mappers +func ToPLSummaryItem(label string, metrics FinancialMetrics) PLSummaryItem { + return PLSummaryItem{ + Label: label, + FinancialMetrics: metrics, + } +} + +// PLSummaryGroup Mappers +func ToPLSummaryGroup(grossProfit, subTotal, netProfit PLSummaryItem) PLSummaryGroup { + return PLSummaryGroup{ + GrossProfit: grossProfit, + SubTotal: subTotal, + NetProfit: netProfit, + } +} + +// ProfitLossData Mappers +func ToProfitLossData(penjualan, pembelian []PLItem, summary PLSummaryGroup) ProfitLossData { + return ProfitLossData{ + Penjualan: penjualan, + Pembelian: pembelian, + Summary: summary, + } +} + +// ProfitLossSection Mappers +func ToProfitLossSection(title string, data ProfitLossData) ProfitLossSection { + return ProfitLossSection{ + Title: title, + Data: data, + } +} + +// ReportResponse Mappers +func ToReportResponse(hppPurchases HppPurchasesSection, profitLoss ProfitLossSection) ReportResponse { + return ReportResponse{ + HppPurchases: hppPurchases, + ProfitLoss: profitLoss, + } +} + +// Helper function to create a complete financial report +func BuildFinancialReport( + hppGroups []HppGroup, + summaryHpp SummaryHpp, + penjualan, pembelian []PLItem, + plSummary PLSummaryGroup, +) ReportResponse { + hppSection := ToHppPurchasesSection("HPP Pembelian", hppGroups, summaryHpp) + plSection := ToProfitLossSection("Laporan Laba Rugi", ToProfitLossData(penjualan, pembelian, plSummary)) + return ToReportResponse(hppSection, plSection) +} diff --git a/internal/modules/closings/route.go b/internal/modules/closings/route.go index a76e8b79..62998f2c 100644 --- a/internal/modules/closings/route.go +++ b/internal/modules/closings/route.go @@ -25,6 +25,8 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService route.Get("/:project_flock_id/overhead", ctrl.GetOverhead) route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", ctrl.GetSapronakByKandang) route.Get("/:project_flock_id/perhitungan_sapronak", ctrl.GetSapronakByProject) + route.Get("/:project_flock_id/keuangan", ctrl.GetClosingKeuangan) route.Get("/:projectFlockId", ctrl.GetClosingSummary) route.Get("/:projectFlockId/sapronak", ctrl.GetClosingSapronak) + } diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index b1780359..e6e74d45 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -31,6 +31,7 @@ type ClosingService interface { GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) GetOverhead(ctx *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error) GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) + GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) } type closingService struct { @@ -379,3 +380,237 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.Ove return &result, nil } + +func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) { + if projectFlockID == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") + } + + _, err := s.Repository.GetByID(c.Context(), projectFlockID, nil) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found") + } + if err != nil { + s.Log.Errorf("Failed to get project flock %d for closing keuangan: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") + } + + budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID) + if err != nil { + s.Log.Errorf("Failed to get budgets for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets") + } + + realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID) + if err != nil { + s.Log.Errorf("Failed to get realizations for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch realizations") + } + + deliveryProducts, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB { + return db.Preload("MarketingProduct"). + Preload("MarketingProduct.ProductWarehouse"). + Preload("MarketingProduct.ProductWarehouse.Product") + }) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + s.Log.Errorf("Failed to get delivery products for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery products") + } + + chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID) + if err != nil { + s.Log.Errorf("Failed to get chickins for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chickins") + } + + var totalPopulation float64 + for _, chickin := range chickins { + totalPopulation += chickin.UsageQty + } + + var totalWeightSold float64 + for _, delivery := range deliveryProducts { + totalWeightSold += delivery.TotalWeight + } + + hppItems := s.buildHppItems(budgets, realizations, totalWeightSold, totalPopulation) + hppGroups := []dto.HppGroup{ + dto.ToHppGroup("Input Produksi", hppItems), + } + + summaryHpp := s.calculateHppSummary(budgets, realizations, totalWeightSold, totalPopulation) + + penjualanItems := s.buildPenjualanItems(deliveryProducts, totalPopulation, totalWeightSold) + pembelianItems := s.buildPembelianItems(budgets, realizations, totalPopulation, totalWeightSold) + plSummary := s.calculatePLSummary(penjualanItems, pembelianItems) + + hppSection := dto.ToHppPurchasesSection("HPP Pembelian", hppGroups, summaryHpp) + plSection := dto.ToProfitLossSection("Laporan Laba Rugi", dto.ToProfitLossData(penjualanItems, pembelianItems, plSummary)) + + report := dto.ToReportResponse(hppSection, plSection) + + return &report, nil +} + +func (s closingService) buildHppItems(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalWeightSold, totalPopulation float64) []dto.HppItem { + var totalBudgetAmount float64 + var totalRealizationAmount float64 + + for _, budget := range budgets { + totalBudgetAmount += budget.Price * budget.Qty + } + + for _, realization := range realizations { + totalRealizationAmount += realization.Price * realization.Qty + } + + budgetRpPerBird := 0.0 + budgetRpPerKg := 0.0 + if totalPopulation > 0 { + budgetRpPerBird = totalBudgetAmount / totalPopulation + } + if totalWeightSold > 0 { + budgetRpPerKg = totalBudgetAmount / totalWeightSold + } + + realizationRpPerBird := 0.0 + realizationRpPerKg := 0.0 + if totalPopulation > 0 { + realizationRpPerBird = totalRealizationAmount / totalPopulation + } + if totalWeightSold > 0 { + realizationRpPerKg = totalRealizationAmount / totalWeightSold + } + + items := []dto.HppItem{ + dto.ToHppItem("Total HPP Produksi", dto.ToComparison( + dto.ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudgetAmount), + dto.ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealizationAmount), + )), + } + + return items +} + +func (s closingService) calculateHppSummary(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalWeightSold, totalPopulation float64) dto.SummaryHpp { + var totalBudget float64 + var totalRealization float64 + + for _, budget := range budgets { + totalBudget += budget.Price * budget.Qty + } + + for _, realization := range realizations { + totalRealization += realization.Price * realization.Qty + } + + budgetRpPerBird := 0.0 + budgetRpPerKg := 0.0 + if totalPopulation > 0 { + budgetRpPerBird = totalBudget / totalPopulation + } + if totalWeightSold > 0 { + budgetRpPerKg = totalBudget / totalWeightSold + } + + realizationRpPerBird := 0.0 + realizationRpPerKg := 0.0 + if totalPopulation > 0 { + realizationRpPerBird = totalRealization / totalPopulation + } + if totalWeightSold > 0 { + realizationRpPerKg = totalRealization / totalWeightSold + } + + return dto.ToSummaryHpp("Total HPP", dto.ToComparison( + dto.ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget), + dto.ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization), + )) +} + +func (s closingService) buildPenjualanItems(deliveryProducts []entity.MarketingDeliveryProduct, totalPopulation, totalWeightSold float64) []dto.PLItem { + var totalAmount float64 + + for _, delivery := range deliveryProducts { + totalAmount += delivery.TotalPrice + } + + rpPerBird := 0.0 + rpPerKg := 0.0 + if totalPopulation > 0 { + rpPerBird = totalAmount / totalPopulation + } + if totalWeightSold > 0 { + rpPerKg = totalAmount / totalWeightSold + } + + items := []dto.PLItem{ + dto.ToPLItem("Penjualan", dto.ToFinancialMetrics(rpPerBird, rpPerKg, totalAmount)), + } + + return items +} + +func (s closingService) buildPembelianItems(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalPopulation, totalWeightSold float64) []dto.PLItem { + var totalBudget float64 + var totalRealization float64 + + for _, budget := range budgets { + totalBudget += budget.Price * budget.Qty + } + + for _, realization := range realizations { + totalRealization += realization.Price * realization.Qty + } + + budgetRpPerBird := 0.0 + budgetRpPerKg := 0.0 + if totalPopulation > 0 { + budgetRpPerBird = totalBudget / totalPopulation + } + if totalWeightSold > 0 { + budgetRpPerKg = totalBudget / totalWeightSold + } + + realizationRpPerBird := 0.0 + realizationRpPerKg := 0.0 + if totalPopulation > 0 { + realizationRpPerBird = totalRealization / totalPopulation + } + if totalWeightSold > 0 { + realizationRpPerKg = totalRealization / totalWeightSold + } + + items := []dto.PLItem{ + dto.ToPLItem("Beban Pokok Produksi", dto.ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget)), + dto.ToPLItem("Realisasi Beban Pokok", dto.ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization)), + } + + return items +} + +func (s closingService) calculatePLSummary(penjualanItems, pembelianItems []dto.PLItem) dto.PLSummaryGroup { + var totalPenjualan float64 + var totalPenjualanPerBird float64 + var totalPembelian float64 + var totalPembelianPerBird float64 + + for _, item := range penjualanItems { + totalPenjualan += item.Amount + totalPenjualanPerBird += item.RpPerBird + } + + for _, item := range pembelianItems { + totalPembelian += item.Amount + totalPembelianPerBird += item.RpPerBird + } + + grossProfit := totalPenjualan - totalPembelian + grossProfitPerBird := totalPenjualanPerBird - totalPembelianPerBird + + return dto.ToPLSummaryGroup( + dto.ToPLSummaryItem("Laba Kotor", dto.ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)), + dto.ToPLSummaryItem("Sub Total", dto.ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)), + dto.ToPLSummaryItem("Laba Bersih", dto.ToFinancialMetrics(grossProfitPerBird, 0, grossProfit)), + ) +} diff --git a/internal/modules/expenses/repositories/expense_realization.repository.go b/internal/modules/expenses/repositories/expense_realization.repository.go index e4d57b79..d1931cdd 100644 --- a/internal/modules/expenses/repositories/expense_realization.repository.go +++ b/internal/modules/expenses/repositories/expense_realization.repository.go @@ -46,10 +46,10 @@ func (r *ExpenseRealizationRepositoryImpl) GetByProjectFlockID(ctx context.Conte Preload("ExpenseNonstock.Nonstock.Uom"). Preload("ExpenseNonstock.Expense"). Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id"). - Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = expense_nonstocks.project_flock_kandang_id"). Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id"). - Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). - Where("expenses.category = ?", "BOP"). + Joins("LEFT JOIN project_flock_kandangs ON project_flock_kandangs.id = expense_nonstocks.project_flock_kandang_id"). + Joins("LEFT JOIN kandangs ON kandangs.id = expense_nonstocks.kandang_id"). + Where("project_flock_kandangs.project_flock_id = ? OR kandangs.id IN (SELECT kandang_id FROM project_flock_kandangs WHERE project_flock_id = ?)", projectFlockID, projectFlockID). Find(&realizations).Error return realizations, err } diff --git a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go index 85d850a6..94d23103 100644 --- a/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go +++ b/internal/modules/marketing/repositories/salesorder_delivery_product.repository.go @@ -2,6 +2,7 @@ package repository import ( "context" + "strings" "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" @@ -31,8 +32,6 @@ func NewMarketingDeliveryProductRepository(db *gorm.DB) MarketingDeliveryProduct func (r *MarketingDeliveryProductRepositoryImpl) GetDeliveryProductsByProjectFlockID(ctx context.Context, projectFlockID uint, callback func(*gorm.DB) *gorm.DB) ([]entity.MarketingDeliveryProduct, error) { var deliveryProducts []entity.MarketingDeliveryProduct - // JOIN digunakan untuk filter WHERE clause ke ProjectFlockID yang berada 3 level relasi atas - // Entity relations digunakan di Preload (callback) untuk load data, bukan untuk filter db := r.DB().WithContext(ctx). Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id"). Joins("JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id"). @@ -91,16 +90,17 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C Preload("Marketing.SalesPerson"). Preload("ProductWarehouse"). Preload("ProductWarehouse.Product"). - Preload("ProductWarehouse.Warehouse") + Preload("ProductWarehouse.Warehouse"). + Preload("ProductWarehouse.ProjectFlockKandang") }). Joins("JOIN marketing_products ON marketing_products.id = marketing_delivery_products.marketing_product_id"). Joins("JOIN marketings ON marketings.id = marketing_products.marketing_id") - if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.ProjectFlockKandangId > 0 { + if filters.ProductId > 0 || filters.WarehouseId > 0 || filters.Search != "" { db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id") } - if filters.ProductId > 0 { + if filters.ProductId > 0 || filters.Search != "" { db = db.Joins("LEFT JOIN products ON products.id = product_warehouses.product_id") } @@ -109,8 +109,13 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C } if filters.Search != "" { - db = db.Where("marketing_delivery_products.vehicle_number ILIKE ?", - "%"+filters.Search+"%") + db = db.Joins("LEFT JOIN customers ON customers.id = marketings.customer_id") + } + + if filters.Search != "" { + searchPattern := "%" + filters.Search + "%" + db = db.Where("marketing_delivery_products.vehicle_number ILIKE ? OR marketings.so_number ILIKE ? OR customers.name ILIKE ? OR products.name ILIKE ?", + searchPattern, searchPattern, searchPattern, searchPattern) } if filters.CustomerId > 0 { @@ -121,10 +126,6 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C db = db.Where("marketings.sales_person_id = ?", filters.SalesPersonId) } - if filters.MarketingId > 0 { - db = db.Where("marketings.id = ?", filters.MarketingId) - } - if filters.ProductId > 0 { db = db.Where("product_warehouses.product_id = ?", filters.ProductId) } @@ -133,17 +134,90 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C db = db.Where("product_warehouses.warehouse_id = ?", filters.WarehouseId) } - if filters.ProjectFlockKandangId > 0 { - db = db.Where("product_warehouses.project_flock_kandang_id = ?", filters.ProjectFlockKandangId) - } - - if filters.DeliveryDate != "" { - if deliveryDate, err := utils.ParseDateString(filters.DeliveryDate); err == nil { - nextDate := deliveryDate.AddDate(0, 0, 1) - db = db.Where("marketing_delivery_products.delivery_date >= ? AND marketing_delivery_products.delivery_date < ?", deliveryDate, nextDate) + if filters.FilterBy != "" && (filters.StartDate != "" || filters.EndDate != "") { + if filters.FilterBy == "delivery_date" { + if filters.StartDate != "" { + if startDate, err := utils.ParseDateString(filters.StartDate); err == nil { + db = db.Where("marketing_delivery_products.delivery_date >= ?", startDate) + } + } + if filters.EndDate != "" { + if endDate, err := utils.ParseDateString(filters.EndDate); err == nil { + nextDate := endDate.AddDate(0, 0, 1) + db = db.Where("marketing_delivery_products.delivery_date < ?", nextDate) + } + } + } else if filters.FilterBy == "realization_date" { + if filters.StartDate != "" { + if startDate, err := utils.ParseDateString(filters.StartDate); err == nil { + db = db.Where("marketings.created_at >= ?", startDate) + } + } + if filters.EndDate != "" { + if endDate, err := utils.ParseDateString(filters.EndDate); err == nil { + nextDate := endDate.AddDate(0, 0, 1) + db = db.Where("marketings.created_at < ?", nextDate) + } + } } } + sortColumn := "marketing_delivery_products.id" + sortOrder := "DESC" + + if filters.SortBy != "" { + switch filters.SortBy { + case "delivery_date": + sortColumn = "marketing_delivery_products.delivery_date" + case "customer": + sortColumn = "customers.name" + if !containsJoin(db, "customers") { + db = db.Joins("LEFT JOIN customers ON customers.id = marketings.customer_id") + } + case "warehouse": + sortColumn = "warehouses.name" + if !containsJoin(db, "warehouses") { + db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id"). + Joins("LEFT JOIN warehouses ON warehouses.id = product_warehouses.warehouse_id") + } + case "product": + sortColumn = "products.name" + if !containsJoin(db, "products") { + db = db.Joins("LEFT JOIN product_warehouses ON product_warehouses.id = marketing_products.product_warehouse_id"). + Joins("LEFT JOIN products ON products.id = product_warehouses.product_id") + } + case "sales_person": + sortColumn = "sales_users.name" + if !containsJoin(db, "sales_users") { + db = db.Joins("LEFT JOIN users AS sales_users ON sales_users.id = marketings.sales_person_id") + } + case "vehicle_number": + sortColumn = "marketing_delivery_products.vehicle_number" + case "sales_amount": + sortColumn = "marketing_delivery_products.total_price" + case "hpp_amount": + sortColumn = "marketing_delivery_products.total_price" + case "qty": + sortColumn = "marketing_delivery_products.qty" + case "average_weight": + sortColumn = "marketing_delivery_products.avg_weight" + case "total_weight": + sortColumn = "marketing_delivery_products.total_weight" + case "sales_price": + sortColumn = "marketing_delivery_products.unit_price" + case "hpp_price": + sortColumn = "marketing_delivery_products.unit_price" + case "aging_days": + sortColumn = "marketing_delivery_products.delivery_date" + } + } + + if filters.SortOrder != "" && (filters.SortOrder == "asc" || filters.SortOrder == "desc") { + sortOrder = strings.ToUpper(filters.SortOrder) + } + + db = db.Order(sortColumn + " " + sortOrder) + if err := db.Count(&total).Error; err != nil { return nil, 0, err } @@ -151,10 +225,15 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetAllWithFilters(ctx context.C if err := db. Offset(offset). Limit(limit). - Order("marketing_delivery_products.id DESC"). Find(&deliveryProducts).Error; err != nil { return nil, 0, err } return deliveryProducts, total, nil } + +func containsJoin(db *gorm.DB, tableName string) bool { + statement := db.Statement + joinSQL := statement.SQL.String() + return strings.Contains(joinSQL, "JOIN "+tableName) +} diff --git a/internal/modules/repports/controllers/repport.controller.go b/internal/modules/repports/controllers/repport.controller.go index 21d3c49a..b94ec8c2 100644 --- a/internal/modules/repports/controllers/repport.controller.go +++ b/internal/modules/repports/controllers/repport.controller.go @@ -62,16 +62,18 @@ func (c *RepportController) GetExpense(ctx *fiber.Ctx) error { func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error { query := &validation.MarketingQuery{ - Page: ctx.QueryInt("page", 1), - Limit: ctx.QueryInt("limit", 10), - Search: ctx.Query("search", ""), - CustomerId: int64(ctx.QueryInt("customer_id", 0)), - ProjectFlockKandangId: int64(ctx.QueryInt("project_flock_kandang_id", 0)), - DeliveryDate: ctx.Query("delivery_date", ""), - ProductId: int64(ctx.QueryInt("product_id", 0)), - WarehouseId: int64(ctx.QueryInt("warehouse_id", 0)), - SalesPersonId: int64(ctx.QueryInt("sales_person_id", 0)), - MarketingId: int64(ctx.QueryInt("marketing_id", 0)), + Page: ctx.QueryInt("page", 1), + Limit: ctx.QueryInt("limit", 10), + Search: ctx.Query("search", ""), + CustomerId: int64(ctx.QueryInt("customer_id", 0)), + ProductId: int64(ctx.QueryInt("product_id", 0)), + WarehouseId: int64(ctx.QueryInt("warehouse_id", 0)), + SalesPersonId: int64(ctx.QueryInt("sales_person_id", 0)), + FilterBy: ctx.Query("filter_by", ""), + StartDate: ctx.Query("start_date", ""), + EndDate: ctx.Query("end_date", ""), + SortBy: ctx.Query("sort_by", ""), + SortOrder: ctx.Query("sort_order", ""), } if query.Page < 1 || query.Limit < 1 { @@ -84,7 +86,7 @@ func (c *RepportController) GetMarketing(ctx *fiber.Ctx) error { } return ctx.Status(fiber.StatusOK). - JSON(response.SuccessWithPaginate[dto.RepportMarketingListDTO]{ + JSON(response.SuccessWithPaginate[dto.RepportMarketingItemDTO]{ Code: fiber.StatusOK, Status: "success", Message: "Get marketing report successfully", diff --git a/internal/modules/repports/dto/repportMarketing.dto.go b/internal/modules/repports/dto/repportMarketing.dto.go index 9cbd57ba..77c5f5d8 100644 --- a/internal/modules/repports/dto/repportMarketing.dto.go +++ b/internal/modules/repports/dto/repportMarketing.dto.go @@ -1,219 +1,121 @@ package dto import ( - "time" - entity "gitlab.com/mbugroup/lti-api.git/internal/entities" - approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" - marketingDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto" customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" + warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) -// === DTO Structs === +// === Main Report Item DTO === -type RepportMarketingBaseDTO struct { - Id uint `json:"id"` - SoNumber string `json:"so_number"` - SoDate time.Time `json:"so_date"` - Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"` - SalesPerson *userDTO.UserRelationDTO `json:"sales_person,omitempty"` - Notes string `json:"notes"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` +type RepportMarketingItemDTO struct { + DoDate string `json:"do_date"` + RealizationDate string `json:"realization_date"` + AgingDays int `json:"aging_days"` + Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"` + Customer *customerDTO.CustomerRelationDTO `json:"customer,omitempty"` + DoNumber string `json:"do_number"` + Sales *userDTO.UserRelationDTO `json:"sales,omitempty"` + VehicleNumber string `json:"vehicle_number"` + Product *productDTO.ProductRelationDTO `json:"product,omitempty"` + MarketingType string `json:"marketing_type"` + Qty float64 `json:"qty"` + AverageWeightKg float64 `json:"average_weight_kg"` + TotalWeightKg float64 `json:"total_weight_kg"` + SalesPricePerKg float64 `json:"sales_price_per_kg"` + HppPricePerKg float64 `json:"hpp_price_per_kg"` + SalesAmount float64 `json:"sales_amount"` + HppAmount float64 `json:"hpp_amount"` } -type RepportMarketingProductDTO struct { - Id uint `json:"id"` - MarketingProductId uint `json:"marketing_product_id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - AvgWeight float64 `json:"avg_weight"` - TotalWeight float64 `json:"total_weight"` - TotalPrice float64 `json:"total_price"` - Product *productDTO.ProductRelationDTO `json:"product,omitempty"` - CreatedAt time.Time `json:"created_at"` -} +// === Report Response DTO === -type RepportMarketingDeliveryDTO struct { - Id uint `json:"id"` - MarketingProductId uint `json:"marketing_product_id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - TotalWeight float64 `json:"total_weight"` - AvgWeight float64 `json:"avg_weight"` - TotalPrice float64 `json:"total_price"` - DeliveryDate *time.Time `json:"delivery_date,omitempty"` - VehicleNumber string `json:"vehicle_number"` - DoNumber string `json:"do_number"` - Product *productDTO.ProductRelationDTO `json:"product,omitempty"` - CreatedAt time.Time `json:"created_at"` -} - -type RepportMarketingListDTO struct { - RepportMarketingBaseDTO - MarketingProduct RepportMarketingProductDTO `json:"marketing_product"` - MarketingDelivery RepportMarketingDeliveryDTO `json:"marketing_delivery"` - TotalMarketingProduct float64 `json:"total_marketing_product"` - TotalMarketingDelivery float64 `json:"total_marketing_delivery"` - LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval,omitempty"` +type RepportMarketingResponseDTO struct { + Items []RepportMarketingItemDTO `json:"items"` } // === MAPPERS === -func ToRepportMarketingBaseDTO(m *entity.Marketing) RepportMarketingBaseDTO { - if m == nil { - return RepportMarketingBaseDTO{} +// ToRepportMarketingItemDTO maps marketing delivery product to detailed report item +func ToRepportMarketingItemDTO(mdp entity.MarketingDeliveryProduct, hppPricePerKg float64) RepportMarketingItemDTO { + agingDays := 0 + + doDate := "" + if mdp.DeliveryDate != nil { + doDate = mdp.DeliveryDate.Format("02-Jan-2006") } - var customer *customerDTO.CustomerRelationDTO - if m.Customer.Id != 0 { - mapped := customerDTO.ToCustomerRelationDTO(m.Customer) - customer = &mapped + realizationDate := "" + if mdp.DeliveryDate != nil { + realizationDate = mdp.DeliveryDate.Format("02-Jan-2006") } - var salesPerson *userDTO.UserRelationDTO - if m.SalesPerson.Id != 0 { - mapped := userDTO.ToUserRelationDTO(m.SalesPerson) - salesPerson = &mapped + // Calculate sales_amount = total_weight_kg * sales_price_per_kg + salesAmount := mdp.TotalWeight * mdp.UnitPrice + // Calculate hpp_amount = total_weight_kg * hpp_price_per_kg + hppAmount := mdp.TotalWeight * hppPricePerKg + + item := RepportMarketingItemDTO{ + DoDate: doDate, + RealizationDate: realizationDate, + AgingDays: agingDays, + DoNumber: mdp.MarketingProduct.Marketing.SoNumber, + MarketingType: "ayam", + Qty: mdp.Qty, + AverageWeightKg: mdp.AvgWeight, + TotalWeightKg: mdp.TotalWeight, + SalesPricePerKg: mdp.UnitPrice, + HppPricePerKg: hppPricePerKg, + SalesAmount: salesAmount, + HppAmount: hppAmount, } - return RepportMarketingBaseDTO{ - Id: m.Id, - SoNumber: m.SoNumber, - SoDate: m.SoDate, - Customer: customer, - SalesPerson: salesPerson, - Notes: m.Notes, - CreatedAt: m.CreatedAt, - UpdatedAt: m.UpdatedAt, - } -} - -func ToRepportMarketingProductDTO(mp *entity.MarketingProduct) RepportMarketingProductDTO { - if mp == nil { - return RepportMarketingProductDTO{} + // Map warehouse with full details + if mdp.MarketingProduct.ProductWarehouse.WarehouseId != 0 { + mapped := warehouseDTO.ToWarehouseRelationDTO(mdp.MarketingProduct.ProductWarehouse.Warehouse) + item.Warehouse = &mapped } - var product *productDTO.ProductRelationDTO - if mp.ProductWarehouse.Product.Id != 0 { - mapped := productDTO.ToProductRelationDTO(mp.ProductWarehouse.Product) - product = &mapped + // Map customer using CustomerRelationDTO + if mdp.MarketingProduct.Marketing.CustomerId != 0 { + mapped := customerDTO.ToCustomerRelationDTO(mdp.MarketingProduct.Marketing.Customer) + item.Customer = &mapped } - return RepportMarketingProductDTO{ - Id: mp.Id, - MarketingProductId: mp.Id, - Qty: mp.Qty, - UnitPrice: mp.UnitPrice, - AvgWeight: mp.AvgWeight, - TotalWeight: mp.TotalWeight, - TotalPrice: mp.TotalPrice, - Product: product, - CreatedAt: time.Now(), - } -} - -func ToRepportMarketingDeliveryDTO(mdp *entity.MarketingDeliveryProduct, soNumber string) RepportMarketingDeliveryDTO { - if mdp == nil { - return RepportMarketingDeliveryDTO{} + // Map sales person + if mdp.MarketingProduct.Marketing.SalesPersonId != 0 { + mapped := userDTO.ToUserRelationDTO(mdp.MarketingProduct.Marketing.SalesPerson) + item.Sales = &mapped } - var product *productDTO.ProductRelationDTO - if mdp.MarketingProduct.ProductWarehouse.Product.Id != 0 { + // Map vehicle number + item.VehicleNumber = mdp.VehicleNumber + + // Map product using ProductRelationDTO + if mdp.MarketingProduct.ProductWarehouse.ProductId != 0 { mapped := productDTO.ToProductRelationDTO(mdp.MarketingProduct.ProductWarehouse.Product) - product = &mapped + item.Product = &mapped } - warehouseId := uint(0) - if mdp.MarketingProduct.ProductWarehouse.Id != 0 { - warehouseId = mdp.MarketingProduct.ProductWarehouse.WarehouseId - } - - doNumber := marketingDTO.GenerateDeliveryOrderNumber(soNumber, mdp.DeliveryDate, warehouseId) - - return RepportMarketingDeliveryDTO{ - Id: mdp.Id, - MarketingProductId: mdp.MarketingProductId, - Qty: mdp.Qty, - UnitPrice: mdp.UnitPrice, - TotalWeight: mdp.TotalWeight, - AvgWeight: mdp.AvgWeight, - TotalPrice: mdp.TotalPrice, - DeliveryDate: mdp.DeliveryDate, - VehicleNumber: mdp.VehicleNumber, - DoNumber: doNumber, - Product: product, - CreatedAt: time.Now(), - } + return item } -func ToRepportMarketingListDTO(baseDTO RepportMarketingBaseDTO, mp *entity.MarketingProduct, mdp *entity.MarketingDeliveryProduct, latestApproval *approvalDTO.ApprovalRelationDTO) RepportMarketingListDTO { - var marketingProduct RepportMarketingProductDTO - var marketingDelivery RepportMarketingDeliveryDTO - - if mp != nil { - marketingProduct = ToRepportMarketingProductDTO(mp) - } - - if mdp != nil { - marketingDelivery = ToRepportMarketingDeliveryDTO(mdp, baseDTO.SoNumber) - } - - totalMarketingProduct := float64(0) - totalMarketingDelivery := float64(0) - - if mp != nil { - totalMarketingProduct = mp.Qty * mp.UnitPrice - } - - if mdp != nil { - totalMarketingDelivery = mdp.Qty * mdp.UnitPrice - } - - return RepportMarketingListDTO{ - RepportMarketingBaseDTO: baseDTO, - MarketingProduct: marketingProduct, - MarketingDelivery: marketingDelivery, - TotalMarketingProduct: totalMarketingProduct, - TotalMarketingDelivery: totalMarketingDelivery, - LatestApproval: latestApproval, +// ToRepportMarketingItemDTOs maps array of delivery products to report items with HPP calculation +func ToRepportMarketingItemDTOs(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) []RepportMarketingItemDTO { + items := make([]RepportMarketingItemDTO, 0, len(mdps)) + for _, mdp := range mdps { + items = append(items, ToRepportMarketingItemDTO(mdp, hppPricePerKg)) } + return items } -func ToRepportMarketingListDTOs(deliveryProducts []entity.MarketingDeliveryProduct) []RepportMarketingListDTO { - result := make([]RepportMarketingListDTO, 0, len(deliveryProducts)) +// ToRepportMarketingResponseDTO creates complete marketing report response +func ToRepportMarketingResponseDTO(mdps []entity.MarketingDeliveryProduct, hppPricePerKg float64) RepportMarketingResponseDTO { + items := ToRepportMarketingItemDTOs(mdps, hppPricePerKg) - marketingMap := make(map[uint]entity.MarketingDeliveryProduct) - for _, dp := range deliveryProducts { - if dp.MarketingProduct.Marketing.Id == 0 { - continue - } - marketingID := dp.MarketingProduct.Marketing.Id - if _, exists := marketingMap[marketingID]; !exists { - marketingMap[marketingID] = dp - } + return RepportMarketingResponseDTO{ + Items: items, } - - for _, deliveryProduct := range marketingMap { - if deliveryProduct.MarketingProduct.Marketing.Id == 0 { - continue - } - - marketing := &deliveryProduct.MarketingProduct.Marketing - baseDTO := ToRepportMarketingBaseDTO(marketing) - - var latestApproval *approvalDTO.ApprovalRelationDTO - if marketing.LatestApproval != nil { - mapped := approvalDTO.ToApprovalDTO(*marketing.LatestApproval) - latestApproval = &mapped - } - - mdp := &deliveryProduct - dto := ToRepportMarketingListDTO(baseDTO, &deliveryProduct.MarketingProduct, mdp, latestApproval) - result = append(result, dto) - } - - return result } diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 3adc5c0a..4db200ab 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -1,6 +1,9 @@ package service import ( + "context" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -18,7 +21,7 @@ import ( type RepportService interface { GetExpense(ctx *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error) - GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingListDTO, int64, error) + GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, error) } type repportService struct { @@ -77,7 +80,7 @@ func (s *repportService) GetExpense(c *fiber.Ctx, params *validation.ExpenseQuer return result, total, nil } -func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingListDTO, int64, error) { +func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } @@ -89,27 +92,88 @@ func (s *repportService) GetMarketing(c *fiber.Ctx, params *validation.Marketing return nil, 0, err } - marketingIDMap := make(map[uint]bool) - marketingIDs := make([]uint, 0) - for _, dp := range deliveryProducts { - if marketingID := dp.MarketingProduct.Marketing.Id; marketingID > 0 && !marketingIDMap[marketingID] { - marketingIDs = append(marketingIDs, marketingID) - marketingIDMap[marketingID] = true - } - } + projectFlockIDs := s.collectProjectFlockIDs(deliveryProducts) + hppMap := s.buildHppMap(c.Context(), projectFlockIDs, deliveryProducts) + items := s.mapDeliveryProductsToDTOs(deliveryProducts, hppMap) - approvals, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowMarketing, marketingIDs, func(db *gorm.DB) *gorm.DB { - return db.Preload("ActionUser") - }) - if err != nil { - s.Log.Warnf("LatestByTargets error: %v", err) - } - - for i := range deliveryProducts { - if approval, exists := approvals[deliveryProducts[i].MarketingProduct.Marketing.Id]; exists && approval != nil { - deliveryProducts[i].MarketingProduct.Marketing.LatestApproval = approval - } - } - - return dto.ToRepportMarketingListDTOs(deliveryProducts), total, nil + return items, total, nil +} + +func (s *repportService) collectProjectFlockIDs(deliveryProducts []entity.MarketingDeliveryProduct) []uint { + projectFlockIDMap := make(map[uint]bool) + projectFlockIDs := make([]uint, 0) + + for _, dp := range deliveryProducts { + if projectFlockKandang := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil { + if projectFlockKandang.ProjectFlockId > 0 && !projectFlockIDMap[projectFlockKandang.ProjectFlockId] { + projectFlockIDs = append(projectFlockIDs, projectFlockKandang.ProjectFlockId) + projectFlockIDMap[projectFlockKandang.ProjectFlockId] = true + } + } + } + + return projectFlockIDs +} + +func (s *repportService) buildHppMap(ctx context.Context, projectFlockIDs []uint, deliveryProducts []entity.MarketingDeliveryProduct) map[uint]float64 { + hppMap := make(map[uint]float64) + for _, projectFlockID := range projectFlockIDs { + hppPerKg := s.calculateHppPricePerKg(ctx, projectFlockID, deliveryProducts) + hppMap[projectFlockID] = hppPerKg + } + return hppMap +} + +func (s *repportService) mapDeliveryProductsToDTOs(deliveryProducts []entity.MarketingDeliveryProduct, hppMap map[uint]float64) []dto.RepportMarketingItemDTO { + items := make([]dto.RepportMarketingItemDTO, 0, len(deliveryProducts)) + for _, dp := range deliveryProducts { + hppPerKg := float64(0) + if projectFlockKandang := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil { + if hpp, exists := hppMap[projectFlockKandang.ProjectFlockId]; exists { + hppPerKg = hpp + } + } + items = append(items, dto.ToRepportMarketingItemDTO(dp, hppPerKg)) + } + return items +} + +func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFlockID uint, deliveryProducts []entity.MarketingDeliveryProduct) float64 { + if projectFlockID == 0 { + return 0 + } + + realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(ctx, projectFlockID) + if err != nil { + return 0 + } + + if len(realizations) == 0 { + return 0 + } + + totalActualCost := float64(0) + for _, realization := range realizations { + cost := realization.Price * realization.Qty + totalActualCost += cost + } + + if totalActualCost == 0 { + return 0 + } + + totalWeightSold := float64(0) + for _, dp := range deliveryProducts { + if dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil && + dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang.ProjectFlockId == projectFlockID { + totalWeightSold += dp.TotalWeight + } + } + + if totalWeightSold == 0 { + return 0 + } + + hppPerKg := totalActualCost / totalWeightSold + return hppPerKg } diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index 7efc51f9..e568952d 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -16,14 +16,16 @@ type ExpenseQuery struct { } type MarketingQuery struct { - Page int `query:"page" validate:"omitempty,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"` - Search string `query:"search" validate:"omitempty,max=100"` - CustomerId int64 `query:"customer_id" validate:"omitempty"` - ProjectFlockKandangId int64 `query:"project_flock_kandang_id" validate:"omitempty"` - DeliveryDate string `query:"delivery_date" validate:"omitempty"` - ProductId int64 `query:"product_id" validate:"omitempty"` - WarehouseId int64 `query:"warehouse_id" validate:"omitempty"` - SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"` - MarketingId int64 `query:"marketing_id" validate:"omitempty"` + Page int `query:"page" validate:"omitempty,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,min=1,max=100,gt=0"` + Search string `query:"search" validate:"omitempty,max=100"` + CustomerId int64 `query:"customer_id" validate:"omitempty"` + ProductId int64 `query:"product_id" validate:"omitempty"` + WarehouseId int64 `query:"warehouse_id" validate:"omitempty"` + SalesPersonId int64 `query:"sales_person_id" validate:"omitempty"` + FilterBy string `query:"filter_by" validate:"omitempty,oneof=realization_date delivery_date"` + StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"` + EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"` + SortBy string `query:"sort_by" validate:"omitempty,oneof=delivery_date customer warehouse product sales_person vehicle_number sales_amount hpp_amount qty average_weight total_weight sales_price hpp_price aging_days"` + SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` }