diff --git a/internal/database/seed/seeder.go b/internal/database/seed/seeder.go index 99188e73..24425917 100644 --- a/internal/database/seed/seeder.go +++ b/internal/database/seed/seeder.go @@ -363,6 +363,7 @@ func seedProductCategories(tx *gorm.DB, createdBy uint) (map[string]uint, error) Name string Code string }{ + {"Pullet", "PLT"}, {"Bahan Baku", "RAW"}, {"Day Old Chick", "DOC"}, {"Telur", "EGG"}, @@ -569,6 +570,54 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, Flags: []utils.FlagType{utils.FlagDOC}, }, + { + Name: "Ayam Afkir", + Brand: "-", + Sku: "1", + Uom: "Ekor", + Category: "Day Old Chick", + Price: 1, + + + }, + { + Name: "Ayam Mati", + Brand: "-", + Sku: "2", + Uom: "Ekor", + Category: "Day Old Chick", + Price: 1, + + + }, + { + Name: "Ayam Culling", + Brand: "-", + Sku: "3", + Uom: "Ekor", + Category: "Day Old Chick", + Price: 1, + + + }, + { + Name: "Telur Konsumsi Baik", + Brand: "-", + Sku: "4", + Uom: "Unit", + Category: "Telur", + Price: 1, + + }, + { + Name: "Telur Pecah", + Brand: "-", + Sku: "5", + Uom: "Unit", + Category: "Telur", + Price: 1, + + }, { Name: "281 SPECIAL STARTER", Brand: "281 STARTER", @@ -580,22 +629,6 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, Flags: []utils.FlagType{utils.FlagPakan, utils.FlagStarter}, }, - { - Name: "Telur Konsumsi Baik", - Brand: "Layer Farm", - Sku: "EGG-GOOD", - Uom: "Unit", - Category: "Telur", - Price: 1800, - }, - { - Name: "Telur Pecah", - Brand: "Layer Farm", - Sku: "EGG-CRACK", - Uom: "Unit", - Category: "Telur", - Price: 900, - }, } for _, seed := range seeds { diff --git a/internal/modules/production/project_flocks/controllers/projectflock.controller.go b/internal/modules/production/project_flocks/controllers/projectflock.controller.go index 668743b3..d3b0061c 100644 --- a/internal/modules/production/project_flocks/controllers/projectflock.controller.go +++ b/internal/modules/production/project_flocks/controllers/projectflock.controller.go @@ -222,11 +222,11 @@ func (u *ProjectflockController) Approval(c *fiber.Ctx) error { } func (u *ProjectflockController) GetFlockPeriodSummary(c *fiber.Ctx) error { - param := c.Params("flock_id") + param := c.Params("project_flock_kandang_id") id, err := strconv.Atoi(param) if err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid Flock Id") + return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id") } summary, err := u.ProjectflockService.GetFlockPeriodSummary(c, uint(id)) diff --git a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go index f18d0654..e6a36c87 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -2,6 +2,7 @@ package repository import ( "context" + "strings" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gorm.io/gorm" @@ -16,6 +17,7 @@ type ProjectFlockKandangRepository interface { ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error) FindKandangsWithRecordings(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]entity.Kandang, error) + MaxPeriodByBaseName(ctx context.Context, baseName string) (int, error) WithTx(tx *gorm.DB) ProjectFlockKandangRepository DB() *gorm.DB } @@ -24,6 +26,8 @@ type projectFlockKandangRepositoryImpl struct { db *gorm.DB } +const flockBaseNameExpression = "LOWER(TRIM(regexp_replace(project_flocks.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')))" + func NewProjectFlockKandangRepository(db *gorm.DB) ProjectFlockKandangRepository { return &projectFlockKandangRepositoryImpl{db: db} } @@ -149,3 +153,17 @@ func (r *projectFlockKandangRepositoryImpl) FindKandangsWithRecordings(ctx conte Scan(&kandangs).Error return kandangs, err } + +func (r *projectFlockKandangRepositoryImpl) MaxPeriodByBaseName(ctx context.Context, baseName string) (int, error) { + if strings.TrimSpace(baseName) == "" { + return 0, nil + } + var max int + err := r.db.WithContext(ctx). + Table("project_flock_kandangs pfk"). + Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id"). + Where(flockBaseNameExpression+" = LOWER(?)", baseName). + Select("COALESCE(MAX(pf.period), 0)"). + Scan(&max).Error + return max, err +} diff --git a/internal/modules/production/project_flocks/route.go b/internal/modules/production/project_flocks/route.go index 38f14bb0..7642b90c 100644 --- a/internal/modules/production/project_flocks/route.go +++ b/internal/modules/production/project_flocks/route.go @@ -27,6 +27,6 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj route.Delete("/:id", ctrl.DeleteOne) route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang) route.Post("/approvals", ctrl.Approval) - route.Get("/flocks/:flock_id/periods", ctrl.GetFlockPeriodSummary) + route.Get("/kandangs/:project_flock_kandang_id/periods", ctrl.GetFlockPeriodSummary) } diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index 47589f08..e01d3385 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -52,8 +52,8 @@ type projectflockService struct { } type FlockPeriodSummary struct { - Flock entity.Flock - NextPeriod int + Flock entity.Flock + NextPeriod int } func NewProjectflockService( @@ -719,28 +719,57 @@ func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID u return total, nil } -func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error) { - flock, err := s.FlockRepo.GetByID(c.Context(), flockID, func(db *gorm.DB) *gorm.DB { - return db.Preload("CreatedUser") - }) - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "Flock not found") - } - if err != nil { - s.Log.Errorf("Failed get flock %d for period summary: %+v", flockID, err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch flock") - } +func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, projectFlockKandangID uint) (*FlockPeriodSummary, error) { + if projectFlockKandangID == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required") + } - maxPeriod, err := s.Repository.GetMaxPeriodByBaseName(c.Context(), flock.Name) - if err != nil { - s.Log.Errorf("Failed to compute next period for flock %d: %+v", flockID, err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to compute next period") - } + pivot, err := s.pivotRepo().GetByID(c.Context(), projectFlockKandangID) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found") + } + if err != nil { + s.Log.Errorf("Failed to fetch project_flock_kandang %d: %+v", projectFlockKandangID, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang") + } - return &FlockPeriodSummary{ - Flock: *flock, - NextPeriod: maxPeriod + 1, - }, nil + var baseName string + var referenceFlock *entity.Flock + if pivot.ProjectFlock.Id != 0 { + baseName = pfutils.DeriveBaseName(pivot.ProjectFlock.FlockName) + } + + if strings.TrimSpace(baseName) != "" { + referenceFlock, err = s.FlockRepo.GetByName(c.Context(), baseName) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + s.Log.Errorf("Failed to fetch flock %q: %+v", baseName, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch flock") + } + } + + if referenceFlock == nil { + referenceFlock = &entity.Flock{Name: pivot.ProjectFlock.FlockName} + } + + maxPeriod := pivot.ProjectFlock.Period + if strings.TrimSpace(baseName) != "" { + if headerMax, err := s.Repository.GetMaxPeriodByBaseName(c.Context(), baseName); err != nil { + s.Log.Warnf("Unable to compute header period for base %q: %+v", baseName, err) + } else if headerMax > maxPeriod { + maxPeriod = headerMax + } + + if pivotMax, err := s.pivotRepo().MaxPeriodByBaseName(c.Context(), baseName); err != nil { + s.Log.Warnf("Unable to compute pivot period for base %q: %+v", baseName, err) + } else if pivotMax > maxPeriod { + maxPeriod = pivotMax + } + } + + return &FlockPeriodSummary{ + Flock: *referenceFlock, + NextPeriod: maxPeriod + 1, + }, nil } func uniqueUintSlice(values []uint) []uint { diff --git a/internal/modules/production/recordings/dto/recording.dto.go b/internal/modules/production/recordings/dto/recording.dto.go index 07135e1d..e8d04758 100644 --- a/internal/modules/production/recordings/dto/recording.dto.go +++ b/internal/modules/production/recordings/dto/recording.dto.go @@ -14,21 +14,22 @@ import ( // === DTO Structs === type RecordingBaseDTO struct { - Id uint `json:"id"` - ProjectFlockKandangId uint `json:"project_flock_kandang_id"` - RecordDatetime time.Time `json:"record_datetime"` - Day *int `json:"day,omitempty"` - ProjectFlockCategory *string `json:"project_flock_category,omitempty"` - TotalDepletionQty *float64 `json:"total_depletion_qty,omitempty"` - CumDepletionRate *float64 `json:"cum_depletion_rate,omitempty"` - DailyGain *float64 `json:"daily_gain,omitempty"` - AvgDailyGain *float64 `json:"avg_daily_gain,omitempty"` - CumIntake *int `json:"cum_intake,omitempty"` - FcrValue *float64 `json:"fcr_value,omitempty"` - TotalChickQty *float64 `json:"total_chick_qty,omitempty"` - Approval approvalDTO.ApprovalBaseDTO `json:"approval"` - EggGradingStatus *string `json:"egg_grading_status,omitempty"` - EggGradingPendingQty *int `json:"egg_grading_pending_qty,omitempty"` + Id uint `json:"id"` + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` + RecordDatetime time.Time `json:"record_datetime"` + Day *int `json:"day,omitempty"` + ProjectFlockCategory *string `json:"project_flock_category,omitempty"` + TotalDepletionQty *float64 `json:"total_depletion_qty,omitempty"` + CumDepletionRate *float64 `json:"cum_depletion_rate,omitempty"` + DailyGain *float64 `json:"daily_gain,omitempty"` + AvgDailyGain *float64 `json:"avg_daily_gain,omitempty"` + CumIntake *int `json:"cum_intake,omitempty"` + FcrValue *float64 `json:"fcr_value,omitempty"` + TotalChickQty *float64 `json:"total_chick_qty,omitempty"` + Approval approvalDTO.ApprovalBaseDTO `json:"approval"` + EggGradingStatus *string `json:"egg_grading_status,omitempty"` + EggGradingPendingQty *int `json:"egg_grading_pending_qty,omitempty"` + EggGradingCompletedQty *int `json:"egg_grading_completed_qty,omitempty"` } type RecordingListDTO struct { @@ -102,24 +103,25 @@ func ToRecordingBaseDTO(e entity.Recording) RecordingBaseDTO { latestApproval = snapshot } - gradingStatus, gradingPending := computeEggGradingStatus(e) + gradingStatus, gradingPending, gradingCompleted := computeEggGradingStatus(e) return RecordingBaseDTO{ - Id: e.Id, - ProjectFlockKandangId: e.ProjectFlockKandangId, - RecordDatetime: e.RecordDatetime, - Day: e.Day, - ProjectFlockCategory: projectFlockCategory, - TotalDepletionQty: e.TotalDepletionQty, - CumDepletionRate: e.CumDepletionRate, - DailyGain: e.DailyGain, - AvgDailyGain: e.AvgDailyGain, - CumIntake: e.CumIntake, - FcrValue: e.FcrValue, - TotalChickQty: e.TotalChickQty, - Approval: latestApproval, - EggGradingStatus: gradingStatus, - EggGradingPendingQty: gradingPending, + Id: e.Id, + ProjectFlockKandangId: e.ProjectFlockKandangId, + RecordDatetime: e.RecordDatetime, + Day: e.Day, + ProjectFlockCategory: projectFlockCategory, + TotalDepletionQty: e.TotalDepletionQty, + CumDepletionRate: e.CumDepletionRate, + DailyGain: e.DailyGain, + AvgDailyGain: e.AvgDailyGain, + CumIntake: e.CumIntake, + FcrValue: e.FcrValue, + TotalChickQty: e.TotalChickQty, + Approval: latestApproval, + EggGradingStatus: gradingStatus, + EggGradingPendingQty: gradingPending, + EggGradingCompletedQty: gradingCompleted, } } @@ -243,14 +245,17 @@ func toRecordingProductWarehouseDTO(pw *entity.ProductWarehouse) *RecordingProdu return &dto } -func computeEggGradingStatus(e entity.Recording) (*string, *int) { - if len(e.Eggs) == 0 { - return nil, nil +const goodEggProductWarehouseID uint = 5 + +func computeEggGradingStatus(e entity.Recording) (*string, *int, *int) { + goodEggs := filterGoodEggs(e.Eggs) + if len(goodEggs) == 0 { + return nil, nil, nil } totalEggs := 0 totalGraded := 0.0 - for _, egg := range e.Eggs { + for _, egg := range goodEggs { totalEggs += egg.Qty for _, grading := range egg.GradingEggs { totalGraded += grading.Qty @@ -258,20 +263,41 @@ func computeEggGradingStatus(e entity.Recording) (*string, *int) { } if totalEggs == 0 { - return nil, nil + return nil, nil, nil } - pending := float64(totalEggs) - totalGraded + pendingFloat := float64(totalEggs) - totalGraded + if pendingFloat < 0 { + pendingFloat = 0 + } + pendingInt := int(math.Round(pendingFloat)) + completedInt := int(math.Round(totalGraded)) + if completedInt < 0 { + completedInt = 0 + } - if pending > 0.5 { + if pendingInt > 0 { status := "GRADING_TELUR" - pendingInt := int(math.Round(pending)) - return &status, &pendingInt + return &status, &pendingInt, &completedInt } status := "GRADING_SELESAI" zero := 0 - return &status, &zero + return &status, &zero, &completedInt +} + +func filterGoodEggs(eggs []entity.RecordingEgg) []entity.RecordingEgg { + if len(eggs) == 0 { + return nil + } + + result := make([]entity.RecordingEgg, 0, len(eggs)) + for _, egg := range eggs { + if egg.ProductWarehouseId == goodEggProductWarehouseID { + result = append(result, egg) + } + } + return result } func defaultRecordingLatestApproval(e entity.Recording) approvalDTO.ApprovalBaseDTO {