mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'dev/teguh' into 'feat/BE/US-75/chick-in-doc'
FIX/BE][US#75] : adjust some param and body request with FE needs, and fix minor bugs on Master data See merge request mbugroup/lti-api!46
This commit is contained in:
@@ -2,6 +2,7 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -32,3 +33,21 @@ func ExistsByName[T any](ctx context.Context, db *gorm.DB, name string, excludeI
|
|||||||
}
|
}
|
||||||
return count > 0, nil
|
return count > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ExistsByField[T any](ctx context.Context, db *gorm.DB, field string, value any, excludeID *uint) (bool, error) {
|
||||||
|
if field == "" {
|
||||||
|
return false, fmt.Errorf("field is required")
|
||||||
|
}
|
||||||
|
var count int64
|
||||||
|
q := db.WithContext(ctx).
|
||||||
|
Model(new(T)).
|
||||||
|
Where(fmt.Sprintf("%s = ?", field), value).
|
||||||
|
Where("deleted_at IS NULL")
|
||||||
|
if excludeID != nil {
|
||||||
|
q = q.Where("id <> ?", *excludeID)
|
||||||
|
}
|
||||||
|
if err := q.Count(&count).Error; err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return count > 0, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
--? Child Indexes(optional, biar rapi tapi klo gada juga ilang pas di drop)
|
||||||
|
DROP INDEX IF EXISTS idx_recording_stocks_product;
|
||||||
|
DROP INDEX IF EXISTS idx_recording_stocks_recording;
|
||||||
|
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_recording_depl_recording;
|
||||||
|
|
||||||
|
DROP INDEX IF EXISTS idx_recording_bws_recording;
|
||||||
|
|
||||||
|
--? Child Tables
|
||||||
|
DROP TABLE IF EXISTS recording_stocks;
|
||||||
|
DROP TABLE IF EXISTS recording_depletions;
|
||||||
|
DROP TABLE IF EXISTS recording_bws;
|
||||||
|
|
||||||
|
--? Parent Indexes ON recordings
|
||||||
|
DROP INDEX IF EXISTS uq_recordings_flock_record_date;
|
||||||
|
DROP INDEX IF EXISTS idx_recordings_flock_datetime;
|
||||||
|
|
||||||
|
--? Parent table
|
||||||
|
DROP TABLE IF EXISTS recordings;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
--? RECORDINGS (tabel induk recording harian)
|
||||||
|
CREATE TABLE IF NOT EXISTS recordings (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
project_flock_id BIGINT NOT NULL,
|
||||||
|
record_datetime TIMESTAMPTZ NOT NULL,
|
||||||
|
record_date DATE,
|
||||||
|
status INT NOT NULL DEFAULT 0, --? 0=draft,1=submitted,2=approved,3=rejected
|
||||||
|
ontime INT NOT NULL DEFAULT 0, --? 1=ontime,0=late (pakai INT/BOOLEAN sesuai preferensi)
|
||||||
|
day INT,
|
||||||
|
total_depletion INT,
|
||||||
|
cum_depletion_rate NUMERIC(7,3),
|
||||||
|
daily_gain NUMERIC(7,3),
|
||||||
|
avg_daily_gain NUMERIC(7,3),
|
||||||
|
cum_intake INT,
|
||||||
|
fcr_value NUMERIC(7,3),
|
||||||
|
total_chick BIGINT,
|
||||||
|
daily_depletion_rate NUMERIC(7,3),
|
||||||
|
cum_depletion INT,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
deleted_at TIMESTAMPTZ,
|
||||||
|
created_by BIGINT,
|
||||||
|
|
||||||
|
CONSTRAINT fk_recordings_project_flock
|
||||||
|
FOREIGN KEY (project_flock_id) REFERENCES project_flock_kandangs(id),
|
||||||
|
|
||||||
|
CONSTRAINT fk_recordings_created_by
|
||||||
|
FOREIGN KEY (created_by) REFERENCES users(id),
|
||||||
|
|
||||||
|
|
||||||
|
CONSTRAINT chk_recordings_status
|
||||||
|
CHECK (status IN (0,1,2,3)),
|
||||||
|
|
||||||
|
CONSTRAINT chk_recordings_ontime
|
||||||
|
CHECK (ontime IN (0,1)),
|
||||||
|
|
||||||
|
CONSTRAINT chk_recordings_day
|
||||||
|
CHECK (day IS NULL OR day >= 1),
|
||||||
|
|
||||||
|
CONSTRAINT chk_recordings_nonnegatives
|
||||||
|
CHECK (
|
||||||
|
(total_depletion IS NULL OR total_depletion >= 0) AND
|
||||||
|
(cum_depletion IS NULL OR cum_depletion >= 0) AND
|
||||||
|
(total_chick IS NULL OR total_chick >= 0) AND
|
||||||
|
(cum_intake IS NULL OR cum_intake >= 0) AND
|
||||||
|
(daily_gain IS NULL OR daily_gain >= 0) AND
|
||||||
|
(avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND
|
||||||
|
(fcr_value IS NULL OR fcr_value > 0) AND
|
||||||
|
(daily_depletion_rate IS NULL OR daily_depletion_rate >= 0) AND
|
||||||
|
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
--? Set record_date otomatis berdasarkan record_datetime (pakai zona Asia/Jakarta)
|
||||||
|
CREATE OR REPLACE FUNCTION trg_set_record_date() RETURNS trigger AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.record_date := (NEW.record_datetime AT TIME ZONE 'Asia/Jakarta')::date;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS recordings_set_record_date_trg ON recordings;
|
||||||
|
CREATE TRIGGER recordings_set_record_date_trg
|
||||||
|
BEFORE INSERT OR UPDATE OF record_datetime ON recordings
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION trg_set_record_date();
|
||||||
|
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_recordings_flock_datetime
|
||||||
|
ON recordings (project_flock_id, record_datetime);
|
||||||
|
|
||||||
|
--? Unique harian (1 recording per hari dan per flock)
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_recordings_flock_record_date
|
||||||
|
ON recordings (project_flock_id, record_date)
|
||||||
|
WHERE deleted_at IS NULL;
|
||||||
|
|
||||||
|
|
||||||
|
--? RECORDING_BWS (BW per recording)
|
||||||
|
CREATE TABLE IF NOT EXISTS recording_bws (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
recording_id BIGINT NOT NULL,
|
||||||
|
weight NUMERIC(8,2) NOT NULL, --? bobot per ekor/kelompok
|
||||||
|
qty INT NOT NULL DEFAULT 1, --? jumlah ekor pada bobot ini
|
||||||
|
notes VARCHAR,
|
||||||
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
|
|
||||||
|
CONSTRAINT fk_recording_bws_recording
|
||||||
|
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
CONSTRAINT chk_recording_bws_nonneg
|
||||||
|
CHECK (weight >= 0 AND qty >= 1)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_recording_bws_recording
|
||||||
|
ON recording_bws (recording_id);
|
||||||
|
|
||||||
|
--? RECORDING_DEPLETIONS
|
||||||
|
CREATE TABLE IF NOT EXISTS recording_depletions (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
recording_id BIGINT NOT NULL,
|
||||||
|
product_warehouse_id BIGINT NOT NULL,
|
||||||
|
total BIGINT NOT NULL,
|
||||||
|
notes VARCHAR,
|
||||||
|
|
||||||
|
CONSTRAINT fk_recording_depl_recording
|
||||||
|
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
CONSTRAINT fk_recording_depl_prodwh
|
||||||
|
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id),
|
||||||
|
|
||||||
|
CONSTRAINT chk_recording_depl_total
|
||||||
|
CHECK (total >= 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_recording_depl_recording
|
||||||
|
ON recording_depletions (recording_id);
|
||||||
|
|
||||||
|
--? RECORDING_STOCKS
|
||||||
|
CREATE TABLE IF NOT EXISTS recording_stocks (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
recording_id BIGINT NOT NULL,
|
||||||
|
product_warehouse_id BIGINT NOT NULL,
|
||||||
|
increase NUMERIC(10,3), --? penambahan (boleh NULL)
|
||||||
|
decrease NUMERIC(10,3), --? pengurangan (boleh NULL)
|
||||||
|
usage_amount BIGINT, --? pemakaian (opsional, jika konsep dipisah dari decrease)
|
||||||
|
notes VARCHAR,
|
||||||
|
|
||||||
|
CONSTRAINT fk_recording_stocks_recording
|
||||||
|
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE,
|
||||||
|
|
||||||
|
CONSTRAINT fk_recording_stocks_prodwh
|
||||||
|
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id),
|
||||||
|
|
||||||
|
CONSTRAINT chk_recording_stocks_nonneg
|
||||||
|
CHECK (
|
||||||
|
(increase IS NULL OR increase >= 0) AND
|
||||||
|
(decrease IS NULL OR decrease >= 0) AND
|
||||||
|
(usage_amount IS NULL OR usage_amount >= 0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_recording_stocks_recording
|
||||||
|
ON recording_stocks (recording_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_recording_stocks_product
|
||||||
|
ON recording_stocks (product_warehouse_id);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@@ -7,12 +7,29 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Recording struct {
|
type Recording struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS NULL"`
|
ProjectFlockKandangId uint `gorm:"column:project_flock_id;not null;index"`
|
||||||
CreatedBy uint `gorm:"not null"`
|
RecordDatetime time.Time `gorm:"column:record_datetime;not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
RecordDate *time.Time `gorm:"column:record_date"`
|
||||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
Ontime int `gorm:"column:ontime;not null;default:0"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
Day *int `gorm:"column:day"`
|
||||||
|
TotalDepletion *int `gorm:"column:total_depletion"`
|
||||||
|
CumDepletionRate *float64 `gorm:"column:cum_depletion_rate"`
|
||||||
|
DailyGain *float64 `gorm:"column:daily_gain"`
|
||||||
|
AvgDailyGain *float64 `gorm:"column:avg_daily_gain"`
|
||||||
|
CumIntake *int64 `gorm:"column:cum_intake"`
|
||||||
|
FcrValue *float64 `gorm:"column:fcr_value"`
|
||||||
|
TotalChick *int64 `gorm:"column:total_chick"`
|
||||||
|
DailyDepletionRate *float64 `gorm:"column:daily_depletion_rate"`
|
||||||
|
CumDepletion *int `gorm:"column:cum_depletion"`
|
||||||
|
CreatedBy uint `gorm:"column:created_by"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|
||||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
|
||||||
|
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||||
|
BodyWeights []RecordingBW `gorm:"foreignKey:RecordingId;references:Id"`
|
||||||
|
Depletions []RecordingDepletion `gorm:"foreignKey:RecordingId;references:Id"`
|
||||||
|
Stocks []RecordingStock `gorm:"foreignKey:RecordingId;references:Id"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
package entities
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type RecordingBW struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
||||||
|
Weight float64 `gorm:"column:weight;not null"`
|
||||||
|
Qty int `gorm:"column:qty;not null;default:1"`
|
||||||
|
Notes *string `gorm:"column:notes"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||||
|
|
||||||
|
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
type RecordingDepletion struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
||||||
|
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||||
|
Total int64 `gorm:"column:total;not null"`
|
||||||
|
Notes *string `gorm:"column:notes"`
|
||||||
|
|
||||||
|
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||||
|
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package entities
|
||||||
|
|
||||||
|
type RecordingStock struct {
|
||||||
|
Id uint `gorm:"primaryKey"`
|
||||||
|
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
||||||
|
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||||
|
Increase *float64 `gorm:"column:increase"`
|
||||||
|
Decrease *float64 `gorm:"column:decrease"`
|
||||||
|
UsageAmount *int64 `gorm:"column:usage_amount"`
|
||||||
|
Notes *string `gorm:"column:notes"`
|
||||||
|
|
||||||
|
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||||
|
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||||
|
}
|
||||||
+5
-2
@@ -28,6 +28,11 @@ func (u *ProductWarehouseController) GetAll(c *fiber.Ctx) error {
|
|||||||
Limit: c.QueryInt("limit", 10),
|
Limit: c.QueryInt("limit", 10),
|
||||||
ProductId: uint(c.QueryInt("product_id", 0)),
|
ProductId: uint(c.QueryInt("product_id", 0)),
|
||||||
WarehouseId: uint(c.QueryInt("warehouse_id", 0)),
|
WarehouseId: uint(c.QueryInt("warehouse_id", 0)),
|
||||||
|
Flags: c.Query("flags", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
result, totalResults, err := u.ProductWarehouseService.GetAll(c, query)
|
result, totalResults, err := u.ProductWarehouseService.GetAll(c, query)
|
||||||
@@ -71,5 +76,3 @@ func (u *ProductWarehouseController) GetOne(c *fiber.Ctx) error {
|
|||||||
Data: dto.ToProductWarehouseListDTO(*result),
|
Data: dto.ToProductWarehouseListDTO(*result),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+28
-11
@@ -16,6 +16,7 @@ type ProductWarehouseRepository interface {
|
|||||||
ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error)
|
ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error)
|
||||||
ExistsByID(ctx context.Context, id uint) (bool, error)
|
ExistsByID(ctx context.Context, id uint) (bool, error)
|
||||||
GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error)
|
GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error)
|
||||||
|
GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProductWarehouseRepositoryImpl struct {
|
type ProductWarehouseRepositoryImpl struct {
|
||||||
@@ -30,6 +31,17 @@ func NewProductWarehouseRepository(db *gorm.DB) ProductWarehouseRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ProductWarehouseRepositoryImpl) IsProductExist(ctx context.Context, productId uint) (bool, error) {
|
||||||
|
return repository.Exists[entity.Product](ctx, r.db, productId)
|
||||||
|
}
|
||||||
|
func (r *ProductWarehouseRepositoryImpl) IsWarehouseExist(ctx context.Context, warehouseId uint) (bool, error) {
|
||||||
|
return repository.Exists[entity.Warehouse](ctx, r.db, warehouseId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProductWarehouseRepositoryImpl) ExistsByID(ctx context.Context, id uint) (bool, error) {
|
||||||
|
return repository.Exists[entity.ProductWarehouse](ctx, r.db, id)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExists(ctx context.Context, productId, warehouseId uint, excludeID *uint) (bool, error) {
|
func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExists(ctx context.Context, productId, warehouseId uint, excludeID *uint) (bool, error) {
|
||||||
var count int64
|
var count int64
|
||||||
query := r.db.WithContext(ctx).Model(&entity.ProductWarehouse{}).
|
query := r.db.WithContext(ctx).Model(&entity.ProductWarehouse{}).
|
||||||
@@ -43,17 +55,6 @@ func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExists(ctx context.Cont
|
|||||||
return count > 0, nil
|
return count > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *ProductWarehouseRepositoryImpl) IsProductExist(ctx context.Context, productId uint) (bool, error) {
|
|
||||||
return repository.Exists[entity.Product](ctx, r.db, productId)
|
|
||||||
}
|
|
||||||
func (r *ProductWarehouseRepositoryImpl) IsWarehouseExist(ctx context.Context, warehouseId uint) (bool, error) {
|
|
||||||
return repository.Exists[entity.Warehouse](ctx, r.db, warehouseId)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ProductWarehouseRepositoryImpl) ExistsByID(ctx context.Context, id uint) (bool, error) {
|
|
||||||
return repository.Exists[entity.ProductWarehouse](ctx, r.db, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error) {
|
func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error) {
|
||||||
var count int64
|
var count int64
|
||||||
if err := r.db.WithContext(ctx).
|
if err := r.db.WithContext(ctx).
|
||||||
@@ -72,3 +73,19 @@ func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehous
|
|||||||
}
|
}
|
||||||
return &productWarehouse, nil
|
return &productWarehouse, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) {
|
||||||
|
var productWarehouses []entity.ProductWarehouse
|
||||||
|
err := r.db.WithContext(ctx).
|
||||||
|
Table("product_warehouses").
|
||||||
|
Select("product_warehouses.*").
|
||||||
|
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||||
|
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
|
||||||
|
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
|
||||||
|
Order("product_warehouses.created_at DESC").
|
||||||
|
Find(&productWarehouses).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return productWarehouses, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -49,8 +49,30 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
|
|||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if params.ProductId > 0 {
|
||||||
|
isProductExist, err := s.Repository.IsProductExist(c.Context(), params.ProductId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if !isProductExist {
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Product not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.WarehouseId > 0 {
|
||||||
|
isWarehouseExist, err := s.Repository.IsWarehouseExist(c.Context(), params.WarehouseId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
if !isWarehouseExist {
|
||||||
|
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
offset := (params.Page - 1) * params.Limit
|
offset := (params.Page - 1) * params.Limit
|
||||||
|
|
||||||
|
cleanFlags := utils.ParseFlags(params.Flags)
|
||||||
|
|
||||||
productWarehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
productWarehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
db = s.withRelations(db)
|
db = s.withRelations(db)
|
||||||
|
|
||||||
@@ -62,6 +84,12 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
|
|||||||
db = db.Where("warehouse_id = ?", params.WarehouseId)
|
db = db.Where("warehouse_id = ?", params.WarehouseId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(cleanFlags) > 0 {
|
||||||
|
db = db.Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||||
|
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", "products").
|
||||||
|
Where("flags.name IN ?", cleanFlags)
|
||||||
|
}
|
||||||
|
|
||||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+5
-4
@@ -13,8 +13,9 @@ type Update struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||||
ProductId uint `query:"product_id" validate:"omitempty,number,min=1"`
|
ProductId uint `query:"product_id" validate:"omitempty,number,min=1"`
|
||||||
WarehouseId uint `query:"warehouse_id" validate:"omitempty,number,min=1"`
|
WarehouseId uint `query:"warehouse_id" validate:"omitempty,number,min=1"`
|
||||||
|
Flags string `query:"flags" validate:"omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ func (u *AreaController) GetAll(c *fiber.Ctx) error {
|
|||||||
Search: c.Query("search", ""),
|
Search: c.Query("search", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
result, totalResults, err := u.AreaService.GetAll(c, query)
|
result, totalResults, err := u.AreaService.GetAll(c, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ func (u *BankController) GetAll(c *fiber.Ctx) error {
|
|||||||
Search: c.Query("search", ""),
|
Search: c.Query("search", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
result, totalResults, err := u.BankService.GetAll(c, query)
|
result, totalResults, err := u.BankService.GetAll(c, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
type BankRepository interface {
|
type BankRepository interface {
|
||||||
repository.BaseRepository[entity.Bank]
|
repository.BaseRepository[entity.Bank]
|
||||||
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
|
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
|
||||||
|
AccountNumberExists(ctx context.Context, accountNumber string, excludeID *uint) (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type BankRepositoryImpl struct {
|
type BankRepositoryImpl struct {
|
||||||
@@ -28,3 +29,7 @@ func NewBankRepository(db *gorm.DB) BankRepository {
|
|||||||
func (r *BankRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
|
func (r *BankRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
|
||||||
return repository.ExistsByName[entity.Bank](ctx, r.db, name, excludeID)
|
return repository.ExistsByName[entity.Bank](ctx, r.db, name, excludeID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *BankRepositoryImpl) AccountNumberExists(ctx context.Context, accountNumber string, excludeID *uint) (bool, error) {
|
||||||
|
return repository.ExistsByField[entity.Bank](ctx, r.db, "account_number", accountNumber, excludeID)
|
||||||
|
}
|
||||||
|
|||||||
@@ -87,6 +87,13 @@ func (s *bankService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.B
|
|||||||
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Bank with name %s already exists", req.Name))
|
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Bank with name %s already exists", req.Name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if exists, err := s.Repository.AccountNumberExists(c.Context(), req.AccountNumber, nil); err != nil {
|
||||||
|
s.Log.Errorf("Failed to check bank account number: %+v", err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check bank account number")
|
||||||
|
} else if exists {
|
||||||
|
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Bank with account number %s already exists", req.AccountNumber))
|
||||||
|
}
|
||||||
|
|
||||||
createBody := &entity.Bank{
|
createBody := &entity.Bank{
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Alias: req.Alias,
|
Alias: req.Alias,
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ func (u *CustomerController) GetAll(c *fiber.Ctx) error {
|
|||||||
Search: c.Query("search", ""),
|
Search: c.Query("search", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
result, totalResults, err := u.CustomerService.GetAll(c, query)
|
result, totalResults, err := u.CustomerService.GetAll(c, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ func (u *FcrController) GetAll(c *fiber.Ctx) error {
|
|||||||
Search: c.Query("search", ""),
|
Search: c.Query("search", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
result, totalResults, err := u.FcrService.GetAll(c, query)
|
result, totalResults, err := u.FcrService.GetAll(c, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ func (u *FlockController) GetAll(c *fiber.Ctx) error {
|
|||||||
Search: c.Query("search", ""),
|
Search: c.Query("search", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
result, totalResults, err := u.FlockService.GetAll(c, query)
|
result, totalResults, err := u.FlockService.GetAll(c, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ func (u *KandangController) GetAll(c *fiber.Ctx) error {
|
|||||||
PicId: c.QueryInt("pic_id", 0),
|
PicId: c.QueryInt("pic_id", 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
result, totalResults, err := u.KandangService.GetAll(c, query)
|
result, totalResults, err := u.KandangService.GetAll(c, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ func (u *LocationController) GetAll(c *fiber.Ctx) error {
|
|||||||
AreaId: c.QueryInt("area_id", 0),
|
AreaId: c.QueryInt("area_id", 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
result, totalResults, err := u.LocationService.GetAll(c, query)
|
result, totalResults, err := u.LocationService.GetAll(c, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ func (u *NonstockController) GetAll(c *fiber.Ctx) error {
|
|||||||
Search: c.Query("search", ""),
|
Search: c.Query("search", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
result, totalResults, err := u.NonstockService.GetAll(c, query)
|
result, totalResults, err := u.NonstockService.GetAll(c, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ func (u *ProductCategoryController) GetAll(c *fiber.Ctx) error {
|
|||||||
Search: c.Query("search", ""),
|
Search: c.Query("search", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
result, totalResults, err := u.ProductCategoryService.GetAll(c, query)
|
result, totalResults, err := u.ProductCategoryService.GetAll(c, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ func (u *ProductController) GetAll(c *fiber.Ctx) error {
|
|||||||
ProductCategoryID: c.QueryInt("product_category_id", 0),
|
ProductCategoryID: c.QueryInt("product_category_id", 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
result, totalResults, err := u.ProductService.GetAll(c, query)
|
result, totalResults, err := u.ProductService.GetAll(c, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ func (u *SupplierController) GetAll(c *fiber.Ctx) error {
|
|||||||
Search: c.Query("search", ""),
|
Search: c.Query("search", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
result, totalResults, err := u.SupplierService.GetAll(c, query)
|
result, totalResults, err := u.SupplierService.GetAll(c, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
type SupplierRepository interface {
|
type SupplierRepository interface {
|
||||||
repository.BaseRepository[entity.Supplier]
|
repository.BaseRepository[entity.Supplier]
|
||||||
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
|
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
|
||||||
|
AliasExists(ctx context.Context, alias string, excludeID *uint) (bool, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SupplierRepositoryImpl struct {
|
type SupplierRepositoryImpl struct {
|
||||||
@@ -29,3 +29,7 @@ func NewSupplierRepository(db *gorm.DB) SupplierRepository {
|
|||||||
func (r *SupplierRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
|
func (r *SupplierRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
|
||||||
return repository.ExistsByName[entity.Supplier](ctx, r.db, name, excludeID)
|
return repository.ExistsByName[entity.Supplier](ctx, r.db, name, excludeID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *SupplierRepositoryImpl) AliasExists(ctx context.Context, alias string, excludeID *uint) (bool, error) {
|
||||||
|
return repository.ExistsByField[entity.Supplier](ctx, r.db, "alias", alias, excludeID)
|
||||||
|
}
|
||||||
|
|||||||
@@ -88,6 +88,13 @@ func (s *supplierService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
|
|||||||
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Supplier with name %s already exists", req.Name))
|
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Supplier with name %s already exists", req.Name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if exists, err := s.Repository.AliasExists(c.Context(), strings.TrimSpace(strings.ToUpper(req.Alias)), nil); err != nil {
|
||||||
|
s.Log.Errorf("Failed to check supplier alias: %+v", err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check supplier alias")
|
||||||
|
} else if exists {
|
||||||
|
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Supplier with alias %s already exists", strings.TrimSpace(strings.ToUpper(req.Alias))))
|
||||||
|
}
|
||||||
|
|
||||||
typ := strings.ToUpper(req.Type)
|
typ := strings.ToUpper(req.Type)
|
||||||
if !utils.IsValidCustomerSupplierType(typ) {
|
if !utils.IsValidCustomerSupplierType(typ) {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid supplier type")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid supplier type")
|
||||||
@@ -143,6 +150,12 @@ func (s supplierService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint
|
|||||||
}
|
}
|
||||||
|
|
||||||
if req.Alias != nil {
|
if req.Alias != nil {
|
||||||
|
if exists, err := s.Repository.AliasExists(c.Context(), strings.TrimSpace(strings.ToUpper(*req.Alias)), &id); err != nil {
|
||||||
|
s.Log.Errorf("Failed to check supplier alias: %+v", err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check supplier alias")
|
||||||
|
} else if exists {
|
||||||
|
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Supplier with alias %s already exists", strings.TrimSpace(strings.ToUpper(*req.Alias))))
|
||||||
|
}
|
||||||
updateBody["alias"] = strings.TrimSpace(strings.ToUpper(*req.Alias))
|
updateBody["alias"] = strings.TrimSpace(strings.ToUpper(*req.Alias))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ func (u *UomController) GetAll(c *fiber.Ctx) error {
|
|||||||
Search: c.Query("search", ""),
|
Search: c.Query("search", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
result, totalResults, err := u.UomService.GetAll(c, query)
|
result, totalResults, err := u.UomService.GetAll(c, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ func (u *WarehouseController) GetAll(c *fiber.Ctx) error {
|
|||||||
AreaId: c.QueryInt("area_id", 0),
|
AreaId: c.QueryInt("area_id", 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
result, totalResults, err := u.WarehouseService.GetAll(c, query)
|
result, totalResults, err := u.WarehouseService.GetAll(c, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -121,14 +121,8 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var productWarehouses []entity.ProductWarehouse
|
// move complex DB query into repository for cleaner service
|
||||||
err = s.ProductWarehouseRepo.DB().
|
productWarehouses, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), "DOC", warehouse.Id)
|
||||||
WithContext(c.Context()).
|
|
||||||
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
|
||||||
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("created_at DESC").
|
|
||||||
Find(&productWarehouses).Error
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to get product warehouses: %+v", err)
|
s.Log.Errorf("Failed to get product warehouses: %+v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -136,8 +130,6 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
|||||||
if len(productWarehouses) == 0 {
|
if len(productWarehouses) == 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse")
|
return nil, fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jumlahkan semua quantity DOC
|
|
||||||
totalQuantity := 0.0
|
totalQuantity := 0.0
|
||||||
for _, pw := range productWarehouses {
|
for _, pw := range productWarehouses {
|
||||||
totalQuantity += pw.Quantity
|
totalQuantity += pw.Quantity
|
||||||
@@ -147,7 +139,6 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
|||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Insufficient quantity in Product Warehouses")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Insufficient quantity in Product Warehouses")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buat satu chickin dengan total quantity
|
|
||||||
chickinDate, err := utils.ParseDateString(req.ChickInDate)
|
chickinDate, err := utils.ParseDateString(req.ChickInDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to parse chickin date: %+v", err)
|
s.Log.Errorf("Failed to parse chickin date: %+v", err)
|
||||||
@@ -157,7 +148,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
|||||||
ProjectFlockKandangId: projectflockkandang.Id,
|
ProjectFlockKandangId: projectflockkandang.Id,
|
||||||
ChickInDate: chickinDate,
|
ChickInDate: chickinDate,
|
||||||
Quantity: totalQuantity,
|
Quantity: totalQuantity,
|
||||||
Note: "",
|
Note: req.Note,
|
||||||
CreatedBy: 1, //todo: ganti dengan user login
|
CreatedBy: 1, //todo: ganti dengan user login
|
||||||
}
|
}
|
||||||
err = s.Repository.CreateOne(c.Context(), newChickin, nil)
|
err = s.Repository.CreateOne(c.Context(), newChickin, nil)
|
||||||
@@ -176,7 +167,6 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// add ke detail chickin
|
|
||||||
newChickinDetail := &entity.ProjectChickinDetail{
|
newChickinDetail := &entity.ProjectChickinDetail{
|
||||||
ProjectChickinId: newChickin.Id,
|
ProjectChickinId: newChickin.Id,
|
||||||
ProductWarehouseId: pw.Id,
|
ProductWarehouseId: pw.Id,
|
||||||
@@ -232,6 +222,9 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
if req.ChickInDate != "" {
|
if req.ChickInDate != "" {
|
||||||
updateBody["chick_in_date"] = req.ChickInDate
|
updateBody["chick_in_date"] = req.ChickInDate
|
||||||
}
|
}
|
||||||
|
if req.Note != "" {
|
||||||
|
updateBody["note"] = req.Note
|
||||||
|
}
|
||||||
if len(updateBody) == 0 {
|
if len(updateBody) == 0 {
|
||||||
return s.GetOne(c, id)
|
return s.GetOne(c, id)
|
||||||
}
|
}
|
||||||
@@ -293,7 +286,6 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
return rollback(err)
|
return rollback(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper: restore quantities from details; returns (restored bool, error)
|
|
||||||
restoreFromDetails := func() (bool, error) {
|
restoreFromDetails := func() (bool, error) {
|
||||||
var details []entity.ProjectChickinDetail
|
var details []entity.ProjectChickinDetail
|
||||||
if err := tx.WithContext(c.Context()).Where("project_chickin_id = ?", chickin.Id).Find(&details).Error; err != nil {
|
if err := tx.WithContext(c.Context()).Where("project_chickin_id = ?", chickin.Id).Find(&details).Error; err != nil {
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ package validation
|
|||||||
type Create struct {
|
type Create struct {
|
||||||
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
|
||||||
ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"`
|
ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"`
|
||||||
|
Note string `json:"note" validate:"omitempty`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"`
|
ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"`
|
||||||
|
Note string `json:"note" validate:"omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
|
|||||||
@@ -246,17 +246,39 @@ func (u *ProjectflockController) GetFlockPeriodSummary(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
|
func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
|
||||||
projectFlockIdStr := c.Query("project_flock_id", "")
|
projectFlockId := c.QueryInt("project_flock_id", 0)
|
||||||
kandangIdStr := c.Query("kandang_id", "")
|
kandangId := c.QueryInt("kandang_id", 0)
|
||||||
|
|
||||||
result, err := u.ProjectflockService.GetProjectFlockKandangByParams(c, "", projectFlockIdStr, kandangIdStr)
|
if projectFlockId == 0 || kandangId == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id or kandang_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, availableStock, err := u.ProjectflockService.GetProjectFlockKandangByProjectAndKandang(c, uint(projectFlockId), uint(kandangId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dtoResult := dto.ToProjectFlockKandangDTO(*result)
|
||||||
|
dtoResult.AvailableQuantity = float64(availableStock)
|
||||||
|
|
||||||
|
// populate available quantity for each kandang inside project_flock
|
||||||
|
if dtoResult.ProjectFlock != nil {
|
||||||
|
for i := range dtoResult.ProjectFlock.Kandangs {
|
||||||
|
kand := &dtoResult.ProjectFlock.Kandangs[i]
|
||||||
|
if kand.Id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if q, qerr := u.ProjectflockService.GetAvailableDocQuantity(c, kand.Id); qerr == nil {
|
||||||
|
kand.AvailableQuantity = q
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// remove inner kandangs from project_flock to avoid duplication
|
||||||
|
dtoResult.ProjectFlock.Kandangs = nil
|
||||||
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
return c.Status(fiber.StatusOK).
|
||||||
JSON(response.Success{Code: fiber.StatusOK,
|
JSON(response.Success{Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Get projectflock kandang successfully",
|
Message: "Get projectflock kandang successfully",
|
||||||
Data: dto.ToProjectFlockKandangDTO(*result)})
|
Data: dtoResult})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,9 @@ import (
|
|||||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// internal DTO used only for lookup response: project flock with kandangs carrying pivot ids
|
|
||||||
type KandangWithPivotDTO struct {
|
type KandangWithPivotDTO struct {
|
||||||
kandangDTO.KandangBaseDTO
|
kandangDTO.KandangBaseDTO
|
||||||
ProjectFlockKandangId *uint `json:"project_flock_kandang_id,omitempty"`
|
AvailableQuantity float64 `json:"available_quantity"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProjectFlockWithPivotDTO struct {
|
type ProjectFlockWithPivotDTO struct {
|
||||||
@@ -28,11 +27,13 @@ type ProjectFlockWithPivotDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ProjectFlockKandangDTO struct {
|
type ProjectFlockKandangDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
ProjectFlockId uint `json:"project_flock_id"`
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
||||||
KandangId uint `json:"kandang_id"`
|
ProjectFlockId uint `json:"project_flock_id"`
|
||||||
Kandang *kandangDTO.KandangBaseDTO `json:"kandang,omitempty"`
|
KandangId uint `json:"kandang_id"`
|
||||||
ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"`
|
Kandang *kandangDTO.KandangBaseDTO `json:"kandang,omitempty"`
|
||||||
|
ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"`
|
||||||
|
AvailableQuantity float64 `json:"available_quantity"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO {
|
func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO {
|
||||||
@@ -44,7 +45,7 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
|
|||||||
|
|
||||||
var pf *ProjectFlockWithPivotDTO
|
var pf *ProjectFlockWithPivotDTO
|
||||||
if e.ProjectFlock.Id != 0 {
|
if e.ProjectFlock.Id != 0 {
|
||||||
// build project flock with kandangs that include pivot ids
|
|
||||||
pfLocal := ProjectFlockWithPivotDTO{
|
pfLocal := ProjectFlockWithPivotDTO{
|
||||||
ProjectFlockBaseDTO: ProjectFlockBaseDTO{
|
ProjectFlockBaseDTO: ProjectFlockBaseDTO{
|
||||||
Id: e.ProjectFlock.Id,
|
Id: e.ProjectFlock.Id,
|
||||||
@@ -53,7 +54,6 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
|
|||||||
Category: e.ProjectFlock.Category,
|
Category: e.ProjectFlock.Category,
|
||||||
}
|
}
|
||||||
|
|
||||||
// fill related small summaries
|
|
||||||
if e.ProjectFlock.Flock.Id != 0 {
|
if e.ProjectFlock.Flock.Id != 0 {
|
||||||
mapped := ToFlockSummaryDTO(e.ProjectFlock.Flock)
|
mapped := ToFlockSummaryDTO(e.ProjectFlock.Flock)
|
||||||
pfLocal.Flock = &mapped
|
pfLocal.Flock = &mapped
|
||||||
@@ -75,23 +75,16 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
|
|||||||
pfLocal.CreatedUser = &mapped
|
pfLocal.CreatedUser = &mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
// build pivot map
|
|
||||||
pivotMap := make(map[uint]uint)
|
pivotMap := make(map[uint]uint)
|
||||||
for _, ph := range e.ProjectFlock.KandangHistory {
|
for _, ph := range e.ProjectFlock.KandangHistory {
|
||||||
pivotMap[ph.KandangId] = ph.Id
|
pivotMap[ph.KandangId] = ph.Id
|
||||||
}
|
}
|
||||||
|
|
||||||
// populate kandangs with pivot ids
|
|
||||||
for _, k := range e.ProjectFlock.Kandangs {
|
for _, k := range e.ProjectFlock.Kandangs {
|
||||||
kb := kandangDTO.ToKandangBaseDTO(k)
|
kb := kandangDTO.ToKandangBaseDTO(k)
|
||||||
var pid *uint
|
|
||||||
if v, ok := pivotMap[k.Id]; ok {
|
|
||||||
vv := v
|
|
||||||
pid = &vv
|
|
||||||
}
|
|
||||||
pfLocal.Kandangs = append(pfLocal.Kandangs, KandangWithPivotDTO{
|
pfLocal.Kandangs = append(pfLocal.Kandangs, KandangWithPivotDTO{
|
||||||
KandangBaseDTO: kb,
|
KandangBaseDTO: kb,
|
||||||
ProjectFlockKandangId: pid,
|
AvailableQuantity: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,10 +92,12 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ProjectFlockKandangDTO{
|
return ProjectFlockKandangDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
ProjectFlockId: e.ProjectFlockId,
|
ProjectFlockKandangId: e.Id,
|
||||||
KandangId: e.KandangId,
|
ProjectFlockId: e.ProjectFlockId,
|
||||||
Kandang: kandang,
|
KandangId: e.KandangId,
|
||||||
ProjectFlock: pf,
|
Kandang: kandang,
|
||||||
|
ProjectFlock: pf,
|
||||||
|
AvailableQuantity: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import (
|
|||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
rFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
|
rFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
|
||||||
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||||
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
rProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
rProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
|
||||||
sProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
|
sProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
|
||||||
@@ -27,6 +29,8 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid
|
|||||||
kandangRepo := rKandang.NewKandangRepository(db)
|
kandangRepo := rKandang.NewKandangRepository(db)
|
||||||
projectflockRepo := rProjectflock.NewProjectflockRepository(db)
|
projectflockRepo := rProjectflock.NewProjectflockRepository(db)
|
||||||
projectflockKandangRepo := rProjectflock.NewProjectFlockKandangRepository(db)
|
projectflockKandangRepo := rProjectflock.NewProjectFlockKandangRepository(db)
|
||||||
|
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||||
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
@@ -35,7 +39,7 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid
|
|||||||
panic(fmt.Sprintf("failed to register project flock approval workflow: %v", err))
|
panic(fmt.Sprintf("failed to register project flock approval workflow: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, approvalService, validate)
|
projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, warehouseRepo, productWarehouseRepo, approvalService, validate)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
ProjectflockRoutes(router, userService, projectflockService)
|
ProjectflockRoutes(router, userService, projectflockService)
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
productWarehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
|
flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
|
||||||
kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||||
|
warehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
|
||||||
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
@@ -29,20 +30,23 @@ type ProjectflockService interface {
|
|||||||
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error)
|
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error)
|
||||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error)
|
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error)
|
||||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||||
GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, error)
|
GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, int, error)
|
||||||
|
GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error)
|
||||||
GetFlockPeriodSummary(ctx *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error)
|
GetFlockPeriodSummary(ctx *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error)
|
||||||
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
|
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type projectflockService struct {
|
type projectflockService struct {
|
||||||
Log *logrus.Logger
|
Log *logrus.Logger
|
||||||
Validate *validator.Validate
|
Validate *validator.Validate
|
||||||
Repository repository.ProjectflockRepository
|
Repository repository.ProjectflockRepository
|
||||||
FlockRepo flockRepository.FlockRepository
|
FlockRepo flockRepository.FlockRepository
|
||||||
KandangRepo kandangRepository.KandangRepository
|
KandangRepo kandangRepository.KandangRepository
|
||||||
PivotRepo repository.ProjectFlockKandangRepository
|
WarehouseRepo warehouseRepository.WarehouseRepository
|
||||||
ApprovalSvc commonSvc.ApprovalService
|
ProductWarehouseRepo productWarehouseRepository.ProductWarehouseRepository
|
||||||
approvalWorkflow approvalutils.ApprovalWorkflowKey
|
ProjectFlockKandangRepo repository.ProjectFlockKandangRepository
|
||||||
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
|
approvalWorkflow approvalutils.ApprovalWorkflowKey
|
||||||
}
|
}
|
||||||
|
|
||||||
type FlockPeriodSummary struct {
|
type FlockPeriodSummary struct {
|
||||||
@@ -54,19 +58,23 @@ func NewProjectflockService(
|
|||||||
repo repository.ProjectflockRepository,
|
repo repository.ProjectflockRepository,
|
||||||
flockRepo flockRepository.FlockRepository,
|
flockRepo flockRepository.FlockRepository,
|
||||||
kandangRepo kandangRepository.KandangRepository,
|
kandangRepo kandangRepository.KandangRepository,
|
||||||
pivotRepo repository.ProjectFlockKandangRepository,
|
ProjectFlockKandangRepo repository.ProjectFlockKandangRepository,
|
||||||
|
warehouseRepo warehouseRepository.WarehouseRepository,
|
||||||
|
productWarehouseRepo productWarehouseRepository.ProductWarehouseRepository,
|
||||||
approvalSvc commonSvc.ApprovalService,
|
approvalSvc commonSvc.ApprovalService,
|
||||||
validate *validator.Validate,
|
validate *validator.Validate,
|
||||||
) ProjectflockService {
|
) ProjectflockService {
|
||||||
return &projectflockService{
|
return &projectflockService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
Repository: repo,
|
Repository: repo,
|
||||||
FlockRepo: flockRepo,
|
FlockRepo: flockRepo,
|
||||||
KandangRepo: kandangRepo,
|
KandangRepo: kandangRepo,
|
||||||
PivotRepo: pivotRepo,
|
WarehouseRepo: warehouseRepo,
|
||||||
ApprovalSvc: approvalSvc,
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
approvalWorkflow: utils.ApprovalWorkflowProjectFlock,
|
ProjectFlockKandangRepo: ProjectFlockKandangRepo,
|
||||||
|
ApprovalSvc: approvalSvc,
|
||||||
|
approvalWorkflow: utils.ApprovalWorkflowProjectFlock,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -641,55 +649,48 @@ func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s projectflockService) GetProjectFlockKandang(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, error) {
|
func (s projectflockService) GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, int, error) {
|
||||||
// keep for backward compatibility; delegate to new consolidated method
|
|
||||||
return s.GetProjectFlockKandangByParams(ctx, fmt.Sprintf("%d", id), "", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s projectflockService) GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, error) {
|
availableStock, err := s.GetAvailableDocQuantity(ctx, kandangID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
pfk, err := s.PivotRepo.GetByProjectFlockAndKandang(ctx.Context(), projectFlockID, kandangID)
|
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByProjectFlockAndKandang(ctx.Context(), projectFlockID, kandangID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
|
return nil, 0, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
return pfk, nil
|
|
||||||
|
return projectFlockKandang, int(availableStock), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, error) {
|
func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) {
|
||||||
idStr = strings.TrimSpace(idStr)
|
|
||||||
projectFlockIdStr = strings.TrimSpace(projectFlockIdStr)
|
|
||||||
kandangIdStr = strings.TrimSpace(kandangIdStr)
|
|
||||||
|
|
||||||
if idStr != "" {
|
wh, err := s.WarehouseRepo.GetByKandangID(ctx.Context(), kandangID)
|
||||||
id, err := strconv.Atoi(idStr)
|
if err != nil {
|
||||||
if err != nil || id <= 0 {
|
return 0, err
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
|
||||||
}
|
|
||||||
pfk, err := s.PivotRepo.GetByID(ctx.Context(), uint(id))
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return pfk, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if projectFlockIdStr == "" || kandangIdStr == "" {
|
var productWarehouses []entity.ProductWarehouse
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Missing lookup parameters")
|
err = s.ProductWarehouseRepo.DB().
|
||||||
|
WithContext(ctx.Context()).
|
||||||
|
Joins("JOIN products ON products.id = product_warehouses.product_id").
|
||||||
|
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
|
||||||
|
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", "DOC", wh.Id).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Find(&productWarehouses).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
}
|
}
|
||||||
pfid, err := strconv.Atoi(projectFlockIdStr)
|
|
||||||
if err != nil || pfid <= 0 {
|
total := 0.0
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
for _, pw := range productWarehouses {
|
||||||
|
total += pw.Quantity
|
||||||
}
|
}
|
||||||
kid, err := strconv.Atoi(kandangIdStr)
|
return total, nil
|
||||||
if err != nil || kid <= 0 {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
|
|
||||||
}
|
|
||||||
return s.GetProjectFlockKandangByProjectAndKandang(ctx, uint(pfid), uint(kid))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error) {
|
func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error) {
|
||||||
@@ -784,7 +785,7 @@ func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
|
||||||
}
|
}
|
||||||
|
|
||||||
pivotRepo := s.pivotRepoWithTx(dbTransaction)
|
ProjectFlockKandangRepo := s.ProjectFlockKandangRepoWithTx(dbTransaction)
|
||||||
records := make([]*entity.ProjectFlockKandang, len(kandangIDs))
|
records := make([]*entity.ProjectFlockKandang, len(kandangIDs))
|
||||||
for i, id := range kandangIDs {
|
for i, id := range kandangIDs {
|
||||||
records[i] = &entity.ProjectFlockKandang{
|
records[i] = &entity.ProjectFlockKandang{
|
||||||
@@ -792,7 +793,7 @@ func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *
|
|||||||
KandangId: id,
|
KandangId: id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := pivotRepo.CreateMany(ctx, records); err != nil {
|
if err := ProjectFlockKandangRepo.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
|
||||||
@@ -814,15 +815,15 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.pivotRepoWithTx(dbTransaction).DeleteMany(ctx, projectFlockID, kandangIDs); err != nil {
|
if err := s.ProjectFlockKandangRepoWithTx(dbTransaction).DeleteMany(ctx, projectFlockID, kandangIDs); 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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s projectflockService) pivotRepoWithTx(dbTransaction *gorm.DB) repository.ProjectFlockKandangRepository {
|
func (s projectflockService) ProjectFlockKandangRepoWithTx(dbTransaction *gorm.DB) repository.ProjectFlockKandangRepository {
|
||||||
if s.PivotRepo == nil {
|
if s.ProjectFlockKandangRepo == nil {
|
||||||
return repository.NewProjectFlockKandangRepository(dbTransaction)
|
return repository.NewProjectFlockKandangRepository(dbTransaction)
|
||||||
}
|
}
|
||||||
return s.PivotRepo.WithTx(dbTransaction)
|
return s.ProjectFlockKandangRepo.WithTx(dbTransaction)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,10 +23,14 @@ func NewRecordingController(recordingService service.RecordingService) *Recordin
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *RecordingController) GetAll(c *fiber.Ctx) error {
|
func (u *RecordingController) GetAll(c *fiber.Ctx) error {
|
||||||
|
projectFlockID := c.QueryInt("project_flock_kandang_id", 0)
|
||||||
|
|
||||||
query := &validation.Query{
|
query := &validation.Query{
|
||||||
Page: c.QueryInt("page", 1),
|
Page: c.QueryInt("page", 1),
|
||||||
Limit: c.QueryInt("limit", 10),
|
Limit: c.QueryInt("limit", 10),
|
||||||
Search: c.Query("search", ""),
|
}
|
||||||
|
if projectFlockID > 0 {
|
||||||
|
query.ProjectFlockKandangId = uint(projectFlockID)
|
||||||
}
|
}
|
||||||
|
|
||||||
result, totalResults, err := u.RecordingService.GetAll(c, query)
|
result, totalResults, err := u.RecordingService.GetAll(c, query)
|
||||||
@@ -67,7 +71,30 @@ func (u *RecordingController) GetOne(c *fiber.Ctx) error {
|
|||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Get recording successfully",
|
Message: "Get recording successfully",
|
||||||
Data: dto.ToRecordingListDTO(*result),
|
Data: dto.ToRecordingDetailDTO(*result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *RecordingController) GetNextDay(c *fiber.Ctx) error {
|
||||||
|
projectFlockID := c.QueryInt("project_flock_kandang_id", 0)
|
||||||
|
if projectFlockID <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
nextDay, err := u.RecordingService.GetNextDay(c, uint(projectFlockID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Get next recording day successfully",
|
||||||
|
Data: fiber.Map{
|
||||||
|
"project_flock_kandang_id": projectFlockID,
|
||||||
|
"next_day": nextDay,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +115,7 @@ func (u *RecordingController) CreateOne(c *fiber.Ctx) error {
|
|||||||
Code: fiber.StatusCreated,
|
Code: fiber.StatusCreated,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Create recording successfully",
|
Message: "Create recording successfully",
|
||||||
Data: dto.ToRecordingListDTO(*result),
|
Data: dto.ToRecordingDetailDTO(*result),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +142,7 @@ func (u *RecordingController) UpdateOne(c *fiber.Ctx) error {
|
|||||||
Code: fiber.StatusOK,
|
Code: fiber.StatusOK,
|
||||||
Status: "success",
|
Status: "success",
|
||||||
Message: "Update recording successfully",
|
Message: "Update recording successfully",
|
||||||
Data: dto.ToRecordingListDTO(*result),
|
Data: dto.ToRecordingDetailDTO(*result),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,42 +10,102 @@ import (
|
|||||||
// === DTO Structs ===
|
// === DTO Structs ===
|
||||||
|
|
||||||
type RecordingBaseDTO struct {
|
type RecordingBaseDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
Name string `json:"name"`
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
|
||||||
|
RecordDatetime time.Time `json:"record_datetime"`
|
||||||
|
RecordDate *time.Time `json:"record_date,omitempty"`
|
||||||
|
Ontime bool `json:"ontime"`
|
||||||
|
Day *int `json:"day,omitempty"`
|
||||||
|
TotalDepletion *int `json:"total_depletion,omitempty"`
|
||||||
|
CumDepletionRate *float64 `json:"cum_depletion_rate,omitempty"`
|
||||||
|
DailyGain *float64 `json:"daily_gain,omitempty"`
|
||||||
|
AvgDailyGain *float64 `json:"avg_daily_gain,omitempty"`
|
||||||
|
CumIntake *int64 `json:"cum_intake,omitempty"`
|
||||||
|
FcrValue *float64 `json:"fcr_value,omitempty"`
|
||||||
|
TotalChick *int64 `json:"total_chick,omitempty"`
|
||||||
|
DailyDepletionRate *float64 `json:"daily_depletion_rate,omitempty"`
|
||||||
|
CumDepletion *int `json:"cum_depletion,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecordingListDTO struct {
|
type RecordingListDTO struct {
|
||||||
RecordingBaseDTO
|
RecordingBaseDTO
|
||||||
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
|
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecordingDetailDTO struct {
|
type RecordingDetailDTO struct {
|
||||||
RecordingListDTO
|
RecordingListDTO
|
||||||
|
BodyWeights []RecordingBodyWeightDTO `json:"body_weights"`
|
||||||
|
Depletions []RecordingDepletionDTO `json:"depletions"`
|
||||||
|
Stocks []RecordingStockDTO `json:"stocks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecordingBodyWeightDTO struct {
|
||||||
|
Weight float64 `json:"weight"`
|
||||||
|
Qty int `json:"qty"`
|
||||||
|
Notes *string `json:"notes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecordingDepletionDTO struct {
|
||||||
|
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||||
|
Total int64 `json:"total"`
|
||||||
|
Notes *string `json:"notes,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecordingStockDTO struct {
|
||||||
|
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||||
|
Increase *float64 `json:"increase,omitempty"`
|
||||||
|
Decrease *float64 `json:"decrease,omitempty"`
|
||||||
|
UsageAmount *int64 `json:"usage_amount,omitempty"`
|
||||||
|
Notes *string `json:"notes,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Mapper Functions ===
|
// === Mapper Functions ===
|
||||||
|
|
||||||
func ToRecordingBaseDTO(e entity.Recording) RecordingBaseDTO {
|
func ToRecordingBaseDTO(e entity.Recording) RecordingBaseDTO {
|
||||||
|
recordDate := e.RecordDate
|
||||||
|
if recordDate == nil {
|
||||||
|
rd := time.Date(
|
||||||
|
e.RecordDatetime.Year(),
|
||||||
|
e.RecordDatetime.Month(),
|
||||||
|
e.RecordDatetime.Day(),
|
||||||
|
0, 0, 0, 0,
|
||||||
|
e.RecordDatetime.Location(),
|
||||||
|
)
|
||||||
|
recordDate = &rd
|
||||||
|
}
|
||||||
return RecordingBaseDTO{
|
return RecordingBaseDTO{
|
||||||
Id: e.Id,
|
Id: e.Id,
|
||||||
Name: e.Name,
|
ProjectFlockKandangId: e.ProjectFlockKandangId,
|
||||||
|
RecordDatetime: e.RecordDatetime,
|
||||||
|
RecordDate: recordDate,
|
||||||
|
Ontime: e.Ontime == 1,
|
||||||
|
Day: e.Day,
|
||||||
|
TotalDepletion: e.TotalDepletion,
|
||||||
|
CumDepletionRate: e.CumDepletionRate,
|
||||||
|
DailyGain: e.DailyGain,
|
||||||
|
AvgDailyGain: e.AvgDailyGain,
|
||||||
|
CumIntake: e.CumIntake,
|
||||||
|
FcrValue: e.FcrValue,
|
||||||
|
TotalChick: e.TotalChick,
|
||||||
|
DailyDepletionRate: e.DailyDepletionRate,
|
||||||
|
CumDepletion: e.CumDepletion,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToRecordingListDTO(e entity.Recording) RecordingListDTO {
|
func ToRecordingListDTO(e entity.Recording) RecordingListDTO {
|
||||||
var createdUser *userDTO.UserBaseDTO
|
var createdUser *userDTO.UserBaseDTO
|
||||||
if e.CreatedUser.Id != 0 {
|
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
|
||||||
mapped := userDTO.ToUserBaseDTO(e.CreatedUser)
|
mapped := userDTO.ToUserBaseDTO(*e.CreatedUser)
|
||||||
createdUser = &mapped
|
createdUser = &mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
return RecordingListDTO{
|
return RecordingListDTO{
|
||||||
RecordingBaseDTO: ToRecordingBaseDTO(e),
|
RecordingBaseDTO: ToRecordingBaseDTO(e),
|
||||||
CreatedAt: e.CreatedAt,
|
CreatedAt: e.CreatedAt,
|
||||||
UpdatedAt: e.UpdatedAt,
|
UpdatedAt: e.UpdatedAt,
|
||||||
CreatedUser: createdUser,
|
CreatedUser: createdUser,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,5 +120,46 @@ func ToRecordingListDTOs(e []entity.Recording) []RecordingListDTO {
|
|||||||
func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO {
|
func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO {
|
||||||
return RecordingDetailDTO{
|
return RecordingDetailDTO{
|
||||||
RecordingListDTO: ToRecordingListDTO(e),
|
RecordingListDTO: ToRecordingListDTO(e),
|
||||||
|
BodyWeights: ToRecordingBodyWeightDTOs(e.BodyWeights),
|
||||||
|
Depletions: ToRecordingDepletionDTOs(e.Depletions),
|
||||||
|
Stocks: ToRecordingStockDTOs(e.Stocks),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ToRecordingBodyWeightDTOs(bodyWeights []entity.RecordingBW) []RecordingBodyWeightDTO {
|
||||||
|
result := make([]RecordingBodyWeightDTO, len(bodyWeights))
|
||||||
|
for i, bw := range bodyWeights {
|
||||||
|
result[i] = RecordingBodyWeightDTO{
|
||||||
|
Weight: bw.Weight,
|
||||||
|
Qty: bw.Qty,
|
||||||
|
Notes: bw.Notes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToRecordingDepletionDTOs(depletions []entity.RecordingDepletion) []RecordingDepletionDTO {
|
||||||
|
result := make([]RecordingDepletionDTO, len(depletions))
|
||||||
|
for i, d := range depletions {
|
||||||
|
result[i] = RecordingDepletionDTO{
|
||||||
|
ProductWarehouseId: d.ProductWarehouseId,
|
||||||
|
Total: d.Total,
|
||||||
|
Notes: d.Notes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToRecordingStockDTOs(stocks []entity.RecordingStock) []RecordingStockDTO {
|
||||||
|
result := make([]RecordingStockDTO, len(stocks))
|
||||||
|
for i, s := range stocks {
|
||||||
|
result[i] = RecordingStockDTO{
|
||||||
|
ProductWarehouseId: s.ProductWarehouseId,
|
||||||
|
Increase: s.Increase,
|
||||||
|
Decrease: s.Decrease,
|
||||||
|
UsageAmount: s.UsageAmount,
|
||||||
|
Notes: s.Notes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
|
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||||
sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
|
sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
|
||||||
|
|
||||||
@@ -16,11 +18,12 @@ type RecordingModule struct{}
|
|||||||
|
|
||||||
func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||||
recordingRepo := rRecording.NewRecordingRepository(db)
|
recordingRepo := rRecording.NewRecordingRepository(db)
|
||||||
|
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||||
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
|
|
||||||
recordingService := sRecording.NewRecordingService(recordingRepo, validate)
|
recordingService := sRecording.NewRecordingService(recordingRepo, projectFlockKandangRepo, productWarehouseRepo, validate)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
RecordingRoutes(router, userService, recordingService)
|
RecordingRoutes(router, userService, recordingService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ func RecordingRoutes(v1 fiber.Router, u user.UserService, s recording.RecordingS
|
|||||||
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
|
route.Get("/next-day", ctrl.GetNextDay)
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
route.Get("/:id", ctrl.GetOne)
|
route.Get("/:id", ctrl.GetOne)
|
||||||
route.Patch("/:id", ctrl.UpdateOne)
|
route.Patch("/:id", ctrl.UpdateOne)
|
||||||
|
|||||||
@@ -2,8 +2,15 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
|
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
@@ -17,27 +24,43 @@ import (
|
|||||||
type RecordingService interface {
|
type RecordingService interface {
|
||||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Recording, int64, error)
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Recording, int64, error)
|
||||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.Recording, error)
|
GetOne(ctx *fiber.Ctx, id uint) (*entity.Recording, error)
|
||||||
|
GetNextDay(ctx *fiber.Ctx, projectFlockKandangId uint) (int, error)
|
||||||
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Recording, error)
|
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Recording, error)
|
||||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Recording, error)
|
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Recording, error)
|
||||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type recordingService struct {
|
type recordingService struct {
|
||||||
Log *logrus.Logger
|
Log *logrus.Logger
|
||||||
Validate *validator.Validate
|
Validate *validator.Validate
|
||||||
Repository repository.RecordingRepository
|
Repository repository.RecordingRepository
|
||||||
|
ProjectFlockKandangRepo rProjectFlockKandang.ProjectFlockKandangRepository
|
||||||
|
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRecordingService(repo repository.RecordingRepository, validate *validator.Validate) RecordingService {
|
func NewRecordingService(
|
||||||
|
repo repository.RecordingRepository,
|
||||||
|
projectFlockKandangRepo rProjectFlockKandang.ProjectFlockKandangRepository,
|
||||||
|
productWarehouseRepo rProductWarehouse.ProductWarehouseRepository,
|
||||||
|
validate *validator.Validate,
|
||||||
|
) RecordingService {
|
||||||
return &recordingService{
|
return &recordingService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
Repository: repo,
|
Repository: repo,
|
||||||
|
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s recordingService) withRelations(db *gorm.DB) *gorm.DB {
|
func (s recordingService) withRelations(db *gorm.DB) *gorm.DB {
|
||||||
return db.Preload("CreatedUser")
|
return db.
|
||||||
|
Preload("CreatedUser").
|
||||||
|
Preload("ProjectFlockKandang").
|
||||||
|
Preload("ProjectFlockKandang.ProjectFlock").
|
||||||
|
Preload("BodyWeights").
|
||||||
|
Preload("Depletions").
|
||||||
|
Preload("Stocks")
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -45,14 +68,22 @@ func (s recordingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
|||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
offset := (params.Page - 1) * params.Limit
|
limit := params.Limit
|
||||||
|
if limit == 0 {
|
||||||
|
limit = 10
|
||||||
|
}
|
||||||
|
page := params.Page
|
||||||
|
if page == 0 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
offset := (page - 1) * limit
|
||||||
|
|
||||||
recordings, total, err := s.Repository.GetAll(c.Context(), offset, params.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.withRelations(db)
|
||||||
if params.Search != "" {
|
if params.ProjectFlockKandangId != 0 {
|
||||||
return db.Where("name LIKE ?", "%"+params.Search+"%")
|
db = db.Where("project_flock_id = ?", params.ProjectFlockKandangId)
|
||||||
}
|
}
|
||||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
return db.Order("record_datetime DESC").Order("created_at DESC")
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -74,21 +105,111 @@ func (s recordingService) GetOne(c *fiber.Ctx, id uint) (*entity.Recording, erro
|
|||||||
return recording, nil
|
return recording, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s recordingService) GetNextDay(c *fiber.Ctx, projectFlockKandangId uint) (int, error) {
|
||||||
|
if projectFlockKandangId == 0 {
|
||||||
|
return 0, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
db := s.Repository.DB().WithContext(c.Context())
|
||||||
|
next, err := s.generateNextDay(db, projectFlockKandangId)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to compute next recording day for project_flock_kandang_id=%d: %+v", projectFlockKandangId, err)
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return next, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Recording, error) {
|
func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Recording, error) {
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
createBody := &entity.Recording{
|
if _, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId); err != nil {
|
||||||
Name: req.Name,
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to get project flock kandang: %+v", err)
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
|
if err := s.ensureProductWarehousesExist(c, req.Stocks, req.Depletions); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tx := s.Repository.DB().WithContext(c.Context()).Begin()
|
||||||
|
if tx.Error != nil {
|
||||||
|
s.Log.Errorf("Failed to start recording transaction: %+v", tx.Error)
|
||||||
|
return nil, tx.Error
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
panic(r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
nextDay, err := s.generateNextDay(tx, req.ProjectFlockKandangId)
|
||||||
|
if err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
s.Log.Errorf("Failed to determine recording day: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTime := time.Now().UTC()
|
||||||
|
recordTime := currentTime
|
||||||
|
recordDate := time.Date(
|
||||||
|
recordTime.Year(),
|
||||||
|
recordTime.Month(),
|
||||||
|
recordTime.Day(),
|
||||||
|
0, 0, 0, 0,
|
||||||
|
recordTime.Location(),
|
||||||
|
)
|
||||||
|
ontimeFlag := computeOntime(recordTime, currentTime)
|
||||||
|
|
||||||
|
recording := &entity.Recording{
|
||||||
|
ProjectFlockKandangId: req.ProjectFlockKandangId,
|
||||||
|
RecordDatetime: recordTime,
|
||||||
|
RecordDate: &recordDate,
|
||||||
|
Ontime: boolToInt(ontimeFlag),
|
||||||
|
Day: &nextDay,
|
||||||
|
CreatedBy: 1, // TODO: replace with authenticated user
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Create(recording).Error; err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
s.Log.Errorf("Failed to create recording: %+v", err)
|
s.Log.Errorf("Failed to create recording: %+v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.GetOne(c, createBody.Id)
|
if err := s.persistBodyWeights(tx, recording.Id, req.BodyWeights); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
s.Log.Errorf("Failed to persist body weights: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := s.persistStocks(tx, recording.Id, req.Stocks); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
s.Log.Errorf("Failed to persist stocks: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := s.persistDepletions(tx, recording.Id, req.Depletions); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
s.Log.Errorf("Failed to persist depletions: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.computeAndUpdateMetrics(tx, recording); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
s.Log.Errorf("Failed to compute recording metrics: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit().Error; err != nil {
|
||||||
|
s.Log.Errorf("Failed to commit recording transaction: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.GetOne(c, recording.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Recording, error) {
|
func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Recording, error) {
|
||||||
@@ -96,21 +217,74 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBody := make(map[string]any)
|
tx := s.Repository.DB().WithContext(c.Context()).Begin()
|
||||||
|
if tx.Error != nil {
|
||||||
if req.Name != nil {
|
s.Log.Errorf("Failed to start recording transaction: %+v", tx.Error)
|
||||||
updateBody["name"] = *req.Name
|
return nil, tx.Error
|
||||||
}
|
}
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
panic(r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
if len(updateBody) == 0 {
|
var recording entity.Recording
|
||||||
return s.GetOne(c, id)
|
if err := tx.First(&recording, id).Error; err != nil {
|
||||||
}
|
_ = tx.Rollback()
|
||||||
|
|
||||||
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
|
||||||
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")
|
||||||
}
|
}
|
||||||
s.Log.Errorf("Failed to update recording: %+v", err)
|
s.Log.Errorf("Failed to find recording: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ontimeValue := boolToInt(computeOntime(recording.RecordDatetime, time.Now().UTC()))
|
||||||
|
if err := tx.Model(&entity.Recording{}).Where("id = ?", id).Update("ontime", ontimeValue).Error; err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
s.Log.Errorf("Failed to refresh ontime flag: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
recording.Ontime = ontimeValue
|
||||||
|
|
||||||
|
if req.BodyWeights != nil {
|
||||||
|
if err := s.replaceBodyWeights(tx, recording.Id, req.BodyWeights); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
s.Log.Errorf("Failed to update body weights: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.Stocks != nil {
|
||||||
|
if err := s.ensureProductWarehousesExist(c, req.Stocks, nil); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := s.replaceStocks(tx, recording.Id, req.Stocks); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
s.Log.Errorf("Failed to update stocks: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.Depletions != nil {
|
||||||
|
if err := s.ensureProductWarehousesExist(c, nil, req.Depletions); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := s.replaceDepletions(tx, recording.Id, req.Depletions); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
s.Log.Errorf("Failed to update depletions: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.computeAndUpdateMetrics(tx, &recording); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
s.Log.Errorf("Failed to recompute recording metrics: %+v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Commit().Error; err != nil {
|
||||||
|
s.Log.Errorf("Failed to commit recording transaction: %+v", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,3 +301,501 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Persistence Helpers ===
|
||||||
|
|
||||||
|
func (s *recordingService) ensureProductWarehousesExist(c *fiber.Ctx, stocks []validation.Stock, depletions []validation.Depletion) error {
|
||||||
|
idSet := make(map[uint]struct{})
|
||||||
|
|
||||||
|
for _, stock := range stocks {
|
||||||
|
if stock.ProductWarehouseId != 0 {
|
||||||
|
idSet[stock.ProductWarehouseId] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, dep := range depletions {
|
||||||
|
if dep.ProductWarehouseId != 0 {
|
||||||
|
idSet[dep.ProductWarehouseId] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(idSet) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for id := range idSet {
|
||||||
|
ok, err := s.ProductWarehouseRepo.ExistsByID(c.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to validate product warehouse %d: %+v", id, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product warehouse %d not found", id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return !recordDatetime.Before(reference)
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolToInt(v bool) int {
|
||||||
|
if v {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *recordingService) persistBodyWeights(tx *gorm.DB, recordingID uint, payload []validation.BodyWeight) error {
|
||||||
|
if len(payload) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyWeights := make([]entity.RecordingBW, len(payload))
|
||||||
|
for i, bw := range payload {
|
||||||
|
bodyWeights[i] = entity.RecordingBW{
|
||||||
|
RecordingId: recordingID,
|
||||||
|
Weight: bw.Weight,
|
||||||
|
Qty: bw.Qty,
|
||||||
|
Notes: bw.Notes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Create(&bodyWeights).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *recordingService) persistStocks(tx *gorm.DB, recordingID uint, payload []validation.Stock) error {
|
||||||
|
if len(payload) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
stocks := make([]entity.RecordingStock, len(payload))
|
||||||
|
for i, stock := range payload {
|
||||||
|
stocks[i] = entity.RecordingStock{
|
||||||
|
RecordingId: recordingID,
|
||||||
|
ProductWarehouseId: stock.ProductWarehouseId,
|
||||||
|
Notes: stock.Notes,
|
||||||
|
}
|
||||||
|
if stock.Increase != nil {
|
||||||
|
val := *stock.Increase
|
||||||
|
stocks[i].Increase = &val
|
||||||
|
}
|
||||||
|
if stock.Decrease != nil {
|
||||||
|
val := *stock.Decrease
|
||||||
|
stocks[i].Decrease = &val
|
||||||
|
}
|
||||||
|
if stock.UsageAmount != nil {
|
||||||
|
val := *stock.UsageAmount
|
||||||
|
stocks[i].UsageAmount = &val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Create(&stocks).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *recordingService) persistDepletions(tx *gorm.DB, recordingID uint, payload []validation.Depletion) error {
|
||||||
|
if len(payload) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
depletions := make([]entity.RecordingDepletion, len(payload))
|
||||||
|
for i, depl := range payload {
|
||||||
|
total := depl.Total
|
||||||
|
depletions[i] = entity.RecordingDepletion{
|
||||||
|
RecordingId: recordingID,
|
||||||
|
ProductWarehouseId: depl.ProductWarehouseId,
|
||||||
|
Total: total,
|
||||||
|
Notes: depl.Notes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
day := 0
|
||||||
|
if recording.Day != nil {
|
||||||
|
day = *recording.Day
|
||||||
|
}
|
||||||
|
|
||||||
|
totalDepletion, err := s.sumRecordingDepletions(tx, recording.Id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("sumRecordingDepletions: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
prevRecording, err := s.getPreviousRecording(tx, recording.ProjectFlockKandangId, day)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getPreviousRecording: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var prevCumDepletion int64
|
||||||
|
var prevCumIntake float64
|
||||||
|
var prevAvgWeight float64
|
||||||
|
if prevRecording != nil {
|
||||||
|
if prevRecording.CumDepletion != nil {
|
||||||
|
prevCumDepletion = int64(*prevRecording.CumDepletion)
|
||||||
|
}
|
||||||
|
if prevRecording.CumIntake != nil {
|
||||||
|
prevCumIntake = float64(*prevRecording.CumIntake)
|
||||||
|
}
|
||||||
|
prevAvgWeight, err = s.getAverageBodyWeight(tx, prevRecording.Id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getAverageBodyWeight(prev): %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
totalChick, err := s.getTotalChick(tx, recording.ProjectFlockKandangId)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getTotalChick: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentAvgWeight, err := s.getAverageBodyWeight(tx, recording.Id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getAverageBodyWeight(current): %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
usageInGrams, err := s.getFeedUsageInGrams(tx, recording.Id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getFeedUsageInGrams: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fcrId, err := s.getFcrID(tx, recording.ProjectFlockKandangId)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getFcrID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentAvgGrams := toGrams(currentAvgWeight)
|
||||||
|
currentAvgKg := gramsToKg(currentAvgGrams)
|
||||||
|
prevAvgGrams := toGrams(prevAvgWeight)
|
||||||
|
|
||||||
|
totalDepletionInt := int(totalDepletion)
|
||||||
|
cumDepletion := prevCumDepletion + totalDepletion
|
||||||
|
cumDepletionInt := int(cumDepletion)
|
||||||
|
|
||||||
|
updates := map[string]any{
|
||||||
|
"total_depletion": totalDepletionInt,
|
||||||
|
"cum_depletion": cumDepletionInt,
|
||||||
|
}
|
||||||
|
|
||||||
|
recording.TotalDepletion = &totalDepletionInt
|
||||||
|
recording.CumDepletion = &cumDepletionInt
|
||||||
|
|
||||||
|
if totalChick > 0 {
|
||||||
|
updates["total_chick"] = totalChick
|
||||||
|
recording.TotalChick = &totalChick
|
||||||
|
|
||||||
|
cumRate := (float64(cumDepletion) / float64(totalChick)) * 100
|
||||||
|
updates["cum_depletion_rate"] = cumRate
|
||||||
|
recording.CumDepletionRate = &cumRate
|
||||||
|
|
||||||
|
remainingAfter := totalChick - cumDepletion
|
||||||
|
if remainingAfter <= 0 {
|
||||||
|
remainingAfter = 1
|
||||||
|
}
|
||||||
|
dailyRate := (float64(totalDepletion) / float64(remainingAfter)) * 100
|
||||||
|
updates["daily_depletion_rate"] = dailyRate
|
||||||
|
recording.DailyDepletionRate = &dailyRate
|
||||||
|
} else {
|
||||||
|
updates["total_chick"] = gorm.Expr("NULL")
|
||||||
|
updates["cum_depletion_rate"] = gorm.Expr("NULL")
|
||||||
|
updates["daily_depletion_rate"] = gorm.Expr("NULL")
|
||||||
|
recording.TotalChick = nil
|
||||||
|
recording.CumDepletionRate = nil
|
||||||
|
recording.DailyDepletionRate = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentAvgGrams > 0 && prevAvgGrams > 0 {
|
||||||
|
dailyGainKg := (currentAvgGrams - prevAvgGrams) / 1000
|
||||||
|
updates["daily_gain"] = dailyGainKg
|
||||||
|
recording.DailyGain = &dailyGainKg
|
||||||
|
} else {
|
||||||
|
updates["daily_gain"] = gorm.Expr("NULL")
|
||||||
|
recording.DailyGain = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if fcrId != 0 && currentAvgKg > 0 && day > 0 {
|
||||||
|
if fcrWeightKg, ok, err := s.getFcrStandardWeightKg(tx, fcrId, currentAvgKg); err != nil {
|
||||||
|
return fmt.Errorf("getFcrStandardWeightKg: %w", err)
|
||||||
|
} else if ok {
|
||||||
|
avgDailyGain := (currentAvgKg - fcrWeightKg) / float64(day)
|
||||||
|
updates["avg_daily_gain"] = avgDailyGain
|
||||||
|
recording.AvgDailyGain = &avgDailyGain
|
||||||
|
} else {
|
||||||
|
updates["avg_daily_gain"] = gorm.Expr("NULL")
|
||||||
|
recording.AvgDailyGain = nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updates["avg_daily_gain"] = gorm.Expr("NULL")
|
||||||
|
recording.AvgDailyGain = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if usageInGrams > 0 && totalChick > 0 {
|
||||||
|
var cumIntakeValue float64
|
||||||
|
if prevRecording == nil || prevRecording.CumIntake == nil {
|
||||||
|
cumIntakeValue = usageInGrams / float64(totalChick)
|
||||||
|
} else {
|
||||||
|
remaining := float64(totalChick - cumDepletion)
|
||||||
|
if remaining <= 0 {
|
||||||
|
remaining = float64(totalChick)
|
||||||
|
}
|
||||||
|
cumIntakeValue = prevCumIntake + (usageInGrams / remaining)
|
||||||
|
}
|
||||||
|
cumIntakeRounded := int64(math.Round(cumIntakeValue))
|
||||||
|
updates["cum_intake"] = cumIntakeRounded
|
||||||
|
recording.CumIntake = &cumIntakeRounded
|
||||||
|
} else if prevRecording != nil && prevRecording.CumIntake != nil {
|
||||||
|
// Keep previous cumulative intake if no additional feed usage provided
|
||||||
|
updates["cum_intake"] = *prevRecording.CumIntake
|
||||||
|
recording.CumIntake = prevRecording.CumIntake
|
||||||
|
} else {
|
||||||
|
updates["cum_intake"] = gorm.Expr("NULL")
|
||||||
|
recording.CumIntake = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if usageInGrams > 0 && currentAvgKg > 0 {
|
||||||
|
feedUsageKg := usageInGrams / 1000
|
||||||
|
fcrValue := feedUsageKg / currentAvgKg
|
||||||
|
updates["fcr_value"] = fcrValue
|
||||||
|
recording.FcrValue = &fcrValue
|
||||||
|
} else {
|
||||||
|
updates["fcr_value"] = gorm.Expr("NULL")
|
||||||
|
recording.FcrValue = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Model(&entity.Recording{}).
|
||||||
|
Where("id = ?", recording.Id).
|
||||||
|
Updates(updates).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ===
|
||||||
|
|
||||||
|
func toGrams(weight float64) float64 {
|
||||||
|
if weight <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if weight > 10 {
|
||||||
|
return weight
|
||||||
|
}
|
||||||
|
return weight * 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
func gramsToKg(value float64) float64 {
|
||||||
|
if value <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return value / 1000
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,42 @@
|
|||||||
package validation
|
package validation
|
||||||
|
|
||||||
|
type (
|
||||||
|
BodyWeight struct {
|
||||||
|
Weight float64 `json:"weight" validate:"required"`
|
||||||
|
Qty int `json:"qty" validate:"required,number,min=1"`
|
||||||
|
Notes *string `json:"notes,omitempty" validate:"omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
Stock struct {
|
||||||
|
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
|
||||||
|
Increase *float64 `json:"increase,omitempty" validate:"omitempty"`
|
||||||
|
Decrease *float64 `json:"decrease,omitempty" validate:"omitempty"`
|
||||||
|
UsageAmount *int64 `json:"usage_amount,omitempty" validate:"omitempty,min=0"`
|
||||||
|
Notes *string `json:"notes,omitempty" validate:"omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
Depletion struct {
|
||||||
|
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
|
||||||
|
Total int64 `json:"total" validate:"required,number,min=0"`
|
||||||
|
Notes *string `json:"notes,omitempty" validate:"omitempty"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
type Create struct {
|
type Create struct {
|
||||||
Name string `json:"name" validate:"required_strict,min=3"`
|
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
|
||||||
|
BodyWeights []BodyWeight `json:"body_weights,omitempty" validate:"omitempty,dive"`
|
||||||
|
Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"`
|
||||||
|
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
type Update struct {
|
||||||
Name *string `json:"name,omitempty" validate:"omitempty"`
|
BodyWeights []BodyWeight `json:"body_weights,omitempty" validate:"omitempty,dive"`
|
||||||
|
Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"`
|
||||||
|
Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||||
Search string `query:"search" validate:"omitempty,max=50"`
|
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import "strings"
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
// NormalizeTrim returns the input string without leading/trailing whitespace.
|
// NormalizeTrim returns the input string without leading/trailing whitespace.
|
||||||
func NormalizeTrim(value string) string {
|
func NormalizeTrim(value string) string {
|
||||||
@@ -11,3 +14,36 @@ func NormalizeTrim(value string) string {
|
|||||||
func NormalizeUpper(value string) string {
|
func NormalizeUpper(value string) string {
|
||||||
return strings.ToUpper(NormalizeTrim(value))
|
return strings.ToUpper(NormalizeTrim(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NormalizeFlag trims whitespace, removes surrounding brackets/quotes and returns upper-case flag
|
||||||
|
func NormalizeFlag(value string) string {
|
||||||
|
v := NormalizeTrim(value)
|
||||||
|
v = strings.Trim(v, "[]\"'")
|
||||||
|
return strings.ToUpper(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseFlags parses a raw flags string like "[DOC, PAKAN]" or "DOC,PAKAN"
|
||||||
|
// and returns a deduplicated, sorted slice of normalized flags (upper-case, trimmed).
|
||||||
|
func ParseFlags(raw string) []string {
|
||||||
|
if raw == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parts := strings.Split(raw, ",")
|
||||||
|
set := make(map[string]struct{}, len(parts))
|
||||||
|
for _, p := range parts {
|
||||||
|
f := NormalizeFlag(p)
|
||||||
|
if f == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
set[f] = struct{}{}
|
||||||
|
}
|
||||||
|
if len(set) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
res := make([]string, 0, len(set))
|
||||||
|
for k := range set {
|
||||||
|
res = append(res, k)
|
||||||
|
}
|
||||||
|
sort.Strings(res)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ func (u *{{Pascal .Entity}}Controller) GetAll(c *fiber.Ctx) error {
|
|||||||
Search: c.Query("search", ""),
|
Search: c.Query("search", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if query.Page < 1 || query.Limit < 1 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
|
||||||
|
}
|
||||||
|
|
||||||
result, totalResults, err := u.{{Pascal .Entity}}Service.GetAll(c, query)
|
result, totalResults, err := u.{{Pascal .Entity}}Service.GetAll(c, query)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
Reference in New Issue
Block a user