diff --git a/internal/modules/closings/controllers/closing.controller.go b/internal/modules/closings/controllers/closing.controller.go index c78fd15f..3e64f89b 100644 --- a/internal/modules/closings/controllers/closing.controller.go +++ b/internal/modules/closings/controllers/closing.controller.go @@ -116,7 +116,17 @@ func (u *ClosingController) GetClosingSummary(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId") } - result, err := u.ClosingService.GetClosingSummary(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.GetClosingSummary(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 05a606ca..470f506d 100644 --- a/internal/modules/closings/dto/closing.dto.go +++ b/internal/modules/closings/dto/closing.dto.go @@ -59,6 +59,21 @@ type ClosingSummaryDTO struct { StatusClosing string `json:"closing_status"` } +type ClosingSummaryKandangDTO struct { + FlockID uint `json:"flock_id"` + Period int `json:"period"` + LocationName string `json:"location_name"` + Population int `json:"population"` + PopulationFormatted string `json:"population_formatted"` + ProjectType string `json:"project_type"` + ClosingDate string `json:"closing_date"` + KandangName string `json:"kandang_name"` + ChickInDate string `json:"chick_in_date"` + PicName string `json:"pic_name"` + ApprovalDate string `json:"approval_date"` + ProjectStatus string `json:"project_status"` +} + type ClosingPurchaseDTO struct { InitialPopulation int `json:"initial_population"` ClaimCulling int `json:"claim_culling"` @@ -83,18 +98,18 @@ type ClosingEggSalesDTO struct { } type ClosingPerformanceDTO struct { - Depletion float64 `json:"depletion"` - Age float64 `json:"age_day"` - 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:"fcr_diff"` - AwgAct float64 `json:"awg_act"` - AwgStd float64 `json:"awg_std"` - FeedIntake float64 `json:"feed_intake"` - FeedIntakeStd float64 `json:"feed_intake_std"` + Depletion float64 `json:"depletion"` + Age float64 `json:"age_day"` + 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:"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"` @@ -175,7 +190,7 @@ func sumPopulation(history []entity.ProjectFlockKandang) float64 { var total float64 for _, h := range history { for _, chickin := range h.Chickins { - total += chickin.UsageQty + chickin.PendingUsageQty + total += chickin.UsageQty } } return total diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index 0c543d05..38529b0d 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "math" "strconv" "strings" @@ -35,7 +36,7 @@ type ClosingService interface { GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.ClosingListItemDTO, int64, error) 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) + GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (any, error) GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, error) GetOverhead(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error) GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) @@ -150,11 +151,15 @@ func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint, projectF return realisasi, nil } -func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) { +func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint, kandangID *uint) (any, error) { if projectFlockID == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") } + if kandangID != nil { + return s.getClosingSummaryByKandang(c.Context(), projectFlockID, *kandangID) + } + project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withClosingRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found") @@ -175,6 +180,124 @@ func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*d return &summary, nil } +func (s closingService) getClosingSummaryByKandang(ctx context.Context, projectFlockID uint, kandangID uint) (*dto.ClosingSummaryKandangDTO, error) { + if projectFlockID == 0 || kandangID == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id or kandang id") + } + + db := s.Repository.DB().WithContext(ctx) + + var kandang entity.ProjectFlockKandang + if err := db. + Preload("Kandang"). + Preload("Kandang.Location"). + Preload("Kandang.Pic"). + Where("project_flock_id = ?", projectFlockID). + Where("kandang_id = ?", kandangID). + First(&kandang).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found") + } + s.Log.Errorf("Failed get project flock kandang %d/%d: %+v", projectFlockID, kandangID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang") + } + + var project entity.ProjectFlock + if err := db. + Select("id", "category"). + First(&project, projectFlockID).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found") + } + s.Log.Errorf("Failed get project flock %d: %+v", projectFlockID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") + } + + var population float64 + if err := db. + Table("project_flock_populations pfp"). + Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id"). + Where("pc.project_flock_kandang_id = ?", kandang.Id). + Select("COALESCE(SUM(pfp.total_qty), 0)"). + Scan(&population).Error; err != nil { + s.Log.Errorf("Failed to sum population for project flock kandang %d: %+v", kandang.Id, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch population data") + } + + var chickInDate time.Time + if err := db. + Table("project_chickins"). + Where("project_flock_kandang_id = ?", kandang.Id). + Select("MIN(chick_in_date)"). + Scan(&chickInDate).Error; err != nil { + s.Log.Errorf("Failed to fetch chick in date for project flock kandang %d: %+v", kandang.Id, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chick in date") + } + + statusProject := "Belum Selesai" + var approvalDate string + if s.ApprovalSvc != nil { + records, _, err := s.ApprovalSvc.List(ctx, utils.ApprovalWorkflowProjectFlockKandang.String(), &kandang.Id, 1, 1000, "") + if err != nil { + s.Log.Errorf("Failed to fetch approvals for project flock kandang %d: %+v", kandang.Id, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch approval data") + } + + var ( + minStep uint16 + latestActionAt time.Time + ) + + for _, rec := range records { + if minStep == 0 || rec.StepNumber < minStep { + minStep = rec.StepNumber + } + + if latestActionAt.IsZero() || rec.ActionAt.After(latestActionAt) { + latestActionAt = rec.ActionAt + statusProject = rec.StepName + } + } + + if statusProject == "" && minStep > 0 { + if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowProjectFlockKandang, approvalutils.ApprovalStep(minStep)); ok { + statusProject = label + } + } + + if !latestActionAt.IsZero() { + approvalDate = latestActionAt.Format("2006-01-02") + } + } + + closingDate := "" + if kandang.ClosedAt != nil { + closingDate = kandang.ClosedAt.Format("2006-01-02") + } + + chickInDateStr := "" + if !chickInDate.IsZero() { + chickInDateStr = chickInDate.Format("2006-01-02") + } + + populationInt := int(population) + + return &dto.ClosingSummaryKandangDTO{ + FlockID: projectFlockID, + Period: kandang.Period, + LocationName: kandang.Kandang.Location.Name, + Population: populationInt, + PopulationFormatted: fmt.Sprintf("%d Ekor", populationInt), + ProjectType: project.Category, + ClosingDate: closingDate, + KandangName: kandang.Kandang.Name, + ChickInDate: chickInDateStr, + PicName: kandang.Kandang.Pic.Name, + ApprovalDate: approvalDate, + ProjectStatus: statusProject, + }, nil +} + func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) { if projectFlockID == 0 { return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")