mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-24 07:15:43 +00:00
feat/BE/US-76/TASK-122,133,121,120 Recording add create delete edit
This commit is contained in:
+22
@@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
ALTER TABLE kandangs
|
||||||
|
DROP CONSTRAINT IF EXISTS kandangs_project_flock_id_fkey;
|
||||||
|
|
||||||
|
ALTER TABLE kandangs
|
||||||
|
DROP COLUMN IF EXISTS project_flock_id;
|
||||||
|
|
||||||
|
ALTER TABLE project_chickins
|
||||||
|
DROP CONSTRAINT fk_project_flock_kandang_id,
|
||||||
|
ADD CONSTRAINT fk_project_flock_kandang_id
|
||||||
|
FOREIGN KEY (project_flock_kandang_id)
|
||||||
|
REFERENCES project_flock_kandangs(id)
|
||||||
|
ON UPDATE CASCADE
|
||||||
|
ON DELETE CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE project_flock_populations
|
||||||
|
DROP CONSTRAINT fk_project_flock_kandang_id,
|
||||||
|
ADD CONSTRAINT fk_project_flock_kandang_id
|
||||||
|
FOREIGN KEY (project_flock_kandang_id)
|
||||||
|
REFERENCES project_flock_kandangs(id)
|
||||||
|
ON UPDATE CASCADE
|
||||||
|
ON DELETE CASCADE;
|
||||||
+110
-213
@@ -51,12 +51,12 @@ func Run(db *gorm.DB) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
projectFlocks, err := seedProjectFlocks(tx, adminID, flocks, areas, fcrs, locations)
|
|
||||||
if err != nil {
|
if err := seedProjectFlocks(tx, adminID, flocks, areas, fcrs, locations); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
kandangs, err := seedKandangs(tx, adminID, locations, users, projectFlocks)
|
kandangs, err := seedKandangs(tx, adminID, locations, users)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -243,7 +243,11 @@ func seedFlocks(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, fcrs, locations map[string]uint) (map[string]uint, error) {
|
func seedProjectFlocks(
|
||||||
|
tx *gorm.DB,
|
||||||
|
createdBy uint,
|
||||||
|
flocks, areas, fcrs, locations map[string]uint,
|
||||||
|
) error {
|
||||||
seeds := []struct {
|
seeds := []struct {
|
||||||
Key string
|
Key string
|
||||||
Flock string
|
Flock string
|
||||||
@@ -273,29 +277,30 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, fcrs, locatio
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make(map[string]uint, len(seeds))
|
|
||||||
|
|
||||||
for _, seed := range seeds {
|
for _, seed := range seeds {
|
||||||
flockID, ok := flocks[seed.Flock]
|
flockID, ok := flocks[seed.Flock]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("floc %s not seeded", seed.Flock)
|
return fmt.Errorf("floc %s not seeded", seed.Flock)
|
||||||
}
|
}
|
||||||
areaID, ok := areas[seed.Area]
|
areaID, ok := areas[seed.Area]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("area %s not seeded", seed.Area)
|
return fmt.Errorf("area %s not seeded", seed.Area)
|
||||||
}
|
}
|
||||||
fcrID, ok := fcrs[seed.Fcr]
|
fcrID, ok := fcrs[seed.Fcr]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("fcr %s not seeded", seed.Fcr)
|
return fmt.Errorf("fcr %s not seeded", seed.Fcr)
|
||||||
}
|
}
|
||||||
locationID, ok := locations[seed.Location]
|
locationID, ok := locations[seed.Location]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("location %s not seeded", seed.Location)
|
return fmt.Errorf("location %s not seeded", seed.Location)
|
||||||
}
|
}
|
||||||
|
|
||||||
var projectFlock entity.ProjectFlock
|
var projectFlock entity.ProjectFlock
|
||||||
err := tx.Where("flock_id = ? AND area_id = ? AND category = ? AND fcr_id = ? AND location_id = ? AND period = ?",
|
err := tx.Where(
|
||||||
flockID, areaID, seed.Category, fcrID, locationID, seed.Period).First(&projectFlock).Error
|
"flock_id = ? AND area_id = ? AND category = ? AND fcr_id = ? AND location_id = ? AND period = ?",
|
||||||
|
flockID, areaID, seed.Category, fcrID, locationID, seed.Period,
|
||||||
|
).First(&projectFlock).Error
|
||||||
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
projectFlock = entity.ProjectFlock{
|
projectFlock = entity.ProjectFlock{
|
||||||
FlockId: flockID,
|
FlockId: flockID,
|
||||||
@@ -307,10 +312,10 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, fcrs, locatio
|
|||||||
CreatedBy: createdBy,
|
CreatedBy: createdBy,
|
||||||
}
|
}
|
||||||
if err := tx.Create(&projectFlock).Error; err != nil {
|
if err := tx.Create(&projectFlock).Error; err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
} else {
|
} else {
|
||||||
if err := tx.Model(&entity.ProjectFlock{}).Where("id = ?", projectFlock.Id).Updates(map[string]any{
|
if err := tx.Model(&entity.ProjectFlock{}).Where("id = ?", projectFlock.Id).Updates(map[string]any{
|
||||||
"flock_id": flockID,
|
"flock_id": flockID,
|
||||||
@@ -320,17 +325,16 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, fcrs, locatio
|
|||||||
"location_id": locationID,
|
"location_id": locationID,
|
||||||
"period": seed.Period,
|
"period": seed.Period,
|
||||||
}).Error; err != nil {
|
}).Error; err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ensureProjectFlockApprovals(tx, projectFlock.Id, createdBy); err != nil {
|
if err := ensureProjectFlockApprovals(tx, projectFlock.Id, createdBy); err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
result[seed.Key] = projectFlock.Id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureProjectFlockApprovals(tx *gorm.DB, projectFlockID uint, actorID uint) error {
|
func ensureProjectFlockApprovals(tx *gorm.DB, projectFlockID uint, actorID uint) error {
|
||||||
@@ -385,17 +389,16 @@ func ensureProjectFlockApprovals(tx *gorm.DB, projectFlockID uint, actorID uint)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users map[string]uint, projectFlocks map[string]uint) (map[string]uint, error) {
|
func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users map[string]uint) (map[string]uint, error) {
|
||||||
seeds := []struct {
|
seeds := []struct {
|
||||||
Name string
|
Name string
|
||||||
Status utils.KandangStatus
|
Status utils.KandangStatus
|
||||||
Location string
|
Location string
|
||||||
PicKey string
|
PicKey string
|
||||||
ProjectFlockKey *string
|
|
||||||
}{
|
}{
|
||||||
{Name: "Singaparna 1", Status: utils.KandangStatusActive, Location: "Singaparna", PicKey: "admin", ProjectFlockKey: strPtr("Singaparna Period 1")},
|
{Name: "Singaparna 1", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"},
|
||||||
{Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"},
|
{Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"},
|
||||||
{Name: "Cikaum 1", Status: utils.KandangStatusActive, Location: "Cikaum", PicKey: "admin", ProjectFlockKey: strPtr("Cikaum Period 1")},
|
{Name: "Cikaum 1", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"},
|
||||||
{Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"},
|
{Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -411,14 +414,6 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
|
|||||||
return nil, fmt.Errorf("user %s not seeded", seed.PicKey)
|
return nil, fmt.Errorf("user %s not seeded", seed.PicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
var projectFlockID *uint
|
|
||||||
if seed.ProjectFlockKey != nil {
|
|
||||||
pfID, ok := projectFlocks[*seed.ProjectFlockKey]
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("project flock %s not seeded", *seed.ProjectFlockKey)
|
|
||||||
}
|
|
||||||
projectFlockID = uintPtr(pfID)
|
|
||||||
}
|
|
||||||
|
|
||||||
var kandang entity.Kandang
|
var kandang entity.Kandang
|
||||||
err := tx.Where("name = ?", seed.Name).First(&kandang).Error
|
err := tx.Where("name = ?", seed.Name).First(&kandang).Error
|
||||||
@@ -428,15 +423,11 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
|
|||||||
Status: string(seed.Status),
|
Status: string(seed.Status),
|
||||||
LocationId: locID,
|
LocationId: locID,
|
||||||
PicId: picID,
|
PicId: picID,
|
||||||
ProjectFlockId: projectFlockID,
|
|
||||||
CreatedBy: createdBy,
|
CreatedBy: createdBy,
|
||||||
}
|
}
|
||||||
if err := tx.Create(&kandang).Error; err != nil {
|
if err := tx.Create(&kandang).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := syncPivotRelation(tx, projectFlockID, kandang.Id); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
@@ -445,17 +436,9 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
|
|||||||
"pic_id": picID,
|
"pic_id": picID,
|
||||||
"status": string(seed.Status),
|
"status": string(seed.Status),
|
||||||
}
|
}
|
||||||
if projectFlockID != nil {
|
|
||||||
updates["project_flock_id"] = *projectFlockID
|
|
||||||
} else {
|
|
||||||
updates["project_flock_id"] = nil
|
|
||||||
}
|
|
||||||
if err := tx.Model(&entity.Kandang{}).Where("id = ?", kandang.Id).Updates(updates).Error; err != nil {
|
if err := tx.Model(&entity.Kandang{}).Where("id = ?", kandang.Id).Updates(updates).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := syncPivotRelation(tx, projectFlockID, kandang.Id); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
result[seed.Name] = kandang.Id
|
result[seed.Name] = kandang.Id
|
||||||
}
|
}
|
||||||
@@ -463,37 +446,6 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncPivotRelation(tx *gorm.DB, projectFlockID *uint, kandangID uint) error {
|
|
||||||
if err := detachActivePivot(tx, kandangID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if projectFlockID == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return ensureActivePivot(tx, *projectFlockID, kandangID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func detachActivePivot(tx *gorm.DB, kandangID uint) error {
|
|
||||||
return tx.Where("kandang_id = ?", kandangID).
|
|
||||||
Delete(&entity.ProjectFlockKandang{}).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensureActivePivot(tx *gorm.DB, projectFlockID, kandangID uint) error {
|
|
||||||
var pivot entity.ProjectFlockKandang
|
|
||||||
err := tx.Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID).
|
|
||||||
First(&pivot).Error
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
newRecord := entity.ProjectFlockKandang{
|
|
||||||
ProjectFlockId: projectFlockID,
|
|
||||||
KandangId: kandangID,
|
|
||||||
}
|
|
||||||
return tx.Create(&newRecord).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func seedWarehouses(tx *gorm.DB, createdBy uint, areas map[string]uint, locations map[string]uint, kandangs map[string]uint) error {
|
func seedWarehouses(tx *gorm.DB, createdBy uint, areas map[string]uint, locations map[string]uint, kandangs map[string]uint) error {
|
||||||
seeds := []struct {
|
seeds := []struct {
|
||||||
@@ -1133,153 +1085,71 @@ func seedTransferStock(tx *gorm.DB, createdBy uint) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func seedChickin(tx *gorm.DB, createdBy uint) error {
|
func seedChickin(tx *gorm.DB, createdBy uint) error {
|
||||||
seeds := []struct {
|
// gunakan identitas yang stabil, bukan ID pivot
|
||||||
ProjectFlockKandangId uint
|
seeds := []struct {
|
||||||
ChickInDate string
|
KandangName string
|
||||||
Quantity float64
|
LocationName string
|
||||||
Note string
|
Period int
|
||||||
}{
|
ChickInDate string
|
||||||
{ProjectFlockKandangId: 1, ChickInDate: "2025-10-20", Quantity: 100, Note: "Seeder chickin 1"},
|
Quantity float64
|
||||||
{ProjectFlockKandangId: 2, ChickInDate: "2025-10-21", Quantity: 200, Note: "Seeder chickin 2"},
|
Note string
|
||||||
}
|
}{
|
||||||
|
{"Singaparna 1", "Singaparna", 1, "2025-10-20", 100, "Seeder chickin 1"},
|
||||||
|
{"Cikaum 1", "Cikaum", 1, "2025-10-21", 200, "Seeder chickin 2"},
|
||||||
|
}
|
||||||
|
|
||||||
for _, seed := range seeds {
|
for _, s := range seeds {
|
||||||
chickinDate, err := time.Parse("2006-01-02", seed.ChickInDate)
|
pfkID, err := ensurePFK(tx, s.KandangName, s.LocationName, s.Period)
|
||||||
if err != nil {
|
if err != nil { return err }
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert ProjectChickin jika belum ada
|
date, err := time.Parse("2006-01-02", s.ChickInDate)
|
||||||
var chickin entity.ProjectChickin
|
if err != nil { return err }
|
||||||
err = tx.Where("project_flock_kandang_id = ? AND chick_in_date = ?", seed.ProjectFlockKandangId, chickinDate).
|
|
||||||
First(&chickin).Error
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
chickin = entity.ProjectChickin{
|
|
||||||
ProjectFlockKandangId: seed.ProjectFlockKandangId,
|
|
||||||
ChickInDate: chickinDate,
|
|
||||||
Quantity: seed.Quantity,
|
|
||||||
Note: seed.Note,
|
|
||||||
CreatedBy: createdBy,
|
|
||||||
}
|
|
||||||
if err := tx.Create(&chickin).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var population entity.ProjectFlockPopulation
|
// upsert project_chickin (idempotent)
|
||||||
err = tx.Where("project_flock_kandang_id = ?", seed.ProjectFlockKandangId).First(&population).Error
|
var chickin entity.ProjectChickin
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
err = tx.Where("project_flock_kandang_id = ? AND chick_in_date = ?", pfkID, date).First(&chickin).Error
|
||||||
population = entity.ProjectFlockPopulation{
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
ProjectFlockKandangId: seed.ProjectFlockKandangId,
|
chickin = entity.ProjectChickin{
|
||||||
InitialQuantity: seed.Quantity,
|
ProjectFlockKandangId: pfkID,
|
||||||
CurrentQuantity: seed.Quantity,
|
ChickInDate: date,
|
||||||
ReservedQuantity: 0,
|
Quantity: s.Quantity,
|
||||||
CreatedBy: createdBy,
|
Note: s.Note,
|
||||||
}
|
CreatedBy: createdBy,
|
||||||
if err := tx.Create(&population).Error; err != nil {
|
}
|
||||||
return err
|
if err := tx.Create(&chickin).Error; err != nil { return err }
|
||||||
}
|
} else if err != nil {
|
||||||
} else if err != nil {
|
return err
|
||||||
return err
|
}
|
||||||
} else {
|
|
||||||
// Update population quantities
|
|
||||||
if err := tx.Model(&entity.ProjectFlockPopulation{}).
|
|
||||||
Where("id = ?", population.Id).
|
|
||||||
Updates(map[string]any{
|
|
||||||
"initial_quantity": population.InitialQuantity + seed.Quantity,
|
|
||||||
"current_quantity": population.CurrentQuantity + seed.Quantity,
|
|
||||||
"reserved_quantity": 0,
|
|
||||||
}).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var pfk entity.ProjectFlockKandang
|
// upsert population
|
||||||
if err := tx.Where("id = ?", seed.ProjectFlockKandangId).First(&pfk).Error; err != nil {
|
var pop entity.ProjectFlockPopulation
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
err = tx.Where("project_flock_kandang_id = ?", pfkID).First(&pop).Error
|
||||||
// no pivot found; skip creating details
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
continue
|
pop = entity.ProjectFlockPopulation{
|
||||||
}
|
ProjectFlockKandangId: pfkID,
|
||||||
return err
|
InitialQuantity: s.Quantity,
|
||||||
}
|
CurrentQuantity: s.Quantity,
|
||||||
|
ReservedQuantity: 0,
|
||||||
var warehouse entity.Warehouse
|
CreatedBy: createdBy,
|
||||||
if err := tx.Where("kandang_id = ?", pfk.KandangId).First(&warehouse).Error; err != nil {
|
}
|
||||||
// if warehouse not found, cannot create details
|
if err := tx.Create(&pop).Error; err != nil { return err }
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
} else if err != nil {
|
||||||
continue
|
return err
|
||||||
}
|
} else {
|
||||||
return err
|
if err := tx.Model(&entity.ProjectFlockPopulation{}).
|
||||||
}
|
Where("id = ?", pop.Id).
|
||||||
|
Updates(map[string]any{
|
||||||
var productWarehouses []entity.ProductWarehouse
|
"initial_quantity": pop.InitialQuantity + s.Quantity,
|
||||||
err = tx.Table("product_warehouses").
|
"current_quantity": pop.CurrentQuantity + s.Quantity,
|
||||||
Select("product_warehouses.*").
|
"reserved_quantity": 0,
|
||||||
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
}).Error; err != nil { return err }
|
||||||
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
|
}
|
||||||
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", "DOC", warehouse.Id).
|
}
|
||||||
Order("product_warehouses.created_at DESC").
|
return nil
|
||||||
Find(&productWarehouses).Error
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no product warehouses found, keep existing chickin.Quantity and skip details
|
|
||||||
if len(productWarehouses) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// sum all pw quantities and set chickin.Quantity to that total (mimic CreateOne)
|
|
||||||
totalQty := 0.0
|
|
||||||
for _, pw := range productWarehouses {
|
|
||||||
totalQty += pw.Quantity
|
|
||||||
}
|
|
||||||
|
|
||||||
if chickin.Quantity != totalQty {
|
|
||||||
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Update("quantity", totalQty).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
chickin.Quantity = totalQty
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pw := range productWarehouses {
|
|
||||||
// ensure detail exists or create it with full pw.Quantity
|
|
||||||
var detail entity.ProjectChickinDetail
|
|
||||||
err = tx.Where("project_chickin_id = ? AND product_warehouse_id = ?", chickin.Id, pw.Id).First(&detail).Error
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
detail = entity.ProjectChickinDetail{
|
|
||||||
ProjectChickinId: chickin.Id,
|
|
||||||
ProductWarehouseId: pw.Id,
|
|
||||||
Quantity: pw.Quantity,
|
|
||||||
CreatedBy: createdBy,
|
|
||||||
}
|
|
||||||
if err := tx.Create(&detail).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
if detail.Quantity != pw.Quantity {
|
|
||||||
if err := tx.Model(&entity.ProjectChickinDetail{}).Where("id = ?", detail.Id).Update("quantity", pw.Quantity).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// zero out pw quantity
|
|
||||||
if err := tx.Model(&entity.ProductWarehouse{}).Where("id = ?", pw.Id).Update("quantity", 0).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func ptr[T any](v T) *T {
|
func ptr[T any](v T) *T {
|
||||||
return &v
|
return &v
|
||||||
}
|
}
|
||||||
@@ -1295,3 +1165,30 @@ func intPtr(v int) *int {
|
|||||||
func uintPtr(v uint) *uint {
|
func uintPtr(v uint) *uint {
|
||||||
return &v
|
return &v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ensurePFK(tx *gorm.DB, kandangName, locationName string, period int) (uint, error) {
|
||||||
|
var kandang entity.Kandang
|
||||||
|
if err := tx.Where("name = ?", kandangName).First(&kandang).Error; err != nil {
|
||||||
|
return 0, fmt.Errorf("kandang %q not found: %w", kandangName, err)
|
||||||
|
}
|
||||||
|
var loc entity.Location
|
||||||
|
if err := tx.Where("name = ?", locationName).First(&loc).Error; err != nil {
|
||||||
|
return 0, fmt.Errorf("location %q not found: %w", locationName, err)
|
||||||
|
}
|
||||||
|
var pf entity.ProjectFlock
|
||||||
|
if err := tx.Where("location_id = ? AND period = ?", loc.Id, period).First(&pf).Error; err != nil {
|
||||||
|
return 0, fmt.Errorf("project_flock for %s period %d not found: %w", locationName, period, err)
|
||||||
|
}
|
||||||
|
var pfk entity.ProjectFlockKandang
|
||||||
|
if err := tx.Where("project_flock_id = ? AND kandang_id = ?", pf.Id, kandang.Id).First(&pfk).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
pfk = entity.ProjectFlockKandang{ ProjectFlockId: pf.Id, KandangId: kandang.Id }
|
||||||
|
if err := tx.Create(&pfk).Error; err != nil {
|
||||||
|
return 0, fmt.Errorf("create pivot pfk(%d,%d) failed: %w", pf.Id, kandang.Id, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pfk.Id, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,18 +7,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Kandang struct {
|
type Kandang struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
|
Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
|
||||||
Status string `gorm:"type:varchar(50);not null"`
|
Status string `gorm:"type:varchar(50);not null"`
|
||||||
LocationId uint `gorm:"not null"`
|
LocationId uint `gorm:"not null"`
|
||||||
PicId uint `gorm:"not null"`
|
PicId uint `gorm:"not null"`
|
||||||
ProjectFlockId *uint `gorm:"column:project_flock_id"`
|
CreatedBy uint `gorm:"not null"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
||||||
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
Pic User `gorm:"foreignKey:PicId;references:Id"`
|
||||||
Pic User `gorm:"foreignKey:PicId;references:Id"`
|
ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"`
|
||||||
ProjectFlock *ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const ()
|
|||||||
|
|
||||||
type ProjectChickin struct {
|
type ProjectChickin struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
ProjectFlockKandangId uint `gorm:"not null"`
|
ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||||
ChickInDate time.Time `gorm:"not null"`
|
ChickInDate time.Time `gorm:"not null"`
|
||||||
Quantity float64 `gorm:"not null"`
|
Quantity float64 `gorm:"not null"`
|
||||||
Note string `gorm:"type:text"`
|
Note string `gorm:"type:text"`
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
|
|
||||||
type ProjectFlockPopulation struct {
|
type ProjectFlockPopulation struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
ProjectFlockKandangId uint `gorm:"not null"`
|
ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||||
InitialQuantity float64 `gorm:"type:numeric(15,3);not null"`
|
InitialQuantity float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
CurrentQuantity float64 `gorm:"type:numeric(15,3);not null"`
|
CurrentQuantity float64 `gorm:"type:numeric(15,3);not null"`
|
||||||
ReservedQuantity float64 `gorm:"type:numeric(15,3)"`
|
ReservedQuantity float64 `gorm:"type:numeric(15,3)"`
|
||||||
@@ -18,5 +18,6 @@ type ProjectFlockPopulation struct {
|
|||||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
|
||||||
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||||
|
|
||||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ type ProjectFlock struct {
|
|||||||
Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"`
|
Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"`
|
||||||
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
Kandangs []Kandang `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
Kandangs []Kandang `gorm:"many2many:project_flock_kandangs;joinTableForeignKey:project_flock_id;joinTableReferences:kandang_id" json:"kandangs,omitempty"`
|
||||||
KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id" json:"-"`
|
||||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
|
||||||
|
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ type ProjectFlockKandang struct {
|
|||||||
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"`
|
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"`
|
||||||
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
|
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
|
||||||
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
|
||||||
|
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
||||||
|
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
@@ -18,6 +19,8 @@ type KandangRepository interface {
|
|||||||
GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.Kandang, error)
|
GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.Kandang, error)
|
||||||
HasActiveKandangForProjectFlock(ctx context.Context, projectFlockID uint, excludeID *uint) (bool, error)
|
HasActiveKandangForProjectFlock(ctx context.Context, projectFlockID uint, excludeID *uint) (bool, error)
|
||||||
UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error
|
UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error
|
||||||
|
UpsertProjectFlockKandang(ctx context.Context, projectFlockID, kandangID uint) error
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type KandangRepositoryImpl struct {
|
type KandangRepositoryImpl struct {
|
||||||
@@ -58,14 +61,15 @@ func (r *KandangRepositoryImpl) ProjectFlockExists(ctx context.Context, projectF
|
|||||||
|
|
||||||
func (r *KandangRepositoryImpl) HasActiveKandangForProjectFlock(ctx context.Context, projectFlockID uint, excludeID *uint) (bool, error) {
|
func (r *KandangRepositoryImpl) HasActiveKandangForProjectFlock(ctx context.Context, projectFlockID uint, excludeID *uint) (bool, error) {
|
||||||
var count int64
|
var count int64
|
||||||
q := r.db.WithContext(ctx).
|
q := r.db.WithContext(ctx).
|
||||||
Model(&entity.Kandang{}).
|
Table("kandangs k").
|
||||||
Where("project_flock_id = ?", projectFlockID).
|
Joins("JOIN project_flock_kandangs pfk ON pfk.kandang_id = k.id").
|
||||||
Where("status = ?", utils.KandangStatusActive).
|
Where("pfk.project_flock_id = ?", projectFlockID).
|
||||||
Where("deleted_at IS NULL")
|
Where("k.status = ?", utils.KandangStatusActive).
|
||||||
if excludeID != nil {
|
Where("k.deleted_at IS NULL")
|
||||||
q = q.Where("id <> ?", *excludeID)
|
if excludeID != nil {
|
||||||
}
|
q = q.Where("k.id <> ?", *excludeID)
|
||||||
|
}
|
||||||
if err := q.Count(&count).Error; err != nil {
|
if err := q.Count(&count).Error; err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@@ -74,18 +78,49 @@ func (r *KandangRepositoryImpl) HasActiveKandangForProjectFlock(ctx context.Cont
|
|||||||
|
|
||||||
func (r *KandangRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.Kandang, error) {
|
func (r *KandangRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.Kandang, error) {
|
||||||
kandang := new(entity.Kandang)
|
kandang := new(entity.Kandang)
|
||||||
err := r.db.WithContext(ctx).
|
err := r.db.WithContext(ctx).
|
||||||
Where("project_flock_id = ?", projectFlockID).
|
Table("kandangs k").
|
||||||
First(kandang).Error
|
Joins("JOIN project_flock_kandangs pfk ON pfk.kandang_id = k.id").
|
||||||
if err != nil {
|
Where("pfk.project_flock_id = ?", projectFlockID).
|
||||||
return nil, err
|
Where("k.deleted_at IS NULL").
|
||||||
}
|
Order("k.id ASC").
|
||||||
|
Limit(1).
|
||||||
|
Find(kandang).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if kandang.Id == 0 {
|
||||||
|
return nil, gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
return kandang, nil
|
return kandang, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *KandangRepositoryImpl) UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error {
|
func (r *KandangRepositoryImpl) UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error {
|
||||||
return r.db.WithContext(ctx).
|
sub := r.db.WithContext(ctx).
|
||||||
Model(&entity.Kandang{}).
|
Table("project_flock_kandangs").
|
||||||
Where("project_flock_id = ?", projectFlockID).
|
Select("kandang_id").
|
||||||
Update("status", string(status)).Error
|
Where("project_flock_id = ?", projectFlockID)
|
||||||
|
|
||||||
|
return r.db.WithContext(ctx).
|
||||||
|
Model(&entity.Kandang{}).
|
||||||
|
Where("id IN (?)", sub).
|
||||||
|
Where("deleted_at IS NULL").
|
||||||
|
Update("status", string(status)).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *KandangRepositoryImpl) UpsertProjectFlockKandang(ctx context.Context, projectFlockID, kandangID uint) error {
|
||||||
|
var link entity.ProjectFlockKandang
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID).
|
||||||
|
First(&link).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
link = entity.ProjectFlockKandang{
|
||||||
|
ProjectFlockId: projectFlockID,
|
||||||
|
KandangId: kandangID,
|
||||||
|
}
|
||||||
|
return r.db.WithContext(ctx).Create(&link).Error
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ func NewKandangService(repo repository.KandangRepository, validate *validator.Va
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s kandangService) withRelations(db *gorm.DB) *gorm.DB {
|
func (s kandangService) withRelations(db *gorm.DB) *gorm.DB {
|
||||||
return db.Preload("CreatedUser").Preload("Location").Preload("Pic")
|
return db.Preload("CreatedUser").Preload("Location").Preload("Pic").Preload("ProjectFlockKandangs.ProjectFlock")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Kandang, int64, error) {
|
func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Kandang, int64, error) {
|
||||||
@@ -110,7 +111,6 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
|||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid kandang status")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid kandang status")
|
||||||
}
|
}
|
||||||
|
|
||||||
var projectFlockID *uint
|
|
||||||
if req.ProjectFlockId != nil {
|
if req.ProjectFlockId != nil {
|
||||||
if exists, err := s.Repository.ProjectFlockExists(c.Context(), *req.ProjectFlockId); err != nil {
|
if exists, err := s.Repository.ProjectFlockExists(c.Context(), *req.ProjectFlockId); err != nil {
|
||||||
s.Log.Errorf("Failed to check project flock existence: %+v", err)
|
s.Log.Errorf("Failed to check project flock existence: %+v", err)
|
||||||
@@ -128,8 +128,6 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
idCopy := *req.ProjectFlockId
|
|
||||||
projectFlockID = &idCopy
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: created by dummy
|
//TODO: created by dummy
|
||||||
@@ -138,7 +136,6 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
|||||||
LocationId: req.LocationId,
|
LocationId: req.LocationId,
|
||||||
Status: status,
|
Status: status,
|
||||||
PicId: req.PicId,
|
PicId: req.PicId,
|
||||||
ProjectFlockId: projectFlockID,
|
|
||||||
CreatedBy: 1,
|
CreatedBy: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,6 +144,12 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.ProjectFlockId != nil {
|
||||||
|
if err := s.Repository.UpsertProjectFlockKandang(c.Context(), *req.ProjectFlockId, createBody.Id); err != nil {
|
||||||
|
s.Log.Errorf("Failed to link kandang to project_flock via pivot: %+v", err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to link kandang to project flock")
|
||||||
|
}
|
||||||
|
}
|
||||||
return s.GetOne(c, createBody.Id)
|
return s.GetOne(c, createBody.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +204,6 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
finalStatus = status
|
finalStatus = status
|
||||||
}
|
}
|
||||||
|
|
||||||
projectFlockIDToUse := existing.ProjectFlockId
|
|
||||||
if req.ProjectFlockId != nil {
|
if req.ProjectFlockId != nil {
|
||||||
if exists, err := s.Repository.ProjectFlockExists(c.Context(), *req.ProjectFlockId); err != nil {
|
if exists, err := s.Repository.ProjectFlockExists(c.Context(), *req.ProjectFlockId); err != nil {
|
||||||
s.Log.Errorf("Failed to check project flock existence: %+v", err)
|
s.Log.Errorf("Failed to check project flock existence: %+v", err)
|
||||||
@@ -209,30 +211,33 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
} else if !exists {
|
} else if !exists {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Project flock with id %d not found", *req.ProjectFlockId))
|
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Project flock with id %d not found", *req.ProjectFlockId))
|
||||||
}
|
}
|
||||||
idCopy := *req.ProjectFlockId
|
|
||||||
projectFlockIDToUse = &idCopy
|
|
||||||
updateBody["project_flock_id"] = idCopy
|
|
||||||
}
|
|
||||||
|
|
||||||
if projectFlockIDToUse != nil && finalStatus == string(utils.KandangStatusActive) {
|
// Kalau status jadi ACTIVE, pastikan tidak ada kandang aktif lain pada project flock tsb (hitung via pivot)
|
||||||
if active, err := s.Repository.HasActiveKandangForProjectFlock(c.Context(), *projectFlockIDToUse, &id); err != nil {
|
if finalStatus == string(utils.KandangStatusActive) {
|
||||||
s.Log.Errorf("Failed to check kandang activity for project flock %d: %+v", *projectFlockIDToUse, err)
|
if active, err := s.Repository.HasActiveKandangForProjectFlock(c.Context(), *req.ProjectFlockId, &id); err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check active kandang for project flock")
|
s.Log.Errorf("Failed to check kandang activity for project flock %d: %+v", *req.ProjectFlockId, err)
|
||||||
} else if active {
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check active kandang for project flock")
|
||||||
return nil, fiber.NewError(fiber.StatusConflict, "Project flock already has an active kandang")
|
} else if active {
|
||||||
|
return nil, fiber.NewError(fiber.StatusConflict, "Project flock already has an active kandang")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(updateBody) == 0 {
|
if len(updateBody) > 0 {
|
||||||
return s.GetOne(c, id)
|
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Kandang not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to update kandang: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
if req.ProjectFlockId != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if err := s.Repository.UpsertProjectFlockKandang(c.Context(), *req.ProjectFlockId, id); err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Kandang not found")
|
s.Log.Errorf("Failed to upsert pivot kandang-project_flock: %+v", err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to link kandang to project flock")
|
||||||
}
|
}
|
||||||
s.Log.Errorf("Failed to update kandang: %+v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.GetOne(c, id)
|
return s.GetOne(c, id)
|
||||||
|
|||||||
@@ -28,4 +28,5 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
|
|||||||
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("/flocks/:flock_id/periods", ctrl.GetFlockPeriodSummary)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,9 +107,14 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
|
|||||||
db = db.Where("project_flocks.period = ?", params.Period)
|
db = db.Where("project_flocks.period = ?", params.Period)
|
||||||
}
|
}
|
||||||
if len(params.KandangIds) > 0 {
|
if len(params.KandangIds) > 0 {
|
||||||
db = db.Where("EXISTS (SELECT 1 FROM kandangs WHERE kandangs.project_flock_id = project_flocks.id AND kandangs.id IN ?)", params.KandangIds)
|
db = db.Where(`
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM project_flock_kandangs pfk
|
||||||
|
WHERE pfk.project_flock_id = project_flocks.id
|
||||||
|
AND pfk.kandang_id IN ?
|
||||||
|
)`, params.KandangIds)
|
||||||
}
|
}
|
||||||
|
|
||||||
if params.Search != "" {
|
if params.Search != "" {
|
||||||
normalizedSearch := strings.ToLower(strings.TrimSpace(params.Search))
|
normalizedSearch := strings.ToLower(strings.TrimSpace(params.Search))
|
||||||
if normalizedSearch == "" {
|
if normalizedSearch == "" {
|
||||||
@@ -250,10 +255,11 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
|||||||
if len(kandangs) != len(kandangIDs) {
|
if len(kandangs) != len(kandangIDs) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
|
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
|
||||||
}
|
}
|
||||||
for _, kandang := range kandangs {
|
// larang kalau ada yg sudah terikat ke project lain
|
||||||
if kandang.ProjectFlockId != nil {
|
if linked, err := s.anyKandangLinkedToOtherProject(c.Context(), s.Repository.DB(), kandangIDs, nil); err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Kandang %s sudah memiliki project flock", kandang.Name))
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage")
|
||||||
}
|
} else if linked {
|
||||||
|
return nil, fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terikat dengan project flock lain")
|
||||||
}
|
}
|
||||||
|
|
||||||
createBody := &entity.ProjectFlock{
|
createBody := &entity.ProjectFlock{
|
||||||
@@ -394,11 +400,12 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
|
|||||||
if len(kandangs) != len(newKandangIDs) {
|
if len(kandangs) != len(newKandangIDs) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
|
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
|
||||||
}
|
}
|
||||||
for _, k := range kandangs {
|
if linked, err := s.anyKandangLinkedToOtherProject(c.Context(), s.Repository.DB(), newKandangIDs, &id); err != nil {
|
||||||
if k.ProjectFlockId != nil && *k.ProjectFlockId != id {
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage")
|
||||||
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Kandang %s sudah terikat dengan project flock lain", k.Name))
|
} else if linked {
|
||||||
}
|
return nil, fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terikat dengan project flock lain")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hasChanges := hasBodyChanges || hasKandangChanges
|
hasChanges := hasBodyChanges || hasKandangChanges
|
||||||
@@ -754,7 +761,7 @@ func (s projectflockService) buildOrderExpressions(sortBy, sortOrder string) []s
|
|||||||
}
|
}
|
||||||
case "kandangs":
|
case "kandangs":
|
||||||
return []string{
|
return []string{
|
||||||
fmt.Sprintf("(SELECT COUNT(*) FROM kandangs WHERE kandangs.project_flock_id = project_flocks.id) %s", direction),
|
fmt.Sprintf("(SELECT COUNT(*) FROM project_flock_kandangs pfk WHERE pfk.project_flock_id = project_flocks.id) %s", direction),
|
||||||
fmt.Sprintf("project_flocks.id %s", direction),
|
fmt.Sprintf("project_flocks.id %s", direction),
|
||||||
}
|
}
|
||||||
case "period":
|
case "period":
|
||||||
@@ -775,24 +782,50 @@ func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := dbTransaction.Model(&entity.Kandang{}).
|
if err := dbTransaction.
|
||||||
|
Model(&entity.Kandang{}).
|
||||||
Where("id IN ?", kandangIDs).
|
Where("id IN ?", kandangIDs).
|
||||||
Updates(map[string]any{
|
Updates(map[string]any{
|
||||||
"project_flock_id": projectFlockID,
|
"status": string(utils.KandangStatusPengajuan),
|
||||||
"status": string(utils.KandangStatusPengajuan),
|
|
||||||
}).Error; err != nil {
|
}).Error; err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status")
|
||||||
}
|
}
|
||||||
|
|
||||||
pivotRepo := s.pivotRepoWithTx(dbTransaction)
|
var already []uint
|
||||||
records := make([]*entity.ProjectFlockKandang, len(kandangIDs))
|
if err := dbTransaction.
|
||||||
for i, id := range kandangIDs {
|
Table("project_flock_kandangs").
|
||||||
records[i] = &entity.ProjectFlockKandang{
|
Where("project_flock_id = ? AND kandang_id IN ?", projectFlockID, kandangIDs).
|
||||||
ProjectFlockId: projectFlockID,
|
Pluck("kandang_id", &already).Error; err != nil {
|
||||||
KandangId: id,
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing pivot")
|
||||||
|
}
|
||||||
|
exists := make(map[uint]struct{}, len(already))
|
||||||
|
for _, id := range already {
|
||||||
|
exists[id] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var toAttach []uint
|
||||||
|
seen := make(map[uint]struct{}, len(kandangIDs))
|
||||||
|
for _, id := range kandangIDs {
|
||||||
|
if _, ok := seen[id]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[id] = struct{}{}
|
||||||
|
if _, ok := exists[id]; !ok {
|
||||||
|
toAttach = append(toAttach, id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := pivotRepo.CreateMany(ctx, records); err != nil {
|
if len(toAttach) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
records := make([]*entity.ProjectFlockKandang, 0, len(toAttach))
|
||||||
|
for _, id := range toAttach {
|
||||||
|
records = append(records, &entity.ProjectFlockKandang{
|
||||||
|
ProjectFlockId: projectFlockID,
|
||||||
|
KandangId: id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err := s.pivotRepoWithTx(dbTransaction).CreateMany(ctx, records); err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -803,15 +836,15 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
updates := map[string]any{"project_flock_id": nil}
|
|
||||||
if resetStatus {
|
if resetStatus {
|
||||||
updates["status"] = string(utils.KandangStatusNonActive)
|
if err := dbTransaction.
|
||||||
}
|
Model(&entity.Kandang{}).
|
||||||
|
Where("id IN ?", kandangIDs).
|
||||||
if err := dbTransaction.Model(&entity.Kandang{}).
|
Updates(map[string]any{
|
||||||
Where("id IN ?", kandangIDs).
|
"status": string(utils.KandangStatusNonActive),
|
||||||
Updates(updates).Error; err != nil {
|
}).Error; err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.pivotRepoWithTx(dbTransaction).DeleteMany(ctx, projectFlockID, kandangIDs); err != nil {
|
if err := s.pivotRepoWithTx(dbTransaction).DeleteMany(ctx, projectFlockID, kandangIDs); err != nil {
|
||||||
@@ -820,9 +853,24 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (s projectflockService) pivotRepoWithTx(dbTransaction *gorm.DB) repository.ProjectFlockKandangRepository {
|
func (s projectflockService) pivotRepoWithTx(dbTransaction *gorm.DB) repository.ProjectFlockKandangRepository {
|
||||||
if s.PivotRepo == nil {
|
if s.PivotRepo == nil {
|
||||||
return repository.NewProjectFlockKandangRepository(dbTransaction)
|
return repository.NewProjectFlockKandangRepository(dbTransaction)
|
||||||
}
|
}
|
||||||
return s.PivotRepo.WithTx(dbTransaction)
|
return s.PivotRepo.WithTx(dbTransaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s projectflockService) anyKandangLinkedToOtherProject(ctx context.Context, db *gorm.DB, kandangIDs []uint, exceptProjectID *uint) (bool, error) {
|
||||||
|
q := db.WithContext(ctx).
|
||||||
|
Table("project_flock_kandangs").
|
||||||
|
Where("kandang_id IN ?", kandangIDs)
|
||||||
|
if exceptProjectID != nil {
|
||||||
|
q = q.Where("project_flock_id <> ?", *exceptProjectID)
|
||||||
|
}
|
||||||
|
var count int64
|
||||||
|
if err := q.Count(&count).Error; err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return count > 0, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,38 @@
|
|||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
"errors"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RecordingRepository interface {
|
type RecordingRepository interface {
|
||||||
repository.BaseRepository[entity.Recording]
|
repository.BaseRepository[entity.Recording]
|
||||||
|
|
||||||
|
WithRelations(db *gorm.DB) *gorm.DB
|
||||||
|
GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error)
|
||||||
|
|
||||||
|
CreateBodyWeights(tx *gorm.DB, bodyWeights []entity.RecordingBW) error
|
||||||
|
DeleteBodyWeights(tx *gorm.DB, recordingID uint) error
|
||||||
|
|
||||||
|
CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error
|
||||||
|
DeleteStocks(tx *gorm.DB, recordingID uint) error
|
||||||
|
|
||||||
|
CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error
|
||||||
|
DeleteDepletions(tx *gorm.DB, recordingID uint) error
|
||||||
|
|
||||||
|
SumRecordingDepletions(tx *gorm.DB, recordingID uint) (int64, error)
|
||||||
|
FindPreviousRecording(tx *gorm.DB, projectFlockKandangId uint, currentDay int) (*entity.Recording, error)
|
||||||
|
GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error)
|
||||||
|
GetAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error)
|
||||||
|
GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error)
|
||||||
|
GetFcrID(tx *gorm.DB, projectFlockKandangId uint) (uint, error)
|
||||||
|
GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecordingRepositoryImpl struct {
|
type RecordingRepositoryImpl struct {
|
||||||
@@ -19,3 +44,235 @@ func NewRecordingRepository(db *gorm.DB) RecordingRepository {
|
|||||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.Recording](db),
|
BaseRepositoryImpl: repository.NewBaseRepository[entity.Recording](db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.
|
||||||
|
Preload("CreatedUser").
|
||||||
|
Preload("ProjectFlockKandang").
|
||||||
|
Preload("ProjectFlockKandang.ProjectFlock").
|
||||||
|
Preload("BodyWeights").
|
||||||
|
Preload("Depletions").
|
||||||
|
Preload("Depletions.ProductWarehouse").
|
||||||
|
Preload("Depletions.ProductWarehouse.Product").
|
||||||
|
Preload("Depletions.ProductWarehouse.Warehouse").
|
||||||
|
Preload("Stocks").
|
||||||
|
Preload("Stocks.ProductWarehouse").
|
||||||
|
Preload("Stocks.ProductWarehouse.Product").
|
||||||
|
Preload("Stocks.ProductWarehouse.Warehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error) {
|
||||||
|
var days []int
|
||||||
|
if err := tx.Model(&entity.Recording{}).
|
||||||
|
Where("project_flock_id = ?", projectFlockKandangId).
|
||||||
|
Where("day IS NOT NULL").
|
||||||
|
Pluck("day", &days).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return nextRecordingDay(days), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) CreateBodyWeights(tx *gorm.DB, bodyWeights []entity.RecordingBW) error {
|
||||||
|
if len(bodyWeights) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return tx.Create(&bodyWeights).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) DeleteBodyWeights(tx *gorm.DB, recordingID uint) error {
|
||||||
|
return tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingBW{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error {
|
||||||
|
if len(stocks) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return tx.Create(&stocks).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) DeleteStocks(tx *gorm.DB, recordingID uint) error {
|
||||||
|
return tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingStock{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error {
|
||||||
|
if len(depletions) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return tx.Create(&depletions).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) DeleteDepletions(tx *gorm.DB, recordingID uint) error {
|
||||||
|
return tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingDepletion{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) SumRecordingDepletions(tx *gorm.DB, recordingID uint) (int64, error) {
|
||||||
|
var result int64
|
||||||
|
if err := tx.Model(&entity.RecordingDepletion{}).
|
||||||
|
Where("recording_id = ?", recordingID).
|
||||||
|
Select("COALESCE(SUM(total), 0)").
|
||||||
|
Scan(&result).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) FindPreviousRecording(tx *gorm.DB, projectFlockKandangId uint, currentDay int) (*entity.Recording, error) {
|
||||||
|
if currentDay <= 1 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var prev entity.Recording
|
||||||
|
err := tx.
|
||||||
|
Where("project_flock_id = ? AND day < ?", projectFlockKandangId, currentDay).
|
||||||
|
Where("day IS NOT NULL").
|
||||||
|
Order("day DESC").
|
||||||
|
Limit(1).
|
||||||
|
Find(&prev).Error
|
||||||
|
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) || prev.Id == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &prev, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error) {
|
||||||
|
var population entity.ProjectFlockPopulation
|
||||||
|
err := tx.
|
||||||
|
Where("project_flock_kandang_id = ?", projectFlockKandangId).
|
||||||
|
Order("created_at DESC").
|
||||||
|
First(&population).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return int64(math.Round(population.InitialQuantity)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) GetAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error) {
|
||||||
|
var result struct {
|
||||||
|
TotalWeight float64
|
||||||
|
TotalQty float64
|
||||||
|
}
|
||||||
|
if err := tx.Model(&entity.RecordingBW{}).
|
||||||
|
Select("COALESCE(SUM(weight * qty), 0) AS total_weight, COALESCE(SUM(qty), 0) AS total_qty").
|
||||||
|
Where("recording_id = ?", recordingID).
|
||||||
|
Scan(&result).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if result.TotalQty == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
return result.TotalWeight / result.TotalQty, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) {
|
||||||
|
var rows []struct {
|
||||||
|
UsageAmount float64
|
||||||
|
UomName string
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.
|
||||||
|
Table("recording_stocks").
|
||||||
|
Select("COALESCE(recording_stocks.usage_amount, 0) AS usage_amount, LOWER(uoms.name) AS uom_name").
|
||||||
|
Joins("JOIN product_warehouses ON product_warehouses.id = recording_stocks.product_warehouse_id").
|
||||||
|
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||||
|
Joins("JOIN uoms ON uoms.id = products.uom_id").
|
||||||
|
Where("recording_stocks.recording_id = ?", recordingID).
|
||||||
|
Scan(&rows).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var total float64
|
||||||
|
for _, row := range rows {
|
||||||
|
if row.UsageAmount <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch strings.TrimSpace(row.UomName) {
|
||||||
|
case "kilogram", "kg", "kilograms", "kilo":
|
||||||
|
total += row.UsageAmount * 1000
|
||||||
|
case "gram", "g", "grams":
|
||||||
|
total += row.UsageAmount
|
||||||
|
default:
|
||||||
|
total += row.UsageAmount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) GetFcrID(tx *gorm.DB, projectFlockKandangId uint) (uint, error) {
|
||||||
|
var result struct {
|
||||||
|
FcrID uint
|
||||||
|
}
|
||||||
|
if err := tx.Table("project_flock_kandangs").
|
||||||
|
Select("project_flocks.fcr_id AS fcr_id").
|
||||||
|
Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id").
|
||||||
|
Where("project_flock_kandangs.id = ?", projectFlockKandangId).
|
||||||
|
Scan(&result).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return result.FcrID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RecordingRepositoryImpl) GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error) {
|
||||||
|
if fcrId == 0 {
|
||||||
|
return 0, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var standard entity.FcrStandard
|
||||||
|
err := tx.
|
||||||
|
Where("fcr_id = ? AND weight >= ?", fcrId, currentWeightKg).
|
||||||
|
Order("weight ASC").
|
||||||
|
First(&standard).Error
|
||||||
|
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
err = tx.
|
||||||
|
Where("fcr_id = ?", fcrId).
|
||||||
|
Order("weight DESC").
|
||||||
|
First(&standard).Error
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return 0, false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return 0, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
weight := standard.Weight
|
||||||
|
if weight > 10 {
|
||||||
|
return weight / 1000, true, nil
|
||||||
|
}
|
||||||
|
return weight, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func nextRecordingDay(days []int) int {
|
||||||
|
if len(days) == 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
unique := make(map[int]struct{}, len(days))
|
||||||
|
for _, day := range days {
|
||||||
|
if day > 0 {
|
||||||
|
unique[day] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized := make([]int, 0, len(unique))
|
||||||
|
for day := range unique {
|
||||||
|
normalized = append(normalized, day)
|
||||||
|
}
|
||||||
|
sort.Ints(normalized)
|
||||||
|
|
||||||
|
for idx, day := range normalized {
|
||||||
|
expected := idx + 1
|
||||||
|
if day != expected {
|
||||||
|
return expected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(normalized) + 1
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
@@ -53,22 +51,6 @@ func NewRecordingService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s recordingService) withRelations(db *gorm.DB) *gorm.DB {
|
|
||||||
return db.
|
|
||||||
Preload("CreatedUser").
|
|
||||||
Preload("ProjectFlockKandang").
|
|
||||||
Preload("ProjectFlockKandang.ProjectFlock").
|
|
||||||
Preload("BodyWeights").
|
|
||||||
Preload("Depletions").
|
|
||||||
Preload("Depletions.ProductWarehouse").
|
|
||||||
Preload("Depletions.ProductWarehouse.Product").
|
|
||||||
Preload("Depletions.ProductWarehouse.Warehouse").
|
|
||||||
Preload("Stocks").
|
|
||||||
Preload("Stocks.ProductWarehouse").
|
|
||||||
Preload("Stocks.ProductWarehouse.Product").
|
|
||||||
Preload("Stocks.ProductWarehouse.Warehouse")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Recording, int64, error) {
|
func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Recording, int64, error) {
|
||||||
if err := s.Validate.Struct(params); err != nil {
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
@@ -85,7 +67,7 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
|||||||
offset := (page - 1) * limit
|
offset := (page - 1) * limit
|
||||||
|
|
||||||
recordings, total, err := s.Repository.GetAll(c.Context(), offset, limit, func(db *gorm.DB) *gorm.DB {
|
recordings, total, err := s.Repository.GetAll(c.Context(), offset, limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = s.withRelations(db)
|
db = s.Repository.WithRelations(db)
|
||||||
if params.ProjectFlockKandangId != 0 {
|
if params.ProjectFlockKandangId != 0 {
|
||||||
db = db.Where("project_flock_id = ?", params.ProjectFlockKandangId)
|
db = db.Where("project_flock_id = ?", params.ProjectFlockKandangId)
|
||||||
}
|
}
|
||||||
@@ -100,7 +82,9 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s recordingService) GetOne(c *fiber.Ctx, id uint) (*entity.Recording, error) {
|
func (s recordingService) GetOne(c *fiber.Ctx, id uint) (*entity.Recording, error) {
|
||||||
recording, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
recording, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return s.Repository.WithRelations(db)
|
||||||
|
})
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Recording not found")
|
return nil, fiber.NewError(fiber.StatusNotFound, "Recording not found")
|
||||||
}
|
}
|
||||||
@@ -117,7 +101,7 @@ func (s recordingService) GetNextDay(c *fiber.Ctx, projectFlockKandangId uint) (
|
|||||||
}
|
}
|
||||||
|
|
||||||
db := s.Repository.DB().WithContext(c.Context())
|
db := s.Repository.DB().WithContext(c.Context())
|
||||||
next, err := s.generateNextDay(db, projectFlockKandangId)
|
next, err := s.Repository.GenerateNextDay(db, projectFlockKandangId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to compute next recording day for project_flock_kandang_id=%d: %+v", projectFlockKandangId, err)
|
s.Log.Errorf("Failed to compute next recording day for project_flock_kandang_id=%d: %+v", projectFlockKandangId, err)
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -155,7 +139,7 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
nextDay, err := s.generateNextDay(tx, req.ProjectFlockKandangId)
|
nextDay, err := s.Repository.GenerateNextDay(tx, req.ProjectFlockKandangId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
s.Log.Errorf("Failed to determine recording day: %+v", err)
|
s.Log.Errorf("Failed to determine recording day: %+v", err)
|
||||||
@@ -184,21 +168,25 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
|
|||||||
|
|
||||||
if err := tx.Create(recording).Error; err != nil {
|
if err := tx.Create(recording).Error; err != nil {
|
||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
|
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||||
|
dateStr := recordDate.Format("2006-01-02")
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Recording for project flock %d on %s already exists", req.ProjectFlockKandangId, dateStr))
|
||||||
|
}
|
||||||
s.Log.Errorf("Failed to create recording: %+v", err)
|
s.Log.Errorf("Failed to create recording: %+v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.persistBodyWeights(tx, recording.Id, req.BodyWeights); err != nil {
|
if err := s.Repository.CreateBodyWeights(tx, mapBodyWeights(recording.Id, req.BodyWeights)); err != nil {
|
||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
s.Log.Errorf("Failed to persist body weights: %+v", err)
|
s.Log.Errorf("Failed to persist body weights: %+v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := s.persistStocks(tx, recording.Id, req.Stocks); err != nil {
|
if err := s.Repository.CreateStocks(tx, mapStocks(recording.Id, req.Stocks)); err != nil {
|
||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
s.Log.Errorf("Failed to persist stocks: %+v", err)
|
s.Log.Errorf("Failed to persist stocks: %+v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := s.persistDepletions(tx, recording.Id, req.Depletions); err != nil {
|
if err := s.Repository.CreateDepletions(tx, mapDepletions(recording.Id, req.Depletions)); err != nil {
|
||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
s.Log.Errorf("Failed to persist depletions: %+v", err)
|
s.Log.Errorf("Failed to persist depletions: %+v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -254,7 +242,12 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
recording.Ontime = ontimeValue
|
recording.Ontime = ontimeValue
|
||||||
|
|
||||||
if req.BodyWeights != nil {
|
if req.BodyWeights != nil {
|
||||||
if err := s.replaceBodyWeights(tx, recording.Id, req.BodyWeights); err != nil {
|
if err := s.Repository.DeleteBodyWeights(tx, recording.Id); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
s.Log.Errorf("Failed to clear body weights: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := s.Repository.CreateBodyWeights(tx, mapBodyWeights(recording.Id, req.BodyWeights)); err != nil {
|
||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
s.Log.Errorf("Failed to update body weights: %+v", err)
|
s.Log.Errorf("Failed to update body weights: %+v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -265,7 +258,12 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := s.replaceStocks(tx, recording.Id, req.Stocks); err != nil {
|
if err := s.Repository.DeleteStocks(tx, recording.Id); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
s.Log.Errorf("Failed to clear stocks: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := s.Repository.CreateStocks(tx, mapStocks(recording.Id, req.Stocks)); err != nil {
|
||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
s.Log.Errorf("Failed to update stocks: %+v", err)
|
s.Log.Errorf("Failed to update stocks: %+v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -276,7 +274,12 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := s.replaceDepletions(tx, recording.Id, req.Depletions); err != nil {
|
if err := s.Repository.DeleteDepletions(tx, recording.Id); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
s.Log.Errorf("Failed to clear depletions: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := s.Repository.CreateDepletions(tx, mapDepletions(recording.Id, req.Depletions)); err != nil {
|
||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
s.Log.Errorf("Failed to update depletions: %+v", err)
|
s.Log.Errorf("Failed to update depletions: %+v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -342,45 +345,6 @@ func (s *recordingService) ensureProductWarehousesExist(c *fiber.Ctx, stocks []v
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *recordingService) generateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error) {
|
|
||||||
var days []int
|
|
||||||
if err := tx.Model(&entity.Recording{}).
|
|
||||||
Where("project_flock_id = ?", projectFlockKandangId).
|
|
||||||
Where("day IS NOT NULL").
|
|
||||||
Pluck("day", &days).Error; err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return nextRecordingDay(days), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func nextRecordingDay(days []int) int {
|
|
||||||
if len(days) == 0 {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
unique := make(map[int]struct{}, len(days))
|
|
||||||
for _, day := range days {
|
|
||||||
if day > 0 {
|
|
||||||
unique[day] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
normalized := make([]int, 0, len(unique))
|
|
||||||
for day := range unique {
|
|
||||||
normalized = append(normalized, day)
|
|
||||||
}
|
|
||||||
sort.Ints(normalized)
|
|
||||||
|
|
||||||
for idx, day := range normalized {
|
|
||||||
expected := idx + 1
|
|
||||||
if day != expected {
|
|
||||||
return expected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return len(normalized) + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
func computeOntime(recordDatetime, reference time.Time) bool {
|
func computeOntime(recordDatetime, reference time.Time) bool {
|
||||||
return !recordDatetime.Before(reference)
|
return !recordDatetime.Before(reference)
|
||||||
}
|
}
|
||||||
@@ -392,107 +356,81 @@ func boolToInt(v bool) int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *recordingService) persistBodyWeights(tx *gorm.DB, recordingID uint, payload []validation.BodyWeight) error {
|
func mapBodyWeights(recordingID uint, payload []validation.BodyWeight) []entity.RecordingBW {
|
||||||
if len(payload) == 0 {
|
if len(payload) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
bodyWeights := make([]entity.RecordingBW, len(payload))
|
items := make([]entity.RecordingBW, len(payload))
|
||||||
for i, bw := range payload {
|
for i, bw := range payload {
|
||||||
bodyWeights[i] = entity.RecordingBW{
|
items[i] = entity.RecordingBW{
|
||||||
RecordingId: recordingID,
|
RecordingId: recordingID,
|
||||||
Weight: bw.Weight,
|
Weight: bw.Weight,
|
||||||
Qty: bw.Qty,
|
Qty: bw.Qty,
|
||||||
Notes: bw.Notes,
|
Notes: bw.Notes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return items
|
||||||
return tx.Create(&bodyWeights).Error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *recordingService) persistStocks(tx *gorm.DB, recordingID uint, payload []validation.Stock) error {
|
func mapStocks(recordingID uint, payload []validation.Stock) []entity.RecordingStock {
|
||||||
if len(payload) == 0 {
|
if len(payload) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
stocks := make([]entity.RecordingStock, len(payload))
|
items := make([]entity.RecordingStock, len(payload))
|
||||||
for i, stock := range payload {
|
for i, stock := range payload {
|
||||||
stocks[i] = entity.RecordingStock{
|
items[i] = entity.RecordingStock{
|
||||||
RecordingId: recordingID,
|
RecordingId: recordingID,
|
||||||
ProductWarehouseId: stock.ProductWarehouseId,
|
ProductWarehouseId: stock.ProductWarehouseId,
|
||||||
Notes: stock.Notes,
|
Notes: stock.Notes,
|
||||||
}
|
}
|
||||||
if stock.Increase != nil {
|
if stock.Increase != nil {
|
||||||
val := *stock.Increase
|
val := *stock.Increase
|
||||||
stocks[i].Increase = &val
|
items[i].Increase = &val
|
||||||
}
|
}
|
||||||
if stock.Decrease != nil {
|
if stock.Decrease != nil {
|
||||||
val := *stock.Decrease
|
val := *stock.Decrease
|
||||||
stocks[i].Decrease = &val
|
items[i].Decrease = &val
|
||||||
}
|
}
|
||||||
if stock.UsageAmount != nil {
|
if stock.UsageAmount != nil {
|
||||||
val := *stock.UsageAmount
|
val := *stock.UsageAmount
|
||||||
stocks[i].UsageAmount = &val
|
items[i].UsageAmount = &val
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return items
|
||||||
return tx.Create(&stocks).Error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *recordingService) persistDepletions(tx *gorm.DB, recordingID uint, payload []validation.Depletion) error {
|
func mapDepletions(recordingID uint, payload []validation.Depletion) []entity.RecordingDepletion {
|
||||||
if len(payload) == 0 {
|
if len(payload) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
depletions := make([]entity.RecordingDepletion, len(payload))
|
items := make([]entity.RecordingDepletion, len(payload))
|
||||||
for i, depl := range payload {
|
for i, dep := range payload {
|
||||||
total := depl.Total
|
total := dep.Total
|
||||||
depletions[i] = entity.RecordingDepletion{
|
items[i] = entity.RecordingDepletion{
|
||||||
RecordingId: recordingID,
|
RecordingId: recordingID,
|
||||||
ProductWarehouseId: depl.ProductWarehouseId,
|
ProductWarehouseId: dep.ProductWarehouseId,
|
||||||
Total: total,
|
Total: total,
|
||||||
Notes: depl.Notes,
|
Notes: dep.Notes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return items
|
||||||
return tx.Create(&depletions).Error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *recordingService) replaceBodyWeights(tx *gorm.DB, recordingID uint, payload []validation.BodyWeight) error {
|
|
||||||
if err := tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingBW{}).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return s.persistBodyWeights(tx, recordingID, payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *recordingService) replaceStocks(tx *gorm.DB, recordingID uint, payload []validation.Stock) error {
|
|
||||||
if err := tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingStock{}).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return s.persistStocks(tx, recordingID, payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *recordingService) replaceDepletions(tx *gorm.DB, recordingID uint, payload []validation.Depletion) error {
|
|
||||||
if err := tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingDepletion{}).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return s.persistDepletions(tx, recordingID, payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Metrics Calculation ===
|
|
||||||
|
|
||||||
func (s *recordingService) computeAndUpdateMetrics(tx *gorm.DB, recording *entity.Recording) error {
|
func (s *recordingService) computeAndUpdateMetrics(tx *gorm.DB, recording *entity.Recording) error {
|
||||||
day := 0
|
day := 0
|
||||||
if recording.Day != nil {
|
if recording.Day != nil {
|
||||||
day = *recording.Day
|
day = *recording.Day
|
||||||
}
|
}
|
||||||
|
|
||||||
totalDepletion, err := s.sumRecordingDepletions(tx, recording.Id)
|
totalDepletion, err := s.Repository.SumRecordingDepletions(tx, recording.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("sumRecordingDepletions: %w", err)
|
return fmt.Errorf("sumRecordingDepletions: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
prevRecording, err := s.getPreviousRecording(tx, recording.ProjectFlockKandangId, day)
|
prevRecording, err := s.Repository.FindPreviousRecording(tx, recording.ProjectFlockKandangId, day)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("getPreviousRecording: %w", err)
|
return fmt.Errorf("getPreviousRecording: %w", err)
|
||||||
}
|
}
|
||||||
@@ -507,28 +445,28 @@ func (s *recordingService) computeAndUpdateMetrics(tx *gorm.DB, recording *entit
|
|||||||
if prevRecording.CumIntake != nil {
|
if prevRecording.CumIntake != nil {
|
||||||
prevCumIntake = float64(*prevRecording.CumIntake)
|
prevCumIntake = float64(*prevRecording.CumIntake)
|
||||||
}
|
}
|
||||||
prevAvgWeight, err = s.getAverageBodyWeight(tx, prevRecording.Id)
|
prevAvgWeight, err = s.Repository.GetAverageBodyWeight(tx, prevRecording.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("getAverageBodyWeight(prev): %w", err)
|
return fmt.Errorf("getAverageBodyWeight(prev): %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
totalChick, err := s.getTotalChick(tx, recording.ProjectFlockKandangId)
|
totalChick, err := s.Repository.GetTotalChick(tx, recording.ProjectFlockKandangId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("getTotalChick: %w", err)
|
return fmt.Errorf("getTotalChick: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
currentAvgWeight, err := s.getAverageBodyWeight(tx, recording.Id)
|
currentAvgWeight, err := s.Repository.GetAverageBodyWeight(tx, recording.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("getAverageBodyWeight(current): %w", err)
|
return fmt.Errorf("getAverageBodyWeight(current): %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
usageInGrams, err := s.getFeedUsageInGrams(tx, recording.Id)
|
usageInGrams, err := s.Repository.GetFeedUsageInGrams(tx, recording.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("getFeedUsageInGrams: %w", err)
|
return fmt.Errorf("getFeedUsageInGrams: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fcrId, err := s.getFcrID(tx, recording.ProjectFlockKandangId)
|
fcrId, err := s.Repository.GetFcrID(tx, recording.ProjectFlockKandangId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("getFcrID: %w", err)
|
return fmt.Errorf("getFcrID: %w", err)
|
||||||
}
|
}
|
||||||
@@ -551,11 +489,11 @@ func (s *recordingService) computeAndUpdateMetrics(tx *gorm.DB, recording *entit
|
|||||||
|
|
||||||
if totalChick > 0 {
|
if totalChick > 0 {
|
||||||
remainingChick := totalChick - cumDepletion
|
remainingChick := totalChick - cumDepletion
|
||||||
if remainingChick < 0 {
|
if remainingChick < 0 {
|
||||||
remainingChick = 0
|
remainingChick = 0
|
||||||
}
|
}
|
||||||
updates["total_chick"] = remainingChick
|
updates["total_chick"] = remainingChick
|
||||||
recording.TotalChick = &remainingChick
|
recording.TotalChick = &remainingChick
|
||||||
|
|
||||||
cumRate := (float64(cumDepletion) / float64(totalChick)) * 100
|
cumRate := (float64(cumDepletion) / float64(totalChick)) * 100
|
||||||
updates["cum_depletion_rate"] = cumRate
|
updates["cum_depletion_rate"] = cumRate
|
||||||
@@ -587,7 +525,7 @@ func (s *recordingService) computeAndUpdateMetrics(tx *gorm.DB, recording *entit
|
|||||||
}
|
}
|
||||||
|
|
||||||
if fcrId != 0 && currentAvgKg > 0 && day > 0 {
|
if fcrId != 0 && currentAvgKg > 0 && day > 0 {
|
||||||
if fcrWeightKg, ok, err := s.getFcrStandardWeightKg(tx, fcrId, currentAvgKg); err != nil {
|
if fcrWeightKg, ok, err := s.Repository.GetFcrStandardWeightKg(tx, fcrId, currentAvgKg); err != nil {
|
||||||
return fmt.Errorf("getFcrStandardWeightKg: %w", err)
|
return fmt.Errorf("getFcrStandardWeightKg: %w", err)
|
||||||
} else if ok {
|
} else if ok {
|
||||||
avgDailyGain := (currentAvgKg - fcrWeightKg) / float64(day)
|
avgDailyGain := (currentAvgKg - fcrWeightKg) / float64(day)
|
||||||
@@ -644,153 +582,6 @@ func (s *recordingService) computeAndUpdateMetrics(tx *gorm.DB, recording *entit
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Query Helpers ===
|
|
||||||
|
|
||||||
func (s *recordingService) sumRecordingDepletions(tx *gorm.DB, recordingID uint) (int64, error) {
|
|
||||||
var result int64
|
|
||||||
if err := tx.Model(&entity.RecordingDepletion{}).
|
|
||||||
Where("recording_id = ?", recordingID).
|
|
||||||
Select("COALESCE(SUM(total), 0)").
|
|
||||||
Scan(&result).Error; err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *recordingService) getPreviousRecording(tx *gorm.DB, projectFlockKandangId uint, currentDay int) (*entity.Recording, error) {
|
|
||||||
if currentDay <= 1 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var prev entity.Recording
|
|
||||||
err := tx.
|
|
||||||
Where("project_flock_id = ? AND day < ?", projectFlockKandangId, currentDay).
|
|
||||||
Where("day IS NOT NULL").
|
|
||||||
Order("day DESC").
|
|
||||||
Limit(1).
|
|
||||||
Find(&prev).Error
|
|
||||||
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) || prev.Id == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &prev, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *recordingService) getTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error) {
|
|
||||||
var population entity.ProjectFlockPopulation
|
|
||||||
err := tx.
|
|
||||||
Where("project_flock_kandang_id = ?", projectFlockKandangId).
|
|
||||||
Order("created_at DESC").
|
|
||||||
First(&population).Error
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return int64(math.Round(population.InitialQuantity)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *recordingService) getAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error) {
|
|
||||||
var result struct {
|
|
||||||
TotalWeight float64
|
|
||||||
TotalQty float64
|
|
||||||
}
|
|
||||||
if err := tx.Model(&entity.RecordingBW{}).
|
|
||||||
Select("COALESCE(SUM(weight * qty), 0) AS total_weight, COALESCE(SUM(qty), 0) AS total_qty").
|
|
||||||
Where("recording_id = ?", recordingID).
|
|
||||||
Scan(&result).Error; err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if result.TotalQty == 0 {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
return result.TotalWeight / result.TotalQty, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *recordingService) getFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) {
|
|
||||||
var rows []struct {
|
|
||||||
UsageAmount float64
|
|
||||||
UomName string
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.
|
|
||||||
Table("recording_stocks").
|
|
||||||
Select("COALESCE(recording_stocks.usage_amount, 0) AS usage_amount, LOWER(uoms.name) AS uom_name").
|
|
||||||
Joins("JOIN product_warehouses ON product_warehouses.id = recording_stocks.product_warehouse_id").
|
|
||||||
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
|
||||||
Joins("JOIN uoms ON uoms.id = products.uom_id").
|
|
||||||
Where("recording_stocks.recording_id = ?", recordingID).
|
|
||||||
Scan(&rows).Error; err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var total float64
|
|
||||||
for _, row := range rows {
|
|
||||||
if row.UsageAmount <= 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch strings.TrimSpace(row.UomName) {
|
|
||||||
case "kilogram", "kg", "kilograms", "kilo":
|
|
||||||
total += row.UsageAmount * 1000
|
|
||||||
case "gram", "g", "grams":
|
|
||||||
total += row.UsageAmount
|
|
||||||
default:
|
|
||||||
total += row.UsageAmount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return total, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *recordingService) getFcrID(tx *gorm.DB, projectFlockKandangId uint) (uint, error) {
|
|
||||||
var result struct {
|
|
||||||
FcrID uint
|
|
||||||
}
|
|
||||||
if err := tx.Table("project_flock_kandangs").
|
|
||||||
Select("project_flocks.fcr_id AS fcr_id").
|
|
||||||
Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id").
|
|
||||||
Where("project_flock_kandangs.id = ?", projectFlockKandangId).
|
|
||||||
Scan(&result).Error; err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return result.FcrID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *recordingService) getFcrStandardWeightKg(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error) {
|
|
||||||
if fcrId == 0 {
|
|
||||||
return 0, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var standard entity.FcrStandard
|
|
||||||
err := tx.
|
|
||||||
Where("fcr_id = ? AND weight >= ?", fcrId, currentWeightKg).
|
|
||||||
Order("weight ASC").
|
|
||||||
First(&standard).Error
|
|
||||||
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
err = tx.
|
|
||||||
Where("fcr_id = ?", fcrId).
|
|
||||||
Order("weight DESC").
|
|
||||||
First(&standard).Error
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return 0, false, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return 0, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
weight := standard.Weight
|
|
||||||
if weight > 10 {
|
|
||||||
// assume already in grams
|
|
||||||
return weight / 1000, true, nil
|
|
||||||
}
|
|
||||||
return weight, true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Unit Helpers ===
|
// === Unit Helpers ===
|
||||||
|
|
||||||
func toGrams(weight float64) float64 {
|
func toGrams(weight float64) float64 {
|
||||||
|
|||||||
Reference in New Issue
Block a user