From 3c10866208e2cbf2f1dfca047e39900aba7aaa8b Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 13 Jan 2026 13:20:06 +0700 Subject: [PATCH 1/2] feat[BE]: add GetOverheadByProjectFlockKandang endpoint and update related services --- .../controllers/closing.controller.go | 51 +++++++++++++++-- .../closings/dto/closingKeuangan.dto.go | 17 +++--- .../closings/dto/closingOverhead.dto.go | 55 ++++++++++++++++-- internal/modules/closings/module.go | 2 +- internal/modules/closings/route.go | 1 + .../closings/services/closing.service.go | 56 +++++++++++++++---- .../expense_realization.repository.go | 40 +++++++++++++ 7 files changed, 190 insertions(+), 32 deletions(-) diff --git a/internal/modules/closings/controllers/closing.controller.go b/internal/modules/closings/controllers/closing.controller.go index 1a472f03..c4ef4585 100644 --- a/internal/modules/closings/controllers/closing.controller.go +++ b/internal/modules/closings/controllers/closing.controller.go @@ -78,6 +78,36 @@ func (u *ClosingController) GetOne(c *fiber.Ctx) error { }) } +func (u *ClosingController) GetOverheadByProjectFlockKandang(c *fiber.Ctx) error { + projectParam := c.Params("project_flock_id") + kandangParam := c.Params("project_flock_kandang_id") + + projectFlockID, err := strconv.Atoi(projectParam) + if err != nil || projectFlockID <= 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id") + } + + pfkID, err := strconv.Atoi(kandangParam) + if err != nil || pfkID <= 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id") + } + + kandangID := uint(pfkID) + + result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID), &kandangID) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get overhead by project flock kandang successfully", + Data: result, + }) +} + func (u *ClosingController) GetClosingSummary(c *fiber.Ctx) error { param := c.Params("projectFlockId") @@ -153,14 +183,25 @@ func (u *ClosingController) GetPenjualanByProjectFlockKandang(c *fiber.Ctx) erro } func (u *ClosingController) GetOverhead(c *fiber.Ctx) error { - param := c.Params("project_flock_id") + projectParam := c.Params("project_flock_id") + kandangParam := c.Params("project_flock_kandang_id") // bisa kosong - projectFlockID, err := strconv.Atoi(param) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id") + projectFlockID, err := strconv.Atoi(projectParam) + if err != nil || projectFlockID <= 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id") } - result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID)) + var projectFlockKandangID *uint + if kandangParam != "" { + pfkID, err := strconv.Atoi(kandangParam) + if err != nil || pfkID <= 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id") + } + kandangID := uint(pfkID) + projectFlockKandangID = &kandangID + } + + result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID), projectFlockKandangID) if err != nil { return err } diff --git a/internal/modules/closings/dto/closingKeuangan.dto.go b/internal/modules/closings/dto/closingKeuangan.dto.go index 08bfb5fc..fa99a59d 100644 --- a/internal/modules/closings/dto/closingKeuangan.dto.go +++ b/internal/modules/closings/dto/closingKeuangan.dto.go @@ -79,10 +79,11 @@ type HppGroup struct { } type SummaryHpp struct { - Label string `json:"label"` - Comparison `json:"-"` - EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"` - EggRealization *FinancialMetrics `json:"egg_realization,omitempty"` + Label string `json:"label"` + Budgeting FinancialMetrics `json:"budgeting"` + Realization FinancialMetrics `json:"realization"` + EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"` + EggRealization *FinancialMetrics `json:"egg_realization,omitempty"` } type HppPurchasesSection struct { @@ -246,11 +247,9 @@ func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets [ realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, ctx.TotalPopulation, ctx.TotalWeightProduced) summary := SummaryHpp{ - Label: label, - Comparison: ToComparison( - ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget), - ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization), - ), + Label: label, + Budgeting: ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget), + Realization: ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization), } if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) && ctx.TotalEggWeightKg > 0 { diff --git a/internal/modules/closings/dto/closingOverhead.dto.go b/internal/modules/closings/dto/closingOverhead.dto.go index 71975da1..42903794 100644 --- a/internal/modules/closings/dto/closingOverhead.dto.go +++ b/internal/modules/closings/dto/closingOverhead.dto.go @@ -1,6 +1,8 @@ package dto import ( + "encoding/json" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" ) @@ -69,7 +71,7 @@ func ToOverheadDTO(budget *entity.ProjectBudget, realization *entity.ExpenseReal return dto } -func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty, totalActualPopulation float64) OverheadListDTO { +func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty, totalActualPopulation float64, isPerKandang bool, totalKandangCount int) OverheadListDTO { overheadsByNonstockID := make(map[uint]*OverheadDTO) latestDateByNonstockID := make(map[uint]string) @@ -82,9 +84,19 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex itemName, itemUOM := getItemInfo(budgets[i].Nonstock) overheadsByNonstockID[nonstockID].ItemName = itemName overheadsByNonstockID[nonstockID].UOMName = itemUOM - overheadsByNonstockID[nonstockID].BudgetQuantity = budgets[i].Qty - overheadsByNonstockID[nonstockID].BudgetUnitPrice = budgets[i].Price - overheadsByNonstockID[nonstockID].BudgetTotalAmount = calculateTotal(budgets[i].Qty, budgets[i].Price) + + budgetQty := budgets[i].Qty + budgetPrice := budgets[i].Price + budgetTotal := calculateTotal(budgets[i].Qty, budgets[i].Price) + + if isPerKandang && totalKandangCount > 0 { + budgetQty = budgetQty / float64(totalKandangCount) + budgetTotal = budgetTotal / float64(totalKandangCount) + } + + overheadsByNonstockID[nonstockID].BudgetQuantity = budgetQty + overheadsByNonstockID[nonstockID].BudgetUnitPrice = budgetPrice + overheadsByNonstockID[nonstockID].BudgetTotalAmount = budgetTotal } for i := range realizations { @@ -97,8 +109,22 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex overheadsByNonstockID[nonstockID] = &OverheadDTO{} } - overheadsByNonstockID[nonstockID].ActualQuantity += realizations[i].Qty - overheadsByNonstockID[nonstockID].ActualTotalAmount += calculateTotal(realizations[i].Qty, realizations[i].Price) + // Check if this is farm-level expense (multiple project flocks) + qty := realizations[i].Qty + totalAmount := calculateTotal(realizations[i].Qty, realizations[i].Price) + + if realizations[i].ExpenseNonstock.Expense != nil && + realizations[i].ExpenseNonstock.Expense.ProjectFlockId != nil { + projectFlockCount := countProjectFlocksInJSON(*realizations[i].ExpenseNonstock.Expense.ProjectFlockId) + if projectFlockCount > 1 { + // Bagi biaya sesuai jumlah project flock + qty = qty / float64(projectFlockCount) + totalAmount = totalAmount / float64(projectFlockCount) + } + } + + overheadsByNonstockID[nonstockID].ActualQuantity += qty + overheadsByNonstockID[nonstockID].ActualTotalAmount += totalAmount if overheadsByNonstockID[nonstockID].ItemName == "" { itemName, itemUOM := getItemInfo(realizations[i].ExpenseNonstock.Nonstock) @@ -148,6 +174,23 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex // === Helper Functions === +func countProjectFlocksInJSON(projectFlockJSON string) int { + if projectFlockJSON == "" { + return 0 + } + + var projectFlocks []int + if err := json.Unmarshal([]byte(projectFlockJSON), &projectFlocks); err != nil { + return 1 // default to 1 if parsing fails + } + + if len(projectFlocks) == 0 { + return 1 + } + + return len(projectFlocks) +} + func getItemInfo(nonstock *entity.Nonstock) (string, string) { if nonstock != nil && nonstock.Id != 0 { return nonstock.Name, nonstock.Uom.Name diff --git a/internal/modules/closings/module.go b/internal/modules/closings/module.go index c89e6125..658f1bef 100644 --- a/internal/modules/closings/module.go +++ b/internal/modules/closings/module.go @@ -37,7 +37,7 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * approvalRepo := commonRepo.NewApprovalRepository(db) approvalService := commonSvc.NewApprovalService(approvalRepo) - closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, validate) + closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, projectFlockKandangRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, validate) sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate) userService := sUser.NewUserService(userRepo, validate) diff --git a/internal/modules/closings/route.go b/internal/modules/closings/route.go index a9d25758..1cd4559d 100644 --- a/internal/modules/closings/route.go +++ b/internal/modules/closings/route.go @@ -26,6 +26,7 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService route.Get("/:project_flock_id/:project_flock_kandang_id/penjualan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetPenjualanByProjectFlockKandang) route.Get("/:projectFlockId", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSummary) route.Get("/:project_flock_id/overhead", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetOverhead) + route.Get("/:project_flock_id/:project_flock_kandang_id/overhead", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetOverhead) route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByKandang) route.Get("/:project_flock_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByProject) route.Get("/:projectFlockId/sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronak) diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index 6c682b9c..5870aa12 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -34,7 +34,7 @@ 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) - GetOverhead(ctx *fiber.Ctx, projectFlockID 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) GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) @@ -46,6 +46,7 @@ type closingService struct { Validate *validator.Validate Repository repository.ClosingRepository ProjectFlockRepo projectflockRepository.ProjectflockRepository + ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository MarketingRepo marketingRepository.MarketingRepository MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository ApprovalSvc commonSvc.ApprovalService @@ -56,12 +57,13 @@ type closingService struct { RecordingRepo recordingRepository.RecordingRepository } -func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, 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, validate *validator.Validate) ClosingService { return &closingService{ Log: utils.Log, Validate: validate, Repository: repo, ProjectFlockRepo: projectFlockRepo, + ProjectFlockKandangRepo: projectFlockKandangRepo, MarketingRepo: marketingRepo, MarketingDeliveryProductRepo: marketingDeliveryProductRepo, ApprovalSvc: approvalSvc, @@ -355,35 +357,67 @@ func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID return statusProject, statusClosing, nil } -func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error) { +func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error) { budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil { return nil, err } - realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID) + realizations, err := s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlockID, projectFlockKandangID) if err != nil { return nil, err } + // Count total kandang in project flock (for budget division if per kandang) + projectFlockKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID) + if err != nil { + return nil, err + } + totalKandangCount := len(projectFlockKandangs) + chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil { return nil, err } var totalChickinQty float64 - for _, chickin := range chickins { - totalChickinQty += chickin.UsageQty - } + var totalDepletion float64 - totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID) - if err != nil { - s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err) + if projectFlockKandangID != nil { + + for _, chickin := range chickins { + if chickin.ProjectFlockKandangId == *projectFlockKandangID { + totalChickinQty += chickin.UsageQty + } + } + + var depletionResult float64 + err = s.RecordingRepo.DB().WithContext(c.Context()). + Table("recording_depletions"). + Select("COALESCE(SUM(recording_depletions.qty), 0)"). + Joins("JOIN recordings ON recordings.id = recording_depletions.recording_id"). + Where("recordings.project_flock_kandangs_id = ?", *projectFlockKandangID). + Scan(&depletionResult).Error + if err != nil { + s.Log.Warnf("GetTotalDepletionByProjectFlockKandangID error: %v", err) + } else { + totalDepletion = depletionResult + } + } else { + + for _, chickin := range chickins { + totalChickinQty += chickin.UsageQty + } + + totalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID) + if err != nil { + s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err) + } } totalActualPopulation := totalChickinQty - totalDepletion - result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation) + result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation, projectFlockKandangID != nil, totalKandangCount) return &result, nil } diff --git a/internal/modules/expenses/repositories/expense_realization.repository.go b/internal/modules/expenses/repositories/expense_realization.repository.go index f1387483..2c7db649 100644 --- a/internal/modules/expenses/repositories/expense_realization.repository.go +++ b/internal/modules/expenses/repositories/expense_realization.repository.go @@ -2,6 +2,7 @@ package repository import ( "context" + "fmt" "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" @@ -15,6 +16,7 @@ type ExpenseRealizationRepository interface { IdExists(ctx context.Context, id uint64) (bool, error) GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error) GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ExpenseRealization, error) + GetClosingOverhead(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.ExpenseRealization, error) GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.ExpenseQuery) ([]entity.ExpenseRealization, int64, error) } @@ -55,6 +57,44 @@ func (r *ExpenseRealizationRepositoryImpl) GetByProjectFlockID(ctx context.Conte return realizations, err } +func (r *ExpenseRealizationRepositoryImpl) GetClosingOverhead(ctx context.Context, projectFlockID uint, projectFlockKandangID *uint) ([]entity.ExpenseRealization, error) { + var realizations []entity.ExpenseRealization + + db := r.DB().WithContext(ctx). + Preload("ExpenseNonstock"). + Preload("ExpenseNonstock.Nonstock"). + Preload("ExpenseNonstock.Nonstock.Uom"). + Preload("ExpenseNonstock.Nonstock.Flags"). + Preload("ExpenseNonstock.Expense"). + Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id"). + Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id"). + Joins("LEFT JOIN project_flock_kandangs ON project_flock_kandangs.id = expense_nonstocks.project_flock_kandang_id"). + Joins("LEFT JOIN kandangs ON kandangs.id = expense_nonstocks.kandang_id"). + Where("expenses.realization_date IS NOT NULL") + + // Build WHERE clause for project flock filtering + if projectFlockKandangID != nil { + // Per kandang: hanya ambil expense yang specific ke kandang tersebut + // SKIP expense level farm (yang punya multiple project flocks di JSON array) + // IMPORTANT: Untuk kandang_id, pastikan kandang tersebut belong to project_flock_kandang ini + db = db.Where(`( + expense_nonstocks.project_flock_kandang_id = ? OR + (expense_nonstocks.kandang_id = (SELECT kandang_id FROM project_flock_kandangs WHERE id = ?) AND + expense_nonstocks.project_flock_kandang_id IS NULL) + )`, *projectFlockKandangID, *projectFlockKandangID) + } else { + // All kandang: include expense kandang-specific DAN expense level farm + db = db.Where(`( + project_flock_kandangs.project_flock_id = ? OR + kandangs.id IN (SELECT kandang_id FROM project_flock_kandangs WHERE project_flock_id = ?) OR + (expenses.project_flock_id IS NOT NULL AND expenses.project_flock_id::jsonb @> ?::jsonb) + )`, projectFlockID, projectFlockID, fmt.Sprintf("[%d]", projectFlockID)) + } + + err := db.Find(&realizations).Error + return realizations, err +} + func (r *ExpenseRealizationRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, filters *validation.ExpenseQuery) ([]entity.ExpenseRealization, int64, error) { var realizations []entity.ExpenseRealization var total int64 From b088eebac5f9aa8a3256c3c865b47062fe663cac Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 13 Jan 2026 13:36:08 +0700 Subject: [PATCH 2/2] feat[BE]: enhance GetOverhead functionality with project flock kandang count mapping and update related DTOs --- .../controllers/closing.controller.go | 2 +- .../closings/dto/closingOverhead.dto.go | 57 +++++++++++++------ .../closings/services/closing.service.go | 32 +++++++++-- .../expense_realization.repository.go | 10 +--- 4 files changed, 71 insertions(+), 30 deletions(-) diff --git a/internal/modules/closings/controllers/closing.controller.go b/internal/modules/closings/controllers/closing.controller.go index c4ef4585..a10d6a94 100644 --- a/internal/modules/closings/controllers/closing.controller.go +++ b/internal/modules/closings/controllers/closing.controller.go @@ -184,7 +184,7 @@ func (u *ClosingController) GetPenjualanByProjectFlockKandang(c *fiber.Ctx) erro func (u *ClosingController) GetOverhead(c *fiber.Ctx) error { projectParam := c.Params("project_flock_id") - kandangParam := c.Params("project_flock_kandang_id") // bisa kosong + kandangParam := c.Params("project_flock_kandang_id") projectFlockID, err := strconv.Atoi(projectParam) if err != nil || projectFlockID <= 0 { diff --git a/internal/modules/closings/dto/closingOverhead.dto.go b/internal/modules/closings/dto/closingOverhead.dto.go index 42903794..4730474a 100644 --- a/internal/modules/closings/dto/closingOverhead.dto.go +++ b/internal/modules/closings/dto/closingOverhead.dto.go @@ -71,7 +71,7 @@ func ToOverheadDTO(budget *entity.ProjectBudget, realization *entity.ExpenseReal return dto } -func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty, totalActualPopulation float64, isPerKandang bool, totalKandangCount int) OverheadListDTO { +func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty, totalActualPopulation float64, isPerKandang bool, totalKandangCount int, projectFlockKandangCountMap map[uint]int) OverheadListDTO { overheadsByNonstockID := make(map[uint]*OverheadDTO) latestDateByNonstockID := make(map[uint]string) @@ -89,6 +89,7 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex budgetPrice := budgets[i].Price budgetTotal := calculateTotal(budgets[i].Qty, budgets[i].Price) + // Budget division: per kandang view only if isPerKandang && totalKandangCount > 0 { budgetQty = budgetQty / float64(totalKandangCount) budgetTotal = budgetTotal / float64(totalKandangCount) @@ -109,17 +110,35 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex overheadsByNonstockID[nonstockID] = &OverheadDTO{} } - // Check if this is farm-level expense (multiple project flocks) qty := realizations[i].Qty totalAmount := calculateTotal(realizations[i].Qty, realizations[i].Price) + // Farm-level expense division if realizations[i].ExpenseNonstock.Expense != nil && realizations[i].ExpenseNonstock.Expense.ProjectFlockId != nil { - projectFlockCount := countProjectFlocksInJSON(*realizations[i].ExpenseNonstock.Expense.ProjectFlockId) - if projectFlockCount > 1 { - // Bagi biaya sesuai jumlah project flock - qty = qty / float64(projectFlockCount) - totalAmount = totalAmount / float64(projectFlockCount) + projectFlockIDs := parseProjectFlockIDsFromJSON(*realizations[i].ExpenseNonstock.Expense.ProjectFlockId) + + if len(projectFlockIDs) > 0 { + totalKandangInAllProjects := 0 + for _, pfID := range projectFlockIDs { + if count, exists := projectFlockKandangCountMap[pfID]; exists { + totalKandangInAllProjects += count + } + } + + if totalKandangInAllProjects > 0 { + if isPerKandang { + qty = qty / float64(totalKandangInAllProjects) + totalAmount = totalAmount / float64(totalKandangInAllProjects) + } else { + // Overhead ALL: divide by total kandang then multiply by this project's kandang count + perKandangAmount := totalAmount / float64(totalKandangInAllProjects) + perKandangQty := qty / float64(totalKandangInAllProjects) + + qty = perKandangQty * float64(totalKandangCount) + totalAmount = perKandangAmount * float64(totalKandangCount) + } + } } } @@ -172,22 +191,24 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex } } -// === Helper Functions === +func parseProjectFlockIDsFromJSON(projectFlockJSON string) []uint { + if projectFlockJSON == "" { + return []uint{} + } + + var projectFlocks []uint + if err := json.Unmarshal([]byte(projectFlockJSON), &projectFlocks); err != nil { + return []uint{} + } + + return projectFlocks +} func countProjectFlocksInJSON(projectFlockJSON string) int { - if projectFlockJSON == "" { - return 0 - } - - var projectFlocks []int - if err := json.Unmarshal([]byte(projectFlockJSON), &projectFlocks); err != nil { - return 1 // default to 1 if parsing fails - } - + projectFlocks := parseProjectFlockIDsFromJSON(projectFlockJSON) if len(projectFlocks) == 0 { return 1 } - return len(projectFlocks) } diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index 5870aa12..f137901d 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -2,6 +2,7 @@ package service import ( "context" + "encoding/json" "errors" "math" "strconv" @@ -368,13 +369,38 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint, projectFl return nil, err } - // Count total kandang in project flock (for budget division if per kandang) projectFlockKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil { return nil, err } totalKandangCount := len(projectFlockKandangs) + // Build kandang count map for farm expense division + projectFlockKandangCountMap := make(map[uint]int) + projectFlockKandangCountMap[projectFlockID] = totalKandangCount + + involvedProjectFlocks := make(map[uint]bool) + for _, realization := range realizations { + if realization.ExpenseNonstock != nil && + realization.ExpenseNonstock.Expense != nil && + realization.ExpenseNonstock.Expense.ProjectFlockId != nil { + var projectFlockIDs []uint + if err := json.Unmarshal([]byte(*realization.ExpenseNonstock.Expense.ProjectFlockId), &projectFlockIDs); err == nil { + for _, pfID := range projectFlockIDs { + if pfID != projectFlockID { + involvedProjectFlocks[pfID] = true + } + } + } + } + } + + for pfID := range involvedProjectFlocks { + if pfKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), pfID); err == nil { + projectFlockKandangCountMap[pfID] = len(pfKandangs) + } + } + chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID) if err != nil { return nil, err @@ -384,7 +410,6 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint, projectFl var totalDepletion float64 if projectFlockKandangID != nil { - for _, chickin := range chickins { if chickin.ProjectFlockKandangId == *projectFlockKandangID { totalChickinQty += chickin.UsageQty @@ -404,7 +429,6 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint, projectFl totalDepletion = depletionResult } } else { - for _, chickin := range chickins { totalChickinQty += chickin.UsageQty } @@ -417,7 +441,7 @@ func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint, projectFl totalActualPopulation := totalChickinQty - totalDepletion - result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation, projectFlockKandangID != nil, totalKandangCount) + result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation, projectFlockKandangID != nil, totalKandangCount, projectFlockKandangCountMap) return &result, nil } diff --git a/internal/modules/expenses/repositories/expense_realization.repository.go b/internal/modules/expenses/repositories/expense_realization.repository.go index 2c7db649..60ec97a7 100644 --- a/internal/modules/expenses/repositories/expense_realization.repository.go +++ b/internal/modules/expenses/repositories/expense_realization.repository.go @@ -72,18 +72,14 @@ func (r *ExpenseRealizationRepositoryImpl) GetClosingOverhead(ctx context.Contex Joins("LEFT JOIN kandangs ON kandangs.id = expense_nonstocks.kandang_id"). Where("expenses.realization_date IS NOT NULL") - // Build WHERE clause for project flock filtering if projectFlockKandangID != nil { - // Per kandang: hanya ambil expense yang specific ke kandang tersebut - // SKIP expense level farm (yang punya multiple project flocks di JSON array) - // IMPORTANT: Untuk kandang_id, pastikan kandang tersebut belong to project_flock_kandang ini db = db.Where(`( expense_nonstocks.project_flock_kandang_id = ? OR (expense_nonstocks.kandang_id = (SELECT kandang_id FROM project_flock_kandangs WHERE id = ?) AND - expense_nonstocks.project_flock_kandang_id IS NULL) - )`, *projectFlockKandangID, *projectFlockKandangID) + expense_nonstocks.project_flock_kandang_id IS NULL) OR + (expenses.project_flock_id IS NOT NULL AND expenses.project_flock_id::jsonb @> ?::jsonb) + )`, *projectFlockKandangID, *projectFlockKandangID, fmt.Sprintf("[%d]", projectFlockID)) } else { - // All kandang: include expense kandang-specific DAN expense level farm db = db.Where(`( project_flock_kandangs.project_flock_id = ? OR kandangs.id IN (SELECT kandang_id FROM project_flock_kandangs WHERE project_flock_id = ?) OR