feat/BE/US-76/US-78/US-79/TASK-112,120,133,121-Recording growing/TASK-187,189,202,190-Recording Laying/TASK-191,192,194,197,203-Grading Telur

This commit is contained in:
ragilap
2025-10-31 16:04:22 +07:00
parent f869943573
commit 4b39f52d5a
6 changed files with 188 additions and 82 deletions
+49 -16
View File
@@ -363,6 +363,7 @@ func seedProductCategories(tx *gorm.DB, createdBy uint) (map[string]uint, error)
Name string Name string
Code string Code string
}{ }{
{"Pullet", "PLT"},
{"Bahan Baku", "RAW"}, {"Bahan Baku", "RAW"},
{"Day Old Chick", "DOC"}, {"Day Old Chick", "DOC"},
{"Telur", "EGG"}, {"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"}, Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
Flags: []utils.FlagType{utils.FlagDOC}, 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", Name: "281 SPECIAL STARTER",
Brand: "281 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"}, Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
Flags: []utils.FlagType{utils.FlagPakan, utils.FlagStarter}, 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 { for _, seed := range seeds {
@@ -222,11 +222,11 @@ func (u *ProjectflockController) Approval(c *fiber.Ctx) error {
} }
func (u *ProjectflockController) GetFlockPeriodSummary(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) id, err := strconv.Atoi(param)
if err != nil { 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)) summary, err := u.ProjectflockService.GetFlockPeriodSummary(c, uint(id))
@@ -2,6 +2,7 @@ package repository
import ( import (
"context" "context"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm" "gorm.io/gorm"
@@ -16,6 +17,7 @@ type ProjectFlockKandangRepository interface {
ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error)
HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error) HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error)
FindKandangsWithRecordings(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]entity.Kandang, 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 WithTx(tx *gorm.DB) ProjectFlockKandangRepository
DB() *gorm.DB DB() *gorm.DB
} }
@@ -24,6 +26,8 @@ type projectFlockKandangRepositoryImpl struct {
db *gorm.DB db *gorm.DB
} }
const flockBaseNameExpression = "LOWER(TRIM(regexp_replace(project_flocks.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')))"
func NewProjectFlockKandangRepository(db *gorm.DB) ProjectFlockKandangRepository { func NewProjectFlockKandangRepository(db *gorm.DB) ProjectFlockKandangRepository {
return &projectFlockKandangRepositoryImpl{db: db} return &projectFlockKandangRepositoryImpl{db: db}
} }
@@ -149,3 +153,17 @@ func (r *projectFlockKandangRepositoryImpl) FindKandangsWithRecordings(ctx conte
Scan(&kandangs).Error Scan(&kandangs).Error
return kandangs, err 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
}
@@ -27,6 +27,6 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
route.Delete("/:id", ctrl.DeleteOne) route.Delete("/:id", ctrl.DeleteOne)
route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang) route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang)
route.Post("/approvals", ctrl.Approval) route.Post("/approvals", ctrl.Approval)
route.Get("/flocks/:flock_id/periods", ctrl.GetFlockPeriodSummary) route.Get("/kandangs/:project_flock_kandang_id/periods", ctrl.GetFlockPeriodSummary)
} }
@@ -52,8 +52,8 @@ type projectflockService struct {
} }
type FlockPeriodSummary struct { type FlockPeriodSummary struct {
Flock entity.Flock Flock entity.Flock
NextPeriod int NextPeriod int
} }
func NewProjectflockService( func NewProjectflockService(
@@ -719,28 +719,57 @@ func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID u
return total, nil return total, nil
} }
func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error) { func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, projectFlockKandangID uint) (*FlockPeriodSummary, error) {
flock, err := s.FlockRepo.GetByID(c.Context(), flockID, func(db *gorm.DB) *gorm.DB { if projectFlockKandangID == 0 {
return db.Preload("CreatedUser") return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required")
}) }
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")
}
maxPeriod, err := s.Repository.GetMaxPeriodByBaseName(c.Context(), flock.Name) pivot, err := s.pivotRepo().GetByID(c.Context(), projectFlockKandangID)
if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to compute next period for flock %d: %+v", flockID, err) return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found")
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to compute next period") }
} 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{ var baseName string
Flock: *flock, var referenceFlock *entity.Flock
NextPeriod: maxPeriod + 1, if pivot.ProjectFlock.Id != 0 {
}, nil 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 { func uniqueUintSlice(values []uint) []uint {
@@ -14,21 +14,22 @@ import (
// === DTO Structs === // === DTO Structs ===
type RecordingBaseDTO struct { type RecordingBaseDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
ProjectFlockKandangId uint `json:"project_flock_kandang_id"` ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
RecordDatetime time.Time `json:"record_datetime"` RecordDatetime time.Time `json:"record_datetime"`
Day *int `json:"day,omitempty"` Day *int `json:"day,omitempty"`
ProjectFlockCategory *string `json:"project_flock_category,omitempty"` ProjectFlockCategory *string `json:"project_flock_category,omitempty"`
TotalDepletionQty *float64 `json:"total_depletion_qty,omitempty"` TotalDepletionQty *float64 `json:"total_depletion_qty,omitempty"`
CumDepletionRate *float64 `json:"cum_depletion_rate,omitempty"` CumDepletionRate *float64 `json:"cum_depletion_rate,omitempty"`
DailyGain *float64 `json:"daily_gain,omitempty"` DailyGain *float64 `json:"daily_gain,omitempty"`
AvgDailyGain *float64 `json:"avg_daily_gain,omitempty"` AvgDailyGain *float64 `json:"avg_daily_gain,omitempty"`
CumIntake *int `json:"cum_intake,omitempty"` CumIntake *int `json:"cum_intake,omitempty"`
FcrValue *float64 `json:"fcr_value,omitempty"` FcrValue *float64 `json:"fcr_value,omitempty"`
TotalChickQty *float64 `json:"total_chick_qty,omitempty"` TotalChickQty *float64 `json:"total_chick_qty,omitempty"`
Approval approvalDTO.ApprovalBaseDTO `json:"approval"` Approval approvalDTO.ApprovalBaseDTO `json:"approval"`
EggGradingStatus *string `json:"egg_grading_status,omitempty"` EggGradingStatus *string `json:"egg_grading_status,omitempty"`
EggGradingPendingQty *int `json:"egg_grading_pending_qty,omitempty"` EggGradingPendingQty *int `json:"egg_grading_pending_qty,omitempty"`
EggGradingCompletedQty *int `json:"egg_grading_completed_qty,omitempty"`
} }
type RecordingListDTO struct { type RecordingListDTO struct {
@@ -102,24 +103,25 @@ func ToRecordingBaseDTO(e entity.Recording) RecordingBaseDTO {
latestApproval = snapshot latestApproval = snapshot
} }
gradingStatus, gradingPending := computeEggGradingStatus(e) gradingStatus, gradingPending, gradingCompleted := computeEggGradingStatus(e)
return RecordingBaseDTO{ return RecordingBaseDTO{
Id: e.Id, Id: e.Id,
ProjectFlockKandangId: e.ProjectFlockKandangId, ProjectFlockKandangId: e.ProjectFlockKandangId,
RecordDatetime: e.RecordDatetime, RecordDatetime: e.RecordDatetime,
Day: e.Day, Day: e.Day,
ProjectFlockCategory: projectFlockCategory, ProjectFlockCategory: projectFlockCategory,
TotalDepletionQty: e.TotalDepletionQty, TotalDepletionQty: e.TotalDepletionQty,
CumDepletionRate: e.CumDepletionRate, CumDepletionRate: e.CumDepletionRate,
DailyGain: e.DailyGain, DailyGain: e.DailyGain,
AvgDailyGain: e.AvgDailyGain, AvgDailyGain: e.AvgDailyGain,
CumIntake: e.CumIntake, CumIntake: e.CumIntake,
FcrValue: e.FcrValue, FcrValue: e.FcrValue,
TotalChickQty: e.TotalChickQty, TotalChickQty: e.TotalChickQty,
Approval: latestApproval, Approval: latestApproval,
EggGradingStatus: gradingStatus, EggGradingStatus: gradingStatus,
EggGradingPendingQty: gradingPending, EggGradingPendingQty: gradingPending,
EggGradingCompletedQty: gradingCompleted,
} }
} }
@@ -243,14 +245,17 @@ func toRecordingProductWarehouseDTO(pw *entity.ProductWarehouse) *RecordingProdu
return &dto return &dto
} }
func computeEggGradingStatus(e entity.Recording) (*string, *int) { const goodEggProductWarehouseID uint = 5
if len(e.Eggs) == 0 {
return nil, nil func computeEggGradingStatus(e entity.Recording) (*string, *int, *int) {
goodEggs := filterGoodEggs(e.Eggs)
if len(goodEggs) == 0 {
return nil, nil, nil
} }
totalEggs := 0 totalEggs := 0
totalGraded := 0.0 totalGraded := 0.0
for _, egg := range e.Eggs { for _, egg := range goodEggs {
totalEggs += egg.Qty totalEggs += egg.Qty
for _, grading := range egg.GradingEggs { for _, grading := range egg.GradingEggs {
totalGraded += grading.Qty totalGraded += grading.Qty
@@ -258,20 +263,41 @@ func computeEggGradingStatus(e entity.Recording) (*string, *int) {
} }
if totalEggs == 0 { 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" status := "GRADING_TELUR"
pendingInt := int(math.Round(pending)) return &status, &pendingInt, &completedInt
return &status, &pendingInt
} }
status := "GRADING_SELESAI" status := "GRADING_SELESAI"
zero := 0 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 { func defaultRecordingLatestApproval(e entity.Recording) approvalDTO.ApprovalBaseDTO {