diff --git a/.gitignore b/.gitignore index 4a814ebe..24887418 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ Makefile docker-compose.local.yml docker-compose.yaml Dockerfile +Dockerfile.local .gitlab-ci.yml # Go build cache .gocache/ diff --git a/internal/modules/closings/controllers/closing.controller.go b/internal/modules/closings/controllers/closing.controller.go index a10d6a94..c78fd15f 100644 --- a/internal/modules/closings/controllers/closing.controller.go +++ b/internal/modules/closings/controllers/closing.controller.go @@ -228,6 +228,14 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error { Page: c.QueryInt("page", 1), Limit: c.QueryInt("limit", 10), } + if raw := c.Query("kandang_id"); raw != "" { + kandangInt, convErr := strconv.Atoi(raw) + if convErr != nil || kandangInt <= 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id") + } + kandangUint := uint(kandangInt) + query.KandangID = &kandangUint + } if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") @@ -404,7 +412,18 @@ func (u *ClosingController) GetClosingDataProduksi(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId") } - result, err := u.ClosingService.GetClosingDataProduksi(c, uint(id)) + var kandangID *uint + if raw := c.Query("kandang_id"); raw != "" { + kandangInt, convErr := strconv.Atoi(raw) + if convErr != nil || kandangInt <= 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id") + } + kandangUint := uint(kandangInt) + kandangID = &kandangUint + + } + + result, err := u.ClosingService.GetClosingDataProduksi(c, uint(id), kandangID) if err != nil { return err } diff --git a/internal/modules/closings/dto/closing.dto.go b/internal/modules/closings/dto/closing.dto.go index ac172c83..05a606ca 100644 --- a/internal/modules/closings/dto/closing.dto.go +++ b/internal/modules/closings/dto/closing.dto.go @@ -65,33 +65,44 @@ type ClosingPurchaseDTO struct { FinalPopulation int `json:"final_population"` FeedIn float64 `json:"feed_in"` FeedUsed float64 `json:"feed_used"` - FeedUsedPerHead float64 `json:"feed_used_per_head"` + // FeedUsedPerHead float64 `json:"feed_used_per_head"` } type ClosingSalesDTO struct { SalesPopulation int `json:"sales_population"` SalesWeight float64 `json:"sales_weight"` - AverageWeight float64 `json:"average_weight"` - AverageSellingPrice float64 `json:"chicken_average_selling_price"` + AverageWeight float64 `json:"avg_weight"` + AverageSellingPrice float64 `json:"avg_selling_price"` } type ClosingEggSalesDTO struct { EggPieces int `json:"egg_pieces"` - EggMassKg float64 `json:"egg_mass_kg"` - AverageEggWeightKg float64 `json:"average_egg_weight_kg"` - AverageSellingPrice float64 `json:"egg_average_selling_price"` + EggMassKg float64 `json:"egg_mass"` + AverageEggWeightKg float64 `json:"avg_egg_weight"` + AverageSellingPrice float64 `json:"avg_selling_price"` } type ClosingPerformanceDTO struct { Depletion float64 `json:"depletion"` Age float64 `json:"age_day"` - MortalityStd float64 `json:"mortality_std"` - MortalityAct float64 `json:"mortality_act"` - DeffMortality float64 `json:"deff_mortality"` + MortalityStd float64 `json:"mor_std"` + MortalityAct float64 `json:"mor_act"` + DeffMortality float64 `json:"mor_diff"` FcrStd float64 `json:"fcr_std"` FcrAct float64 `json:"fcr_act"` - DeffFcr float64 `json:"deff_fcr"` - Awg float64 `json:"awg"` + DeffFcr float64 `json:"fcr_diff"` + AwgAct float64 `json:"awg_act"` + AwgStd float64 `json:"awg_std"` + FeedIntake float64 `json:"feed_intake"` + FeedIntakeStd float64 `json:"feed_intake_std"` + HenDayAct *float64 `json:"hen_day_act,omitempty"` + HendayStd *float64 `json:"hen_day_std,omitempty"` + EggMass *float64 `json:"egg_mass,omitempty"` + EggMassStd *float64 `json:"egg_mass_std,omitempty"` + EggWeight *float64 `json:"egg_weight,omitempty"` + EggWeightStd *float64 `json:"egg_weight_std,omitempty"` + HenHouseAct *float64 `json:"hen_housed_act,omitempty"` + HenHouseStd *float64 `json:"hen_housed_std,omitempty"` } type ClosingSalesGroupDTO struct { diff --git a/internal/modules/closings/module.go b/internal/modules/closings/module.go index 658f1bef..a79c9f0b 100644 --- a/internal/modules/closings/module.go +++ b/internal/modules/closings/module.go @@ -11,6 +11,7 @@ import ( sClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services" rExpenseRealization "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" + rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories" rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" @@ -33,11 +34,13 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db) chickinRepo := rChickin.NewChickinRepository(db) recordingRepo := rRecording.NewRecordingRepository(db) + standardGrowthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db) + productionStandardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db) purchaseRepo := rPurchase.NewPurchaseRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db) approvalService := commonSvc.NewApprovalService(approvalRepo) - closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, projectFlockKandangRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, validate) + closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, projectFlockKandangRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, standardGrowthDetailRepo, productionStandardDetailRepo, validate) sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate) userService := sUser.NewUserService(userRepo, validate) diff --git a/internal/modules/closings/repositories/closing.repository.go b/internal/modules/closings/repositories/closing.repository.go index 9d08d083..2ce3e496 100644 --- a/internal/modules/closings/repositories/closing.repository.go +++ b/internal/modules/closings/repositories/closing.repository.go @@ -18,6 +18,7 @@ type ClosingRepository interface { repository.BaseRepository[entity.ProjectFlock] GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) + SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error) SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) @@ -166,6 +167,23 @@ func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(c return purchaseAgg.TotalIn, usageAgg.TotalUsed, nil } +func (r *ClosingRepositoryImpl) SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) { + if len(projectFlockKandangIDs) == 0 { + return 0, nil + } + + var total float64 + if err := r.DB().WithContext(ctx). + Model(&entity.ProjectChickin{}). + Where("project_flock_kandang_id IN ?", projectFlockKandangIDs). + Select("COALESCE(SUM(usage_qty), 0)"). + Scan(&total).Error; err != nil { + return 0, err + } + + return total, nil +} + func (r *ClosingRepositoryImpl) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) { if len(projectFlockKandangIDs) == 0 { return 0, nil diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index f137901d..0c543d05 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -17,6 +17,7 @@ import ( expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" + productionStandardRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories" chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" recordingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" @@ -35,8 +36,8 @@ type ClosingService interface { GetProjectFlockByID(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error) GetPenjualan(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) + GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, error) GetOverhead(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error) - GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error) GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) @@ -56,9 +57,11 @@ type closingService struct { ChickinRepo chickinRepository.ProjectChickinRepository PurchaseRepo purchaseRepository.PurchaseRepository RecordingRepo recordingRepository.RecordingRepository + StandardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository + ProductionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository } -func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, validate *validator.Validate) ClosingService { +func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, standardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository, productionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository, validate *validator.Validate) ClosingService { return &closingService{ Log: utils.Log, Validate: validate, @@ -73,6 +76,8 @@ func NewClosingService(repo repository.ClosingRepository, projectFlockRepo proje ChickinRepo: chickinRepo, PurchaseRepo: purchaseRepo, RecordingRepo: recordingRepo, + StandardGrowthDetailRepo: standardGrowthDetailRepo, + ProductionStandardDetailRepo: productionStandardDetailRepo, } } @@ -210,7 +215,7 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa var projectFlockKandangIDs []uint if params.Type == validation.SapronakTypeOutgoing { - projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID) + projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID, params.KandangID) if err != nil { s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err) return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang") @@ -290,12 +295,15 @@ func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, proje return ids, nil } -func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint) ([]uint, error) { +func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint, kandangID *uint) ([]uint, error) { var ids []uint - err := s.Repository.DB().WithContext(ctx). + query := s.Repository.DB().WithContext(ctx). Model(&entity.ProjectFlockKandang{}). - Where("project_flock_id = ?", projectFlockID). - Pluck("id", &ids).Error + Where("project_flock_id = ?", projectFlockID) + if kandangID != nil { + query = query.Where("kandang_id = ?", *kandangID) + } + err := query.Order("id ASC").Pluck("id", &ids).Error if err != nil { return nil, err } @@ -554,12 +562,22 @@ func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, proj return result, nil } -func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error) { +func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, error) { if projectFlockID == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") } - project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withClosingRelations) + projectFlockKandangIDs, err := s.getProjectFlockKandangIDs(c.Context(), projectFlockID, kandangID) + if err != nil { + s.Log.Errorf("Failed to fetch project flock kandangs for %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandangs") + } + + if len(projectFlockKandangIDs) == 0 { + return nil, fiber.NewError(fiber.StatusNotFound, "No project flock kandang found") + } + + project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found") } @@ -568,19 +586,29 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } - var population float64 - for _, history := range project.KandangHistory { - for _, chickin := range history.Chickins { - population += chickin.UsageQty - } + population, err := s.Repository.SumProjectChickinUsageByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs) + if err != nil { + s.Log.Errorf("Failed to sum population for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch population data") } isGrowing := strings.EqualFold(project.Category, string(utils.ProjectFlockCategoryGrowing)) - projectFlockKandangIDs, err := s.getProjectFlockKandangIDs(c.Context(), projectFlockID) + currentWeek, err := s.determineProductionWeek(c.Context(), projectFlockKandangIDs) if err != nil { - s.Log.Errorf("Failed to fetch project flock kandangs for %d: %+v", projectFlockID, err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandangs") + s.Log.Errorf("Failed to determine production week for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to determine production week") + } + + targetAverages, err := s.RecordingRepo.GetAverageTargetMetricsByProjectFlockKandangID(c.Context(), projectFlockKandangIDs[0], !isGrowing) + if err != nil { + s.Log.Errorf("Failed to calculate target metrics for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch target metrics data") + } + var fcrActFromRecording *float64 + if targetAverages.FcrCount > 0 { + fcrAvg := targetAverages.FcrAvg + fcrActFromRecording = &fcrAvg } feedIn, feedUsed, err := s.Repository.SumFeedPurchaseAndUsedByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs) @@ -589,6 +617,40 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch feed purchase data") } + averageFeedIntake := targetAverages.FeedIntakeAvg + + feedIntakeStd := 0.0 + var mortalityStdFromGrowth *float64 + if project.ProductionStandardId > 0 && currentWeek > 0 && s.StandardGrowthDetailRepo != nil { + growthDetail, growthErr := s.StandardGrowthDetailRepo.GetByStandardIDAndWeek(c.Context(), project.ProductionStandardId, currentWeek) + if growthErr != nil { + if !errors.Is(growthErr, gorm.ErrRecordNotFound) { + s.Log.Errorf("Failed to fetch growth detail for project flock %d: %+v", projectFlockID, growthErr) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch growth standard data") + } + } else if growthDetail != nil { + if growthDetail.FeedIntake != nil { + feedIntakeStd = *growthDetail.FeedIntake + } + if growthDetail.MaxDepletion != nil { + mortalityStdFromGrowth = growthDetail.MaxDepletion + } + } + } + + var productionStandardDetail *entity.ProductionStandardDetail + if project.ProductionStandardId > 0 && currentWeek > 0 && s.ProductionStandardDetailRepo != nil { + productionStandardDetail, err = s.ProductionStandardDetailRepo.GetByStandardIDAndWeek(c.Context(), project.ProductionStandardId, currentWeek) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + productionStandardDetail = nil + } else { + s.Log.Errorf("Failed to fetch production standard detail for project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch production standard detail data") + } + } + } + claimCulling, err := s.Repository.SumClaimCullingByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs) if err != nil { s.Log.Errorf("Failed to sum claim culling for project flock %d: %+v", projectFlockID, err) @@ -611,10 +673,10 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales age data") } - feedUsedPerHead := 0.0 - if population > 0 { - feedUsedPerHead = feedUsed / population - } + // feedUsedPerHead := 0.0 + // if population > 0 { + // feedUsedPerHead = feedUsed / population + // } purchase := dto.ClosingPurchaseDTO{ InitialPopulation: int(population), @@ -622,7 +684,7 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint FinalPopulation: int(finalPopulation), FeedIn: feedIn, FeedUsed: feedUsed, - FeedUsedPerHead: feedUsedPerHead, + // FeedUsedPerHead: feedUsedPerHead, } chickenFlagNames := []string{string(utils.FlagPullet)} @@ -655,6 +717,9 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint } chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age, standards) + if fcrActFromRecording != nil { + chickenPerformance.FcrAct = *fcrActFromRecording + } var eggSales *dto.ClosingEggSalesDTO var eggPerformance *dto.ClosingPerformanceDTO @@ -702,6 +767,9 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint } eggPerf := calculatePerformanceMetrics(averageEggWeight, eggSalesWeight, feedUsed, harvestEggQty, eggDepletion, age, standards) + if fcrActFromRecording != nil { + eggPerf.FcrAct = *fcrActFromRecording + } eggPerformance = &eggPerf } @@ -718,15 +786,63 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint DeffMortality: chickenPerformance.DeffMortality, } if eggPerformance != nil { - performance.FcrStd = eggPerformance.FcrStd + // performance.FcrStd = eggPerformance.FcrStd performance.FcrAct = eggPerformance.FcrAct - performance.DeffFcr = eggPerformance.DeffFcr - performance.Awg = eggPerformance.Awg + // performance.DeffFcr = eggPerformance.DeffFcr + performance.AwgAct = eggPerformance.AwgAct } else { - performance.FcrStd = chickenPerformance.FcrStd + // performance.FcrStd = chickenPerformance.FcrStd performance.FcrAct = chickenPerformance.FcrAct - performance.DeffFcr = chickenPerformance.DeffFcr - performance.Awg = chickenPerformance.Awg + // performance.DeffFcr = chickenPerformance.DeffFcr + performance.AwgAct = chickenPerformance.AwgAct + } + performance.FeedIntake = averageFeedIntake + performance.FeedIntakeStd = feedIntakeStd + if targetAverages.CumDepletionRateCount > 0 { + performance.MortalityAct = targetAverages.CumDepletionRateAvg + performance.DeffMortality = performance.MortalityAct - performance.MortalityStd + } + if mortalityStdFromGrowth != nil { + performance.MortalityStd = *mortalityStdFromGrowth + performance.DeffMortality = performance.MortalityAct - performance.MortalityStd + } + if !isGrowing { + if targetAverages.HenDayCount > 0 { + henDayAct := targetAverages.HenDayAvg + performance.HenDayAct = &henDayAct + } + if targetAverages.HenHouseCount > 0 { + henHouseAct := targetAverages.HenHouseAvg + performance.HenHouseAct = &henHouseAct + } + if targetAverages.EggWeightCount > 0 { + eggWeight := targetAverages.EggWeightAvg + performance.EggWeight = &eggWeight + } + if targetAverages.EggMassCount > 0 { + eggMass := targetAverages.EggMassAvg + performance.EggMass = &eggMass + } + } + performance.DeffFcr = performance.FcrStd - performance.FcrAct + if productionStandardDetail != nil { + if productionStandardDetail.StandardFCR != nil { + performance.FcrStd = *productionStandardDetail.StandardFCR + } + if !isGrowing { + if productionStandardDetail.TargetHenDayProduction != nil { + performance.HendayStd = productionStandardDetail.TargetHenDayProduction + } + if productionStandardDetail.TargetHenHouseProduction != nil { + performance.HenHouseStd = productionStandardDetail.TargetHenHouseProduction + } + if productionStandardDetail.TargetEggWeight != nil { + performance.EggWeightStd = productionStandardDetail.TargetEggWeight + } + if productionStandardDetail.TargetEggMass != nil { + performance.EggMassStd = productionStandardDetail.TargetEggMass + } + } } result := dto.ClosingProductionReportDTO{ @@ -772,6 +888,46 @@ func (s closingService) calculateAverageSalesAge(ctx context.Context, projectFlo return totalAgeWeeks / totalQty, nil } +func (s closingService) determineProductionWeek(ctx context.Context, projectFlockKandangIDs []uint) (int, error) { + if len(projectFlockKandangIDs) == 0 { + return 0, nil + } + + firstKandangID := projectFlockKandangIDs[0] + + var chickin entity.ProjectChickin + if err := s.Repository.DB().WithContext(ctx). + Where("project_flock_kandang_id = ?", firstKandangID). + Order("chick_in_date ASC"). + First(&chickin).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return 0, nil + } + return 0, err + } + + recording, err := s.RecordingRepo.GetLatestByProjectFlockKandangID(ctx, firstKandangID) + if err != nil { + return 0, err + } + if recording == nil { + return 0, nil + } + + if recording.RecordDatetime.Before(chickin.ChickInDate) { + return 0, nil + } + + elapsed := recording.RecordDatetime.Sub(chickin.ChickInDate) + weekFloat := elapsed.Hours() / (24 * 7) + week := int(math.Ceil(weekFloat)) + if week <= 0 { + week = 1 + } + + return week, nil +} + func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopulation, depletion, age float64, standards []entity.FcrStandard) dto.ClosingPerformanceDTO { mortalityStd, fcrStd := closestFcrValues(standards, averageWeight) @@ -802,7 +958,7 @@ func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopul FcrStd: fcrStd, FcrAct: fcrAct, DeffFcr: deffFcr, - Awg: awg, + AwgAct: awg, } } diff --git a/internal/modules/closings/validations/closing.validation.go b/internal/modules/closings/validations/closing.validation.go index 610e89b8..0c738407 100644 --- a/internal/modules/closings/validations/closing.validation.go +++ b/internal/modules/closings/validations/closing.validation.go @@ -20,7 +20,8 @@ const ( ) type ClosingSapronakQuery struct { - Type string `query:"type" validate:"required,oneof=incoming outgoing"` - Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + Type string `query:"type" validate:"required,oneof=incoming outgoing"` + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + KandangID *uint `query:"kandang_id" validate:"omitempty,gt=0"` } diff --git a/internal/modules/production/recordings/repositories/recording.repository.go b/internal/modules/production/recordings/repositories/recording.repository.go index dafd92ce..fbb628ff 100644 --- a/internal/modules/production/recordings/repositories/recording.repository.go +++ b/internal/modules/production/recordings/repositories/recording.repository.go @@ -48,12 +48,30 @@ type RecordingRepository interface { GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalDepletion float64, err error) GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error) GetTotalEggProductionWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeightKg float64, err error) + GetAverageTargetMetricsByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint, includeTargets bool) (RecordingTargetAverages, error) } type RecordingRepositoryImpl struct { *repository.BaseRepositoryImpl[entity.Recording] } +type RecordingTargetAverages struct { + HenDayAvg float64 + HenDayCount int64 + HenHouseAvg float64 + HenHouseCount int64 + EggWeightAvg float64 + EggWeightCount int64 + EggMassAvg float64 + EggMassCount int64 + FeedIntakeAvg float64 + FeedIntakeCount int64 + FcrAvg float64 + FcrCount int64 + CumDepletionRateAvg float64 + CumDepletionRateCount int64 +} + func NewRecordingRepository(db *gorm.DB) RecordingRepository { return &RecordingRepositoryImpl{ BaseRepositoryImpl: repository.NewBaseRepository[entity.Recording](db), @@ -442,6 +460,67 @@ func (r *RecordingRepositoryImpl) GetTotalEggProductionWeightByProjectFlockID(ct return result, err } +func (r *RecordingRepositoryImpl) GetAverageTargetMetricsByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint, includeTargets bool) (RecordingTargetAverages, error) { + var row struct { + HenDayTotal float64 + HenHouseTotal float64 + EggWeightTotal float64 + EggMassTotal float64 + FeedIntakeTotal float64 + FcrTotal float64 + CumDepletionRateTotal float64 + TotalCount int64 + } + + selectParts := []string{ + "COALESCE(SUM(feed_intake), 0) AS feed_intake_total", + "COALESCE(SUM(fcr_value), 0) AS fcr_total", + "COALESCE(SUM(cum_depletion_rate), 0) AS cum_depletion_rate_total", + "COUNT(*) AS total_count", + } + if includeTargets { + selectParts = append([]string{ + "COALESCE(SUM(hen_day), 0) AS hen_day_total", + "COALESCE(SUM(hen_house), 0) AS hen_house_total", + "COALESCE(SUM(egg_weight), 0) AS egg_weight_total", + "COALESCE(SUM(egg_mass), 0) AS egg_mass_total", + }, selectParts...) + } + + if err := r.DB().WithContext(ctx). + Table("recordings"). + Select(strings.Join(selectParts, ", ")). + Where("project_flock_kandangs_id = ? AND deleted_at IS NULL", projectFlockKandangID). + Scan(&row).Error; err != nil { + return RecordingTargetAverages{}, err + } + + result := RecordingTargetAverages{ + FeedIntakeCount: row.TotalCount, + FcrCount: row.TotalCount, + CumDepletionRateCount: row.TotalCount, + } + if includeTargets { + result.HenDayCount = row.TotalCount + result.HenHouseCount = row.TotalCount + result.EggWeightCount = row.TotalCount + result.EggMassCount = row.TotalCount + } + if row.TotalCount > 0 { + if includeTargets { + result.HenDayAvg = row.HenDayTotal / float64(row.TotalCount) + result.HenHouseAvg = row.HenHouseTotal / float64(row.TotalCount) + result.EggWeightAvg = row.EggWeightTotal / float64(row.TotalCount) + result.EggMassAvg = row.EggMassTotal / float64(row.TotalCount) + } + result.FeedIntakeAvg = row.FeedIntakeTotal / float64(row.TotalCount) + result.FcrAvg = row.FcrTotal / float64(row.TotalCount) + result.CumDepletionRateAvg = row.CumDepletionRateTotal + } + + return result, nil +} + func nextRecordingDay(days []int) int { if len(days) == 0 { return 1 diff --git a/internal/modules/repports/repositories/debt_supplier.repository.go b/internal/modules/repports/repositories/debt_supplier.repository.go index 3d415606..e8f548d6 100644 --- a/internal/modules/repports/repositories/debt_supplier.repository.go +++ b/internal/modules/repports/repositories/debt_supplier.repository.go @@ -31,11 +31,9 @@ func NewDebtSupplierRepository(db *gorm.DB) DebtSupplierRepository { func resolveDebtSupplierDateColumn(filterBy string) string { switch strings.ToLower(strings.TrimSpace(filterBy)) { - case "receive_date": - return "purchases.receive_date" case "po_date": return "purchases.po_date" - case "do_date", "received_date", "": + case "received_date", "": return "purchase_items.received_date" default: return "purchase_items.received_date" @@ -130,7 +128,7 @@ func (r *debtSupplierRepositoryImpl) GetPurchasesBySuppliers(ctx context.Context Preload("Warehouse.Area"). Order("purchase_items.id ASC") - if strings.EqualFold(strings.TrimSpace(filters.FilterBy), "do_date") || strings.EqualFold(strings.TrimSpace(filters.FilterBy), "received_date") || strings.TrimSpace(filters.FilterBy) == "" { + if strings.EqualFold(strings.TrimSpace(filters.FilterBy), "received_date") || strings.TrimSpace(filters.FilterBy) == "" { if filters.StartDate != "" { if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil { db = db.Where("DATE(purchase_items.received_date) >= ?", dateFrom) diff --git a/internal/modules/repports/repositories/production_result.repository.go b/internal/modules/repports/repositories/production_result.repository.go index f2decedf..19007d0f 100644 --- a/internal/modules/repports/repositories/production_result.repository.go +++ b/internal/modules/repports/repositories/production_result.repository.go @@ -59,7 +59,6 @@ func (r *productionResultRepositoryImpl) GetRecordingsByProjectFlockKandang( dataQuery := r.db.WithContext(ctx). Model(&entity.Recording{}). Where("project_flock_kandangs_id = ?", projectFlockKandangID). - Preload("BodyWeights"). Preload("Eggs", func(db *gorm.DB) *gorm.DB { return db.Select("recording_eggs.*, f.name AS product_flag_name"). Joins("LEFT JOIN product_warehouses pw ON pw.id = recording_eggs.product_warehouse_id"). diff --git a/internal/modules/repports/services/repport.service.go b/internal/modules/repports/services/repport.service.go index 5f3cbbad..c4883b72 100644 --- a/internal/modules/repports/services/repport.service.go +++ b/internal/modules/repports/services/repport.service.go @@ -642,7 +642,7 @@ func (s *repportService) GetPurchaseSupplier(c *fiber.Ctx, params *validation.Pu } func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error) { - if params.FilterBy == "" || strings.EqualFold(strings.TrimSpace(params.FilterBy), "do_date") { + if params.FilterBy == "" { params.FilterBy = "received_date" } @@ -681,25 +681,8 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu } purchasesBySupplier := make(map[uint][]entity.Purchase, len(supplierIDs)) - references := make([]string, 0) - seenRefs := make(map[string]struct{}) for _, purchase := range purchases { - supplierID := purchase.SupplierId - purchasesBySupplier[supplierID] = append(purchasesBySupplier[supplierID], purchase) - - reference := purchase.PrNumber - if purchase.PoNumber != nil && strings.TrimSpace(*purchase.PoNumber) != "" { - reference = *purchase.PoNumber - } - if _, exists := seenRefs[reference]; !exists { - seenRefs[reference] = struct{}{} - references = append(references, reference) - } - } - - paymentTotals, err := s.DebtSupplierRepo.GetPaymentTotalsByReferences(c.Context(), supplierIDs, references) - if err != nil { - return nil, 0, err + purchasesBySupplier[purchase.SupplierId] = append(purchasesBySupplier[purchase.SupplierId], purchase) } paymentsBySupplier := make(map[uint][]entity.Payment, len(supplierIDs)) @@ -724,6 +707,14 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu 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 + } + for _, supplierID := range supplierIDs { supplier, exists := supplierMap[supplierID] if !exists { @@ -731,23 +722,13 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu } initialBalance := initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID] - items := purchasesBySupplier[supplierID] paymentItems := paymentsBySupplier[supplierID] - rows := make([]dto.DebtSupplierRowDTO, 0, len(items)+len(paymentItems)) total := dto.DebtSupplierTotalDTO{} - type debtSupplierRowItem struct { - Row dto.DebtSupplierRowDTO - SortTime time.Time - Order int - DeltaBalance float64 - CountTotals bool - } - combinedRows := make([]debtSupplierRowItem, 0, len(items)+len(paymentItems)) for _, purchase := range items { - row := buildDebtSupplierRow(purchase, paymentTotals, now, location) + row := buildDebtSupplierRow(purchase, now, location) sortTime := resolveDebtSupplierSortTime(purchase, params.FilterBy, location) combinedRows = append(combinedRows, debtSupplierRowItem{ Row: row, @@ -780,6 +761,7 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu balance := initialBalance for i := range combinedRows { balance += combinedRows[i].DeltaBalance + combinedRows[i].Row.DebtPrice = balance combinedRows[i].Row.Balance = balance if combinedRows[i].CountTotals { @@ -788,13 +770,13 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu total.Aging = row.Aging } total.TotalPrice += row.TotalPrice - total.PaymentPrice += row.PaymentPrice - total.DebtPrice += row.DebtPrice } else { - combinedRows[i].Row.DebtPrice = balance + 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-- { @@ -823,18 +805,13 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu return result, totalSuppliers, nil } -func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]float64, now time.Time, loc *time.Location) dto.DebtSupplierRowDTO { +func buildDebtSupplierRow(purchase entity.Purchase, now time.Time, loc *time.Location) dto.DebtSupplierRowDTO { prNumber := purchase.PrNumber poNumber := "" if purchase.PoNumber != nil { poNumber = *purchase.PoNumber } - reference := prNumber - if strings.TrimSpace(poNumber) != "" { - reference = poNumber - } - prDate := purchase.CreatedAt.In(loc) startDate := time.Date(prDate.Year(), prDate.Month(), prDate.Day(), 0, 0, 0, 0, loc) endDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc) @@ -877,9 +854,6 @@ func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]flo } } - paymentPrice := paymentTotals[reference] - debtPrice := paymentPrice - totalPrice - dueDate := "" dueStatus := "-" if purchase.DueDate != nil && !purchase.DueDate.IsZero() { @@ -893,10 +867,6 @@ func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]flo } status := "Belum Lunas" - if debtPrice >= 0 { - status = "Lunas" - } - poDate := "" if purchase.PoDate != nil && !purchase.PoDate.IsZero() { poDate = purchase.PoDate.In(loc).Format("2006-01-02") @@ -913,10 +883,11 @@ func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]flo DueDate: dueDate, DueStatus: dueStatus, TotalPrice: totalPrice, - PaymentPrice: paymentPrice, - DebtPrice: debtPrice, + PaymentPrice: 0, + DebtPrice: 0, Status: status, TravelNumber: travelNumber, + Balance: 0, } } @@ -946,32 +917,30 @@ func buildDebtSupplierPaymentRow(payment entity.Payment, loc *time.Location) dto DebtPrice: 0, Status: "Pembayaran", TravelNumber: "-", + Balance: 0, } } func resolveDebtSupplierSortTime(purchase entity.Purchase, filterBy string, loc *time.Location) time.Time { - switch strings.ToLower(strings.TrimSpace(filterBy)) { - case "po_date": + if strings.EqualFold(strings.TrimSpace(filterBy), "po_date") { if purchase.PoDate != nil && !purchase.PoDate.IsZero() { return purchase.PoDate.In(loc) } - case "pr_date": - return purchase.CreatedAt.In(loc) - default: - earliest := time.Time{} - for _, item := range purchase.Items { - if item.ReceivedDate == nil || item.ReceivedDate.IsZero() { - continue - } - received := item.ReceivedDate.In(loc) - if earliest.IsZero() || received.Before(earliest) { - earliest = received - } + } + + earliest := time.Time{} + for _, item := range purchase.Items { + if item.ReceivedDate == nil || item.ReceivedDate.IsZero() { + continue } - if !earliest.IsZero() { - return earliest + received := item.ReceivedDate.In(loc) + if earliest.IsZero() || received.Before(earliest) { + earliest = received } } + if !earliest.IsZero() { + return earliest + } return purchase.CreatedAt.In(loc) } diff --git a/internal/modules/repports/validations/repport.validation.go b/internal/modules/repports/validations/repport.validation.go index 5dde8f51..6d50f3e6 100644 --- a/internal/modules/repports/validations/repport.validation.go +++ b/internal/modules/repports/validations/repport.validation.go @@ -50,7 +50,7 @@ type DebtSupplierQuery struct { SupplierIDs []int64 `query:"-" validate:"omitempty,dive,gt=0"` StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"` EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"` - FilterBy string `query:"filter_by" validate:"omitempty,oneof=received_date po_date pr_date do_date"` + FilterBy string `query:"filter_by" validate:"omitempty,oneof=received_date po_date"` SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` }