package service import ( "context" "fmt" "math" "sort" "strconv" "strings" "time" "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto" repportRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations" "gitlab.com/mbugroup/lti-api.git/internal/utils" approvalService "gitlab.com/mbugroup/lti-api.git/internal/common/service" approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto" customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto" warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" chickinRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" purchaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "github.com/sirupsen/logrus" "gorm.io/gorm" ) type RepportService interface { GetExpense(ctx *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error) GetMarketing(ctx *fiber.Ctx, params *validation.MarketingQuery) ([]dto.RepportMarketingItemDTO, int64, error) GetPurchaseSupplier(ctx *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error) GetDebtSupplier(ctx *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error) GetCustomerPayment(ctx *fiber.Ctx, params *validation.CustomerPaymentQuery) ([]dto.CustomerPaymentReportItem, int64, error) } type repportService struct { Log *logrus.Logger Validate *validator.Validate DB *gorm.DB ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository PurchaseRepo purchaseRepo.PurchaseRepository ChickinRepo chickinRepo.ProjectChickinRepository RecordingRepo recordingRepo.RecordingRepository ApprovalSvc approvalService.ApprovalService PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository DebtSupplierRepo repportRepo.DebtSupplierRepository HppPerKandangRepo repportRepo.HppPerKandangRepository ProductionResultRepo repportRepo.ProductionResultRepository CustomerPaymentRepo repportRepo.CustomerPaymentRepository } type HppCostAggregate struct { FeedCost float64 OvkCost float64 DocCost float64 DocQty float64 BudgetCost float64 ExpenseCost float64 } func NewRepportService( db *gorm.DB, validate *validator.Validate, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository, marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, purchaseRepo purchaseRepo.PurchaseRepository, chickinRepo chickinRepo.ProjectChickinRepository, recordingRepo recordingRepo.RecordingRepository, approvalSvc approvalService.ApprovalService, purchaseSupplierRepo repportRepo.PurchaseSupplierRepository, debtSupplierRepo repportRepo.DebtSupplierRepository, hppPerKandangRepo repportRepo.HppPerKandangRepository, productionResultRepo repportRepo.ProductionResultRepository, customerPaymentRepo repportRepo.CustomerPaymentRepository, ) RepportService { return &repportService{ Log: utils.Log, Validate: validate, DB: db, ExpenseRealizationRepo: expenseRealizationRepo, MarketingDeliveryRepo: marketingDeliveryRepo, PurchaseRepo: purchaseRepo, ChickinRepo: chickinRepo, RecordingRepo: recordingRepo, ApprovalSvc: approvalSvc, PurchaseSupplierRepo: purchaseSupplierRepo, DebtSupplierRepo: debtSupplierRepo, HppPerKandangRepo: hppPerKandangRepo, ProductionResultRepo: productionResultRepo, CustomerPaymentRepo: customerPaymentRepo, } } func (s *repportService) GetExpense(c *fiber.Ctx, params *validation.ExpenseQuery) ([]dto.RepportExpenseListDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } offset := (params.Page - 1) * params.Limit realizations, total, err := s.ExpenseRealizationRepo.GetAllWithFilters(c.Context(), offset, params.Limit, params) if err != nil { s.Log.Errorf("GetAllWithFilters error: %v", err) return nil, 0, err } result := dto.ToRepportExpenseListDTOs(realizations) expenseIDs := make([]uint, 0, len(result)) for i := range result { expenseIDs = append(expenseIDs, uint(result[i].Id)) } approvals, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowExpense, expenseIDs, func(db *gorm.DB) *gorm.DB { return db.Preload("ActionUser") }) if err != nil { s.Log.Warnf("LatestByTargets error: %v", err) } for i := range result { expenseIDAsUint := uint(result[i].Id) if approval, exists := approvals[expenseIDAsUint]; exists && approval != nil { mapped := approvalDTO.ToApprovalDTO(*approval) result[i].LatestApproval = &mapped } } return result, total, nil } 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 } offset := (params.Page - 1) * params.Limit deliveryProducts, total, err := s.MarketingDeliveryRepo.GetAllWithFilters(c.Context(), offset, params.Limit, params) if err != nil { return nil, 0, err } projectFlockIDMap := make(map[uint]bool) hppMap := make(map[uint]float64) for _, dp := range deliveryProducts { if projectFlockKandang := dp.MarketingProduct.ProductWarehouse.ProjectFlockKandang; projectFlockKandang != nil { projectFlockID := projectFlockKandang.ProjectFlockId if projectFlockID > 0 && !projectFlockIDMap[projectFlockID] { projectFlockIDMap[projectFlockID] = true category := projectFlockKandang.ProjectFlock.Category hppPerKg := s.calculateHppPricePerKg(c.Context(), projectFlockID, category) hppMap[projectFlockID] = hppPerKg } } } items := dto.ToRepportMarketingItemDTOsWithHppMap(deliveryProducts, hppMap) return items, total, nil } func (s *repportService) calculateHppPricePerKg(ctx context.Context, projectFlockID uint, category string) float64 { totalCost := s.getTotalProjectCost(ctx, projectFlockID) if totalCost == 0 { return 0 } chickinQty, err := s.ChickinRepo.GetTotalChickinQtyByProjectFlockID(ctx, projectFlockID) if err != nil { s.Log.Warnf("HPP calculation: Failed to get chickin qty for project flock ID %d: %v", projectFlockID, err) } depletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(ctx, projectFlockID) if err != nil { s.Log.Warnf("HPP calculation: Failed to get depletion for project flock ID %d: %v", projectFlockID, err) } avgWeight, err := s.RecordingRepo.GetLatestAvgWeightByProjectFlockID(ctx, projectFlockID) if err != nil { s.Log.Warnf("HPP calculation: Failed to get avg weight for project flock ID %d: %v", projectFlockID, err) } var totalWeight float64 if utils.ProjectFlockCategory(category) == utils.ProjectFlockCategoryGrowing { totalWeight = (chickinQty - depletion) * avgWeight } else { eggWeight, err := s.RecordingRepo.GetTotalEggProductionWeightByProjectFlockID(ctx, projectFlockID) if err != nil { s.Log.Warnf("HPP calculation: Failed to get egg weight for project flock ID %d: %v", projectFlockID, err) } totalWeight = (chickinQty-depletion)*avgWeight + eggWeight } if totalWeight == 0 { return 0 } hppPricePerKg := totalCost / totalWeight return hppPricePerKg } func (s *repportService) getTotalProjectCost(ctx context.Context, projectFlockID uint) float64 { if projectFlockID == 0 { return 0 } purchases, err := s.PurchaseRepo.GetItemsByProjectFlockID(ctx, projectFlockID) if err != nil { s.Log.Errorf("getTotalProjectCost: GetItemsByProjectFlockID error for project flock ID %d: %v", projectFlockID, err) return 0 } cost := float64(0) purchaseCost := float64(0) for _, p := range purchases { purchaseCost += p.TotalPrice } cost += purchaseCost realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(ctx, projectFlockID) if err != nil { s.Log.Warnf("getTotalProjectCost: GetByProjectFlockID error for project flock ID %d: %v", projectFlockID, err) } bopCost := float64(0) for _, r := range realizations { if r.ExpenseNonstock != nil && r.ExpenseNonstock.Expense != nil && r.ExpenseNonstock.Expense.Category == string(utils.ExpenseCategoryBOP) { bopCost += r.Price * r.Qty } } cost += bopCost return cost } func (s *repportService) GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } const ( recordsPerWeek = 7 defaultStartWoa = 18 defaultStdBw = 1951 defaultBw = 0 defaultUniformText = "90% up" ) if params.Limit <= 0 { params.Limit = 10 } if params.Page <= 0 { params.Page = 1 } weeksPerPage := params.Limit recordLimit := weeksPerPage * recordsPerWeek if recordLimit <= 0 { recordLimit = recordsPerWeek } recordOffset := (params.Page - 1) * recordLimit if recordOffset < 0 { recordOffset = 0 } recordings, totalRecordings, err := s.ProductionResultRepo.GetRecordingsByProjectFlockKandang(ctx.Context(), params.ProjectFlockKandangID, recordOffset, recordLimit) if err != nil { return nil, 0, err } dailyResults := make([]dto.ProductionResultDTO, len(recordings)) for i := range recordings { dailyResults[i] = mapRecordingToProductionResultDTO(recordings[i]) if dailyResults[i].StdUniformity == "" { dailyResults[i].StdUniformity = defaultUniformText } } weeklyResults := summarizeProductionResults(dailyResults, recordsPerWeek) var cumulativeButir int64 var cumulativeKg float64 for i := range weeklyResults { weeklyResults[i].Woa = float64(defaultStartWoa + i) weeklyResults[i].StdBw = defaultStdBw weeklyResults[i].Bw = defaultBw if weeklyResults[i].StdUniformity == "" { weeklyResults[i].StdUniformity = defaultUniformText } cumulativeButir += weeklyResults[i].ButiranJumlah weeklyResults[i].TotalButir = cumulativeButir cumulativeKg += weeklyResults[i].KgJumlah weeklyResults[i].TotalKg = cumulativeKg } totalWeeks := int64(math.Ceil(float64(totalRecordings) / float64(recordsPerWeek))) return weeklyResults, totalWeeks, nil } func (s *repportService) GetCustomerPayment(ctx *fiber.Ctx, params *validation.CustomerPaymentQuery) ([]dto.CustomerPaymentReportItem, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } // Determine customer IDs to process var customerIDs []uint var totalCustomers int64 if len(params.CustomerIDs) > 0 { // Specific customer IDs mode (no pagination) customerIDs = params.CustomerIDs totalCustomers = int64(len(customerIDs)) if len(customerIDs) == 0 { return []dto.CustomerPaymentReportItem{}, 0, nil } } else { // Multiple customers mode with pagination page := params.Page limit := params.Limit if page < 1 { page = 1 } if limit < 1 { limit = 10 } offset := (page - 1) * limit var err error customerIDs, totalCustomers, err = s.CustomerPaymentRepo.GetCustomerIDsWithTransactions(ctx.Context(), limit, offset) if err != nil { return nil, 0, err } if len(customerIDs) == 0 { return []dto.CustomerPaymentReportItem{}, 0, nil } } // Process each customer var result []dto.CustomerPaymentReportItem for _, customerID := range customerIDs { item, err := s.processCustomerPayment(ctx.Context(), customerID, params) if err != nil { return nil, 0, err } result = append(result, item) } return result, totalCustomers, nil } func (s *repportService) processCustomerPayment(ctx context.Context, customerID uint, params *validation.CustomerPaymentQuery) (dto.CustomerPaymentReportItem, error) { customer := entity.Customer{} if err := s.DB.WithContext(ctx). Where("id = ?", customerID). First(&customer).Error; err != nil { return dto.CustomerPaymentReportItem{}, err } initialBalance, err := s.CustomerPaymentRepo.GetInitialBalanceByCustomer(ctx, customerID) if err != nil { return dto.CustomerPaymentReportItem{}, err } cid := customerID transactions, err := s.CustomerPaymentRepo.GetCustomerPaymentTransactions(ctx, &cid) if err != nil { return dto.CustomerPaymentReportItem{}, err } rows := make([]dto.CustomerPaymentReportRow, 0, len(transactions)) runningBalance := initialBalance for i, tx := range transactions { previousBalance := runningBalance row := dto.ToCustomerPaymentReportRow(tx) if tx.TransactionType == "SALES" { runningBalance -= tx.TotalPrice status, paymentDate := s.determineSalesStatusAndPaymentDate(transactions, i, previousBalance, runningBalance) row.Status = status if status == "LUNAS" { if previousBalance >= tx.TotalPrice { days := 0 row.AgingDay = &days } else if paymentDate != nil { // Aging = payment_date - trans_date (SO date) days := int(paymentDate.Sub(tx.TransDate).Hours() / 24) if days < 0 { days = 0 } row.AgingDay = &days } else { days := 0 row.AgingDay = &days } } else { // Aging = current_date - trans_date (SO date) days := int(time.Since(tx.TransDate).Hours() / 24) if days < 0 { days = 0 } row.AgingDay = &days } } else if tx.TransactionType == "PAYMENT" { runningBalance += tx.PaymentAmount row.Status = "" row.AgingDay = nil } row.AccountsReceivable = runningBalance rows = append(rows, row) } if params.StartDate != "" || params.EndDate != "" { filteredRows := make([]dto.CustomerPaymentReportRow, 0, len(rows)) location, err := time.LoadLocation("Asia/Jakarta") if err != nil { return dto.CustomerPaymentReportItem{}, err } var startDate, endDate *time.Time if params.StartDate != "" { parsed, err := time.ParseInLocation("2006-01-02", params.StartDate, location) if err != nil { return dto.CustomerPaymentReportItem{}, err } startDate = &parsed } if params.EndDate != "" { parsed, err := time.ParseInLocation("2006-01-02", params.EndDate, location) if err != nil { return dto.CustomerPaymentReportItem{}, err } // End date should be inclusive, so set to end of day endOfDay := time.Date(parsed.Year(), parsed.Month(), parsed.Day(), 23, 59, 59, 999999999, location) endDate = &endOfDay } for _, row := range rows { transDate := row.TransDate.In(location) // Check if transaction date is within range if startDate != nil && transDate.Before(*startDate) { continue } if endDate != nil && transDate.After(*endDate) { continue } filteredRows = append(filteredRows, row) } rows = filteredRows } summary := dto.CalculateCustomerPaymentSummary(rows, initialBalance) return dto.CustomerPaymentReportItem{ Customer: customerDTO.ToCustomerRelationDTO(customer), InitialBalance: initialBalance, Rows: rows, Summary: summary, }, nil } func (s *repportService) determineSalesStatusAndPaymentDate(transactions []repportRepo.CustomerPaymentTransaction, currentIndex int, previousBalance, currentBalance float64) (string, *time.Time) { currentSales := transactions[currentIndex] // Status Logic: // 1. LUNAS: previousBalance >= salesAmount (paid from deposit) // 2. LUNAS: future payments make AR >= 0 (eventually paid) // 3. DIBAYAR SEBAGIAN: has payment but not enough // 4. BELUM LUNAS: no payment at all if previousBalance >= currentSales.TotalPrice { return "LUNAS", nil } hasPartialPaymentFromBalance := previousBalance > 0 && previousBalance < currentSales.TotalPrice futureBalance := currentBalance hasPayment := false var paymentDateThatMadeItLunas *time.Time for i := currentIndex + 1; i < len(transactions); i++ { if transactions[i].TransactionType == "PAYMENT" { futureBalance += transactions[i].PaymentAmount hasPayment = true if futureBalance >= 0 { paymentDateThatMadeItLunas = &transactions[i].TransDate return "LUNAS", paymentDateThatMadeItLunas } } else if transactions[i].TransactionType == "SALES" { futureBalance -= transactions[i].TotalPrice } } if hasPayment || hasPartialPaymentFromBalance { return "DIBAYAR SEBAGIAN", nil } return "BELUM LUNAS", nil } func mapRecordingToProductionResultDTO(record entity.Recording) dto.ProductionResultDTO { result := dto.ProductionResultDTO{ CreatedAt: record.CreatedAt, UpdatedAt: record.UpdatedAt, StdUniformity: "90% up", DepKum: valueOrZero(record.CumDepletionRate), DepStd: valueOrZero(record.TotalDepletionQty), Fcr: valueOrZero(record.FcrValue), Hh: valueOrZero(record.TotalChickQty), } if record.Day != nil { result.Woa = float64(*record.Day) } if record.CumIntake != nil { result.Fi = float64(*record.CumIntake) } // avgWeight := calculateAverageBodyWeight(record.BodyWeights) avgWeight := 1.0 if avgWeight > 0 { result.Bw = avgWeight } eggSummary := summarizeEggs(record.Eggs) result.ButiranUtuh = eggSummary.Utuh result.ButiranPutih = eggSummary.Putih result.ButiranRetak = eggSummary.Retak result.ButiranPecah = eggSummary.Pecah result.ButiranJumlah = eggSummary.TotalQty result.TotalButir = eggSummary.TotalQty result.KgUtuh = eggSummary.KgUtuh result.KgPutih = eggSummary.KgPutih result.KgRetak = eggSummary.KgRetak result.KgPecah = eggSummary.KgPecah result.KgJumlah = eggSummary.TotalKg result.TotalKg = eggSummary.TotalKg if eggSummary.TotalQty > 0 { total := float64(eggSummary.TotalQty) result.PersenUtuh = roundFloat((float64(result.ButiranUtuh)/total)*100, 2) result.PersenPutih = roundFloat((float64(result.ButiranPutih)/total)*100, 2) result.PersenRetak = roundFloat((float64(result.ButiranRetak)/total)*100, 2) result.PersenPecah = roundFloat((float64(result.ButiranPecah)/total)*100, 2) result.Ew = (eggSummary.TotalKg * 1000) / total result.Em = eggSummary.TotalKg } return result } // func calculateAverageBodyWeight(bodyWeights []entity.RecordingBW) float64 { // var totalQty float64 // var totalWeight float64 // for _, bw := range bodyWeights { // totalQty += bw.Qty // if bw.TotalWeight > 0 { // totalWeight += bw.TotalWeight // } else { // totalWeight += bw.AvgWeight * bw.Qty // } // } // if totalQty == 0 { // return 0 // } // return totalWeight / totalQty // } type eggSummary struct { TotalQty int64 TotalKg float64 Utuh int64 Putih int64 Retak int64 Pecah int64 KgUtuh float64 KgPutih float64 KgRetak float64 KgPecah float64 } func summarizeEggs(eggs []entity.RecordingEgg) eggSummary { var summary eggSummary for _, egg := range eggs { qty := int64(egg.Qty) weightKg := valueOrZero(egg.Weight) summary.TotalQty += qty summary.TotalKg += weightKg if flagType, ok := getEggFlagType(egg); ok { switch flagType { case utils.FlagTelurUtuh: summary.Utuh += qty summary.KgUtuh += weightKg case utils.FlagTelurPutih: summary.Putih += qty summary.KgPutih += weightKg case utils.FlagTelurRetak: summary.Retak += qty summary.KgRetak += weightKg case utils.FlagTelurPecah: summary.Pecah += qty summary.KgPecah += weightKg } } } return summary } func valueOrZero(value *float64) float64 { if value == nil { return 0 } return *value } func roundFloat(val float64, precision int) float64 { if precision < 0 { return val } factor := math.Pow(10, float64(precision)) return math.Round(val*factor) / factor } func getEggFlagType(egg entity.RecordingEgg) (utils.FlagType, bool) { if egg.ProductFlagName == nil || *egg.ProductFlagName == "" { return "", false } flagType := utils.FlagType(*egg.ProductFlagName) switch flagType { case utils.FlagTelurUtuh, utils.FlagTelurPutih, utils.FlagTelurRetak, utils.FlagTelurPecah: return flagType, true } return "", false } func summarizeProductionResults(daily []dto.ProductionResultDTO, groupSize int) []dto.ProductionResultDTO { if groupSize <= 0 || len(daily) == 0 { return daily } result := make([]dto.ProductionResultDTO, 0, (len(daily)+groupSize-1)/groupSize) for i := 0; i < len(daily); i += groupSize { end := i + groupSize if end > len(daily) { end = len(daily) } result = append(result, aggregateProductionResultGroup(daily[i:end])) } return result } func aggregateProductionResultGroup(group []dto.ProductionResultDTO) dto.ProductionResultDTO { count := len(group) if count == 0 { return dto.ProductionResultDTO{} } agg := dto.ProductionResultDTO{ CreatedAt: group[0].CreatedAt, UpdatedAt: group[0].UpdatedAt, StdUniformity: group[0].StdUniformity, } var sumBw, sumStdBw, sumUniformity float64 var sumDepStd float64 var sumKgUtuh, sumKgPutih, sumKgRetak, sumKgPecah float64 var sumKgJumlah, sumTotalKg float64 var sumPersenUtuh, sumPersenPutih, sumPersenRetak, sumPersenPecah float64 var percentSamples int var sumHd, sumHdStd float64 var sumFi, sumFiStd float64 var sumEm, sumEmStd float64 var sumEw, sumEwStd float64 var sumFcr, sumFcrStd float64 var sumHh, sumHhStd float64 var sumButiranUtuh, sumButiranPutih int64 var sumButiranRetak, sumButiranPecah int64 var sumButiranJumlah, sumTotalButir int64 for _, item := range group { sumBw += item.Bw sumStdBw += item.StdBw sumUniformity += item.Uniformity sumDepStd += item.DepStd sumKgUtuh += item.KgUtuh sumKgPutih += item.KgPutih sumKgRetak += item.KgRetak sumKgPecah += item.KgPecah sumKgJumlah += item.KgJumlah sumTotalKg += item.TotalKg if item.ButiranJumlah > 0 { sumPersenUtuh += item.PersenUtuh sumPersenPutih += item.PersenPutih sumPersenRetak += item.PersenRetak sumPersenPecah += item.PersenPecah percentSamples++ } sumHd += item.Hd sumHdStd += item.HdStd sumFi += item.Fi sumFiStd += item.FiStd sumEm += item.Em sumEmStd += item.EmStd sumEw += item.Ew sumEwStd += item.EwStd sumFcr += item.Fcr sumFcrStd += item.FcrStd sumHh += item.Hh sumHhStd += item.HhStd sumButiranUtuh += item.ButiranUtuh sumButiranPutih += item.ButiranPutih sumButiranRetak += item.ButiranRetak sumButiranPecah += item.ButiranPecah sumButiranJumlah += item.ButiranJumlah sumTotalButir += item.TotalButir } divider := float64(count) if divider == 0 { divider = 1 } agg.Bw = sumBw / divider agg.StdBw = sumStdBw / divider agg.Uniformity = sumUniformity / divider agg.DepKum = group[count-1].DepKum agg.DepStd = sumDepStd / divider agg.KgUtuh = sumKgUtuh agg.KgPutih = sumKgPutih agg.KgRetak = sumKgRetak agg.KgPecah = sumKgPecah agg.KgJumlah = sumKgJumlah agg.TotalKg = sumTotalKg agg.ButiranUtuh = sumButiranUtuh agg.ButiranPutih = sumButiranPutih agg.ButiranRetak = sumButiranRetak agg.ButiranPecah = sumButiranPecah agg.ButiranJumlah = sumButiranJumlah agg.TotalButir = sumTotalButir if percentSamples > 0 { percentDivider := float64(percentSamples) agg.PersenUtuh = roundFloat(sumPersenUtuh/percentDivider, 2) agg.PersenPutih = roundFloat(sumPersenPutih/percentDivider, 2) agg.PersenRetak = roundFloat(sumPersenRetak/percentDivider, 2) agg.PersenPecah = roundFloat(sumPersenPecah/percentDivider, 2) } agg.Hd = sumHd / divider agg.HdStd = sumHdStd / divider agg.Fi = sumFi / divider agg.FiStd = sumFiStd / divider agg.Em = sumEm / divider agg.EmStd = sumEmStd / divider agg.Ew = sumEw / divider agg.EwStd = sumEwStd / divider agg.Fcr = sumFcr / divider agg.FcrStd = sumFcrStd / divider agg.Hh = sumHh / divider agg.HhStd = sumHhStd / divider return agg } func (s *repportService) GetPurchaseSupplier(c *fiber.Ctx, params *validation.PurchaseSupplierQuery) ([]dto.PurchaseSupplierDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } offset := (params.Page - 1) * params.Limit if offset < 0 { offset = 0 } suppliers, totalSuppliers, err := s.PurchaseSupplierRepo.GetSuppliersWithPurchases(c.Context(), offset, params.Limit, params) if err != nil { return nil, 0, err } if totalSuppliers == 0 || len(suppliers) == 0 { return []dto.PurchaseSupplierDTO{}, totalSuppliers, nil } supplierMap := make(map[uint]entity.Supplier, len(suppliers)) supplierIDs := make([]uint, 0, len(suppliers)) for _, supplier := range suppliers { supplierMap[supplier.Id] = supplier supplierIDs = append(supplierIDs, supplier.Id) } items, err := s.PurchaseSupplierRepo.GetItemsBySuppliers(c.Context(), supplierIDs, params) if err != nil { return nil, 0, err } itemsBySupplier := make(map[uint][]entity.PurchaseItem) for _, item := range items { if item.Purchase == nil { continue } supplierID := item.Purchase.SupplierId itemsBySupplier[supplierID] = append(itemsBySupplier[supplierID], item) } result := make([]dto.PurchaseSupplierDTO, 0, len(supplierIDs)) for _, supplierID := range supplierIDs { supplier, exists := supplierMap[supplierID] if !exists { continue } supplierItems := itemsBySupplier[supplierID] dtoItem := dto.ToPurchaseSupplierDTO(supplier, supplierItems) result = append(result, dtoItem) } return result, totalSuppliers, nil } func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error) { if params.FilterBy == "" || strings.EqualFold(strings.TrimSpace(params.FilterBy), "do_date") { params.FilterBy = "received_date" } if err := s.Validate.Struct(params); err != nil { return nil, 0, err } offset := (params.Page - 1) * params.Limit if offset < 0 { offset = 0 } suppliers, totalSuppliers, err := s.DebtSupplierRepo.GetSuppliersWithPurchases(c.Context(), offset, params.Limit, params) if err != nil { return nil, 0, err } if totalSuppliers == 0 || len(suppliers) == 0 { return []dto.DebtSupplierDTO{}, totalSuppliers, nil } supplierMap := make(map[uint]entity.Supplier, len(suppliers)) supplierIDs := make([]uint, 0, len(suppliers)) for _, supplier := range suppliers { supplierMap[supplier.Id] = supplier supplierIDs = append(supplierIDs, supplier.Id) } purchases, err := s.DebtSupplierRepo.GetPurchasesBySuppliers(c.Context(), supplierIDs, params) if err != nil { return nil, 0, err } payments, err := s.DebtSupplierRepo.GetPaymentsBySuppliers(c.Context(), supplierIDs, params) if err != nil { return nil, 0, err } purchasesBySupplier := make(map[uint][]entity.Purchase, len(supplierIDs)) references := make([]string, 0) seenRefs := make(map[string]struct{}) for _, purchase := range purchases { supplierID := purchase.SupplierId purchasesBySupplier[supplierID] = append(purchasesBySupplier[supplierID], purchase) reference := purchase.PrNumber if purchase.PoNumber != nil && strings.TrimSpace(*purchase.PoNumber) != "" { reference = *purchase.PoNumber } if _, exists := seenRefs[reference]; !exists { seenRefs[reference] = struct{}{} references = append(references, reference) } } paymentTotals, err := s.DebtSupplierRepo.GetPaymentTotalsByReferences(c.Context(), supplierIDs, references) if err != nil { return nil, 0, err } paymentsBySupplier := make(map[uint][]entity.Payment, len(supplierIDs)) for _, payment := range payments { paymentsBySupplier[payment.PartyId] = append(paymentsBySupplier[payment.PartyId], payment) } initialPurchaseTotals, err := s.DebtSupplierRepo.GetPurchaseTotalsBeforeDate(c.Context(), supplierIDs, params) if err != nil { return nil, 0, err } initialPaymentTotals, err := s.DebtSupplierRepo.GetPaymentTotalsBeforeDate(c.Context(), supplierIDs, params) if err != nil { return nil, 0, err } location, err := time.LoadLocation("Asia/Jakarta") if err != nil { return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration") } now := time.Now().In(location) result := make([]dto.DebtSupplierDTO, 0, len(supplierIDs)) for _, supplierID := range supplierIDs { supplier, exists := supplierMap[supplierID] if !exists { continue } initialBalance := initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID] items := purchasesBySupplier[supplierID] paymentItems := paymentsBySupplier[supplierID] rows := make([]dto.DebtSupplierRowDTO, 0, len(items)+len(paymentItems)) total := dto.DebtSupplierTotalDTO{} type debtSupplierRowItem struct { Row dto.DebtSupplierRowDTO SortTime time.Time Order int DeltaBalance float64 CountTotals bool } combinedRows := make([]debtSupplierRowItem, 0, len(items)+len(paymentItems)) for _, purchase := range items { row := buildDebtSupplierRow(purchase, paymentTotals, now, location) sortTime := resolveDebtSupplierSortTime(purchase, params.FilterBy, location) combinedRows = append(combinedRows, debtSupplierRowItem{ Row: row, SortTime: sortTime, Order: 0, DeltaBalance: -row.TotalPrice, CountTotals: true, }) } for _, payment := range paymentItems { row := buildDebtSupplierPaymentRow(payment, location) sortTime := payment.PaymentDate.In(location) combinedRows = append(combinedRows, debtSupplierRowItem{ Row: row, SortTime: sortTime, Order: 1, DeltaBalance: payment.Nominal, CountTotals: false, }) } sort.SliceStable(combinedRows, func(i, j int) bool { if combinedRows[i].SortTime.Equal(combinedRows[j].SortTime) { return combinedRows[i].Order < combinedRows[j].Order } return combinedRows[i].SortTime.Before(combinedRows[j].SortTime) }) balance := initialBalance for i := range combinedRows { balance += combinedRows[i].DeltaBalance combinedRows[i].Row.Balance = balance if combinedRows[i].CountTotals { row := combinedRows[i].Row if row.Aging > total.Aging { total.Aging = row.Aging } total.TotalPrice += row.TotalPrice total.PaymentPrice += row.PaymentPrice total.DebtPrice += row.DebtPrice } else { combinedRows[i].Row.DebtPrice = balance } } sortDesc := strings.EqualFold(params.SortOrder, "desc") if sortDesc { for i := len(combinedRows) - 1; i >= 0; i-- { rows = append(rows, combinedRows[i].Row) } } else { for i := range combinedRows { rows = append(rows, combinedRows[i].Row) } } var supplierDTORef *supplierDTO.SupplierRelationDTO if supplier.Id != 0 { mapped := supplierDTO.ToSupplierRelationDTO(supplier) supplierDTORef = &mapped } result = append(result, dto.DebtSupplierDTO{ Supplier: supplierDTORef, InitialBalance: initialBalance, Rows: rows, Total: total, }) } return result, totalSuppliers, nil } func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]float64, now time.Time, loc *time.Location) dto.DebtSupplierRowDTO { prNumber := purchase.PrNumber poNumber := "" if purchase.PoNumber != nil { poNumber = *purchase.PoNumber } reference := prNumber if strings.TrimSpace(poNumber) != "" { reference = poNumber } prDate := purchase.CreatedAt.In(loc) startDate := time.Date(prDate.Year(), prDate.Month(), prDate.Day(), 0, 0, 0, 0, loc) endDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc) aging := int(endDate.Sub(startDate).Hours() / 24) totalPrice := 0.0 travelNumber := "-" receivedDate := "" var area *areaDTO.AreaRelationDTO var warehouse *warehouseDTO.WarehouseRelationDTO if len(purchase.Items) > 0 { firstItem := purchase.Items[0] if firstItem.TravelNumber != nil && strings.TrimSpace(*firstItem.TravelNumber) != "" { travelNumber = *firstItem.TravelNumber } if firstItem.Warehouse != nil && firstItem.Warehouse.Id != 0 { mappedWarehouse := warehouseDTO.ToWarehouseRelationDTO(*firstItem.Warehouse) warehouse = &mappedWarehouse if firstItem.Warehouse.Area.Id != 0 { mappedArea := areaDTO.ToAreaRelationDTO(firstItem.Warehouse.Area) area = &mappedArea } } earliestReceived := time.Time{} for _, item := range purchase.Items { totalPrice += item.TotalPrice if item.ReceivedDate == nil || item.ReceivedDate.IsZero() { continue } received := item.ReceivedDate.In(loc) if earliestReceived.IsZero() || received.Before(earliestReceived) { earliestReceived = received } } if !earliestReceived.IsZero() { receivedDate = earliestReceived.Format("2006-01-02") } } paymentPrice := paymentTotals[reference] debtPrice := paymentPrice - totalPrice dueDate := "" dueStatus := "-" if purchase.DueDate != nil && !purchase.DueDate.IsZero() { due := purchase.DueDate.In(loc) dueDate = due.Format("2006-01-02") if now.After(due) { dueStatus = "Sudah Jatuh Tempo" } else { dueStatus = "Mendekati Jatuh Tempo" } } status := "Belum Lunas" if debtPrice >= 0 { status = "Lunas" } poDate := "" if purchase.PoDate != nil && !purchase.PoDate.IsZero() { poDate = purchase.PoDate.In(loc).Format("2006-01-02") } return dto.DebtSupplierRowDTO{ PrNumber: prNumber, PoNumber: poNumber, PoDate: poDate, ReceivedDate: receivedDate, Aging: aging, Area: area, Warehouse: warehouse, DueDate: dueDate, DueStatus: dueStatus, TotalPrice: totalPrice, PaymentPrice: paymentPrice, DebtPrice: debtPrice, Status: status, TravelNumber: travelNumber, } } func buildDebtSupplierPaymentRow(payment entity.Payment, loc *time.Location) dto.DebtSupplierRowDTO { referenceNumber := "" if payment.ReferenceNumber != nil { referenceNumber = *payment.ReferenceNumber } prNumber := payment.PaymentCode if strings.TrimSpace(prNumber) == "" { prNumber = referenceNumber } return dto.DebtSupplierRowDTO{ PrNumber: prNumber, PoNumber: referenceNumber, PoDate: "-", ReceivedDate: payment.PaymentDate.In(loc).Format("2006-01-02"), Aging: 0, Area: nil, Warehouse: nil, DueDate: "-", DueStatus: "-", TotalPrice: 0, PaymentPrice: payment.Nominal, DebtPrice: 0, Status: "Pembayaran", TravelNumber: "-", } } func resolveDebtSupplierSortTime(purchase entity.Purchase, filterBy string, loc *time.Location) time.Time { switch strings.ToLower(strings.TrimSpace(filterBy)) { case "po_date": if purchase.PoDate != nil && !purchase.PoDate.IsZero() { return purchase.PoDate.In(loc) } case "pr_date": return purchase.CreatedAt.In(loc) default: earliest := time.Time{} for _, item := range purchase.Items { if item.ReceivedDate == nil || item.ReceivedDate.IsZero() { continue } received := item.ReceivedDate.In(loc) if earliest.IsZero() || received.Before(earliest) { earliest = received } } if !earliest.IsZero() { return earliest } } return purchase.CreatedAt.In(loc) } func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) { params, filters, err := s.parseHppPerKandangQuery(ctx) if err != nil { return nil, nil, err } if err := s.Validate.Struct(params); err != nil { return nil, nil, fiber.NewError(fiber.StatusBadRequest, err.Error()) } location, err := time.LoadLocation("Asia/Jakarta") if err != nil { return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration") } periodDate, err := time.ParseInLocation("2006-01-02", params.Period, location) if err != nil { return nil, nil, fiber.NewError(fiber.StatusBadRequest, "period must follow format YYYY-MM-DD") } startOfDay := time.Date(periodDate.Year(), periodDate.Month(), periodDate.Day(), 0, 0, 0, 0, location) endOfDay := startOfDay.Add(24 * time.Hour) repoRows, err := s.HppPerKandangRepo.GetRowsByPeriod(ctx.Context(), startOfDay, endOfDay, params.AreaIDs, params.LocationIDs, params.KandangIDs) if err != nil { return nil, nil, err } costRows, supplierRows, err := s.HppPerKandangRepo.GetFeedOvkDocCostByPeriod(ctx.Context(), startOfDay, endOfDay, params.AreaIDs, params.LocationIDs, params.KandangIDs) if err != nil { return nil, nil, err } costMap := make(map[uint]HppCostAggregate, len(costRows)) for _, row := range costRows { costMap[row.KandangID] = HppCostAggregate{ FeedCost: row.FeedCost, OvkCost: row.OvkCost, DocCost: row.DocCost, DocQty: row.DocQty, BudgetCost: row.BudgetCost, ExpenseCost: row.ExpenseCost, } } docSupplierMap := make(map[uint][]dto.HppPerKandangSupplierDTO) feedSupplierMap := make(map[uint][]dto.HppPerKandangSupplierDTO) docSeen := make(map[uint]map[uint]bool) feedSeen := make(map[uint]map[uint]bool) for _, sup := range supplierRows { if sup.SupplierID == 0 { continue } targetMap := feedSupplierMap seen := feedSeen category := "FEED" if strings.EqualFold(sup.Category, "DOC") { targetMap = docSupplierMap seen = docSeen category = "DOC" } if seen[sup.KandangID] == nil { seen[sup.KandangID] = make(map[uint]bool) } if seen[sup.KandangID][sup.SupplierID] { continue } seen[sup.KandangID][sup.SupplierID] = true targetMap[sup.KandangID] = append(targetMap[sup.KandangID], dto.HppPerKandangSupplierDTO{ ID: int64(sup.SupplierID), Name: sup.SupplierName, Alias: sup.SupplierAlias, Category: category, }) } type weightRangeKey struct { Min float64 Max float64 } type weightRangeAggregate struct { Summary *dto.HppPerKandangSummaryWeightRangeDTO EggHppSum float64 EggHppCount int } dataRows := make([]dto.HppPerKandangRowDTO, 0, len(repoRows)) perRangeMap := make(map[weightRangeKey]*weightRangeAggregate) var totalBirds int64 var totalWeight float64 var totalEggPieces int64 var totalEggKg float64 var totalRemainingValueRp int64 var totalEggValueRp int64 var totalHppSum float64 var totalHppCount int var totalDocPriceSum float64 var totalDocPriceCount int var totalEggHppSum float64 var totalEggHppCount int for _, row := range repoRows { birdsFloat := row.RemainingChickenBirds if math.IsNaN(birdsFloat) || math.IsInf(birdsFloat, 0) { birdsFloat = 0 } weightFloat := row.RemainingChickenWeight if math.IsNaN(weightFloat) || math.IsInf(weightFloat, 0) { weightFloat = 0 } eggPiecesFloat := row.EggProductionPieces if math.IsNaN(eggPiecesFloat) || math.IsInf(eggPiecesFloat, 0) { eggPiecesFloat = 0 } eggWeightFloat := row.EggProductionWeightKg if math.IsNaN(eggWeightFloat) || math.IsInf(eggWeightFloat, 0) { eggWeightFloat = 0 } avgWeight := 0.0 if birdsFloat > 0 { avgWeight = weightFloat / birdsFloat } weightMin := math.Floor(avgWeight*10) / 10 if weightMin < 0 { weightMin = 0 } weightMax := weightMin + 0.09 rangeKey := weightRangeKey{Min: weightMin, Max: weightMax} rowBirds := int64(math.Round(birdsFloat)) costEntry := costMap[row.KandangID] totalCost := costEntry.FeedCost + costEntry.OvkCost + costEntry.DocCost + costEntry.BudgetCost + costEntry.ExpenseCost hppRp := 0.0 if weightFloat > 0 { hppRp = totalCost / weightFloat } eggHpp := 0.0 if eggWeightFloat > 0 { eggHpp = totalCost / eggWeightFloat } rowEggPieces := int64(math.Round(eggPiecesFloat)) rowEggValue := int64(eggHpp * eggWeightFloat) rowRemainingValue := int64(hppRp * weightFloat) avgDocPrice := int64(0) if costEntry.DocQty > 0 { avgDocPrice = int64(math.Round(costEntry.DocCost / costEntry.DocQty)) } dataRows = append(dataRows, dto.HppPerKandangRowDTO{ ID: int(row.KandangID), Kandang: dto.HppPerKandangRowKandangDTO{ ID: int64(row.KandangID), Name: row.KandangName, Status: row.KandangStatus, Location: dto.HppPerKandangLocationDTO{ ID: int64(row.LocationID), Name: row.LocationName, }, Pic: dto.HppPerKandangPICDTO{ ID: int64(row.PicID), Name: row.PicName, }, }, WeightRange: dto.HppPerKandangWeightRangeDTO{ WeightMin: weightMin, WeightMax: weightMax, }, RemainingChickenBirds: rowBirds, RemainingChickenWeightKg: weightFloat, AvgWeightKg: avgWeight, // FeedCostRp: costEntry.FeedCost, // OvkCostRp: costEntry.OvkCost, DocSuppliers: docSupplierMap[row.KandangID], FeedSuppliers: feedSupplierMap[row.KandangID], EggProductionPieces: rowEggPieces, EggProductionKg: eggWeightFloat, AverageDocPriceRp: avgDocPrice, HppRp: hppRp, EggHppRpPerKg: eggHpp, RemainingValueRp: rowRemainingValue, EggValueRp: rowEggValue, }) totalBirds += rowBirds totalWeight += weightFloat totalEggPieces += rowEggPieces totalEggKg += eggWeightFloat totalRemainingValueRp += rowRemainingValue totalEggValueRp += rowEggValue if weightFloat > 0 { totalHppSum += hppRp totalHppCount++ } if avgDocPrice > 0 { totalDocPriceSum += float64(avgDocPrice) totalDocPriceCount++ } if eggWeightFloat > 0 { totalEggHppSum += eggHpp totalEggHppCount++ } rangeAgg, exists := perRangeMap[rangeKey] if !exists { rangeAgg = &weightRangeAggregate{ Summary: &dto.HppPerKandangSummaryWeightRangeDTO{ WeightRange: dto.HppPerKandangWeightRangeDTO{ WeightMin: weightMin, WeightMax: weightMax, }, Label: fmt.Sprintf("%.2f - %.2f", weightMin, weightMax), }, } perRangeMap[rangeKey] = rangeAgg } rangeSummary := rangeAgg.Summary rangeSummary.RemainingChickenBirds += rowBirds rangeSummary.RemainingChickenWeightKg += row.RemainingChickenWeight rangeSummary.EggProductionPieces += rowEggPieces rangeSummary.EggProductionKg += eggWeightFloat rangeSummary.RemainingValueRp += rowRemainingValue rangeSummary.EggValueRp += rowEggValue if eggWeightFloat > 0 { rangeAgg.EggHppSum += eggHpp rangeAgg.EggHppCount++ } } rangeKeys := make([]weightRangeKey, 0, len(perRangeMap)) for key := range perRangeMap { rangeKeys = append(rangeKeys, key) } sort.Slice(rangeKeys, func(i, j int) bool { if rangeKeys[i].Min == rangeKeys[j].Min { return rangeKeys[i].Max < rangeKeys[j].Max } return rangeKeys[i].Min < rangeKeys[j].Min }) perRangeSummary := make([]dto.HppPerKandangSummaryWeightRangeDTO, 0, len(rangeKeys)) for idx, key := range rangeKeys { agg := perRangeMap[key] entry := agg.Summary entry.ID = idx + 1 if entry.RemainingChickenBirds > 0 { entry.AvgWeightKg = entry.RemainingChickenWeightKg / float64(entry.RemainingChickenBirds) } if agg.EggHppCount > 0 { entry.EggHppRpPerKg = agg.EggHppSum / float64(agg.EggHppCount) } perRangeSummary = append(perRangeSummary, *entry) } totalSummary := dto.HppPerKandangSummaryTotalDTO{ TotalRemainingChickenBirds: totalBirds, TotalRemainingChickenWeightKg: totalWeight, TotalEggProductionPieces: totalEggPieces, TotalEggProductionKg: totalEggKg, TotalRemainingValueRp: totalRemainingValueRp, TotalEggValueRp: totalEggValueRp, } if totalBirds > 0 { totalSummary.AverageWeightKg = totalWeight / float64(totalBirds) } if totalEggHppCount > 0 { totalSummary.AverageEggHppRpPerKg = totalEggHppSum / float64(totalEggHppCount) } if totalHppCount > 0 { totalSummary.TotalHppRp = totalHppSum / float64(totalHppCount) } if totalDocPriceCount > 0 { totalSummary.TotalAverageDocPriceRp = totalDocPriceSum / float64(totalDocPriceCount) } limit := params.Limit if limit <= 0 { limit = 10 } totalCount := len(dataRows) offset := (params.Page - 1) * limit if offset < 0 { offset = 0 } if offset > totalCount { offset = totalCount } end := offset + limit if end > totalCount { end = totalCount } pagedRows := dataRows[offset:end] data := dto.HppPerKandangResponseData{ Period: params.Period, Rows: pagedRows, Summary: dto.HppPerKandangSummaryDTO{ PerWeightRange: perRangeSummary, Total: totalSummary, }, } totalResults := int64(totalCount) totalPages := int64(0) if totalResults > 0 { totalPages = int64(math.Ceil(float64(totalResults) / float64(limit))) } if totalPages == 0 { totalPages = 1 } meta := &dto.HppPerKandangMetaDTO{ Page: params.Page, Limit: limit, TotalPages: totalPages, TotalResults: totalResults, Filters: filters, } return &data, meta, nil } func (s *repportService) parseHppPerKandangQuery(ctx *fiber.Ctx) (*validation.HppPerKandangQuery, dto.HppPerKandangFiltersDTO, error) { page := ctx.QueryInt("page", 1) if page < 1 { page = 1 } limit := ctx.QueryInt("limit", 10) if limit < 1 { limit = 10 } rawArea := ctx.Query("area_id", "") rawLocation := ctx.Query("location_id", "") rawKandang := ctx.Query("kandang_id", "") rawWeightMin := ctx.Query("weight_min", "") rawWeightMax := ctx.Query("weight_max", "") period := ctx.Query("period", "") showUnrecorded := ctx.QueryBool("show_unrecorded", false) areaIDs, err := parseCommaSeparatedInt64s(rawArea) if err != nil { return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error()) } locationIDs, err := parseCommaSeparatedInt64s(rawLocation) if err != nil { return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error()) } kandangIDs, err := parseCommaSeparatedInt64s(rawKandang) if err != nil { return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error()) } weightMin, err := parseOptionalFloat64(rawWeightMin) if err != nil { return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error()) } weightMax, err := parseOptionalFloat64(rawWeightMax) if err != nil { return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error()) } params := &validation.HppPerKandangQuery{ Page: page, Limit: limit, Period: period, ShowUnrecorded: showUnrecorded, AreaIDs: areaIDs, LocationIDs: locationIDs, KandangIDs: kandangIDs, WeightMin: weightMin, WeightMax: weightMax, } showUnrecordedFilter := "" if showUnrecorded { showUnrecordedFilter = "true" } filters := dto.NewHppPerKandangFiltersDTO( rawArea, rawLocation, rawKandang, rawWeightMin, rawWeightMax, period, showUnrecordedFilter, ) return params, filters, nil } func parseCommaSeparatedInt64s(raw string) ([]int64, error) { raw = strings.TrimSpace(raw) if raw == "" { return nil, nil } parts := strings.Split(raw, ",") result := make([]int64, 0, len(parts)) for _, part := range parts { part = strings.TrimSpace(part) if part == "" { continue } id, err := strconv.ParseInt(part, 10, 64) if err != nil { return nil, fmt.Errorf("invalid integer value '%s'", part) } result = append(result, id) } return result, nil } func parseOptionalFloat64(raw string) (*float64, error) { raw = strings.TrimSpace(raw) if raw == "" { return nil, nil } value, err := strconv.ParseFloat(raw, 64) if err != nil { return nil, fmt.Errorf("invalid float value '%s'", raw) } return &value, nil }