Merge branch 'feat/BE/sapronak/data-produksi' into 'development'

Feat/be/sapronak/data produksi

See merge request mbugroup/lti-api!170
This commit is contained in:
Hafizh A. Y.
2026-01-13 17:39:33 +00:00
8 changed files with 332 additions and 46 deletions
@@ -228,6 +228,14 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
Page: c.QueryInt("page", 1), Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10), 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 { if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") 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") 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 { if err != nil {
return err return err
} }
+22 -11
View File
@@ -65,33 +65,44 @@ type ClosingPurchaseDTO struct {
FinalPopulation int `json:"final_population"` FinalPopulation int `json:"final_population"`
FeedIn float64 `json:"feed_in"` FeedIn float64 `json:"feed_in"`
FeedUsed float64 `json:"feed_used"` FeedUsed float64 `json:"feed_used"`
FeedUsedPerHead float64 `json:"feed_used_per_head"` // FeedUsedPerHead float64 `json:"feed_used_per_head"`
} }
type ClosingSalesDTO struct { type ClosingSalesDTO struct {
SalesPopulation int `json:"sales_population"` SalesPopulation int `json:"sales_population"`
SalesWeight float64 `json:"sales_weight"` SalesWeight float64 `json:"sales_weight"`
AverageWeight float64 `json:"average_weight"` AverageWeight float64 `json:"avg_weight"`
AverageSellingPrice float64 `json:"chicken_average_selling_price"` AverageSellingPrice float64 `json:"avg_selling_price"`
} }
type ClosingEggSalesDTO struct { type ClosingEggSalesDTO struct {
EggPieces int `json:"egg_pieces"` EggPieces int `json:"egg_pieces"`
EggMassKg float64 `json:"egg_mass_kg"` EggMassKg float64 `json:"egg_mass"`
AverageEggWeightKg float64 `json:"average_egg_weight_kg"` AverageEggWeightKg float64 `json:"avg_egg_weight"`
AverageSellingPrice float64 `json:"egg_average_selling_price"` AverageSellingPrice float64 `json:"avg_selling_price"`
} }
type ClosingPerformanceDTO struct { type ClosingPerformanceDTO struct {
Depletion float64 `json:"depletion"` Depletion float64 `json:"depletion"`
Age float64 `json:"age_day"` Age float64 `json:"age_day"`
MortalityStd float64 `json:"mortality_std"` MortalityStd float64 `json:"mor_std"`
MortalityAct float64 `json:"mortality_act"` MortalityAct float64 `json:"mor_act"`
DeffMortality float64 `json:"deff_mortality"` DeffMortality float64 `json:"mor_diff"`
FcrStd float64 `json:"fcr_std"` FcrStd float64 `json:"fcr_std"`
FcrAct float64 `json:"fcr_act"` FcrAct float64 `json:"fcr_act"`
DeffFcr float64 `json:"deff_fcr"` DeffFcr float64 `json:"fcr_diff"`
Awg float64 `json:"awg"` 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 { type ClosingSalesGroupDTO struct {
+4 -1
View File
@@ -11,6 +11,7 @@ import (
sClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services" sClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
rExpenseRealization "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" rExpenseRealization "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/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" 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" 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" 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) expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db)
chickinRepo := rChickin.NewChickinRepository(db) chickinRepo := rChickin.NewChickinRepository(db)
recordingRepo := rRecording.NewRecordingRepository(db) recordingRepo := rRecording.NewRecordingRepository(db)
standardGrowthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db)
productionStandardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db)
purchaseRepo := rPurchase.NewPurchaseRepository(db) purchaseRepo := rPurchase.NewPurchaseRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo) 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) sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
@@ -18,6 +18,7 @@ type ClosingRepository interface {
repository.BaseRepository[entity.ProjectFlock] repository.BaseRepository[entity.ProjectFlock]
GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error)
SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, 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) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error) SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error)
SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (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 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) { func (r *ClosingRepositoryImpl) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
if len(projectFlockKandangIDs) == 0 { if len(projectFlockKandangIDs) == 0 {
return 0, nil return 0, nil
@@ -17,6 +17,7 @@ import (
expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
marketingRepository "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" 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" 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" 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) GetProjectFlockByID(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
GetPenjualan(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) GetPenjualan(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error)
GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, 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) 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) GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error)
GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error)
@@ -56,9 +57,11 @@ type closingService struct {
ChickinRepo chickinRepository.ProjectChickinRepository ChickinRepo chickinRepository.ProjectChickinRepository
PurchaseRepo purchaseRepository.PurchaseRepository PurchaseRepo purchaseRepository.PurchaseRepository
RecordingRepo recordingRepository.RecordingRepository 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{ return &closingService{
Log: utils.Log, Log: utils.Log,
Validate: validate, Validate: validate,
@@ -73,6 +76,8 @@ func NewClosingService(repo repository.ClosingRepository, projectFlockRepo proje
ChickinRepo: chickinRepo, ChickinRepo: chickinRepo,
PurchaseRepo: purchaseRepo, PurchaseRepo: purchaseRepo,
RecordingRepo: recordingRepo, RecordingRepo: recordingRepo,
StandardGrowthDetailRepo: standardGrowthDetailRepo,
ProductionStandardDetailRepo: productionStandardDetailRepo,
} }
} }
@@ -210,7 +215,7 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
var projectFlockKandangIDs []uint var projectFlockKandangIDs []uint
if params.Type == validation.SapronakTypeOutgoing { if params.Type == validation.SapronakTypeOutgoing {
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID) projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID, params.KandangID)
if err != nil { if err != nil {
s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err) 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") 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 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 var ids []uint
err := s.Repository.DB().WithContext(ctx). query := s.Repository.DB().WithContext(ctx).
Model(&entity.ProjectFlockKandang{}). Model(&entity.ProjectFlockKandang{}).
Where("project_flock_id = ?", projectFlockID). Where("project_flock_id = ?", projectFlockID)
Pluck("id", &ids).Error if kandangID != nil {
query = query.Where("kandang_id = ?", *kandangID)
}
err := query.Order("id ASC").Pluck("id", &ids).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -554,12 +562,22 @@ func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, proj
return result, nil 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 { if projectFlockID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") 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) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found") 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") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
} }
var population float64 population, err := s.Repository.SumProjectChickinUsageByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs)
for _, history := range project.KandangHistory { if err != nil {
for _, chickin := range history.Chickins { s.Log.Errorf("Failed to sum population for project flock %d: %+v", projectFlockID, err)
population += chickin.UsageQty return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch population data")
}
} }
isGrowing := strings.EqualFold(project.Category, string(utils.ProjectFlockCategoryGrowing)) 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 { if err != nil {
s.Log.Errorf("Failed to fetch project flock kandangs for %d: %+v", projectFlockID, err) s.Log.Errorf("Failed to determine production week for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandangs") 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) 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") 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) claimCulling, err := s.Repository.SumClaimCullingByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs)
if err != nil { if err != nil {
s.Log.Errorf("Failed to sum claim culling for project flock %d: %+v", projectFlockID, err) 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") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales age data")
} }
feedUsedPerHead := 0.0 // feedUsedPerHead := 0.0
if population > 0 { // if population > 0 {
feedUsedPerHead = feedUsed / population // feedUsedPerHead = feedUsed / population
} // }
purchase := dto.ClosingPurchaseDTO{ purchase := dto.ClosingPurchaseDTO{
InitialPopulation: int(population), InitialPopulation: int(population),
@@ -622,7 +684,7 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
FinalPopulation: int(finalPopulation), FinalPopulation: int(finalPopulation),
FeedIn: feedIn, FeedIn: feedIn,
FeedUsed: feedUsed, FeedUsed: feedUsed,
FeedUsedPerHead: feedUsedPerHead, // FeedUsedPerHead: feedUsedPerHead,
} }
chickenFlagNames := []string{string(utils.FlagPullet)} 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) chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age, standards)
if fcrActFromRecording != nil {
chickenPerformance.FcrAct = *fcrActFromRecording
}
var eggSales *dto.ClosingEggSalesDTO var eggSales *dto.ClosingEggSalesDTO
var eggPerformance *dto.ClosingPerformanceDTO 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) eggPerf := calculatePerformanceMetrics(averageEggWeight, eggSalesWeight, feedUsed, harvestEggQty, eggDepletion, age, standards)
if fcrActFromRecording != nil {
eggPerf.FcrAct = *fcrActFromRecording
}
eggPerformance = &eggPerf eggPerformance = &eggPerf
} }
@@ -718,15 +786,63 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
DeffMortality: chickenPerformance.DeffMortality, DeffMortality: chickenPerformance.DeffMortality,
} }
if eggPerformance != nil { if eggPerformance != nil {
performance.FcrStd = eggPerformance.FcrStd // performance.FcrStd = eggPerformance.FcrStd
performance.FcrAct = eggPerformance.FcrAct performance.FcrAct = eggPerformance.FcrAct
performance.DeffFcr = eggPerformance.DeffFcr // performance.DeffFcr = eggPerformance.DeffFcr
performance.Awg = eggPerformance.Awg performance.AwgAct = eggPerformance.AwgAct
} else { } else {
performance.FcrStd = chickenPerformance.FcrStd // performance.FcrStd = chickenPerformance.FcrStd
performance.FcrAct = chickenPerformance.FcrAct performance.FcrAct = chickenPerformance.FcrAct
performance.DeffFcr = chickenPerformance.DeffFcr // performance.DeffFcr = chickenPerformance.DeffFcr
performance.Awg = chickenPerformance.Awg 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{ result := dto.ClosingProductionReportDTO{
@@ -772,6 +888,46 @@ func (s closingService) calculateAverageSalesAge(ctx context.Context, projectFlo
return totalAgeWeeks / totalQty, nil 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 { func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopulation, depletion, age float64, standards []entity.FcrStandard) dto.ClosingPerformanceDTO {
mortalityStd, fcrStd := closestFcrValues(standards, averageWeight) mortalityStd, fcrStd := closestFcrValues(standards, averageWeight)
@@ -802,7 +958,7 @@ func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopul
FcrStd: fcrStd, FcrStd: fcrStd,
FcrAct: fcrAct, FcrAct: fcrAct,
DeffFcr: deffFcr, DeffFcr: deffFcr,
Awg: awg, AwgAct: awg,
} }
} }
@@ -20,7 +20,8 @@ const (
) )
type ClosingSapronakQuery struct { type ClosingSapronakQuery struct {
Type string `query:"type" validate:"required,oneof=incoming outgoing"` Type string `query:"type" validate:"required,oneof=incoming outgoing"`
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
KandangID *uint `query:"kandang_id" validate:"omitempty,gt=0"`
} }
@@ -48,12 +48,30 @@ type RecordingRepository interface {
GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalDepletion float64, err error) GetTotalDepletionByProjectFlockID(ctx context.Context, projectFlockID uint) (totalDepletion float64, err error)
GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error) GetLatestAvgWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (avgWeight float64, err error)
GetTotalEggProductionWeightByProjectFlockID(ctx context.Context, projectFlockID uint) (totalWeightKg 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 { type RecordingRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Recording] *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 { func NewRecordingRepository(db *gorm.DB) RecordingRepository {
return &RecordingRepositoryImpl{ return &RecordingRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Recording](db), BaseRepositoryImpl: repository.NewBaseRepository[entity.Recording](db),
@@ -433,6 +451,67 @@ func (r *RecordingRepositoryImpl) GetTotalEggProductionWeightByProjectFlockID(ct
return result, err 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 { func nextRecordingDay(days []int) int {
if len(days) == 0 { if len(days) == 0 {
return 1 return 1
@@ -59,7 +59,6 @@ func (r *productionResultRepositoryImpl) GetRecordingsByProjectFlockKandang(
dataQuery := r.db.WithContext(ctx). dataQuery := r.db.WithContext(ctx).
Model(&entity.Recording{}). Model(&entity.Recording{}).
Where("project_flock_kandangs_id = ?", projectFlockKandangID). Where("project_flock_kandangs_id = ?", projectFlockKandangID).
Preload("BodyWeights").
Preload("Eggs", func(db *gorm.DB) *gorm.DB { Preload("Eggs", func(db *gorm.DB) *gorm.DB {
return db.Select("recording_eggs.*, f.name AS product_flag_name"). 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"). Joins("LEFT JOIN product_warehouses pw ON pw.id = recording_eggs.product_warehouse_id").