mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
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:
@@ -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))
|
||||||
|
|||||||
+18
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user