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" 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) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error) } type repportService struct { Log *logrus.Logger Validate *validator.Validate ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository PurchaseRepo purchaseRepo.PurchaseRepository ChickinRepo chickinRepo.ProjectChickinRepository RecordingRepo recordingRepo.RecordingRepository ApprovalSvc approvalService.ApprovalService PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository HppPerKandangRepo repportRepo.HppPerKandangRepository ProductionResultRepo repportRepo.ProductionResultRepository } type HppCostAggregate struct { FeedCost float64 OvkCost float64 DocCost float64 DocQty float64 BudgetCost float64 ExpenseCost float64 } func NewRepportService( validate *validator.Validate, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository, marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, purchaseRepo purchaseRepo.PurchaseRepository, chickinRepo chickinRepo.ProjectChickinRepository, recordingRepo recordingRepo.RecordingRepository, approvalSvc approvalService.ApprovalService, purchaseSupplierRepo repportRepo.PurchaseSupplierRepository, hppPerKandangRepo repportRepo.HppPerKandangRepository, productionResultRepo repportRepo.ProductionResultRepository, ) RepportService { return &repportService{ Log: utils.Log, Validate: validate, ExpenseRealizationRepo: expenseRealizationRepo, MarketingDeliveryRepo: marketingDeliveryRepo, PurchaseRepo: purchaseRepo, ChickinRepo: chickinRepo, RecordingRepo: recordingRepo, ApprovalSvc: approvalSvc, PurchaseSupplierRepo: purchaseSupplierRepo, HppPerKandangRepo: hppPerKandangRepo, ProductionResultRepo: productionResultRepo, } } 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 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) 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) 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 }