package service import ( "context" "encoding/json" "errors" "fmt" "math" "sort" "strconv" "strings" "time" "gitlab.com/mbugroup/lti-api.git/internal/config" m "gitlab.com/mbugroup/lti-api.git/internal/middleware" "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" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" 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" customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories" productionStandardRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories" 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) GetExpenseDepreciation(ctx *fiber.Ctx) ([]dto.ExpenseDepreciationRowDTO, *dto.ExpenseDepreciationMetaDTO, error) GetExpenseDepreciationManualInputs(ctx *fiber.Ctx) ([]dto.ExpenseDepreciationManualInputRowDTO, *dto.ExpenseDepreciationMetaDTO, error) UpsertExpenseDepreciationManualInput(ctx *fiber.Ctx, req *validation.ExpenseDepreciationManualInputUpsert) (*dto.ExpenseDepreciationManualInputRowDTO, 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) GetHppV2Breakdown(ctx *fiber.Ctx, params *validation.HppV2BreakdownQuery) (*approvalService.HppV2Breakdown, error) GetProductionResult(ctx *fiber.Ctx, params *validation.ProductionResultQuery) ([]dto.ProductionResultDTO, int64, error) GetCustomerPayment(ctx *fiber.Ctx, params *validation.CustomerPaymentQuery) ([]dto.CustomerPaymentReportItem, int64, error) DB() *gorm.DB } type repportService struct { Log *logrus.Logger Validate *validator.Validate db *gorm.DB ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository ExpenseDepreciationRepo repportRepo.ExpenseDepreciationRepository MarketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository PurchaseRepo purchaseRepo.PurchaseRepository ChickinRepo chickinRepo.ProjectChickinRepository RecordingRepo recordingRepo.RecordingRepository ApprovalSvc approvalService.ApprovalService HppSvc approvalService.HppService HppV2Svc approvalService.HppV2Service HppCostRepo commonRepo.HppCostRepository PurchaseSupplierRepo repportRepo.PurchaseSupplierRepository DebtSupplierRepo repportRepo.DebtSupplierRepository HppPerKandangRepo repportRepo.HppPerKandangRepository ProductionResultRepo repportRepo.ProductionResultRepository CustomerPaymentRepo repportRepo.CustomerPaymentRepository CustomerRepo customerRepo.CustomerRepository StandardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository ProductionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository } 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, expenseDepreciationRepo repportRepo.ExpenseDepreciationRepository, marketingDeliveryRepo marketingRepo.MarketingDeliveryProductRepository, purchaseRepo purchaseRepo.PurchaseRepository, chickinRepo chickinRepo.ProjectChickinRepository, recordingRepo recordingRepo.RecordingRepository, approvalSvc approvalService.ApprovalService, hppSvc approvalService.HppService, hppV2Svc approvalService.HppV2Service, hppCostRepo commonRepo.HppCostRepository, purchaseSupplierRepo repportRepo.PurchaseSupplierRepository, debtSupplierRepo repportRepo.DebtSupplierRepository, hppPerKandangRepo repportRepo.HppPerKandangRepository, productionResultRepo repportRepo.ProductionResultRepository, customerPaymentRepo repportRepo.CustomerPaymentRepository, customerRepo customerRepo.CustomerRepository, standardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository, productionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository, ) RepportService { return &repportService{ Log: utils.Log, Validate: validate, db: db, ExpenseRealizationRepo: expenseRealizationRepo, ExpenseDepreciationRepo: expenseDepreciationRepo, MarketingDeliveryRepo: marketingDeliveryRepo, PurchaseRepo: purchaseRepo, ChickinRepo: chickinRepo, RecordingRepo: recordingRepo, ApprovalSvc: approvalSvc, HppSvc: hppSvc, HppV2Svc: hppV2Svc, HppCostRepo: hppCostRepo, PurchaseSupplierRepo: purchaseSupplierRepo, DebtSupplierRepo: debtSupplierRepo, HppPerKandangRepo: hppPerKandangRepo, ProductionResultRepo: productionResultRepo, CustomerPaymentRepo: customerPaymentRepo, CustomerRepo: customerRepo, StandardGrowthDetailRepo: standardGrowthDetailRepo, ProductionStandardDetailRepo: productionStandardDetailRepo, } } func (s *repportService) DB() *gorm.DB { return s.db } 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) GetExpenseDepreciation(ctx *fiber.Ctx) ([]dto.ExpenseDepreciationRowDTO, *dto.ExpenseDepreciationMetaDTO, error) { params, filters, err := s.parseExpenseDepreciationQuery(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()) } if s.ExpenseDepreciationRepo == nil { return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "expense depreciation repository is not configured") } 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") } candidateRows, err := s.ExpenseDepreciationRepo.GetCandidateFarms( ctx.Context(), periodDate, params.AreaIDs, params.LocationIDs, params.ProjectFlockIDs, ) if err != nil { return nil, nil, err } limit := params.Limit if limit <= 0 { limit = 10 } if len(candidateRows) == 0 { meta := &dto.ExpenseDepreciationMetaDTO{ Page: params.Page, Limit: limit, TotalPages: 1, TotalResults: 0, Filters: filters, } return []dto.ExpenseDepreciationRowDTO{}, meta, nil } farmIDs := make([]uint, 0, len(candidateRows)) farmNameByID := make(map[uint]string, len(candidateRows)) for _, row := range candidateRows { farmIDs = append(farmIDs, row.ProjectFlockID) farmNameByID[row.ProjectFlockID] = row.FarmName } snapshotByFarmID := make(map[uint]entity.FarmDepreciationSnapshot) if params.ForceRecompute { computedSnapshots, computeErr := s.computeExpenseDepreciationSnapshots(ctx.Context(), periodDate, farmIDs, farmNameByID) if computeErr != nil { return nil, nil, computeErr } if len(computedSnapshots) > 0 { if err := s.ExpenseDepreciationRepo.UpsertSnapshots(ctx.Context(), computedSnapshots); err != nil { return nil, nil, err } snapshotByFarmID = make(map[uint]entity.FarmDepreciationSnapshot, len(computedSnapshots)) for _, row := range computedSnapshots { snapshotByFarmID[row.ProjectFlockId] = row } } } else { snapshots, err := s.ExpenseDepreciationRepo.GetSnapshotsByPeriodAndFarmIDs(ctx.Context(), periodDate, farmIDs) if err != nil { return nil, nil, err } snapshotByFarmID = make(map[uint]entity.FarmDepreciationSnapshot, len(snapshots)) for _, row := range snapshots { snapshotByFarmID[row.ProjectFlockId] = row } missingFarmIDs := make([]uint, 0) for _, farmID := range farmIDs { if _, exists := snapshotByFarmID[farmID]; exists { continue } missingFarmIDs = append(missingFarmIDs, farmID) } if len(missingFarmIDs) > 0 { computedSnapshots, computeErr := s.computeExpenseDepreciationSnapshots(ctx.Context(), periodDate, missingFarmIDs, farmNameByID) if computeErr != nil { return nil, nil, computeErr } if len(computedSnapshots) > 0 { if err := s.ExpenseDepreciationRepo.UpsertSnapshots(ctx.Context(), computedSnapshots); err != nil { return nil, nil, err } for _, row := range computedSnapshots { snapshotByFarmID[row.ProjectFlockId] = row } } } } rows := make([]dto.ExpenseDepreciationRowDTO, 0, len(candidateRows)) for _, candidate := range candidateRows { snapshot, exists := snapshotByFarmID[candidate.ProjectFlockID] if !exists { rows = append(rows, dto.ExpenseDepreciationRowDTO{ ProjectFlockID: int64(candidate.ProjectFlockID), FarmName: candidate.FarmName, Period: params.Period, DepreciationPercentEffective: 0, DepreciationValue: 0, PulletCostDayNTotal: 0, Components: map[string]any{}, }) continue } rows = append(rows, dto.ExpenseDepreciationRowDTO{ ProjectFlockID: int64(snapshot.ProjectFlockId), FarmName: candidate.FarmName, Period: params.Period, DepreciationPercentEffective: snapshot.DepreciationPercentEffective, DepreciationValue: snapshot.DepreciationValue, PulletCostDayNTotal: snapshot.PulletCostDayNTotal, Components: parseSnapshotComponents(snapshot.Components), }) } totalResults := int64(len(rows)) totalPages := int64(0) if totalResults > 0 { totalPages = int64(math.Ceil(float64(totalResults) / float64(limit))) } if totalPages == 0 { totalPages = 1 } offset := (params.Page - 1) * limit if offset < 0 { offset = 0 } if offset > len(rows) { offset = len(rows) } end := offset + limit if end > len(rows) { end = len(rows) } meta := &dto.ExpenseDepreciationMetaDTO{ Page: params.Page, Limit: limit, TotalPages: totalPages, TotalResults: totalResults, Filters: filters, } return rows[offset:end], meta, nil } func (s *repportService) GetExpenseDepreciationManualInputs(ctx *fiber.Ctx) ([]dto.ExpenseDepreciationManualInputRowDTO, *dto.ExpenseDepreciationMetaDTO, error) { params, filters, err := s.parseExpenseDepreciationQuery(ctx) if err != nil { return nil, nil, err } if s.ExpenseDepreciationRepo == nil { return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "expense depreciation repository is not configured") } repoRows, err := s.ExpenseDepreciationRepo.GetLatestManualInputsByFarms( ctx.Context(), params.AreaIDs, params.LocationIDs, params.ProjectFlockIDs, ) if err != nil { return nil, nil, err } rows := make([]dto.ExpenseDepreciationManualInputRowDTO, 0, len(repoRows)) for _, row := range repoRows { rows = append(rows, dto.ExpenseDepreciationManualInputRowDTO{ ID: int64(row.Id), ProjectFlockID: int64(row.ProjectFlockID), FarmName: row.FarmName, TotalCost: row.TotalCost, CutoverDate: row.CutoverDate.Format("2006-01-02"), Note: row.Note, }) } limit := params.Limit if limit <= 0 { limit = 10 } totalResults := int64(len(rows)) totalPages := int64(0) if totalResults > 0 { totalPages = int64(math.Ceil(float64(totalResults) / float64(limit))) } if totalPages == 0 { totalPages = 1 } offset := (params.Page - 1) * limit if offset < 0 { offset = 0 } if offset > len(rows) { offset = len(rows) } end := offset + limit if end > len(rows) { end = len(rows) } meta := &dto.ExpenseDepreciationMetaDTO{ Page: params.Page, Limit: limit, TotalPages: totalPages, TotalResults: totalResults, Filters: filters, } return rows[offset:end], meta, nil } func (s *repportService) UpsertExpenseDepreciationManualInput(ctx *fiber.Ctx, req *validation.ExpenseDepreciationManualInputUpsert) (*dto.ExpenseDepreciationManualInputRowDTO, error) { if req == nil { return nil, fiber.NewError(fiber.StatusBadRequest, "request is required") } if err := s.Validate.Struct(req); err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, err.Error()) } if s.ExpenseDepreciationRepo == nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "expense depreciation repository is not configured") } location, err := time.LoadLocation("Asia/Jakarta") if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration") } cutoverDate, err := time.ParseInLocation("2006-01-02", req.CutoverDate, location) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "cutover_date must follow format YYYY-MM-DD") } row := entity.FarmDepreciationManualInput{ ProjectFlockId: req.ProjectFlockID, TotalCost: req.TotalCost, CutoverDate: cutoverDate, Note: req.Note, } if err := s.ExpenseDepreciationRepo.UpsertManualInput(ctx.Context(), &row); err != nil { return nil, err } if err := s.ExpenseDepreciationRepo.DeleteSnapshotsFromDate( ctx.Context(), cutoverDate, []uint{row.ProjectFlockId}, ); err != nil { return nil, err } response := &dto.ExpenseDepreciationManualInputRowDTO{ ID: int64(row.Id), ProjectFlockID: int64(row.ProjectFlockId), TotalCost: row.TotalCost, CutoverDate: row.CutoverDate.Format("2006-01-02"), Note: row.Note, } listRows, listErr := s.ExpenseDepreciationRepo.GetLatestManualInputsByFarms( ctx.Context(), nil, nil, []int64{int64(row.ProjectFlockId)}, ) if listErr == nil { for _, listRow := range listRows { if listRow.ProjectFlockID == row.ProjectFlockId { response.FarmName = listRow.FarmName break } } } return response, nil } type depreciationKandangComponent struct { ProjectFlockKandangID uint `json:"project_flock_kandang_id"` KandangID uint `json:"kandang_id"` KandangName string `json:"kandang_name"` TransferID uint `json:"transfer_id"` TransferDate string `json:"transfer_date"` SourceProjectFlockID uint `json:"source_project_flock_id"` HouseType string `json:"house_type"` DayN int `json:"day_n"` DepreciationPercent float64 `json:"depreciation_percent"` TransferQty float64 `json:"transfer_qty"` PulletCostDayN float64 `json:"pullet_cost_day_n"` DepreciationValue float64 `json:"depreciation_value"` DepreciationSource string `json:"depreciation_source,omitempty"` ManualInputID *uint `json:"manual_input_id,omitempty"` CutoverDate string `json:"cutover_date,omitempty"` OriginDate string `json:"origin_date,omitempty"` StartScheduleDay *int `json:"start_schedule_day,omitempty"` } type depreciationFarmComponents struct { KandangCount int `json:"kandang_count"` Kandang []depreciationKandangComponent `json:"kandang"` } func (s *repportService) computeExpenseDepreciationSnapshots( ctx context.Context, periodDate time.Time, farmIDs []uint, farmNameByID map[uint]string, ) ([]entity.FarmDepreciationSnapshot, error) { _ = farmNameByID if len(farmIDs) == 0 { return []entity.FarmDepreciationSnapshot{}, nil } if s.HppCostRepo == nil { return nil, errors.New("hpp cost repository is not configured") } if s.HppV2Svc == nil { return nil, errors.New("hpp v2 service is not configured") } result := make([]entity.FarmDepreciationSnapshot, 0, len(farmIDs)) for _, farmID := range farmIDs { kandangIDs, err := s.HppCostRepo.GetProjectFlockKandangIDs(ctx, farmID) if err != nil { return nil, err } components := depreciationFarmComponents{ Kandang: make([]depreciationKandangComponent, 0, len(kandangIDs)), } totalDepreciationValue := 0.0 totalPulletCostDayN := 0.0 for _, kandangID := range kandangIDs { breakdown, err := s.HppV2Svc.CalculateHppBreakdown(kandangID, &periodDate) if err != nil { return nil, err } if breakdown == nil { continue } depreciationComponent := hppV2FindDepreciationComponent(breakdown) if depreciationComponent == nil { continue } for _, part := range depreciationComponent.Parts { if part.Total <= 0 { continue } houseType := approvalService.NormalizeDepreciationHouseType(breakdown.HouseType) component := depreciationKandangComponent{ ProjectFlockKandangID: breakdown.ProjectFlockKandangID, KandangID: breakdown.KandangID, KandangName: breakdown.KandangName, SourceProjectFlockID: hppV2DetailUint(part.Details, "source_project_flock_id"), HouseType: houseType, DayN: hppV2DetailInt(part.Details, "schedule_day"), DepreciationPercent: hppV2DetailFloat(part.Details, "depreciation_percent"), PulletCostDayN: hppV2DetailFloat(part.Details, "pullet_cost_day_n"), DepreciationValue: part.Total, DepreciationSource: part.Code, OriginDate: hppV2DetailString(part.Details, "origin_date"), } if component.HouseType == "" { component.HouseType = approvalService.NormalizeDepreciationHouseType(hppV2DetailString(part.Details, "house_type")) } if ref := hppV2FindReference(part.References, "laying_transfer"); ref != nil { component.TransferID = ref.ID component.TransferDate = ref.Date component.TransferQty = ref.Qty } if part.Code == "manual_cutover" { if startDay := hppV2DetailInt(part.Details, "start_schedule_day"); startDay > 0 { component.StartScheduleDay = &startDay } component.CutoverDate = hppV2DetailString(part.Details, "cutover_date") if manualID := hppV2DetailUint(part.Details, "manual_input_id"); manualID > 0 { component.ManualInputID = &manualID } if component.ManualInputID == nil { if ref := hppV2FindReference(part.References, "farm_depreciation_manual_input"); ref != nil && ref.ID > 0 { manualID := ref.ID component.ManualInputID = &manualID } } } totalPulletCostDayN += component.PulletCostDayN totalDepreciationValue += component.DepreciationValue components.Kandang = append(components.Kandang, component) } } components.KandangCount = len(components.Kandang) effectivePercent := approvalService.CalculateEffectiveDepreciationPercent(totalDepreciationValue, totalPulletCostDayN) componentsJSON, marshalErr := json.Marshal(components) if marshalErr != nil { return nil, marshalErr } result = append(result, entity.FarmDepreciationSnapshot{ ProjectFlockId: farmID, PeriodDate: periodDate, DepreciationPercentEffective: effectivePercent, DepreciationValue: totalDepreciationValue, PulletCostDayNTotal: totalPulletCostDayN, Components: componentsJSON, }) } return result, nil } func hppV2FindDepreciationComponent(breakdown *approvalService.HppV2Breakdown) *approvalService.HppV2Component { if breakdown == nil { return nil } for idx := range breakdown.Components { if breakdown.Components[idx].Code == "DEPRECIATION" { return &breakdown.Components[idx] } } return nil } func hppV2FindReference(references []approvalService.HppV2Reference, refType string) *approvalService.HppV2Reference { if refType == "" { return nil } for idx := range references { if references[idx].Type == refType { return &references[idx] } } return nil } func hppV2DetailFloat(details map[string]any, key string) float64 { if details == nil || key == "" { return 0 } raw, exists := details[key] if !exists || raw == nil { return 0 } switch value := raw.(type) { case float64: return value case float32: return float64(value) case int: return float64(value) case int8: return float64(value) case int16: return float64(value) case int32: return float64(value) case int64: return float64(value) case uint: return float64(value) case uint8: return float64(value) case uint16: return float64(value) case uint32: return float64(value) case uint64: return float64(value) case string: parsed, err := strconv.ParseFloat(strings.TrimSpace(value), 64) if err != nil { return 0 } return parsed default: return 0 } } func hppV2DetailInt(details map[string]any, key string) int { return int(math.Round(hppV2DetailFloat(details, key))) } func hppV2DetailUint(details map[string]any, key string) uint { value := hppV2DetailInt(details, key) if value < 0 { return 0 } return uint(value) } func hppV2DetailString(details map[string]any, key string) string { if details == nil || key == "" { return "" } raw, exists := details[key] if !exists || raw == nil { return "" } switch value := raw.(type) { case string: return value case time.Time: return value.Format("2006-01-02") default: return fmt.Sprintf("%v", value) } } func parseSnapshotComponents(raw []byte) any { if len(raw) == 0 { return map[string]any{} } var out any if err := json.Unmarshal(raw, &out); err != nil { return map[string]any{} } return out } func valueOrEmptyString(v *string) string { if v == nil { return "" } return *v } 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 } customerGroups := make(map[uint][]entity.MarketingDeliveryProduct) for _, dp := range deliveryProducts { customerID := dp.MarketingProduct.Marketing.CustomerId customerGroups[customerID] = append(customerGroups[customerID], dp) } agingMap := make(map[int]int) for customerID := range customerGroups { transactions, err := s.CustomerPaymentRepo.GetCustomerPaymentTransactions(c.Context(), &customerID) if err != nil { continue } initialBalance, err := s.CustomerPaymentRepo.GetInitialBalanceByCustomer(c.Context(), customerID) if err != nil { initialBalance = 0 } runningBalance := initialBalance for i, tx := range transactions { if tx.TransactionType == "SALES" { previousBalance := runningBalance runningBalance -= tx.TotalPrice currentBalance := runningBalance _, paymentDate := s.determineSalesStatusAndPaymentDate(transactions, i, previousBalance, currentBalance) if paymentDate != nil { agingDays := int(paymentDate.Sub(tx.TransDate).Hours() / 24) agingMap[int(tx.TransactionID)] = agingDays } else { agingDays := int(time.Since(tx.TransDate).Hours() / 24) agingMap[int(tx.TransactionID)] = agingDays } } else if tx.TransactionType == "PAYMENT" { runningBalance += tx.PaymentAmount } } } deliveryIDs := make([]uint, 0, len(deliveryProducts)) for _, delivery := range deliveryProducts { deliveryIDs = append(deliveryIDs, delivery.Id) } attributionRows, err := s.MarketingDeliveryRepo.GetAttributionRowsByDeliveryProductIDs(c.Context(), deliveryIDs) if err != nil { return nil, 0, err } hppByDelivery := buildMarketingHppByDelivery(c.Context(), s.HppV2Svc, attributionRows) categoryByDelivery := buildMarketingCategoryByDelivery(deliveryProducts, attributionRows) items := dto.ToMarketingReportItems(deliveryProducts, hppByDelivery, categoryByDelivery, agingMap) return items, total, nil } func buildMarketingHppByDelivery( ctx context.Context, hppSvc approvalService.HppV2Service, attributionRows []commonRepo.MarketingDeliveryAttributionRow, ) map[uint]float64 { if len(attributionRows) == 0 { return map[uint]float64{} } hppByKandang := make(map[uint]float64) weightedByDelivery := make(map[uint]float64) totalQtyByDelivery := make(map[uint]float64) for _, row := range attributionRows { if row.MarketingDeliveryProductID == 0 || row.ProjectFlockKandangID == 0 || row.AllocatedQty <= 0 { continue } hppPerKg, exists := hppByKandang[row.ProjectFlockKandangID] if !exists { hppPerKg = 0 if hppSvc != nil && utils.ProjectFlockCategory(row.ProjectFlockCategory) == utils.ProjectFlockCategoryLaying { if hppCost, err := hppSvc.CalculateHppCost(row.ProjectFlockKandangID, nil); err == nil && hppCost != nil { hppPerKg = hppCost.Real.HargaKg } } hppByKandang[row.ProjectFlockKandangID] = hppPerKg } weightedByDelivery[row.MarketingDeliveryProductID] += row.AllocatedQty * hppPerKg totalQtyByDelivery[row.MarketingDeliveryProductID] += row.AllocatedQty } result := make(map[uint]float64, len(totalQtyByDelivery)) for deliveryID, totalQty := range totalQtyByDelivery { if totalQty <= 0 { continue } result[deliveryID] = weightedByDelivery[deliveryID] / totalQty } return result } func buildMarketingCategoryByDelivery( deliveryProducts []entity.MarketingDeliveryProduct, attributionRows []commonRepo.MarketingDeliveryAttributionRow, ) map[uint]string { result := make(map[uint]string, len(deliveryProducts)) for _, row := range attributionRows { if row.MarketingDeliveryProductID == 0 || strings.TrimSpace(row.ProjectFlockCategory) == "" { continue } if _, exists := result[row.MarketingDeliveryProductID]; !exists { result[row.MarketingDeliveryProductID] = row.ProjectFlockCategory } } for _, delivery := range deliveryProducts { if _, exists := result[delivery.Id]; exists { continue } if delivery.AttributedProjectFlockKandang != nil { result[delivery.Id] = delivery.AttributedProjectFlockKandang.ProjectFlock.Category continue } if delivery.MarketingProduct.ProductWarehouse.ProjectFlockKandang != nil { result[delivery.Id] = delivery.MarketingProduct.ProductWarehouse.ProjectFlockKandang.ProjectFlock.Category } } return result } 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 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 productionStandardID uint if s.ProductionResultRepo != nil { standardID, err := s.ProductionResultRepo.GetProductionStandardIDByProjectFlockKandangID(ctx.Context(), params.ProjectFlockKandangID) if err != nil { if !errors.Is(err, gorm.ErrRecordNotFound) { return nil, 0, err } } else { productionStandardID = standardID } } standardDetailCache := make(map[int]*entity.ProductionStandardDetail) growthDetailCache := make(map[int]*entity.StandardGrowthDetail) weeks := make([]int, len(weeklyResults)) for i := range weeklyResults { weeks[i] = int(weeklyResults[i].Woa) } uniformityMap, err := s.getUniformityByWeek(ctx.Context(), params.ProjectFlockKandangID, weeks) if err != nil { return nil, 0, err } var cumulativeButir int64 var cumulativeKg float64 for i := range weeklyResults { weeklyResults[i].StdBw = defaultStdBw weeklyResults[i].Bw = defaultBw if weeklyResults[i].StdUniformity == "" { weeklyResults[i].StdUniformity = defaultUniformText } if uniformity, ok := uniformityMap[int(weeklyResults[i].Woa)]; ok { weeklyResults[i].Uniformity = uniformity.Uniformity if uniformity.AvgWeight != nil { weeklyResults[i].Bw = *uniformity.AvgWeight } } cumulativeButir += weeklyResults[i].ButiranJumlah weeklyResults[i].TotalButir = cumulativeButir cumulativeKg += weeklyResults[i].KgJumlah weeklyResults[i].TotalKg = cumulativeKg if productionStandardID == 0 { continue } week := int(weeklyResults[i].Woa) if s.ProductionStandardDetailRepo != nil { detail, ok := standardDetailCache[week] if !ok { fetched, fetchErr := s.ProductionStandardDetailRepo.GetByStandardIDAndWeek(ctx.Context(), productionStandardID, week) if fetchErr != nil { if !errors.Is(fetchErr, gorm.ErrRecordNotFound) { return nil, 0, fetchErr } } else { detail = fetched } standardDetailCache[week] = detail } if detail != nil { if detail.TargetHenDayProduction != nil { weeklyResults[i].HdStd = *detail.TargetHenDayProduction } if detail.TargetHenHouseProduction != nil { weeklyResults[i].HhStd = *detail.TargetHenHouseProduction } if detail.TargetEggWeight != nil { weeklyResults[i].EwStd = *detail.TargetEggWeight } if detail.TargetEggMass != nil { weeklyResults[i].EmStd = *detail.TargetEggMass } if detail.StandardFCR != nil { weeklyResults[i].FcrStd = *detail.StandardFCR } } } if s.StandardGrowthDetailRepo != nil { detail, ok := growthDetailCache[week] if !ok { fetched, fetchErr := s.StandardGrowthDetailRepo.GetByStandardIDAndWeek(ctx.Context(), productionStandardID, week) if fetchErr != nil { if !errors.Is(fetchErr, gorm.ErrRecordNotFound) { return nil, 0, fetchErr } } else { detail = fetched } growthDetailCache[week] = detail } if detail != nil && detail.FeedIntake != nil { weeklyResults[i].FiStd = *detail.FeedIntake } if detail != nil && detail.TargetMeanBw != nil { weeklyResults[i].StdBw = *detail.TargetMeanBw } if detail != nil { weeklyResults[i].DepStd = valueOrZero(detail.MaxDepletion) } } } 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 params.SortBy == "" { params.SortBy = "customer" } if params.SortOrder == "" { params.SortOrder = "asc" } if err := s.Validate.Struct(params); err != nil { return nil, 0, err } locationScope, err := m.ResolveLocationScope(ctx, s.DB()) if err != nil { return nil, 0, err } areaScope, err := m.ResolveAreaScope(ctx, s.DB()) if err != nil { return nil, 0, err } restrictScope := locationScope.Restrict || areaScope.Restrict var allowedCustomerIDs []uint if restrictScope { if (locationScope.Restrict && len(locationScope.IDs) == 0) || (areaScope.Restrict && len(areaScope.IDs) == 0) { return []dto.CustomerPaymentReportItem{}, 0, nil } allowedCustomerIDs, err = s.getCustomerIDsByScope(ctx.Context(), locationScope.IDs, areaScope.IDs) if err != nil { return nil, 0, err } if len(allowedCustomerIDs) == 0 { return []dto.CustomerPaymentReportItem{}, 0, nil } } var customerIDs []uint var totalCustomers int64 if len(params.CustomerIDs) > 0 { customerIDs = params.CustomerIDs if restrictScope { customerIDs = intersectUint(customerIDs, allowedCustomerIDs) } totalCustomers = int64(len(customerIDs)) if len(customerIDs) == 0 { return []dto.CustomerPaymentReportItem{}, 0, nil } } else { 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, allowedCustomerIDs, params.SortBy, params.SortOrder) if err != nil { return nil, 0, err } if len(customerIDs) == 0 { return []dto.CustomerPaymentReportItem{}, 0, nil } } var result []dto.CustomerPaymentReportItem for _, customerID := range customerIDs { item, err := s.processCustomerPayment(ctx.Context(), customerID, params) if err != nil { return nil, 0, err } if len(item.Rows) > 0 { result = append(result, item) } } totalCustomers = int64(len(result)) return result, totalCustomers, nil } func (s *repportService) getCustomerIDsByScope(ctx context.Context, locationIDs, areaIDs []uint) ([]uint, error) { if len(locationIDs) == 0 && len(areaIDs) == 0 { return []uint{}, nil } db := s.db.WithContext(ctx). Table("customers c"). Select("DISTINCT c.id"). Joins("JOIN marketings m ON m.customer_id = c.id"). Joins("JOIN marketing_products mp ON mp.marketing_id = m.id"). Joins("JOIN marketing_delivery_products mdp ON mdp.marketing_product_id = mp.id"). Joins("JOIN product_warehouses pw ON pw.id = mdp.product_warehouse_id"). Joins("JOIN warehouses w ON w.id = pw.warehouse_id"). Where("mdp.delivery_date IS NOT NULL"). Where("m.deleted_at IS NULL"). Where("c.deleted_at IS NULL") if len(locationIDs) > 0 { db = db.Where("w.location_id IN ?", locationIDs) } if len(areaIDs) > 0 { db = db.Where("w.area_id IN ?", areaIDs) } var customerIDs []uint if err := db.Pluck("c.id", &customerIDs).Error; err != nil { return nil, err } return customerIDs, nil } func (s *repportService) processCustomerPayment(ctx context.Context, customerID uint, params *validation.CustomerPaymentQuery) (dto.CustomerPaymentReportItem, error) { customer, err := s.CustomerRepo.GetByID(ctx, customerID, nil) if 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 paymentDate != nil { days := int(paymentDate.Sub(tx.TransDate).Hours() / 24) row.AgingDay = &days } else { days := 0 row.AgingDay = &days } } else { days := int(time.Since(tx.TransDate).Hours() / 24) 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 } filterBy := strings.ToUpper(strings.TrimSpace(params.FilterBy)) if filterBy == "" { filterBy = utils.CustomerPaymentFilterByTransDate } 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 } endOfDay := time.Date(parsed.Year(), parsed.Month(), parsed.Day(), 23, 59, 59, 999999999, location) endDate = &endOfDay } for _, row := range rows { var compareDate time.Time if filterBy == utils.CustomerPaymentFilterByRealizationDate { if row.DeliveryDate == nil { continue } compareDate = row.DeliveryDate.In(location) } else { compareDate = row.TransDate.In(location) } if startDate != nil && compareDate.Before(*startDate) { continue } if endDate != nil && compareDate.After(*endDate) { continue } filteredRows = append(filteredRows, row) } rows = filteredRows } summary := dto.ToCustomerPaymentReportSummary(rows, initialBalance) return dto.ToCustomerPaymentReportItem(*customer, initialBalance, rows, summary), nil } func (s *repportService) determineSalesStatusAndPaymentDate(transactions []repportRepo.CustomerPaymentTransaction, currentIndex int, previousBalance, currentBalance float64) (string, *time.Time) { currentSales := transactions[currentIndex] if previousBalance >= currentSales.TotalPrice { type paymentAllocation struct { date time.Time amount float64 consumed float64 } allocations := []paymentAllocation{} runningBalance := 0.0 for i := 0; i < currentIndex; i++ { if transactions[i].TransactionType == "PAYMENT" { allocations = append(allocations, paymentAllocation{ date: transactions[i].TransDate, amount: transactions[i].PaymentAmount, consumed: 0, }) runningBalance += transactions[i].PaymentAmount } else if transactions[i].TransactionType == "SALES" { salesAmount := transactions[i].TotalPrice remainingToConsume := salesAmount for j := range allocations { if remainingToConsume <= 0 { break } available := allocations[j].amount - allocations[j].consumed if available > 0 { consume := available if consume > remainingToConsume { consume = remainingToConsume } allocations[j].consumed += consume remainingToConsume -= consume } } runningBalance -= salesAmount } } amountNeeded := currentSales.TotalPrice for _, alloc := range allocations { available := alloc.amount - alloc.consumed if available > 0 { if amountNeeded <= available { return "LUNAS", &alloc.date } else { amountNeeded -= available } } } if len(allocations) > 0 { return "LUNAS", &allocations[0].date } 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), Hd: valueOrZero(record.HenDay), Fi: valueOrZero(record.FeedIntake), Fcr: valueOrZero(record.FcrValue), Hh: valueOrZero(record.HenHouse), Em: valueOrZero(record.EggMass), Ew: valueOrZero(record.EggWeight), } if record.Day != nil && *record.Day > 0 { result.Woa = float64((*record.Day + 6) / 7) // ceil(day/7) } 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) } 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 } type uniformityWeekData struct { Uniformity float64 AvgWeight *float64 } type uniformityChartPayload struct { Statistics *uniformityChartStats `json:"statistics"` } type uniformityChartStats struct { AverageWeight *float64 `json:"average_weight"` } func (s *repportService) getUniformityByWeek(ctx context.Context, projectFlockKandangID uint, weeks []int) (map[int]uniformityWeekData, error) { result := make(map[int]uniformityWeekData, len(weeks)) if projectFlockKandangID == 0 || len(weeks) == 0 { return result, nil } weekExpr := fmt.Sprintf(`CASE WHEN u.uniform_date IS NULL OR pc.chick_in_date IS NULL THEN 0 WHEN u.uniform_date::date < pc.chick_in_date THEN 0 WHEN UPPER(pf.category) = 'LAYING' THEN (((u.uniform_date::date - pc.chick_in_date)::int) / 7) + %d ELSE (((u.uniform_date::date - pc.chick_in_date)::int) / 7) + 1 END`, config.LayingWeekStart()) var rows []entity.ProjectFlockKandangUniformity if err := s.db.WithContext(ctx). Table("project_flock_kandang_uniformity AS u"). Select(fmt.Sprintf("%s AS week, u.uniformity, u.uniform_date, u.id, u.chart_data", weekExpr)). Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = u.project_flock_kandang_id"). Joins("JOIN project_flocks AS pf ON pf.id = pfk.project_flock_id"). Joins(`JOIN ( SELECT project_flock_kandang_id, MIN(chick_in_date)::date AS chick_in_date FROM project_chickins WHERE deleted_at IS NULL GROUP BY project_flock_kandang_id ) AS pc ON pc.project_flock_kandang_id = u.project_flock_kandang_id`). Where("u.project_flock_kandang_id = ?", projectFlockKandangID). Where(fmt.Sprintf("%s IN ?", weekExpr), weeks). Order("uniform_date DESC"). Order("id DESC"). Find(&rows).Error; err != nil { return nil, err } for _, row := range rows { if _, exists := result[row.Week]; exists { continue } result[row.Week] = uniformityWeekData{ Uniformity: row.Uniformity, AvgWeight: extractAverageWeight(row.ChartData, s.Log), } } return result, nil } func extractAverageWeight(raw json.RawMessage, log *logrus.Logger) *float64 { if len(raw) == 0 { return nil } var payload uniformityChartPayload if err := json.Unmarshal(raw, &payload); err != nil { if log != nil { log.WithError(err).Warn("uniformity chart_data decode failed") } return nil } if payload.Statistics == nil { return nil } return payload.Statistics.AverageWeight } 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], groupSize)) } return result } func aggregateProductionResultGroup(group []dto.ProductionResultDTO, groupSize int) 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, Woa: group[0].Woa, } 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 } weeklyDivider := float64(groupSize) if weeklyDivider == 0 { weeklyDivider = divider } 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 = roundFloat(sumHd/weeklyDivider, 2) agg.HdStd = sumHdStd / divider agg.Fi = roundFloat(sumFi/weeklyDivider, 2) agg.FiStd = sumFiStd / divider agg.Em = group[count-1].Em agg.EmStd = sumEmStd / divider agg.Ew = group[count-1].Ew agg.EwStd = sumEwStd / divider agg.Fcr = roundFloat(sumFcr/weeklyDivider, 2) agg.FcrStd = sumFcrStd / divider agg.Hh = roundFloat(sumHh/weeklyDivider, 2) 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 == "" { params.FilterBy = "received_date" } if params.SortBy == "" { params.SortBy = "supplier" } if params.SortOrder == "" { params.SortOrder = "asc" } 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)) for _, purchase := range purchases { purchasesBySupplier[purchase.SupplierId] = append(purchasesBySupplier[purchase.SupplierId], purchase) } 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 } initialBalanceTotals, err := s.DebtSupplierRepo.GetInitialBalanceTotals(c.Context(), supplierIDs) 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)) type debtSupplierRowItem struct { Row dto.DebtSupplierRowDTO SortTime time.Time Order int DeltaBalance float64 CountTotals bool } type debtSupplierAllocation struct { RowIndex int SortTime time.Time Amount float64 Purchase entity.Purchase } type paymentAllocation struct { Date time.Time Amount float64 } for _, supplierID := range supplierIDs { supplier, exists := supplierMap[supplierID] if !exists { continue } initialBalance := initialBalanceTotals[supplierID] + (initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID]) items := purchasesBySupplier[supplierID] paymentItems := paymentsBySupplier[supplierID] total := dto.DebtSupplierTotalDTO{} combinedRows := make([]debtSupplierRowItem, 0, len(items)+len(paymentItems)) purchaseAllocations := make([]debtSupplierAllocation, 0, len(items)) for _, purchase := range items { row := buildDebtSupplierRow(purchase, now, location) sortTime := resolveDebtSupplierSortTime(purchase, params.FilterBy, location) rowIndex := len(combinedRows) combinedRows = append(combinedRows, debtSupplierRowItem{ Row: row, SortTime: sortTime, Order: 0, DeltaBalance: -row.TotalPrice, CountTotals: true, }) purchaseAllocations = append(purchaseAllocations, debtSupplierAllocation{ RowIndex: rowIndex, SortTime: sortTime, Amount: row.TotalPrice, Purchase: purchase, }) } paymentAllocations := make([]paymentAllocation, 0, len(paymentItems)+1) initialAllocation := initialBalanceTotals[supplierID] + initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID] paymentCarry := 0.0 if initialAllocation > 0 && len(purchaseAllocations) > 0 { paymentAllocations = append(paymentAllocations, paymentAllocation{ Date: purchaseAllocations[0].SortTime, Amount: initialAllocation, }) } else if initialAllocation < 0 { paymentCarry = -initialAllocation } 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, }) paymentAllocations = append(paymentAllocations, paymentAllocation{ Date: sortTime, Amount: payment.Nominal, }) } if len(purchaseAllocations) > 0 && len(paymentAllocations) > 0 { sort.SliceStable(purchaseAllocations, func(i, j int) bool { return purchaseAllocations[i].SortTime.Before(purchaseAllocations[j].SortTime) }) sort.SliceStable(paymentAllocations, func(i, j int) bool { return paymentAllocations[i].Date.Before(paymentAllocations[j].Date) }) remaining := make([]float64, len(purchaseAllocations)) for i := range purchaseAllocations { remaining[i] = purchaseAllocations[i].Amount } purchaseIndex := 0 for _, pay := range paymentAllocations { amount := pay.Amount if amount <= 0 { continue } if paymentCarry > 0 { used := math.Min(amount, paymentCarry) paymentCarry -= used amount -= used } for amount > 0 && purchaseIndex < len(remaining) { if remaining[purchaseIndex] <= 0 { purchaseIndex++ continue } used := math.Min(amount, remaining[purchaseIndex]) remaining[purchaseIndex] -= used amount -= used if remaining[purchaseIndex] <= 0.000001 { allocation := purchaseAllocations[purchaseIndex] combinedRows[allocation.RowIndex].Row.Status = "Lunas" combinedRows[allocation.RowIndex].Row.Aging = calculateDebtSupplierAging(allocation.Purchase, pay.Date, location) purchaseIndex++ } } if purchaseIndex >= len(remaining) { break } } } 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.DebtPrice = balance 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 } else { total.PaymentPrice += combinedRows[i].Row.PaymentPrice } } total.DebtPrice = balance rows := make([]dto.DebtSupplierRowDTO, 0, len(combinedRows)) 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, now time.Time, loc *time.Location) dto.DebtSupplierRowDTO { prNumber := purchase.PrNumber poNumber := "" if purchase.PoNumber != nil { poNumber = *purchase.PoNumber } startDate := resolveDebtSupplierReceivedDate(purchase, loc) endDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc) aging := 0 if !startDate.IsZero() { 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") } } 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" 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: 0, DebtPrice: 0, Status: status, TravelNumber: travelNumber, Balance: 0, } } 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: "-", Balance: 0, } } func resolveDebtSupplierSortTime(purchase entity.Purchase, filterBy string, loc *time.Location) time.Time { if strings.EqualFold(strings.TrimSpace(filterBy), "po_date") { if purchase.PoDate != nil && !purchase.PoDate.IsZero() { return purchase.PoDate.In(loc) } } 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 collectDebtSupplierReferences(purchases []entity.Purchase) []string { if len(purchases) == 0 { return nil } seen := make(map[string]struct{}, len(purchases)) result := make([]string, 0, len(purchases)) for _, purchase := range purchases { ref := resolveDebtSupplierReference(purchase) if ref == "" { continue } if _, ok := seen[ref]; ok { continue } seen[ref] = struct{}{} result = append(result, ref) } return result } func resolveDebtSupplierReference(purchase entity.Purchase) string { if purchase.PoNumber != nil { if ref := strings.TrimSpace(*purchase.PoNumber); ref != "" { return ref } } if ref := strings.TrimSpace(purchase.PrNumber); ref != "" { return ref } return "" } func isDebtSupplierPaid(totalPrice, paymentTotal float64) bool { if totalPrice <= 0 { return true } return paymentTotal >= totalPrice-0.000001 } func calculateDebtSupplierAging(purchase entity.Purchase, endDate time.Time, loc *time.Location) int { startDate := resolveDebtSupplierReceivedDate(purchase, loc) if startDate.IsZero() { return 0 } stopDate := time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 0, 0, 0, 0, loc) if stopDate.Before(startDate) { return 0 } return int(stopDate.Sub(startDate).Hours() / 24) } func resolveDebtSupplierReceivedDate(purchase entity.Purchase, loc *time.Location) time.Time { 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 time.Time{} } return time.Date(earliest.Year(), earliest.Month(), earliest.Day(), 0, 0, 0, 0, loc) } func (s *repportService) GetHppV2Breakdown(ctx *fiber.Ctx, params *validation.HppV2BreakdownQuery) (*approvalService.HppV2Breakdown, error) { if err := s.Validate.Struct(params); err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, err.Error()) } if s.HppV2Svc == nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "hpp v2 service is not configured") } periodDate, err := time.ParseInLocation("2006-01-02", params.Period, time.FixedZone("Asia/Jakarta", 7*60*60)) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "period must follow format YYYY-MM-DD") } result, err := s.HppV2Svc.CalculateHppBreakdown(params.ProjectFlockKandangID, &periodDate) if err != nil { return nil, err } return result, 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 } validPfkIDs := make([]uint, 0, len(repoRows)) pfkIndex := make(map[uint]int, len(repoRows)) for idx := range repoRows { row := repoRows[idx] pfkIndex[row.ProjectFlockKandangID] = idx if row.RecordingCount > 0 { validPfkIDs = append(validPfkIDs, row.ProjectFlockKandangID) } } costRows := make([]repportRepo.HppPerKandangCostRow, 0) supplierRows := make([]repportRepo.HppPerKandangSupplierRow, 0) if len(validPfkIDs) > 0 { costRows, supplierRows, err = s.HppPerKandangRepo.GetFeedOvkDocCostByPeriod(ctx.Context(), startOfDay, endOfDay, validPfkIDs) if err != nil { return nil, nil, err } // eggMap, err := s.HppPerKandangRepo.GetWeightRemainingByProjectFlockKandangIDs(ctx.Context(), startOfDay, endOfDay, validPfkIDs) // if err != nil { // return nil, nil, err // } // for pfkID, egg := range eggMap { // if rowIdx, ok := pfkIndex[pfkID]; ok { // repoRows[rowIdx].EggProductionWeightKgRemaining = egg.EggProductionWeightKgRemaining // repoRows[rowIdx].AverageWeightEggPerPiece = egg.AverageWeightEggPerPiece // } // } } costMap := make(map[uint]HppCostAggregate, len(costRows)) for _, row := range costRows { costMap[row.ProjectFlockKandangID] = 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.ProjectFlockKandangID] == nil { seen[sup.ProjectFlockKandangID] = make(map[uint]bool) } if seen[sup.ProjectFlockKandangID][sup.SupplierID] { continue } seen[sup.ProjectFlockKandangID][sup.SupplierID] = true targetMap[sup.ProjectFlockKandangID] = append(targetMap[sup.ProjectFlockKandangID], 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 RemainingBirds int64 RemainingWeightKg float64 AvgWeightSum float64 AvgWeightCount int64 EggHppSum float64 EggHppCount int FeedSuppliers map[int64]dto.HppPerKandangSupplierDTO DocSuppliers map[int64]dto.HppPerKandangSupplierDTO } dataRows := make([]dto.HppPerKandangRowDTO, 0, len(repoRows)) perRangeMap := make(map[weightRangeKey]*weightRangeAggregate) var totalBirds int64 var totalEggPieces int64 var totalEggKg float64 var totalEggValueRp int64 var totalHppCount int var totalDocPriceSum float64 var totalDocPriceCount int var totalEggHppSum float64 var totalEggHppCount int var totalAvgWeightSum float64 var totalAvgWeightCount int64 for _, row := range repoRows { if !params.ShowUnrecorded && row.RecordingCount == 0 { continue } var eggPiecesFloatRemaining float64 var eggRemainingWeightFloatRemaining float64 var eggTotalPiecesFloat float64 var eggWeightFloat float64 var avgWeight float64 eggHpp := 0.0 if s.HppV2Svc != nil { hppCost, err := s.HppV2Svc.CalculateHppCost(row.ProjectFlockKandangID, &periodDate) if err != nil { return nil, nil, err } if hppCost != nil { eggPiecesFloatRemaining = hppCost.Estimation.Butir - hppCost.Real.Butir eggHpp = hppCost.Estimation.HargaKg // eggHpp = hppCost.Real.HargaKg eggTotalPiecesFloat = hppCost.Estimation.Butir eggWeightFloat = hppCost.Estimation.Kg if eggTotalPiecesFloat > 0 { avgWeight = eggWeightFloat / eggTotalPiecesFloat } // eggRemainingWeightFloatRemaining = avgWeight * eggPiecesFloatRemaining eggRemainingWeightFloatRemaining = hppCost.Estimation.Kg - hppCost.Real.Kg } } if math.IsNaN(eggPiecesFloatRemaining) || math.IsInf(eggPiecesFloatRemaining, 0) { eggPiecesFloatRemaining = 0 } if math.IsNaN(eggTotalPiecesFloat) || math.IsInf(eggTotalPiecesFloat, 0) { eggTotalPiecesFloat = 0 } if math.IsNaN(eggRemainingWeightFloatRemaining) || math.IsInf(eggRemainingWeightFloatRemaining, 0) { eggRemainingWeightFloatRemaining = 0 } if math.IsNaN(eggWeightFloat) || math.IsInf(eggWeightFloat, 0) { eggWeightFloat = 0 } if math.IsNaN(avgWeight) || math.IsInf(avgWeight, 0) { avgWeight = 0 } if params.WeightMin != nil && avgWeight < *params.WeightMin { continue } if params.WeightMax != nil && avgWeight > *params.WeightMax { continue } weightMin := math.Floor(avgWeight*10) / 10 if weightMin < 0 { weightMin = 0 } weightMax := weightMin + 0.09 rangeKey := weightRangeKey{Min: weightMin, Max: weightMax} costEntry := costMap[row.ProjectFlockKandangID] rowEggPieces := int64(math.Round(eggPiecesFloatRemaining)) rowEggValue := int64(eggHpp * eggRemainingWeightFloatRemaining) avgDocPrice := int64(0) if costEntry.DocQty > 0 { avgDocPrice = int64(math.Round(costEntry.DocCost / costEntry.DocQty)) } nameWithPeriod := fmt.Sprintf("%s Period %d", row.KandangName, row.ProjectFlockPeriod) dataRows = append(dataRows, dto.HppPerKandangRowDTO{ ID: int(row.ProjectFlockKandangID), 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, }, AvgWeightKg: avgWeight, NameWithPeriode: nameWithPeriod, DocSuppliers: docSupplierMap[row.ProjectFlockKandangID], FeedSuppliers: feedSupplierMap[row.ProjectFlockKandangID], EggProductionPieces: int64(math.Round(eggPiecesFloatRemaining)), EggProductionKg: eggRemainingWeightFloatRemaining, AverageDocPriceRp: avgDocPrice, EggHppRpPerKg: eggHpp, EggValueRp: rowEggValue, }) totalEggPieces += rowEggPieces totalEggKg += eggRemainingWeightFloatRemaining totalEggValueRp += rowEggValue totalAvgWeightSum += avgWeight totalAvgWeightCount++ 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), }, FeedSuppliers: make(map[int64]dto.HppPerKandangSupplierDTO), DocSuppliers: make(map[int64]dto.HppPerKandangSupplierDTO), } perRangeMap[rangeKey] = rangeAgg } rangeSummary := rangeAgg.Summary rangeAgg.AvgWeightSum += avgWeight rangeAgg.AvgWeightCount++ for _, supplier := range feedSupplierMap[row.ProjectFlockKandangID] { if _, ok := rangeAgg.FeedSuppliers[supplier.ID]; !ok { rangeAgg.FeedSuppliers[supplier.ID] = supplier } } for _, supplier := range docSupplierMap[row.ProjectFlockKandangID] { if _, ok := rangeAgg.DocSuppliers[supplier.ID]; !ok { rangeAgg.DocSuppliers[supplier.ID] = supplier } } rangeSummary.EggProductionPieces += rowEggPieces rangeSummary.EggProductionKg += eggRemainingWeightFloatRemaining 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 agg.AvgWeightCount > 0 { entry.AvgWeightKg = agg.AvgWeightSum / float64(agg.AvgWeightCount) } if agg.EggHppCount > 0 { entry.EggHppRpPerKg = agg.EggHppSum / float64(agg.EggHppCount) } entry.FeedSuppliers = make([]dto.HppPerKandangSupplierDTO, 0, len(agg.FeedSuppliers)) for _, supplier := range agg.FeedSuppliers { entry.FeedSuppliers = append(entry.FeedSuppliers, supplier) } entry.DocSuppliers = make([]dto.HppPerKandangSupplierDTO, 0, len(agg.DocSuppliers)) for _, supplier := range agg.DocSuppliers { entry.DocSuppliers = append(entry.DocSuppliers, supplier) } perRangeSummary = append(perRangeSummary, *entry) } totalSummary := dto.HppPerKandangSummaryTotalDTO{ TotalEggProductionPieces: totalEggPieces, TotalEggProductionKg: totalEggKg, TotalEggValueRp: totalEggValueRp, } if totalBirds > 0 { } if totalAvgWeightCount > 0 { totalSummary.AverageWeightKg = totalAvgWeightSum / float64(totalAvgWeightCount) } if totalEggHppCount > 0 { totalSummary.AverageEggHppRpPerKg = totalEggHppSum / float64(totalEggHppCount) } if totalHppCount > 0 { } 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()) } locationScope, err := m.ResolveLocationScope(ctx, s.ExpenseRealizationRepo.DB()) if err != nil { return nil, dto.HppPerKandangFiltersDTO{}, err } areaScope, err := m.ResolveAreaScope(ctx, s.ExpenseRealizationRepo.DB()) if err != nil { return nil, dto.HppPerKandangFiltersDTO{}, err } if locationScope.Restrict { allowed := toInt64Slice(locationScope.IDs) if len(allowed) == 0 { locationIDs = []int64{-1} } else if len(locationIDs) > 0 { locationIDs = intersectInt64(locationIDs, allowed) } else { locationIDs = allowed } } if areaScope.Restrict { allowed := toInt64Slice(areaScope.IDs) if len(allowed) == 0 { areaIDs = []int64{-1} } else if len(areaIDs) > 0 { areaIDs = intersectInt64(areaIDs, allowed) } else { areaIDs = allowed } } 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()) } if weightMin != nil && weightMax != nil && *weightMin > *weightMax { return nil, dto.HppPerKandangFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, "weight_min must be less than or equal to weight_max") } 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 (s *repportService) parseExpenseDepreciationQuery(ctx *fiber.Ctx) (*validation.ExpenseDepreciationQuery, dto.ExpenseDepreciationFiltersDTO, 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", "") rawProjectFlock := ctx.Query("project_flock_id", "") period := strings.TrimSpace(ctx.Query("period", "")) forceRecompute := ctx.QueryBool("force_recompute", false) areaIDs, err := parseCommaSeparatedInt64s(rawArea) if err != nil { return nil, dto.ExpenseDepreciationFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error()) } locationIDs, err := parseCommaSeparatedInt64s(rawLocation) if err != nil { return nil, dto.ExpenseDepreciationFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error()) } projectFlockIDs, err := parseCommaSeparatedInt64s(rawProjectFlock) if err != nil { return nil, dto.ExpenseDepreciationFiltersDTO{}, fiber.NewError(fiber.StatusBadRequest, err.Error()) } locationScope, err := m.ResolveLocationScope(ctx, s.ExpenseRealizationRepo.DB()) if err != nil { return nil, dto.ExpenseDepreciationFiltersDTO{}, err } areaScope, err := m.ResolveAreaScope(ctx, s.ExpenseRealizationRepo.DB()) if err != nil { return nil, dto.ExpenseDepreciationFiltersDTO{}, err } if locationScope.Restrict { allowed := toInt64Slice(locationScope.IDs) if len(allowed) == 0 { locationIDs = []int64{-1} } else if len(locationIDs) > 0 { locationIDs = intersectInt64(locationIDs, allowed) } else { locationIDs = allowed } } if areaScope.Restrict { allowed := toInt64Slice(areaScope.IDs) if len(allowed) == 0 { areaIDs = []int64{-1} } else if len(areaIDs) > 0 { areaIDs = intersectInt64(areaIDs, allowed) } else { areaIDs = allowed } } params := &validation.ExpenseDepreciationQuery{ Page: page, Limit: limit, Period: period, ForceRecompute: forceRecompute, ProjectFlockIDs: projectFlockIDs, AreaIDs: areaIDs, LocationIDs: locationIDs, } filters := dto.NewExpenseDepreciationFiltersDTO( rawArea, rawLocation, rawProjectFlock, period, ) 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 toInt64Slice(ids []uint) []int64 { if len(ids) == 0 { return nil } out := make([]int64, 0, len(ids)) for _, id := range ids { out = append(out, int64(id)) } return out } func intersectInt64(a, b []int64) []int64 { if len(a) == 0 || len(b) == 0 { return nil } set := make(map[int64]struct{}, len(b)) for _, id := range b { set[id] = struct{}{} } out := make([]int64, 0, len(a)) for _, id := range a { if _, ok := set[id]; ok { out = append(out, id) } } return out } func intersectUint(a, b []uint) []uint { if len(a) == 0 || len(b) == 0 { return nil } set := make(map[uint]struct{}, len(b)) for _, id := range b { set[id] = struct{}{} } out := make([]uint, 0, len(a)) for _, id := range a { if _, ok := set[id]; ok { out = append(out, id) } } return out } 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 }