mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 05:21:57 +00:00
Merge branch 'development-before-sso' of https://gitlab.com/mbugroup/lti-api into dev/teguh
This commit is contained in:
+30
@@ -0,0 +1,30 @@
|
||||
ALTER TABLE kandangs
|
||||
DROP CONSTRAINT IF EXISTS kandangs_project_flock_id_fkey;
|
||||
|
||||
ALTER TABLE kandangs DROP COLUMN IF EXISTS project_flock_id;
|
||||
|
||||
-- Only alter if tables exist
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN
|
||||
ALTER TABLE project_chickins
|
||||
DROP CONSTRAINT IF EXISTS fk_project_flock_kandang_id;
|
||||
ALTER TABLE project_chickins
|
||||
ADD CONSTRAINT fk_project_flock_kandang_id
|
||||
FOREIGN KEY (project_flock_kandang_id)
|
||||
REFERENCES project_flock_kandangs(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE;
|
||||
END IF;
|
||||
|
||||
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_populations') THEN
|
||||
ALTER TABLE project_flock_populations
|
||||
DROP CONSTRAINT IF EXISTS fk_project_flock_kandang_id;
|
||||
ALTER TABLE project_flock_populations
|
||||
ADD CONSTRAINT fk_project_flock_kandang_id
|
||||
FOREIGN KEY (project_flock_kandang_id)
|
||||
REFERENCES project_flock_kandangs(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE;
|
||||
END IF;
|
||||
END $$;
|
||||
@@ -0,0 +1,98 @@
|
||||
BEGIN;
|
||||
|
||||
DROP INDEX IF EXISTS project_flocks_base_period_unique;
|
||||
|
||||
ALTER TABLE project_flocks
|
||||
ADD COLUMN IF NOT EXISTS flock_id BIGINT;
|
||||
|
||||
WITH normalized AS (
|
||||
SELECT
|
||||
pf.id,
|
||||
COALESCE(
|
||||
NULLIF(TRIM(regexp_replace(pf.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')), ''),
|
||||
CONCAT('Project Flock ', pf.id)
|
||||
) AS normalized_name,
|
||||
COALESCE(NULLIF(pf.created_by, 0), 1) AS created_by
|
||||
FROM project_flocks pf
|
||||
),
|
||||
seed_flocks AS (
|
||||
SELECT DISTINCT
|
||||
n.normalized_name,
|
||||
MIN(n.created_by) AS created_by
|
||||
FROM normalized n
|
||||
GROUP BY n.normalized_name
|
||||
)
|
||||
INSERT INTO flocks (name, created_by, created_at, updated_at)
|
||||
SELECT sf.normalized_name, sf.created_by, NOW(), NOW()
|
||||
FROM seed_flocks sf
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
WITH normalized AS (
|
||||
SELECT
|
||||
pf.id,
|
||||
COALESCE(
|
||||
NULLIF(TRIM(regexp_replace(pf.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')), ''),
|
||||
CONCAT('Project Flock ', pf.id)
|
||||
) AS normalized_name
|
||||
FROM project_flocks pf
|
||||
),
|
||||
resolved AS (
|
||||
SELECT
|
||||
n.id,
|
||||
f.id AS flock_id
|
||||
FROM normalized n
|
||||
JOIN flocks f ON LOWER(f.name) = LOWER(n.normalized_name)
|
||||
)
|
||||
UPDATE project_flocks pf
|
||||
SET flock_id = resolved.flock_id
|
||||
FROM resolved
|
||||
WHERE pf.id = resolved.id;
|
||||
|
||||
WITH missing AS (
|
||||
SELECT
|
||||
pf.id,
|
||||
COALESCE(
|
||||
NULLIF(TRIM(regexp_replace(pf.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')), ''),
|
||||
CONCAT('Project Flock ', pf.id)
|
||||
) AS normalized_name,
|
||||
COALESCE(NULLIF(pf.created_by, 0), 1) AS created_by
|
||||
FROM project_flocks pf
|
||||
WHERE pf.flock_id IS NULL
|
||||
),
|
||||
seed_missing AS (
|
||||
SELECT DISTINCT normalized_name, created_by FROM missing
|
||||
)
|
||||
INSERT INTO flocks (name, created_by, created_at, updated_at)
|
||||
SELECT sm.normalized_name, sm.created_by, NOW(), NOW()
|
||||
FROM seed_missing sm
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
WITH missing AS (
|
||||
SELECT
|
||||
pf.id,
|
||||
COALESCE(
|
||||
NULLIF(TRIM(regexp_replace(pf.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')), ''),
|
||||
CONCAT('Project Flock ', pf.id)
|
||||
) AS normalized_name
|
||||
FROM project_flocks pf
|
||||
WHERE pf.flock_id IS NULL
|
||||
)
|
||||
UPDATE project_flocks pf
|
||||
SET flock_id = f.id
|
||||
FROM missing m
|
||||
JOIN flocks f ON LOWER(f.name) = LOWER(m.normalized_name)
|
||||
WHERE pf.id = m.id;
|
||||
|
||||
ALTER TABLE project_flocks
|
||||
ALTER COLUMN flock_id SET NOT NULL;
|
||||
|
||||
DROP INDEX IF EXISTS project_flocks_flock_name_unique;
|
||||
|
||||
ALTER TABLE project_flocks
|
||||
DROP COLUMN IF EXISTS flock_name;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS project_flocks_flock_period_unique
|
||||
ON project_flocks (flock_id, period)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,55 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE project_flocks
|
||||
ADD COLUMN IF NOT EXISTS flock_name VARCHAR(255);
|
||||
|
||||
WITH generated_names AS (
|
||||
SELECT
|
||||
pf.id,
|
||||
COALESCE(f.name, CONCAT('Project Flock ', pf.id)) AS base_name,
|
||||
pf.period,
|
||||
ROW_NUMBER() OVER (PARTITION BY COALESCE(f.name, CONCAT('Project Flock ', pf.id)) ORDER BY pf.id) AS rn
|
||||
FROM project_flocks pf
|
||||
LEFT JOIN flocks f ON f.id = pf.flock_id
|
||||
)
|
||||
UPDATE project_flocks pf
|
||||
SET flock_name = CASE
|
||||
WHEN gn.period IS NOT NULL THEN
|
||||
CASE
|
||||
WHEN gn.rn = 1 THEN CONCAT(gn.base_name, ' ', gn.period)
|
||||
ELSE CONCAT(gn.base_name, ' ', gn.period, ' ', gn.rn)
|
||||
END
|
||||
ELSE
|
||||
CASE
|
||||
WHEN gn.rn = 1 THEN gn.base_name
|
||||
ELSE CONCAT(gn.base_name, ' ', gn.rn)
|
||||
END
|
||||
END
|
||||
FROM generated_names gn
|
||||
WHERE pf.id = gn.id
|
||||
AND (pf.flock_name IS NULL OR pf.flock_name = '');
|
||||
|
||||
UPDATE project_flocks
|
||||
SET flock_name = CONCAT('Project Flock ', id)
|
||||
WHERE flock_name IS NULL OR flock_name = '';
|
||||
|
||||
ALTER TABLE project_flocks
|
||||
ALTER COLUMN flock_name SET NOT NULL;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS project_flocks_flock_name_unique
|
||||
ON project_flocks (flock_name)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
DROP INDEX IF EXISTS project_flocks_flock_period_unique;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS project_flocks_base_period_unique
|
||||
ON project_flocks (
|
||||
LOWER(TRIM(regexp_replace(flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g'))),
|
||||
period
|
||||
)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
ALTER TABLE project_flocks
|
||||
DROP COLUMN IF EXISTS flock_id;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,143 @@
|
||||
BEGIN;
|
||||
|
||||
-- Drop newly introduced egg tables
|
||||
DROP TABLE IF EXISTS grading_eggs;
|
||||
DROP TABLE IF EXISTS recording_eggs;
|
||||
|
||||
-- Revert recording_stocks structure
|
||||
ALTER TABLE recording_stocks
|
||||
DROP CONSTRAINT IF EXISTS chk_recording_stocks_nonneg;
|
||||
|
||||
ALTER TABLE recording_stocks
|
||||
DROP COLUMN IF EXISTS usage_qty,
|
||||
DROP COLUMN IF EXISTS pending_qty;
|
||||
|
||||
ALTER TABLE recording_stocks
|
||||
ADD COLUMN increase NUMERIC(10,3),
|
||||
ADD COLUMN decrease NUMERIC(10,3),
|
||||
ADD COLUMN usage_amount BIGINT,
|
||||
ADD COLUMN notes VARCHAR;
|
||||
|
||||
ALTER TABLE recording_stocks
|
||||
ADD 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)
|
||||
);
|
||||
|
||||
-- Revert recording_depletions structure
|
||||
ALTER TABLE recording_depletions
|
||||
DROP CONSTRAINT IF EXISTS chk_recording_depl_qty;
|
||||
|
||||
ALTER TABLE recording_depletions
|
||||
ALTER COLUMN qty TYPE BIGINT USING COALESCE(qty, 0)::BIGINT;
|
||||
|
||||
ALTER TABLE recording_depletions
|
||||
RENAME COLUMN qty TO total;
|
||||
|
||||
ALTER TABLE recording_depletions
|
||||
ADD COLUMN notes VARCHAR;
|
||||
|
||||
ALTER TABLE recording_depletions
|
||||
ADD CONSTRAINT chk_recording_depl_total CHECK (total >= 0);
|
||||
|
||||
-- Revert recording_bws structure
|
||||
ALTER TABLE recording_bws
|
||||
DROP CONSTRAINT IF EXISTS chk_recording_bws_nonneg;
|
||||
|
||||
ALTER TABLE recording_bws
|
||||
ALTER COLUMN qty TYPE INT USING COALESCE(qty, 0)::INT;
|
||||
|
||||
ALTER TABLE recording_bws
|
||||
DROP COLUMN IF EXISTS total_weight;
|
||||
|
||||
ALTER TABLE recording_bws
|
||||
ALTER COLUMN avg_weight TYPE NUMERIC(8,2) USING COALESCE(avg_weight, 0)::NUMERIC(8,2);
|
||||
|
||||
ALTER TABLE recording_bws
|
||||
RENAME COLUMN avg_weight TO weight;
|
||||
|
||||
ALTER TABLE recording_bws
|
||||
ADD COLUMN notes VARCHAR;
|
||||
|
||||
UPDATE recording_bws
|
||||
SET qty = GREATEST(qty, 1);
|
||||
|
||||
ALTER TABLE recording_bws
|
||||
ADD CONSTRAINT chk_recording_bws_nonneg CHECK (weight >= 0 AND qty >= 1);
|
||||
|
||||
-- Revert recordings header
|
||||
DROP INDEX IF EXISTS idx_recordings_flock_datetime;
|
||||
|
||||
ALTER TABLE recordings
|
||||
DROP CONSTRAINT IF EXISTS fk_recordings_project_flock_kandang,
|
||||
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v2;
|
||||
|
||||
ALTER TABLE recordings
|
||||
ALTER COLUMN total_depletion_qty TYPE INT USING COALESCE(total_depletion_qty, 0)::INT,
|
||||
ALTER COLUMN total_chick_qty TYPE BIGINT USING COALESCE(total_chick_qty, 0)::BIGINT;
|
||||
|
||||
ALTER TABLE recordings
|
||||
RENAME COLUMN total_depletion_qty TO total_depletion;
|
||||
|
||||
ALTER TABLE recordings
|
||||
RENAME COLUMN total_chick_qty TO total_chick;
|
||||
|
||||
ALTER TABLE recordings
|
||||
ADD COLUMN record_date DATE,
|
||||
ADD COLUMN status INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN ontime INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN daily_depletion_rate NUMERIC(7,3),
|
||||
ADD COLUMN cum_depletion INT;
|
||||
|
||||
ALTER TABLE recordings
|
||||
RENAME COLUMN project_flock_kandangs_id TO project_flock_id;
|
||||
|
||||
ALTER TABLE recordings
|
||||
ADD CONSTRAINT fk_recordings_project_flock
|
||||
FOREIGN KEY (project_flock_id) REFERENCES project_flock_kandangs(id);
|
||||
|
||||
ALTER TABLE recordings
|
||||
ADD CONSTRAINT chk_recordings_status CHECK (status IN (0,1,2,3));
|
||||
|
||||
ALTER TABLE recordings
|
||||
ADD CONSTRAINT chk_recordings_ontime CHECK (ontime IN (0,1));
|
||||
|
||||
ALTER TABLE recordings
|
||||
ADD 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)
|
||||
);
|
||||
|
||||
-- Ensure new columns carry derived data
|
||||
UPDATE recordings
|
||||
SET record_date = (record_datetime AT TIME ZONE 'Asia/Jakarta')::date
|
||||
WHERE record_date IS NULL;
|
||||
|
||||
-- Restore helper trigger/function and indexes
|
||||
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;
|
||||
|
||||
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 idx_recordings_flock_datetime
|
||||
ON recordings (project_flock_id, record_datetime);
|
||||
|
||||
CREATE UNIQUE INDEX uq_recordings_flock_record_date
|
||||
ON recordings (project_flock_id, record_date)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,168 @@
|
||||
BEGIN;
|
||||
|
||||
-- Drop trigger & helper function tied to record_date before removing the column
|
||||
DROP TRIGGER IF EXISTS recordings_set_record_date_trg ON recordings;
|
||||
DROP FUNCTION IF EXISTS trg_set_record_date();
|
||||
|
||||
-- Drop indexes and constraints that reference legacy columns
|
||||
DROP INDEX IF EXISTS uq_recordings_flock_record_date;
|
||||
DROP INDEX IF EXISTS idx_recordings_flock_datetime;
|
||||
|
||||
ALTER TABLE recordings
|
||||
DROP CONSTRAINT IF EXISTS fk_recordings_project_flock,
|
||||
DROP CONSTRAINT IF EXISTS chk_recordings_status,
|
||||
DROP CONSTRAINT IF EXISTS chk_recordings_ontime,
|
||||
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives;
|
||||
|
||||
-- Align recordings header with the new schema
|
||||
ALTER TABLE recordings
|
||||
RENAME COLUMN project_flock_id TO project_flock_kandangs_id;
|
||||
|
||||
ALTER TABLE recordings
|
||||
DROP COLUMN IF EXISTS record_date,
|
||||
DROP COLUMN IF EXISTS status,
|
||||
DROP COLUMN IF EXISTS ontime,
|
||||
DROP COLUMN IF EXISTS daily_depletion_rate,
|
||||
DROP COLUMN IF EXISTS cum_depletion;
|
||||
|
||||
ALTER TABLE recordings
|
||||
RENAME COLUMN total_depletion TO total_depletion_qty;
|
||||
|
||||
ALTER TABLE recordings
|
||||
RENAME COLUMN total_chick TO total_chick_qty;
|
||||
|
||||
ALTER TABLE recordings
|
||||
ALTER COLUMN total_depletion_qty TYPE NUMERIC(15,3) USING COALESCE(total_depletion_qty, 0)::NUMERIC(15,3),
|
||||
ALTER COLUMN total_chick_qty TYPE NUMERIC(15,3) USING COALESCE(total_chick_qty, 0)::NUMERIC(15,3),
|
||||
ALTER COLUMN cum_intake TYPE INT USING COALESCE(cum_intake, 0)::INT;
|
||||
|
||||
ALTER TABLE recordings
|
||||
ADD CONSTRAINT fk_recordings_project_flock_kandang
|
||||
FOREIGN KEY (project_flock_kandangs_id) REFERENCES project_flock_kandangs(id);
|
||||
|
||||
ALTER TABLE recordings
|
||||
ADD CONSTRAINT chk_recordings_nonnegatives_v2 CHECK (
|
||||
(total_depletion_qty IS NULL OR total_depletion_qty >= 0) AND
|
||||
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0) AND
|
||||
(daily_gain IS NULL OR daily_gain >= 0) AND
|
||||
(avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND
|
||||
(cum_intake IS NULL OR cum_intake >= 0) AND
|
||||
(fcr_value IS NULL OR fcr_value >= 0) AND
|
||||
(total_chick_qty IS NULL OR total_chick_qty >= 0)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_recordings_flock_datetime
|
||||
ON recordings (project_flock_kandangs_id, record_datetime);
|
||||
|
||||
-- recording_bws reshape
|
||||
ALTER TABLE recording_bws
|
||||
RENAME COLUMN weight TO avg_weight;
|
||||
|
||||
ALTER TABLE recording_bws
|
||||
ALTER COLUMN avg_weight TYPE NUMERIC(8,2) USING COALESCE(avg_weight, 0)::NUMERIC(8,2);
|
||||
|
||||
ALTER TABLE recording_bws
|
||||
ADD COLUMN total_weight NUMERIC(10,3);
|
||||
|
||||
UPDATE recording_bws
|
||||
SET total_weight = COALESCE(avg_weight, 0) * COALESCE(qty, 0);
|
||||
|
||||
ALTER TABLE recording_bws
|
||||
ALTER COLUMN total_weight SET NOT NULL;
|
||||
|
||||
ALTER TABLE recording_bws
|
||||
ALTER COLUMN qty TYPE NUMERIC(15,3) USING COALESCE(qty, 0)::NUMERIC(15,3);
|
||||
|
||||
ALTER TABLE recording_bws
|
||||
DROP COLUMN IF EXISTS notes;
|
||||
|
||||
ALTER TABLE recording_bws
|
||||
DROP CONSTRAINT IF EXISTS chk_recording_bws_nonneg;
|
||||
|
||||
ALTER TABLE recording_bws
|
||||
ADD CONSTRAINT chk_recording_bws_nonneg CHECK (
|
||||
avg_weight >= 0 AND qty >= 0 AND total_weight >= 0
|
||||
);
|
||||
|
||||
-- recording_depletions reshape
|
||||
ALTER TABLE recording_depletions
|
||||
RENAME COLUMN total TO qty;
|
||||
|
||||
ALTER TABLE recording_depletions
|
||||
ALTER COLUMN qty TYPE NUMERIC(15,3) USING COALESCE(qty, 0)::NUMERIC(15,3);
|
||||
|
||||
ALTER TABLE recording_depletions
|
||||
DROP COLUMN IF EXISTS notes;
|
||||
|
||||
ALTER TABLE recording_depletions
|
||||
DROP CONSTRAINT IF EXISTS chk_recording_depl_total;
|
||||
|
||||
ALTER TABLE recording_depletions
|
||||
ADD CONSTRAINT chk_recording_depl_qty CHECK (qty >= 0);
|
||||
|
||||
-- recording_stocks reshape
|
||||
ALTER TABLE recording_stocks
|
||||
DROP CONSTRAINT IF EXISTS chk_recording_stocks_nonneg;
|
||||
|
||||
ALTER TABLE recording_stocks
|
||||
DROP COLUMN IF EXISTS increase,
|
||||
DROP COLUMN IF EXISTS decrease,
|
||||
DROP COLUMN IF EXISTS usage_amount,
|
||||
DROP COLUMN IF EXISTS notes;
|
||||
|
||||
ALTER TABLE recording_stocks
|
||||
ADD COLUMN usage_qty NUMERIC(15,3),
|
||||
ADD COLUMN pending_qty NUMERIC(15,3);
|
||||
|
||||
ALTER TABLE recording_stocks
|
||||
ADD CONSTRAINT chk_recording_stocks_nonneg CHECK (
|
||||
(usage_qty IS NULL OR usage_qty >= 0) AND
|
||||
(pending_qty IS NULL OR pending_qty >= 0)
|
||||
);
|
||||
|
||||
-- recording_eggs table
|
||||
CREATE TABLE recording_eggs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
recording_id BIGINT NOT NULL,
|
||||
product_warehouse_id BIGINT NOT NULL,
|
||||
qty INT NOT NULL,
|
||||
created_by BIGINT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT fk_recording_eggs_recording
|
||||
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_recording_eggs_product_warehouse
|
||||
FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id),
|
||||
CONSTRAINT fk_recording_eggs_created_by
|
||||
FOREIGN KEY (created_by) REFERENCES users(id),
|
||||
CONSTRAINT chk_recording_eggs_qty CHECK (qty >= 0)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_recording_eggs_recording
|
||||
ON recording_eggs (recording_id);
|
||||
|
||||
CREATE INDEX idx_recording_eggs_product
|
||||
ON recording_eggs (product_warehouse_id);
|
||||
|
||||
-- grading_eggs table
|
||||
CREATE TABLE grading_eggs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
recording_egg_id BIGINT NOT NULL,
|
||||
qty NUMERIC(15,3) NOT NULL,
|
||||
grade VARCHAR,
|
||||
created_by BIGINT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT fk_grading_eggs_recording_egg
|
||||
FOREIGN KEY (recording_egg_id) REFERENCES recording_eggs(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_grading_eggs_created_by
|
||||
FOREIGN KEY (created_by) REFERENCES users(id),
|
||||
CONSTRAINT chk_grading_eggs_qty CHECK (qty >= 0)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_grading_eggs_recording_egg
|
||||
ON grading_eggs (recording_egg_id);
|
||||
|
||||
COMMIT;
|
||||
@@ -1,5 +0,0 @@
|
||||
DROP TABLE IF EXISTS project_chickin_details;
|
||||
|
||||
DROP TABLE IF EXISTS project_chickins;
|
||||
|
||||
DROP TABLE IF EXISTS project_flock_populations;
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -41,22 +40,15 @@ func Run(db *gorm.DB) error {
|
||||
return err
|
||||
}
|
||||
|
||||
flocks, err := seedFlocks(tx, adminID)
|
||||
if err != nil {
|
||||
if _, err := seedFlocks(tx, adminID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fcrs, err := seedFcr(tx, adminID)
|
||||
if err != nil {
|
||||
if _, err := seedFcr(tx, adminID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
projectFlocks, err := seedProjectFlocks(tx, adminID, flocks, areas, fcrs, locations)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
kandangs, err := seedKandangs(tx, adminID, locations, users, projectFlocks)
|
||||
kandangs, err := seedKandangs(tx, adminID, locations, users)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -93,10 +85,6 @@ func Run(db *gorm.DB) error {
|
||||
if err := seedTransferStock(tx, adminID); err != nil {
|
||||
return err
|
||||
}
|
||||
// if err := seedChickin(tx, adminID); err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
fmt.Println("✅ Master data seeding completed")
|
||||
return nil
|
||||
})
|
||||
@@ -243,159 +231,16 @@ func seedFlocks(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, fcrs, locations map[string]uint) (map[string]uint, error) {
|
||||
seeds := []struct {
|
||||
Key string
|
||||
Flock string
|
||||
Area string
|
||||
Category utils.ProjectFlockCategory
|
||||
Fcr string
|
||||
Location string
|
||||
Period int
|
||||
}{
|
||||
{
|
||||
Key: "Singaparna Period 1",
|
||||
Flock: "Flock Priangan",
|
||||
Area: "Priangan",
|
||||
Category: utils.ProjectFlockCategoryGrowing,
|
||||
Fcr: "FCR DOC",
|
||||
Location: "Singaparna",
|
||||
Period: 1,
|
||||
},
|
||||
{
|
||||
Key: "Cikaum Period 1",
|
||||
Flock: "Flock Banten",
|
||||
Area: "Banten",
|
||||
Category: utils.ProjectFlockCategoryGrowing,
|
||||
Fcr: "FCR DOC",
|
||||
Location: "Cikaum",
|
||||
Period: 1,
|
||||
},
|
||||
}
|
||||
|
||||
result := make(map[string]uint, len(seeds))
|
||||
|
||||
for _, seed := range seeds {
|
||||
flockID, ok := flocks[seed.Flock]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("floc %s not seeded", seed.Flock)
|
||||
}
|
||||
areaID, ok := areas[seed.Area]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("area %s not seeded", seed.Area)
|
||||
}
|
||||
fcrID, ok := fcrs[seed.Fcr]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("fcr %s not seeded", seed.Fcr)
|
||||
}
|
||||
locationID, ok := locations[seed.Location]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("location %s not seeded", seed.Location)
|
||||
}
|
||||
|
||||
var projectFlock entity.ProjectFlock
|
||||
err := tx.Where("flock_id = ? AND area_id = ? AND category = ? AND fcr_id = ? AND location_id = ? AND period = ?",
|
||||
flockID, areaID, seed.Category, fcrID, locationID, seed.Period).First(&projectFlock).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
projectFlock = entity.ProjectFlock{
|
||||
FlockId: flockID,
|
||||
AreaId: areaID,
|
||||
Category: string(seed.Category),
|
||||
FcrId: fcrID,
|
||||
LocationId: locationID,
|
||||
Period: seed.Period,
|
||||
CreatedBy: createdBy,
|
||||
}
|
||||
if err := tx.Create(&projectFlock).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
if err := tx.Model(&entity.ProjectFlock{}).Where("id = ?", projectFlock.Id).Updates(map[string]any{
|
||||
"flock_id": flockID,
|
||||
"area_id": areaID,
|
||||
"category": string(seed.Category),
|
||||
"fcr_id": fcrID,
|
||||
"location_id": locationID,
|
||||
"period": seed.Period,
|
||||
}).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := ensureProjectFlockApprovals(tx, projectFlock.Id, createdBy); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[seed.Key] = projectFlock.Id
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func ensureProjectFlockApprovals(tx *gorm.DB, projectFlockID uint, actorID uint) error {
|
||||
if projectFlockID == 0 || actorID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
workflow := utils.ApprovalWorkflowProjectFlock.String()
|
||||
|
||||
steps := []struct {
|
||||
step approvalutils.ApprovalStep
|
||||
action entity.ApprovalAction
|
||||
}{
|
||||
{step: utils.ProjectFlockStepPengajuan, action: entity.ApprovalActionCreated},
|
||||
{step: utils.ProjectFlockStepAktif, action: entity.ApprovalActionApproved},
|
||||
}
|
||||
|
||||
for _, cfg := range steps {
|
||||
var count int64
|
||||
if err := tx.Model(&entity.Approval{}).
|
||||
Where("approvable_type = ? AND approvable_id = ? AND step_number = ?", workflow, projectFlockID, uint16(cfg.step)).
|
||||
Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
stepName, ok := utils.ProjectFlockApprovalSteps[cfg.step]
|
||||
if !ok || strings.TrimSpace(stepName) == "" {
|
||||
stepName = fmt.Sprintf("Step %d", cfg.step)
|
||||
}
|
||||
|
||||
var actionPtr *entity.ApprovalAction
|
||||
action := cfg.action
|
||||
actionPtr = &action
|
||||
|
||||
record := entity.Approval{
|
||||
ApprovableType: workflow,
|
||||
ApprovableId: projectFlockID,
|
||||
StepNumber: uint16(cfg.step),
|
||||
StepName: stepName,
|
||||
Action: actionPtr,
|
||||
ActionBy: uintPtr(actorID),
|
||||
}
|
||||
|
||||
if err := tx.Create(&record).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users map[string]uint, projectFlocks map[string]uint) (map[string]uint, error) {
|
||||
func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users map[string]uint) (map[string]uint, error) {
|
||||
seeds := []struct {
|
||||
Name string
|
||||
Status utils.KandangStatus
|
||||
Location string
|
||||
PicKey string
|
||||
ProjectFlockKey *string
|
||||
}{
|
||||
{Name: "Singaparna 1", Status: utils.KandangStatusActive, Location: "Singaparna", PicKey: "admin", ProjectFlockKey: strPtr("Singaparna Period 1")},
|
||||
{Name: "Singaparna 1", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"},
|
||||
{Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"},
|
||||
{Name: "Cikaum 1", Status: utils.KandangStatusActive, Location: "Cikaum", PicKey: "admin", ProjectFlockKey: strPtr("Cikaum Period 1")},
|
||||
{Name: "Cikaum 1", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"},
|
||||
{Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"},
|
||||
}
|
||||
|
||||
@@ -411,15 +256,6 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
|
||||
return nil, fmt.Errorf("user %s not seeded", seed.PicKey)
|
||||
}
|
||||
|
||||
var projectFlockID *uint
|
||||
if seed.ProjectFlockKey != nil {
|
||||
pfID, ok := projectFlocks[*seed.ProjectFlockKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("project flock %s not seeded", *seed.ProjectFlockKey)
|
||||
}
|
||||
projectFlockID = uintPtr(pfID)
|
||||
}
|
||||
|
||||
var kandang entity.Kandang
|
||||
err := tx.Where("name = ?", seed.Name).First(&kandang).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -428,15 +264,11 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
|
||||
Status: string(seed.Status),
|
||||
LocationId: locID,
|
||||
PicId: picID,
|
||||
ProjectFlockId: projectFlockID,
|
||||
CreatedBy: createdBy,
|
||||
}
|
||||
if err := tx.Create(&kandang).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := syncPivotRelation(tx, projectFlockID, kandang.Id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
@@ -445,17 +277,9 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
|
||||
"pic_id": picID,
|
||||
"status": string(seed.Status),
|
||||
}
|
||||
if projectFlockID != nil {
|
||||
updates["project_flock_id"] = *projectFlockID
|
||||
} else {
|
||||
updates["project_flock_id"] = nil
|
||||
}
|
||||
if err := tx.Model(&entity.Kandang{}).Where("id = ?", kandang.Id).Updates(updates).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := syncPivotRelation(tx, projectFlockID, kandang.Id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
result[seed.Name] = kandang.Id
|
||||
}
|
||||
@@ -463,38 +287,6 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func syncPivotRelation(tx *gorm.DB, projectFlockID *uint, kandangID uint) error {
|
||||
if err := detachActivePivot(tx, kandangID); err != nil {
|
||||
return err
|
||||
}
|
||||
if projectFlockID == nil {
|
||||
return nil
|
||||
}
|
||||
return ensureActivePivot(tx, *projectFlockID, kandangID)
|
||||
}
|
||||
|
||||
func detachActivePivot(tx *gorm.DB, kandangID uint) error {
|
||||
return tx.Where("kandang_id = ?", kandangID).
|
||||
Delete(&entity.ProjectFlockKandang{}).Error
|
||||
}
|
||||
|
||||
func ensureActivePivot(tx *gorm.DB, projectFlockID, kandangID uint) error {
|
||||
var pivot entity.ProjectFlockKandang
|
||||
err := tx.Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID).
|
||||
First(&pivot).Error
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
newRecord := entity.ProjectFlockKandang{
|
||||
ProjectFlockId: projectFlockID,
|
||||
KandangId: kandangID,
|
||||
}
|
||||
return tx.Create(&newRecord).Error
|
||||
}
|
||||
|
||||
func seedWarehouses(tx *gorm.DB, createdBy uint, areas map[string]uint, locations map[string]uint, kandangs map[string]uint) error {
|
||||
seeds := []struct {
|
||||
Name string
|
||||
@@ -571,9 +363,10 @@ func seedProductCategories(tx *gorm.DB, createdBy uint) (map[string]uint, error)
|
||||
Name string
|
||||
Code string
|
||||
}{
|
||||
{"Pullet", "PLT"},
|
||||
{"Bahan Baku", "RAW"},
|
||||
{"Day Old Chick", "DOC"},
|
||||
{"Pullet", "PULLET"},
|
||||
{"Telur", "EGG"},
|
||||
}
|
||||
|
||||
result := make(map[string]uint, len(seeds))
|
||||
@@ -697,25 +490,14 @@ func seedFcr(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
|
||||
}
|
||||
}{
|
||||
{
|
||||
Name: "FCR DOC",
|
||||
Name: "FCR Layer",
|
||||
Standards: []struct {
|
||||
Weight float64
|
||||
FcrNumber float64
|
||||
Mortality float64
|
||||
}{
|
||||
{Weight: 0.1, FcrNumber: 1.20, Mortality: 1.0},
|
||||
{Weight: 0.3, FcrNumber: 1.35, Mortality: 1.5},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "FCR Pullet",
|
||||
Standards: []struct {
|
||||
Weight float64
|
||||
FcrNumber float64
|
||||
Mortality float64
|
||||
}{
|
||||
{Weight: 0.5, FcrNumber: 1.45, Mortality: 2.0},
|
||||
{Weight: 0.8, FcrNumber: 1.50, Mortality: 2.5},
|
||||
{Weight: 0.8, FcrNumber: 1.60, Mortality: 2.0},
|
||||
{Weight: 1.5, FcrNumber: 1.75, Mortality: 3.5},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -788,6 +570,56 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
|
||||
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
|
||||
Flags: []utils.FlagType{utils.FlagDOC},
|
||||
},
|
||||
{
|
||||
Name: "Ayam Pullet",
|
||||
Brand: "MBU Pullet",
|
||||
Sku: "PLT0001",
|
||||
Uom: "Ekor",
|
||||
Category: "Pullet",
|
||||
Price: 15000,
|
||||
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
|
||||
Flags: []utils.FlagType{utils.FlagPullet},
|
||||
},
|
||||
{
|
||||
Name: "Ayam Afkir",
|
||||
Brand: "-",
|
||||
Sku: "1",
|
||||
Uom: "Ekor",
|
||||
Category: "Day Old Chick",
|
||||
Price: 1,
|
||||
},
|
||||
{
|
||||
Name: "Ayam Mati",
|
||||
Brand: "-",
|
||||
Sku: "2",
|
||||
Uom: "Ekor",
|
||||
Category: "Day Old Chick",
|
||||
Price: 1,
|
||||
},
|
||||
{
|
||||
Name: "Ayam Culling",
|
||||
Brand: "-",
|
||||
Sku: "3",
|
||||
Uom: "Ekor",
|
||||
Category: "Day Old Chick",
|
||||
Price: 1,
|
||||
},
|
||||
{
|
||||
Name: "Telur Konsumsi Baik",
|
||||
Brand: "-",
|
||||
Sku: "4",
|
||||
Uom: "Unit",
|
||||
Category: "Telur",
|
||||
Price: 1,
|
||||
},
|
||||
{
|
||||
Name: "Telur Pecah",
|
||||
Brand: "-",
|
||||
Sku: "5",
|
||||
Uom: "Unit",
|
||||
Category: "Telur",
|
||||
Price: 1,
|
||||
},
|
||||
{
|
||||
Name: "281 SPECIAL STARTER",
|
||||
Brand: "281 STARTER",
|
||||
@@ -799,26 +631,6 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
|
||||
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
|
||||
Flags: []utils.FlagType{utils.FlagPakan, utils.FlagStarter},
|
||||
},
|
||||
{
|
||||
Name: "DOC MAlindo",
|
||||
Brand: "MAlindo",
|
||||
Sku: "MAL0001",
|
||||
Uom: "Ekor",
|
||||
Category: "Day Old Chick",
|
||||
Price: 8000,
|
||||
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
|
||||
Flags: []utils.FlagType{utils.FlagDOC},
|
||||
},
|
||||
{
|
||||
Name: "Ayam Pullet",
|
||||
Brand: "MBU Pullet",
|
||||
Sku: "PUL0001",
|
||||
Uom: "Ekor",
|
||||
Category: "Pullet",
|
||||
Price: 15000,
|
||||
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"},
|
||||
Flags: []utils.FlagType{utils.FlagPullet},
|
||||
},
|
||||
}
|
||||
|
||||
for _, seed := range seeds {
|
||||
@@ -1058,25 +870,44 @@ func seedBanks(tx *gorm.DB, createdBy uint) error {
|
||||
}
|
||||
|
||||
func seedProductWarehouse(tx *gorm.DB, createdBy uint) error {
|
||||
|
||||
seeds := []struct {
|
||||
ProductID uint
|
||||
WarehouseID uint
|
||||
ProductName string
|
||||
WarehouseName string
|
||||
Quantity float64
|
||||
}{
|
||||
{ProductID: 1, WarehouseID: 1, Quantity: 100},
|
||||
{ProductID: 2, WarehouseID: 2, Quantity: 200},
|
||||
{ProductID: 2, WarehouseID: 1, Quantity: 300},
|
||||
{ProductID: 1, WarehouseID: 3, Quantity: 5000},
|
||||
{ProductName: "DOC Broiler", WarehouseName: "Gudang Priangan", Quantity: 100},
|
||||
{ProductName: "281 SPECIAL STARTER", WarehouseName: "Gudang Singaparna", Quantity: 200},
|
||||
{ProductName: "281 SPECIAL STARTER", WarehouseName: "Gudang Banten", Quantity: 300},
|
||||
{ProductName: "DOC Broiler", WarehouseName: "Gudang Singaparna 1", Quantity: 5000},
|
||||
{ProductName: "Telur Konsumsi Baik", WarehouseName: "Gudang Singaparna 1", Quantity: 600},
|
||||
{ProductName: "Telur Pecah", WarehouseName: "Gudang Singaparna 1", Quantity: 80},
|
||||
{ProductName: "Telur Konsumsi Baik", WarehouseName: "Gudang Cikaum 1", Quantity: 450},
|
||||
{ProductName: "Telur Pecah", WarehouseName: "Gudang Cikaum 1", Quantity: 60},
|
||||
}
|
||||
|
||||
for _, seed := range seeds {
|
||||
var product entity.Product
|
||||
if err := tx.Where("name = ?", seed.ProductName).First(&product).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fmt.Errorf("product %q not found for product warehouse seeding", seed.ProductName)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var warehouse entity.Warehouse
|
||||
if err := tx.Where("name = ?", seed.WarehouseName).First(&warehouse).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fmt.Errorf("warehouse %q not found for product warehouse seeding", seed.WarehouseName)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var productWarehouse entity.ProductWarehouse
|
||||
err := tx.Where("product_id = ? AND warehouse_id = ?", seed.ProductID, seed.WarehouseID).First(&productWarehouse).Error
|
||||
err := tx.Where("product_id = ? AND warehouse_id = ?", product.Id, warehouse.Id).First(&productWarehouse).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
productWarehouse = entity.ProductWarehouse{
|
||||
ProductId: seed.ProductID,
|
||||
WarehouseId: seed.WarehouseID,
|
||||
ProductId: product.Id,
|
||||
WarehouseId: warehouse.Id,
|
||||
Quantity: seed.Quantity,
|
||||
CreatedBy: createdBy,
|
||||
}
|
||||
@@ -1085,6 +916,12 @@ func seedProductWarehouse(tx *gorm.DB, createdBy uint) error {
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
} else {
|
||||
if err := tx.Model(&productWarehouse).Updates(map[string]any{
|
||||
"quantity": seed.Quantity,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1165,153 +1002,6 @@ func seedTransferStock(tx *gorm.DB, createdBy uint) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// func seedChickin(tx *gorm.DB, createdBy uint) error {
|
||||
// seeds := []struct {
|
||||
// ProjectFlockKandangId uint
|
||||
// ChickInDate string
|
||||
// Quantity float64
|
||||
// Note string
|
||||
// }{
|
||||
// {ProjectFlockKandangId: 1, ChickInDate: "2025-10-20", Quantity: 100, Note: "Seeder chickin 1"},
|
||||
// {ProjectFlockKandangId: 2, ChickInDate: "2025-10-21", Quantity: 200, Note: "Seeder chickin 2"},
|
||||
// }
|
||||
|
||||
// for _, seed := range seeds {
|
||||
// chickinDate, err := time.Parse("2006-01-02", seed.ChickInDate)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// // Insert ProjectChickin jika belum ada
|
||||
// var chickin entity.ProjectChickin
|
||||
// err = tx.Where("project_flock_kandang_id = ? AND chick_in_date = ?", seed.ProjectFlockKandangId, chickinDate).
|
||||
// First(&chickin).Error
|
||||
// if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// chickin = entity.ProjectChickin{
|
||||
// ProjectFlockKandangId: seed.ProjectFlockKandangId,
|
||||
// ChickInDate: chickinDate,
|
||||
// Quantity: seed.Quantity,
|
||||
// Note: seed.Note,
|
||||
// CreatedBy: createdBy,
|
||||
// }
|
||||
// if err := tx.Create(&chickin).Error; err != nil {
|
||||
// return err
|
||||
// }
|
||||
// } else if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// var population entity.ProjectFlockPopulation
|
||||
// err = tx.Where("project_flock_kandang_id = ?", seed.ProjectFlockKandangId).First(&population).Error
|
||||
// if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// population = entity.ProjectFlockPopulation{
|
||||
// ProjectFlockKandangId: seed.ProjectFlockKandangId,
|
||||
// InitialQuantity: seed.Quantity,
|
||||
// CurrentQuantity: seed.Quantity,
|
||||
// ReservedQuantity: 0,
|
||||
// CreatedBy: createdBy,
|
||||
// }
|
||||
// if err := tx.Create(&population).Error; err != nil {
|
||||
// return err
|
||||
// }
|
||||
// } else if err != nil {
|
||||
// return err
|
||||
// } else {
|
||||
// // Update population quantities
|
||||
// if err := tx.Model(&entity.ProjectFlockPopulation{}).
|
||||
// Where("id = ?", population.Id).
|
||||
// Updates(map[string]any{
|
||||
// "initial_quantity": population.InitialQuantity + seed.Quantity,
|
||||
// "current_quantity": population.CurrentQuantity + seed.Quantity,
|
||||
// "reserved_quantity": 0,
|
||||
// }).Error; err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
|
||||
// var pfk entity.ProjectFlockKandang
|
||||
// if err := tx.Where("id = ?", seed.ProjectFlockKandangId).First(&pfk).Error; err != nil {
|
||||
// if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// // no pivot found; skip creating details
|
||||
// continue
|
||||
// }
|
||||
// return err
|
||||
// }
|
||||
|
||||
// var warehouse entity.Warehouse
|
||||
// if err := tx.Where("kandang_id = ?", pfk.KandangId).First(&warehouse).Error; err != nil {
|
||||
// // if warehouse not found, cannot create details
|
||||
// if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// continue
|
||||
// }
|
||||
// return err
|
||||
// }
|
||||
|
||||
// var productWarehouses []entity.ProductWarehouse
|
||||
// err = tx.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 = ?", "DOC", warehouse.Id).
|
||||
// Order("product_warehouses.created_at DESC").
|
||||
// Find(&productWarehouses).Error
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// // If no product warehouses found, keep existing chickin.Quantity and skip details
|
||||
// if len(productWarehouses) == 0 {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// // sum all pw quantities and set chickin.Quantity to that total (mimic CreateOne)
|
||||
// totalQty := 0.0
|
||||
// for _, pw := range productWarehouses {
|
||||
// totalQty += pw.Quantity
|
||||
// }
|
||||
|
||||
// if chickin.Quantity != totalQty {
|
||||
// if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Update("quantity", totalQty).Error; err != nil {
|
||||
// return err
|
||||
// }
|
||||
// chickin.Quantity = totalQty
|
||||
// }
|
||||
|
||||
// for _, pw := range productWarehouses {
|
||||
// // ensure detail exists or create it with full pw.Quantity
|
||||
// var detail entity.ProjectChickinDetail
|
||||
// err = tx.Where("project_chickin_id = ? AND product_warehouse_id = ?", chickin.Id, pw.Id).First(&detail).Error
|
||||
// if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// detail = entity.ProjectChickinDetail{
|
||||
// ProjectChickinId: chickin.Id,
|
||||
// ProductWarehouseId: pw.Id,
|
||||
// Quantity: pw.Quantity,
|
||||
// CreatedBy: createdBy,
|
||||
// }
|
||||
// if err := tx.Create(&detail).Error; err != nil {
|
||||
// return err
|
||||
// }
|
||||
// } else if err != nil {
|
||||
// return err
|
||||
// } else {
|
||||
// if detail.Quantity != pw.Quantity {
|
||||
// if err := tx.Model(&entity.ProjectChickinDetail{}).Where("id = ?", detail.Id).Update("quantity", pw.Quantity).Error; err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // zero out pw quantity
|
||||
// if err := tx.Model(&entity.ProductWarehouse{}).Where("id = ?", pw.Id).Update("quantity", 0).Error; err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// return nil
|
||||
// }
|
||||
|
||||
func ptr[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ type Kandang struct {
|
||||
Status string `gorm:"type:varchar(50);not null"`
|
||||
LocationId uint `gorm:"not null"`
|
||||
PicId uint `gorm:"not null"`
|
||||
ProjectFlockId *uint `gorm:"column:project_flock_id"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
@@ -20,5 +19,5 @@ type Kandang struct {
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
||||
Pic User `gorm:"foreignKey:PicId;references:Id"`
|
||||
ProjectFlock *ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
||||
ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"`
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ const ()
|
||||
|
||||
type ProjectChickin struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
ProjectFlockKandangId uint `gorm:"not null"`
|
||||
ProjectFlockKandangId uint `gorm:"not null;index;constraint:OnUpdate:CASCADE,OnDelete:CASCADE"`
|
||||
ChickInDate time.Time `gorm:"not null"`
|
||||
ProductWarehouseId uint `gorm:"not null"`
|
||||
UsageQty float64 `gorm:"type:numeric(15,3);not null"`
|
||||
|
||||
@@ -8,24 +8,24 @@ import (
|
||||
|
||||
type ProjectFlock struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
FlockId uint `gorm:"not null;uniqueIndex:project_flocks_flock_period_unique"`
|
||||
FlockName string `gorm:"type:varchar(255);not null;uniqueIndex"`
|
||||
AreaId uint `gorm:"not null"`
|
||||
Category string `gorm:"type:varchar(20);not null"`
|
||||
FcrId uint `gorm:"not null"`
|
||||
LocationId uint `gorm:"not null"`
|
||||
Period int `gorm:"not null;uniqueIndex:project_flocks_flock_period_unique"`
|
||||
Period int `gorm:"not null"`
|
||||
CreatedBy uint `gorm:"not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
|
||||
Flock Flock `gorm:"foreignKey:FlockId;references:Id"`
|
||||
Area Area `gorm:"foreignKey:AreaId;references:Id"`
|
||||
Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"`
|
||||
Location Location `gorm:"foreignKey:LocationId;references:Id"`
|
||||
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Kandangs []Kandang `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
||||
KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
||||
Kandangs []Kandang `gorm:"many2many:project_flock_kandangs;joinTableForeignKey:project_flock_id;joinTableReferences:kandang_id" json:"kandangs,omitempty"`
|
||||
KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id" json:"-"`
|
||||
|
||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
|
||||
@@ -8,20 +8,16 @@ import (
|
||||
|
||||
type Recording struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
ProjectFlockKandangId uint `gorm:"column:project_flock_id;not null;index"`
|
||||
ProjectFlockKandangId uint `gorm:"column:project_flock_kandangs_id;not null;index"`
|
||||
RecordDatetime time.Time `gorm:"column:record_datetime;not null"`
|
||||
RecordDate *time.Time `gorm:"column:record_date"`
|
||||
Ontime int `gorm:"column:ontime;not null;default:0"`
|
||||
Day *int `gorm:"column:day"`
|
||||
TotalDepletion *int `gorm:"column:total_depletion"`
|
||||
TotalDepletionQty *float64 `gorm:"column:total_depletion_qty"`
|
||||
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"`
|
||||
CumIntake *int `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"`
|
||||
TotalChickQty *float64 `gorm:"column:total_chick_qty"`
|
||||
CreatedBy uint `gorm:"column:created_by"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
@@ -32,4 +28,7 @@ type Recording struct {
|
||||
BodyWeights []RecordingBW `gorm:"foreignKey:RecordingId;references:Id"`
|
||||
Depletions []RecordingDepletion `gorm:"foreignKey:RecordingId;references:Id"`
|
||||
Stocks []RecordingStock `gorm:"foreignKey:RecordingId;references:Id"`
|
||||
Eggs []RecordingEgg `gorm:"foreignKey:RecordingId;references:Id"`
|
||||
|
||||
LatestApproval *Approval `gorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
package entities
|
||||
|
||||
import "time"
|
||||
@@ -6,9 +5,9 @@ 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"`
|
||||
AvgWeight float64 `gorm:"column:avg_weight;not null"`
|
||||
Qty float64 `gorm:"column:qty;not null"`
|
||||
TotalWeight float64 `gorm:"column:total_weight;not null"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
|
||||
|
||||
@@ -4,10 +4,8 @@ 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"`
|
||||
Qty float64 `gorm:"column:qty;not null"`
|
||||
|
||||
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package entities
|
||||
|
||||
import "time"
|
||||
|
||||
type RecordingEgg struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
RecordingId uint `gorm:"column:recording_id;not null;index"`
|
||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||
Qty int `gorm:"column:qty;not null"`
|
||||
CreatedBy uint `gorm:"column:created_by"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
GradingEggs []GradingEgg `gorm:"foreignKey:RecordingEggId;references:Id"`
|
||||
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||
}
|
||||
|
||||
type GradingEgg struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
RecordingEggId uint `gorm:"column:recording_egg_id;not null;index"`
|
||||
Qty float64 `gorm:"column:qty;not null"`
|
||||
Grade string `gorm:"column:grade;type:varchar(50)"`
|
||||
CreatedBy uint `gorm:"column:created_by"`
|
||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||||
|
||||
RecordingEgg RecordingEgg `gorm:"foreignKey:RecordingEggId;references:Id"`
|
||||
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
|
||||
}
|
||||
@@ -4,10 +4,8 @@ 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"`
|
||||
UsageQty *float64 `gorm:"column:usage_qty"`
|
||||
PendingQty *float64 `gorm:"column:pending_qty"`
|
||||
|
||||
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
|
||||
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
|
||||
|
||||
@@ -202,21 +202,7 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu
|
||||
if query.TransactionType != "" {
|
||||
db = db.Where("transaction_type = ?", strings.ToUpper(query.TransactionType))
|
||||
}
|
||||
if query.ProductID > 0 {
|
||||
db = db.Joins("JOIN product_warehouses ON product_warehouses.id = stock_logs.product_warehouse_id").
|
||||
Where("product_warehouses.product_id = ?", query.ProductID)
|
||||
}
|
||||
|
||||
if query.WarehouseID > 0 {
|
||||
if query.ProductID > 0 {
|
||||
|
||||
db = db.Where("product_warehouses.warehouse_id = ?", query.WarehouseID)
|
||||
} else {
|
||||
|
||||
db = db.Joins("JOIN product_warehouses ON product_warehouses.id = stock_logs.product_warehouse_id").
|
||||
Where("product_warehouses.warehouse_id = ?", query.WarehouseID)
|
||||
}
|
||||
}
|
||||
db = s.StockLogsRepository.ApplyProductWarehouseFilters(db, uint(query.ProductID), uint(query.WarehouseID))
|
||||
|
||||
return db.Order("created_at DESC")
|
||||
})
|
||||
|
||||
+88
-36
@@ -2,6 +2,7 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
@@ -17,47 +18,37 @@ type ProductWarehouseRepository interface {
|
||||
ExistsByID(ctx context.Context, id uint) (bool, error)
|
||||
GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error)
|
||||
GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error)
|
||||
GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) (*entity.ProductWarehouse, error)
|
||||
GetFirstProductByCategoryCode(ctx context.Context, categoryCode string) (*entity.Product, error)
|
||||
WithTxRepo(tx *gorm.DB) ProductWarehouseRepository
|
||||
DB() *gorm.DB
|
||||
GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error)
|
||||
GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error)
|
||||
GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error)
|
||||
ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB
|
||||
AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error
|
||||
}
|
||||
|
||||
type ProductWarehouseRepositoryImpl struct {
|
||||
*repository.BaseRepositoryImpl[entity.ProductWarehouse]
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewProductWarehouseRepository(db *gorm.DB) ProductWarehouseRepository {
|
||||
return &ProductWarehouseRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProductWarehouse](db),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) WithTxRepo(tx *gorm.DB) ProductWarehouseRepository {
|
||||
return &ProductWarehouseRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProductWarehouse](tx),
|
||||
db: tx,
|
||||
}
|
||||
}
|
||||
func (r *ProductWarehouseRepositoryImpl) DB() *gorm.DB {
|
||||
return r.db
|
||||
}
|
||||
func (r *ProductWarehouseRepositoryImpl) IsProductExist(ctx context.Context, productId uint) (bool, error) {
|
||||
return repository.Exists[entity.Product](ctx, r.db, productId)
|
||||
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)
|
||||
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)
|
||||
return repository.Exists[entity.ProductWarehouse](ctx, r.DB(), id)
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExists(ctx context.Context, productId, warehouseId uint, excludeID *uint) (bool, error) {
|
||||
var count int64
|
||||
query := r.db.WithContext(ctx).Model(&entity.ProductWarehouse{}).
|
||||
query := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}).
|
||||
Where("product_id = ? AND warehouse_id = ?", productId, warehouseId)
|
||||
if excludeID != nil {
|
||||
query = query.Where("id != ?", *excludeID)
|
||||
@@ -70,7 +61,7 @@ func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExists(ctx context.Cont
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error) {
|
||||
var count int64
|
||||
if err := r.db.WithContext(ctx).
|
||||
if err := r.DB().WithContext(ctx).
|
||||
Model(&entity.ProductWarehouse{}).
|
||||
Where("product_id = ? AND warehouse_id = ?", productId, warehouseId).
|
||||
Count(&count).Error; err != nil {
|
||||
@@ -89,34 +80,34 @@ func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehous
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) {
|
||||
var productWarehouses []entity.ProductWarehouse
|
||||
err := r.db.WithContext(ctx).
|
||||
Preload("Product").
|
||||
Preload("Warehouse").
|
||||
Table("product_warehouses").
|
||||
Select("product_warehouses.*").
|
||||
q := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}).
|
||||
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
|
||||
Order("product_warehouses.created_at DESC")
|
||||
|
||||
// preload relations so nested Product and Warehouse are populated
|
||||
err := q.Preload("Product").Preload("Warehouse").Find(&productWarehouses).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return productWarehouses, nil
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) (*entity.ProductWarehouse, error) {
|
||||
func (r *ProductWarehouseRepositoryImpl) GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error) {
|
||||
var productWarehouse entity.ProductWarehouse
|
||||
err := r.db.WithContext(ctx).
|
||||
Preload("Product").
|
||||
Preload("Warehouse").
|
||||
Table("product_warehouses").
|
||||
Select("product_warehouses.*").
|
||||
query := r.DB()
|
||||
if db != nil {
|
||||
query = db
|
||||
}
|
||||
fmt.Println(warehouseId)
|
||||
err := query.WithContext(ctx).
|
||||
Model(&entity.ProductWarehouse{}).
|
||||
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").
|
||||
Limit(1).
|
||||
Preload("Product").Preload("Warehouse").
|
||||
First(&productWarehouse).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -124,10 +115,44 @@ func (r *ProductWarehouseRepositoryImpl) GetLatestByCategoryCodeAndWarehouseID(c
|
||||
return &productWarehouse, nil
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB {
|
||||
if len(flags) == 0 {
|
||||
return db
|
||||
}
|
||||
|
||||
return 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 ?", flags)
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error {
|
||||
if len(deltas) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
base := r.DB().WithContext(ctx)
|
||||
if modifier != nil {
|
||||
base = modifier(base)
|
||||
}
|
||||
|
||||
for id, delta := range deltas {
|
||||
if delta == 0 {
|
||||
continue
|
||||
}
|
||||
if err := base.Model(&entity.ProductWarehouse{}).
|
||||
Where("id = ?", id).
|
||||
Update("quantity", gorm.Expr("COALESCE(quantity,0) + ?", delta)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) GetFirstProductByCategoryCode(ctx context.Context, categoryCode string) (*entity.Product, error) {
|
||||
var product entity.Product
|
||||
err := r.db.WithContext(ctx).
|
||||
Joins("JOIN product_categories ON products.product_category_id = product_categories.id").
|
||||
err := r.DB().WithContext(ctx).
|
||||
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
|
||||
Where("product_categories.code = ?", categoryCode).
|
||||
First(&product).Error
|
||||
if err != nil {
|
||||
@@ -135,3 +160,30 @@ func (r *ProductWarehouseRepositoryImpl) GetFirstProductByCategoryCode(ctx conte
|
||||
}
|
||||
return &product, nil
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error) {
|
||||
var productWarehouses []entity.ProductWarehouse
|
||||
err := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}).
|
||||
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 = ? AND product_warehouses.warehouse_id = ?", flagName, warehouseId).
|
||||
Order("product_warehouses.created_at DESC").
|
||||
Preload("Product").Preload("Warehouse").
|
||||
Find(&productWarehouses).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return productWarehouses, nil
|
||||
}
|
||||
|
||||
func (r *ProductWarehouseRepositoryImpl) GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error) {
|
||||
var product entity.Product
|
||||
err := r.DB().WithContext(ctx).
|
||||
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", "products").
|
||||
Where("flags.name = ?", flagName).
|
||||
First(&product).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &product, nil
|
||||
}
|
||||
|
||||
@@ -84,11 +84,7 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
|
||||
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)
|
||||
}
|
||||
db = s.Repository.ApplyFlagsFilter(db, cleanFlags)
|
||||
|
||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||
})
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
type FlockRepository interface {
|
||||
repository.BaseRepository[entity.Flock]
|
||||
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
|
||||
GetByName(ctx context.Context, name string) (*entity.Flock, error)
|
||||
}
|
||||
|
||||
type FlockRepositoryImpl struct {
|
||||
@@ -28,3 +29,15 @@ func NewFlockRepository(db *gorm.DB) FlockRepository {
|
||||
func (r *FlockRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
|
||||
return repository.ExistsByName[entity.Flock](ctx, r.db, name, excludeID)
|
||||
}
|
||||
|
||||
func (r *FlockRepositoryImpl) GetByName(ctx context.Context, name string) (*entity.Flock, error) {
|
||||
var flock entity.Flock
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("LOWER(name) = LOWER(?)", name).
|
||||
Where("deleted_at IS NULL").
|
||||
First(&flock).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &flock, nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
@@ -18,6 +19,8 @@ type KandangRepository interface {
|
||||
GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.Kandang, error)
|
||||
HasActiveKandangForProjectFlock(ctx context.Context, projectFlockID uint, excludeID *uint) (bool, error)
|
||||
UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error
|
||||
UpsertProjectFlockKandang(ctx context.Context, projectFlockID, kandangID uint) error
|
||||
UpdateStatusByIDs(ctx context.Context, kandangIDs []uint, status utils.KandangStatus) error
|
||||
}
|
||||
|
||||
type KandangRepositoryImpl struct {
|
||||
@@ -59,12 +62,13 @@ func (r *KandangRepositoryImpl) ProjectFlockExists(ctx context.Context, projectF
|
||||
func (r *KandangRepositoryImpl) HasActiveKandangForProjectFlock(ctx context.Context, projectFlockID uint, excludeID *uint) (bool, error) {
|
||||
var count int64
|
||||
q := r.db.WithContext(ctx).
|
||||
Model(&entity.Kandang{}).
|
||||
Where("project_flock_id = ?", projectFlockID).
|
||||
Where("status = ?", utils.KandangStatusActive).
|
||||
Where("deleted_at IS NULL")
|
||||
Table("kandangs k").
|
||||
Joins("JOIN project_flock_kandangs pfk ON pfk.kandang_id = k.id").
|
||||
Where("pfk.project_flock_id = ?", projectFlockID).
|
||||
Where("k.status = ?", utils.KandangStatusActive).
|
||||
Where("k.deleted_at IS NULL")
|
||||
if excludeID != nil {
|
||||
q = q.Where("id <> ?", *excludeID)
|
||||
q = q.Where("k.id <> ?", *excludeID)
|
||||
}
|
||||
if err := q.Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
@@ -75,17 +79,58 @@ func (r *KandangRepositoryImpl) HasActiveKandangForProjectFlock(ctx context.Cont
|
||||
func (r *KandangRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.Kandang, error) {
|
||||
kandang := new(entity.Kandang)
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("project_flock_id = ?", projectFlockID).
|
||||
First(kandang).Error
|
||||
Table("kandangs k").
|
||||
Joins("JOIN project_flock_kandangs pfk ON pfk.kandang_id = k.id").
|
||||
Where("pfk.project_flock_id = ?", projectFlockID).
|
||||
Where("k.deleted_at IS NULL").
|
||||
Order("k.id ASC").
|
||||
Limit(1).
|
||||
Find(kandang).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if kandang.Id == 0 {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
return kandang, nil
|
||||
}
|
||||
|
||||
func (r *KandangRepositoryImpl) UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error {
|
||||
sub := r.db.WithContext(ctx).
|
||||
Table("project_flock_kandangs").
|
||||
Select("kandang_id").
|
||||
Where("project_flock_id = ?", projectFlockID)
|
||||
|
||||
return r.db.WithContext(ctx).
|
||||
Model(&entity.Kandang{}).
|
||||
Where("project_flock_id = ?", projectFlockID).
|
||||
Where("id IN (?)", sub).
|
||||
Where("deleted_at IS NULL").
|
||||
Update("status", string(status)).Error
|
||||
}
|
||||
|
||||
func (r *KandangRepositoryImpl) UpsertProjectFlockKandang(ctx context.Context, projectFlockID, kandangID uint) error {
|
||||
var link entity.ProjectFlockKandang
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID).
|
||||
First(&link).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
link = entity.ProjectFlockKandang{
|
||||
ProjectFlockId: projectFlockID,
|
||||
KandangId: kandangID,
|
||||
}
|
||||
return r.db.WithContext(ctx).Create(&link).Error
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *KandangRepositoryImpl) UpdateStatusByIDs(ctx context.Context, kandangIDs []uint, status utils.KandangStatus) error {
|
||||
if len(kandangIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
return r.db.WithContext(ctx).
|
||||
Model(&entity.Kandang{}).
|
||||
Where("id IN ?", kandangIDs).
|
||||
Where("deleted_at IS NULL").
|
||||
Update("status", string(status)).Error
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@ func NewKandangService(repo repository.KandangRepository, validate *validator.Va
|
||||
}
|
||||
|
||||
func (s kandangService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("CreatedUser").Preload("Location").Preload("Pic")
|
||||
return db.Preload("CreatedUser").Preload("Location").Preload("Pic").Preload("ProjectFlockKandangs.ProjectFlock")
|
||||
|
||||
}
|
||||
|
||||
func (s kandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Kandang, int64, error) {
|
||||
@@ -110,7 +111,6 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid kandang status")
|
||||
}
|
||||
|
||||
var projectFlockID *uint
|
||||
if req.ProjectFlockId != nil {
|
||||
if exists, err := s.Repository.ProjectFlockExists(c.Context(), *req.ProjectFlockId); err != nil {
|
||||
s.Log.Errorf("Failed to check project flock existence: %+v", err)
|
||||
@@ -128,8 +128,6 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
||||
}
|
||||
}
|
||||
|
||||
idCopy := *req.ProjectFlockId
|
||||
projectFlockID = &idCopy
|
||||
}
|
||||
|
||||
//TODO: created by dummy
|
||||
@@ -138,7 +136,6 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
||||
LocationId: req.LocationId,
|
||||
Status: status,
|
||||
PicId: req.PicId,
|
||||
ProjectFlockId: projectFlockID,
|
||||
CreatedBy: 1,
|
||||
}
|
||||
|
||||
@@ -147,6 +144,12 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.ProjectFlockId != nil {
|
||||
if err := s.Repository.UpsertProjectFlockKandang(c.Context(), *req.ProjectFlockId, createBody.Id); err != nil {
|
||||
s.Log.Errorf("Failed to link kandang to project_flock via pivot: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to link kandang to project flock")
|
||||
}
|
||||
}
|
||||
return s.GetOne(c, createBody.Id)
|
||||
}
|
||||
|
||||
@@ -201,7 +204,6 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
||||
finalStatus = status
|
||||
}
|
||||
|
||||
projectFlockIDToUse := existing.ProjectFlockId
|
||||
if req.ProjectFlockId != nil {
|
||||
if exists, err := s.Repository.ProjectFlockExists(c.Context(), *req.ProjectFlockId); err != nil {
|
||||
s.Log.Errorf("Failed to check project flock existence: %+v", err)
|
||||
@@ -209,24 +211,19 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
||||
} else if !exists {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Project flock with id %d not found", *req.ProjectFlockId))
|
||||
}
|
||||
idCopy := *req.ProjectFlockId
|
||||
projectFlockIDToUse = &idCopy
|
||||
updateBody["project_flock_id"] = idCopy
|
||||
}
|
||||
|
||||
if projectFlockIDToUse != nil && finalStatus == string(utils.KandangStatusActive) {
|
||||
if active, err := s.Repository.HasActiveKandangForProjectFlock(c.Context(), *projectFlockIDToUse, &id); err != nil {
|
||||
s.Log.Errorf("Failed to check kandang activity for project flock %d: %+v", *projectFlockIDToUse, err)
|
||||
// Kalau status jadi ACTIVE, pastikan tidak ada kandang aktif lain pada project flock tsb (hitung via pivot)
|
||||
if finalStatus == string(utils.KandangStatusActive) {
|
||||
if active, err := s.Repository.HasActiveKandangForProjectFlock(c.Context(), *req.ProjectFlockId, &id); err != nil {
|
||||
s.Log.Errorf("Failed to check kandang activity for project flock %d: %+v", *req.ProjectFlockId, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check active kandang for project flock")
|
||||
} else if active {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, "Project flock already has an active kandang")
|
||||
}
|
||||
}
|
||||
|
||||
if len(updateBody) == 0 {
|
||||
return s.GetOne(c, id)
|
||||
}
|
||||
|
||||
if len(updateBody) > 0 {
|
||||
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Kandang not found")
|
||||
@@ -234,6 +231,14 @@ func (s kandangService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
||||
s.Log.Errorf("Failed to update kandang: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if req.ProjectFlockId != nil {
|
||||
if err := s.Repository.UpsertProjectFlockKandang(c.Context(), *req.ProjectFlockId, id); err != nil {
|
||||
s.Log.Errorf("Failed to upsert pivot kandang-project_flock: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to link kandang to project flock")
|
||||
}
|
||||
}
|
||||
|
||||
return s.GetOne(c, id)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
flockBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
|
||||
kandangBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
||||
locationBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
|
||||
pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
|
||||
userBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||
)
|
||||
|
||||
@@ -101,9 +102,9 @@ func ToUserBaseDTO(e entity.User) userBaseDTO.UserBaseDTO {
|
||||
|
||||
func ToProjectFlockDTO(e entity.ProjectFlock) ProjectFlockDTO {
|
||||
var flock *flockBaseDTO.FlockBaseDTO
|
||||
if e.Flock.Id != 0 {
|
||||
mapped := flockBaseDTO.ToFlockBaseDTO(e.Flock)
|
||||
flock = &mapped
|
||||
if base := pfutils.DeriveBaseName(e.FlockName); base != "" {
|
||||
summary := flockBaseDTO.FlockBaseDTO{Id: 0, Name: base}
|
||||
flock = &summary
|
||||
}
|
||||
var area *areaBaseDTO.AreaBaseDTO
|
||||
if e.Area.Id != 0 {
|
||||
|
||||
@@ -67,7 +67,6 @@ func (s chickinService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
Preload("ProjectFlockKandang.Kandang.Location.Area").
|
||||
Preload("ProjectFlockKandang.Kandang.Pic").
|
||||
Preload("ProjectFlockKandang.ProjectFlock").
|
||||
Preload("ProjectFlockKandang.ProjectFlock.Flock").
|
||||
Preload("ProjectFlockKandang.ProjectFlock.Area").
|
||||
Preload("ProjectFlockKandang.ProjectFlock.Fcr").
|
||||
Preload("ProjectFlockKandang.ProjectFlock.Location").
|
||||
@@ -538,12 +537,12 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
||||
|
||||
func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId uint, categoryCode string, dbTransaction *gorm.DB, actorID uint) (*entity.ProductWarehouse, error) {
|
||||
|
||||
products, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(ctx.Context(), categoryCode, warehouseId)
|
||||
products, err := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(ctx.Context(), categoryCode, warehouseId)
|
||||
if err == nil && len(products) > 0 {
|
||||
return &products[0], nil
|
||||
}
|
||||
|
||||
product, err := s.ProductWarehouseRepo.GetFirstProductByCategoryCode(ctx.Context(), categoryCode)
|
||||
product, err := s.ProductWarehouseRepo.GetFirstProductByFlag(ctx.Context(), categoryCode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get %s product: %w", categoryCode, err)
|
||||
}
|
||||
|
||||
@@ -83,7 +83,6 @@ func toProjectFlockDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFlockDTO
|
||||
return &ProjectFlockDTO{
|
||||
Id: pf.Id,
|
||||
Period: pf.Period,
|
||||
Flock: pf.Flock,
|
||||
Area: pf.Area,
|
||||
Category: pf.Category,
|
||||
Fcr: pf.Fcr,
|
||||
|
||||
@@ -222,11 +222,11 @@ func (u *ProjectflockController) Approval(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
func (u *ProjectflockController) GetFlockPeriodSummary(c *fiber.Ctx) error {
|
||||
param := c.Params("flock_id")
|
||||
param := c.Params("project_flock_kandang_id")
|
||||
|
||||
id, err := strconv.Atoi(param)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Flock Id")
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
|
||||
}
|
||||
|
||||
summary, err := u.ProjectflockService.GetFlockPeriodSummary(c, uint(id))
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
|
||||
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
||||
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
|
||||
|
||||
// pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
|
||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||
@@ -18,6 +20,7 @@ import (
|
||||
type ProjectFlockBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Period int `json:"period"`
|
||||
FlockName string `json:"flock_name"`
|
||||
}
|
||||
|
||||
type KandangWithProjectFlockIdDTO struct {
|
||||
@@ -29,12 +32,12 @@ type KandangWithProjectFlockIdDTO struct {
|
||||
|
||||
type ProjectFlockListDTO struct {
|
||||
ProjectFlockBaseDTO
|
||||
Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"`
|
||||
// Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"`
|
||||
Area *areaDTO.AreaBaseDTO `json:"area,omitempty"`
|
||||
Category string `json:"category"`
|
||||
Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"`
|
||||
Location *locationDTO.LocationBaseDTO `json:"location,omitempty"`
|
||||
Kandangs []KandangWithProjectFlockIdDTO `json:"kandangs,omitempty"`
|
||||
Kandangs []kandangDTO.KandangBaseDTO `json:"kandangs,omitempty"`
|
||||
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
@@ -57,32 +60,19 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
|
||||
createdUser = &mapped
|
||||
}
|
||||
|
||||
var kandangSummaries []KandangWithProjectFlockIdDTO
|
||||
var kandangSummaries []kandangDTO.KandangBaseDTO
|
||||
if len(e.Kandangs) > 0 {
|
||||
kandangSummaries = make([]KandangWithProjectFlockIdDTO, len(e.Kandangs))
|
||||
|
||||
// Create a map of KandangId -> ProjectFlockKandangId from KandangHistory
|
||||
kandangIdToProjectFlockKandangId := make(map[uint]uint)
|
||||
for _, kh := range e.KandangHistory {
|
||||
kandangIdToProjectFlockKandangId[kh.KandangId] = kh.Id
|
||||
}
|
||||
|
||||
kandangSummaries = make([]kandangDTO.KandangBaseDTO, len(e.Kandangs))
|
||||
for i, kandang := range e.Kandangs {
|
||||
baseDTO := kandangDTO.ToKandangBaseDTO(kandang)
|
||||
kandangSummaries[i] = KandangWithProjectFlockIdDTO{
|
||||
Id: baseDTO.Id,
|
||||
Name: baseDTO.Name,
|
||||
Status: baseDTO.Status,
|
||||
ProjectFlockKandangId: kandangIdToProjectFlockKandangId[kandang.Id],
|
||||
}
|
||||
kandangSummaries[i] = kandangDTO.ToKandangBaseDTO(kandang)
|
||||
}
|
||||
}
|
||||
|
||||
var flockSummary *flockDTO.FlockBaseDTO
|
||||
if e.Flock.Id != 0 {
|
||||
mapped := flockDTO.ToFlockBaseDTO(e.Flock)
|
||||
flockSummary = &mapped
|
||||
}
|
||||
// var flockSummary *flockDTO.FlockBaseDTO
|
||||
// if baseName := pfutils.DeriveBaseName(e.FlockName); baseName != "" {
|
||||
// summary := flockDTO.FlockBaseDTO{Id: 0, Name: baseName}
|
||||
// flockSummary = &summary
|
||||
// }
|
||||
|
||||
var areaSummary *areaDTO.AreaBaseDTO
|
||||
if e.Area.Id != 0 {
|
||||
@@ -110,7 +100,7 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
|
||||
|
||||
return ProjectFlockListDTO{
|
||||
ProjectFlockBaseDTO: createProjectFlockBaseDTO(e),
|
||||
Flock: flockSummary,
|
||||
// Flock: flockSummary,
|
||||
Area: areaSummary,
|
||||
Kandangs: kandangSummaries,
|
||||
Category: e.Category,
|
||||
@@ -166,6 +156,7 @@ func createProjectFlockBaseDTO(e entity.ProjectFlock) ProjectFlockBaseDTO {
|
||||
return ProjectFlockBaseDTO{
|
||||
Id: e.Id,
|
||||
Period: e.Period,
|
||||
FlockName: e.FlockName,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
|
||||
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
|
||||
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto"
|
||||
pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
|
||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||
)
|
||||
|
||||
@@ -50,13 +51,14 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
|
||||
ProjectFlockBaseDTO: ProjectFlockBaseDTO{
|
||||
Id: e.ProjectFlock.Id,
|
||||
Period: e.ProjectFlock.Period,
|
||||
FlockName: e.ProjectFlock.FlockName,
|
||||
},
|
||||
Category: e.ProjectFlock.Category,
|
||||
}
|
||||
|
||||
if e.ProjectFlock.Flock.Id != 0 {
|
||||
mapped := ToFlockSummaryDTO(e.ProjectFlock.Flock)
|
||||
pfLocal.Flock = &mapped
|
||||
if base := pfutils.DeriveBaseName(e.ProjectFlock.FlockName); base != "" {
|
||||
summary := flockDTO.FlockBaseDTO{Id: 0, Name: base}
|
||||
pfLocal.Flock = &summary
|
||||
}
|
||||
if e.ProjectFlock.Area.Id != 0 {
|
||||
mapped := areaDTO.ToAreaBaseDTO(e.ProjectFlock.Area)
|
||||
@@ -75,11 +77,6 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
|
||||
pfLocal.CreatedUser = &mapped
|
||||
}
|
||||
|
||||
pivotMap := make(map[uint]uint)
|
||||
for _, ph := range e.ProjectFlock.KandangHistory {
|
||||
pivotMap[ph.KandangId] = ph.Id
|
||||
}
|
||||
|
||||
for _, k := range e.ProjectFlock.Kandangs {
|
||||
kb := kandangDTO.ToKandangBaseDTO(k)
|
||||
pfLocal.Kandangs = append(pfLocal.Kandangs, KandangWithPivotDTO{
|
||||
|
||||
+184
-17
@@ -3,20 +3,30 @@ package repository
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
const baseNameExpression = "LOWER(TRIM(regexp_replace(flock_name, '\\\\s+\\\\d+(\\\\s+\\\\d+)*$', '', 'g')))"
|
||||
|
||||
type ProjectflockRepository interface {
|
||||
repository.BaseRepository[entity.ProjectFlock]
|
||||
GetAllByFlock(ctx context.Context, flockID uint) ([]entity.ProjectFlock, error)
|
||||
GetActiveByFlock(ctx context.Context, flockID uint) (*entity.ProjectFlock, error)
|
||||
GetMaxPeriodByFlock(ctx context.Context, flockID uint) (int, error)
|
||||
GetNextPeriodForFlock(ctx context.Context, flockID uint) (int, error)
|
||||
IdExists(ctx context.Context, id uint) (bool, error)
|
||||
GetAllByBaseName(ctx context.Context, baseName string) ([]entity.ProjectFlock, error)
|
||||
GetActiveByBaseName(ctx context.Context, baseName string) (*entity.ProjectFlock, error)
|
||||
GetMaxPeriodByBaseName(ctx context.Context, baseName string) (int, error)
|
||||
GetNextSequenceForBase(ctx context.Context, baseName string) (int, error)
|
||||
GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error)
|
||||
WithDefaultRelations() func(*gorm.DB) *gorm.DB
|
||||
ExistsByFlockName(ctx context.Context, flockName string, excludeID *uint) (bool, error)
|
||||
AreaExists(ctx context.Context, id uint) (bool, error)
|
||||
FcrExists(ctx context.Context, id uint) (bool, error)
|
||||
LocationExists(ctx context.Context, id uint) (bool, error)
|
||||
}
|
||||
|
||||
type ProjectflockRepositoryImpl struct {
|
||||
@@ -29,15 +39,11 @@ func NewProjectflockRepository(db *gorm.DB) ProjectflockRepository {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ProjectflockRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
|
||||
return repository.Exists[entity.ProjectFlock](ctx, r.DB(), id)
|
||||
}
|
||||
|
||||
func (r *ProjectflockRepositoryImpl) GetAllByFlock(ctx context.Context, flockID uint) ([]entity.ProjectFlock, error) {
|
||||
func (r *ProjectflockRepositoryImpl) GetAllByBaseName(ctx context.Context, baseName string) ([]entity.ProjectFlock, error) {
|
||||
var records []entity.ProjectFlock
|
||||
if err := r.DB().WithContext(ctx).
|
||||
Unscoped().
|
||||
Where("flock_id = ?", flockID).
|
||||
Where(baseNameExpression+" = LOWER(?)", baseName).
|
||||
Order("period ASC").
|
||||
Find(&records).Error; err != nil {
|
||||
return nil, err
|
||||
@@ -45,10 +51,10 @@ func (r *ProjectflockRepositoryImpl) GetAllByFlock(ctx context.Context, flockID
|
||||
return records, nil
|
||||
}
|
||||
|
||||
func (r *ProjectflockRepositoryImpl) GetActiveByFlock(ctx context.Context, flockID uint) (*entity.ProjectFlock, error) {
|
||||
func (r *ProjectflockRepositoryImpl) GetActiveByBaseName(ctx context.Context, baseName string) (*entity.ProjectFlock, error) {
|
||||
var record entity.ProjectFlock
|
||||
err := r.DB().WithContext(ctx).
|
||||
Where("flock_id = ?", flockID).
|
||||
Where(baseNameExpression+" = LOWER(?)", baseName).
|
||||
Order("period DESC").
|
||||
First(&record).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -60,11 +66,11 @@ func (r *ProjectflockRepositoryImpl) GetActiveByFlock(ctx context.Context, flock
|
||||
return &record, nil
|
||||
}
|
||||
|
||||
func (r *ProjectflockRepositoryImpl) GetMaxPeriodByFlock(ctx context.Context, flockID uint) (int, error) {
|
||||
func (r *ProjectflockRepositoryImpl) GetMaxPeriodByBaseName(ctx context.Context, baseName string) (int, error) {
|
||||
var max int
|
||||
if err := r.DB().WithContext(ctx).
|
||||
Model(&entity.ProjectFlock{}).
|
||||
Where("flock_id = ?", flockID).
|
||||
Where(baseNameExpression+" = LOWER(?)", baseName).
|
||||
Select("COALESCE(MAX(period), 0)").
|
||||
Scan(&max).Error; err != nil {
|
||||
return 0, err
|
||||
@@ -72,13 +78,13 @@ func (r *ProjectflockRepositoryImpl) GetMaxPeriodByFlock(ctx context.Context, fl
|
||||
return max, nil
|
||||
}
|
||||
|
||||
func (r *ProjectflockRepositoryImpl) GetNextPeriodForFlock(ctx context.Context, flockID uint) (int, error) {
|
||||
func (r *ProjectflockRepositoryImpl) GetNextSequenceForBase(ctx context.Context, baseName string) (int, error) {
|
||||
var payload struct {
|
||||
Period int
|
||||
}
|
||||
if err := r.DB().WithContext(ctx).
|
||||
Model(&entity.ProjectFlock{}).
|
||||
Where("flock_id = ?", flockID).
|
||||
Where(baseNameExpression+" = LOWER(?)", baseName).
|
||||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Order("period DESC").
|
||||
Limit(1).
|
||||
@@ -91,3 +97,164 @@ func (r *ProjectflockRepositoryImpl) GetNextPeriodForFlock(ctx context.Context,
|
||||
}
|
||||
return payload.Period + 1, nil
|
||||
}
|
||||
|
||||
func (r *ProjectflockRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
|
||||
return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB {
|
||||
db = r.withDefaultRelations(db)
|
||||
return r.applyQueryFilters(db, params)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *ProjectflockRepositoryImpl) WithDefaultRelations() func(*gorm.DB) *gorm.DB {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return r.withDefaultRelations(db)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ProjectflockRepositoryImpl) withDefaultRelations(db *gorm.DB) *gorm.DB {
|
||||
return db.
|
||||
Preload("CreatedUser").
|
||||
Preload("Area").
|
||||
Preload("Fcr").
|
||||
Preload("Location").
|
||||
Preload("Kandangs")
|
||||
}
|
||||
|
||||
func (r *ProjectflockRepositoryImpl) applyQueryFilters(db *gorm.DB, params *validation.Query) *gorm.DB {
|
||||
if params == nil {
|
||||
return db
|
||||
}
|
||||
|
||||
if params.AreaId > 0 {
|
||||
db = db.Where("project_flocks.area_id = ?", params.AreaId)
|
||||
}
|
||||
if params.LocationId > 0 {
|
||||
db = db.Where("project_flocks.location_id = ?", params.LocationId)
|
||||
}
|
||||
if params.Period > 0 {
|
||||
db = db.Where("project_flocks.period = ?", params.Period)
|
||||
}
|
||||
if len(params.KandangIds) > 0 {
|
||||
db = db.Where(`
|
||||
EXISTS (
|
||||
SELECT 1
|
||||
FROM project_flock_kandangs pfk
|
||||
WHERE pfk.project_flock_id = project_flocks.id
|
||||
AND pfk.kandang_id IN ?
|
||||
)`, params.KandangIds)
|
||||
}
|
||||
|
||||
db = r.applySearchFilters(db, params.Search)
|
||||
|
||||
for _, expr := range r.buildOrderExpressions(params.SortBy, params.SortOrder) {
|
||||
db = db.Order(expr)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func (r *ProjectflockRepositoryImpl) applySearchFilters(db *gorm.DB, rawSearch string) *gorm.DB {
|
||||
if rawSearch == "" {
|
||||
return db
|
||||
}
|
||||
|
||||
normalized := strings.ToLower(strings.TrimSpace(rawSearch))
|
||||
if normalized == "" {
|
||||
return db
|
||||
}
|
||||
|
||||
likeQuery := "%" + normalized + "%"
|
||||
return db.
|
||||
Joins("LEFT JOIN areas ON areas.id = project_flocks.area_id").
|
||||
Joins("LEFT JOIN fcrs ON fcrs.id = project_flocks.fcr_id").
|
||||
Joins("LEFT JOIN locations ON locations.id = project_flocks.location_id").
|
||||
Joins("LEFT JOIN users AS created_users ON created_users.id = project_flocks.created_by").
|
||||
Where(`
|
||||
LOWER(areas.name) LIKE ?
|
||||
OR LOWER(project_flocks.category) LIKE ?
|
||||
OR LOWER(fcrs.name) LIKE ?
|
||||
OR LOWER(locations.name) LIKE ?
|
||||
OR LOWER(locations.address) LIKE ?
|
||||
OR LOWER(created_users.name) LIKE ?
|
||||
OR LOWER(created_users.email) LIKE ?
|
||||
OR LOWER(project_flocks.flock_name) LIKE ?
|
||||
OR LOWER(TRIM(regexp_replace(project_flocks.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g'))) LIKE ?
|
||||
OR LOWER(CAST(project_flocks.period AS TEXT)) LIKE ?
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM kandangs
|
||||
WHERE kandangs.project_flock_id = project_flocks.id
|
||||
AND LOWER(kandangs.name) LIKE ?
|
||||
)
|
||||
`,
|
||||
likeQuery,
|
||||
likeQuery,
|
||||
likeQuery,
|
||||
likeQuery,
|
||||
likeQuery,
|
||||
likeQuery,
|
||||
likeQuery,
|
||||
likeQuery,
|
||||
likeQuery,
|
||||
likeQuery,
|
||||
likeQuery,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *ProjectflockRepositoryImpl) AreaExists(ctx context.Context, id uint) (bool, error) {
|
||||
return repository.Exists[entity.Area](ctx, r.DB(), id)
|
||||
}
|
||||
|
||||
func (r *ProjectflockRepositoryImpl) FcrExists(ctx context.Context, id uint) (bool, error) {
|
||||
return repository.Exists[entity.Fcr](ctx, r.DB(), id)
|
||||
}
|
||||
|
||||
func (r *ProjectflockRepositoryImpl) LocationExists(ctx context.Context, id uint) (bool, error) {
|
||||
return repository.Exists[entity.Location](ctx, r.DB(), id)
|
||||
}
|
||||
|
||||
func (r *ProjectflockRepositoryImpl) buildOrderExpressions(sortBy, sortOrder string) []string {
|
||||
direction := "ASC"
|
||||
if strings.ToLower(sortOrder) == "desc" {
|
||||
direction = "DESC"
|
||||
}
|
||||
|
||||
switch sortBy {
|
||||
case "area":
|
||||
return []string{
|
||||
fmt.Sprintf("(SELECT name FROM areas WHERE areas.id = project_flocks.area_id) %s", direction),
|
||||
fmt.Sprintf("project_flocks.id %s", direction),
|
||||
}
|
||||
case "location":
|
||||
return []string{
|
||||
fmt.Sprintf("(SELECT name FROM locations WHERE locations.id = project_flocks.location_id) %s", direction),
|
||||
fmt.Sprintf("project_flocks.id %s", direction),
|
||||
}
|
||||
case "kandangs":
|
||||
return []string{
|
||||
fmt.Sprintf("(SELECT COUNT(*) FROM project_flock_kandangs pfk WHERE pfk.project_flock_id = project_flocks.id) %s", direction),
|
||||
fmt.Sprintf("project_flocks.id %s", direction),
|
||||
}
|
||||
case "period":
|
||||
return []string{
|
||||
fmt.Sprintf("project_flocks.period %s", direction),
|
||||
fmt.Sprintf("project_flocks.id %s", direction),
|
||||
}
|
||||
default:
|
||||
return []string{
|
||||
"project_flocks.created_at DESC",
|
||||
"project_flocks.updated_at DESC",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ProjectflockRepositoryImpl) ExistsByFlockName(ctx context.Context, flockName string, excludeID *uint) (bool, error) {
|
||||
var count int64
|
||||
q := r.DB().WithContext(ctx).Model(&entity.ProjectFlock{}).Where("flock_name = ?", flockName)
|
||||
if excludeID != nil && *excludeID != 0 {
|
||||
q = q.Where("id <> ?", *excludeID)
|
||||
}
|
||||
if err := q.Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
+66
-3
@@ -2,6 +2,7 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
@@ -14,6 +15,10 @@ type ProjectFlockKandangRepository interface {
|
||||
CreateMany(ctx context.Context, records []*entity.ProjectFlockKandang) error
|
||||
DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error
|
||||
GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error)
|
||||
ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error)
|
||||
HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error)
|
||||
FindKandangsWithRecordings(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]entity.Kandang, error)
|
||||
MaxPeriodByBaseName(ctx context.Context, baseName string) (int, error)
|
||||
WithTx(tx *gorm.DB) ProjectFlockKandangRepository
|
||||
IdExists(ctx context.Context, id uint) (bool, error)
|
||||
DB() *gorm.DB
|
||||
@@ -23,6 +28,8 @@ type projectFlockKandangRepositoryImpl struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
const flockBaseNameExpression = "LOWER(TRIM(regexp_replace(project_flocks.flock_name, '\\s+\\d+(\\s+\\d+)*$', '', 'g')))"
|
||||
|
||||
func NewProjectFlockKandangRepository(db *gorm.DB) ProjectFlockKandangRepository {
|
||||
return &projectFlockKandangRepositoryImpl{db: db}
|
||||
}
|
||||
@@ -47,7 +54,6 @@ func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context) ([]entit
|
||||
var records []entity.ProjectFlockKandang
|
||||
if err := r.db.WithContext(ctx).
|
||||
Preload("ProjectFlock").
|
||||
Preload("ProjectFlock.Flock").
|
||||
Preload("ProjectFlock.Fcr").
|
||||
Preload("ProjectFlock.Area").
|
||||
Preload("ProjectFlock.Location").
|
||||
@@ -80,7 +86,6 @@ func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint
|
||||
record := new(entity.ProjectFlockKandang)
|
||||
if err := r.db.WithContext(ctx).
|
||||
Preload("ProjectFlock").
|
||||
Preload("ProjectFlock.Flock").
|
||||
Preload("ProjectFlock.Fcr").
|
||||
Preload("ProjectFlock.Area").
|
||||
Preload("ProjectFlock.Location").
|
||||
@@ -102,7 +107,6 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx cont
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID).
|
||||
Preload("ProjectFlock").
|
||||
Preload("ProjectFlock.Flock").
|
||||
Preload("ProjectFlock.Fcr").
|
||||
Preload("ProjectFlock.Area").
|
||||
Preload("ProjectFlock.Location").
|
||||
@@ -118,3 +122,62 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx cont
|
||||
}
|
||||
return record, nil
|
||||
}
|
||||
|
||||
func (r *projectFlockKandangRepositoryImpl) ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) {
|
||||
if len(kandangIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var existing []uint
|
||||
err := r.db.WithContext(ctx).
|
||||
Table("project_flock_kandangs").
|
||||
Where("project_flock_id = ? AND kandang_id IN ?", projectFlockID, kandangIDs).
|
||||
Pluck("kandang_id", &existing).Error
|
||||
return existing, err
|
||||
}
|
||||
|
||||
func (r *projectFlockKandangRepositoryImpl) HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error) {
|
||||
if len(kandangIDs) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
q := r.db.WithContext(ctx).
|
||||
Table("project_flock_kandangs").
|
||||
Where("kandang_id IN ?", kandangIDs)
|
||||
if exceptProjectID != nil {
|
||||
q = q.Where("project_flock_id <> ?", *exceptProjectID)
|
||||
}
|
||||
var count int64
|
||||
if err := q.Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (r *projectFlockKandangRepositoryImpl) FindKandangsWithRecordings(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]entity.Kandang, error) {
|
||||
if len(kandangIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var kandangs []entity.Kandang
|
||||
err := r.db.WithContext(ctx).
|
||||
Table("recordings AS r").
|
||||
Select("pfk.kandang_id AS id, COALESCE(k.name, '') AS name").
|
||||
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
|
||||
Joins("LEFT JOIN kandangs AS k ON k.id = pfk.kandang_id").
|
||||
Where("pfk.project_flock_id = ? AND pfk.kandang_id IN ?", projectFlockID, kandangIDs).
|
||||
Group("pfk.kandang_id, k.name").
|
||||
Scan(&kandangs).Error
|
||||
return kandangs, err
|
||||
}
|
||||
|
||||
func (r *projectFlockKandangRepositoryImpl) MaxPeriodByBaseName(ctx context.Context, baseName string) (int, error) {
|
||||
if strings.TrimSpace(baseName) == "" {
|
||||
return 0, nil
|
||||
}
|
||||
var max int
|
||||
err := r.db.WithContext(ctx).
|
||||
Table("project_flock_kandangs pfk").
|
||||
Joins("JOIN project_flocks pf ON pf.id = pfk.project_flock_id").
|
||||
Where(flockBaseNameExpression+" = LOWER(?)", baseName).
|
||||
Select("COALESCE(MAX(pf.period), 0)").
|
||||
Scan(&max).Error
|
||||
return max, err
|
||||
}
|
||||
|
||||
@@ -27,5 +27,6 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
|
||||
route.Delete("/:id", ctrl.DeleteOne)
|
||||
route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang)
|
||||
route.Post("/approvals", ctrl.Approval)
|
||||
route.Get("/flocks/:flock_id/periods", ctrl.GetFlockPeriodSummary)
|
||||
route.Get("/kandangs/:project_flock_kandang_id/periods", ctrl.GetFlockPeriodSummary)
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
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"
|
||||
pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
|
||||
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||
@@ -29,9 +31,9 @@ type ProjectflockService interface {
|
||||
GetOne(ctx *fiber.Ctx, id uint) (*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)
|
||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||
GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, int, error)
|
||||
GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error)
|
||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||
GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error)
|
||||
GetFlockPeriodSummary(ctx *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error)
|
||||
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
|
||||
}
|
||||
@@ -44,7 +46,7 @@ type projectflockService struct {
|
||||
KandangRepo kandangRepository.KandangRepository
|
||||
WarehouseRepo warehouseRepository.WarehouseRepository
|
||||
ProductWarehouseRepo productWarehouseRepository.ProductWarehouseRepository
|
||||
ProjectFlockKandangRepo repository.ProjectFlockKandangRepository
|
||||
PivotRepo repository.ProjectFlockKandangRepository
|
||||
ApprovalSvc commonSvc.ApprovalService
|
||||
approvalWorkflow approvalutils.ApprovalWorkflowKey
|
||||
}
|
||||
@@ -58,7 +60,7 @@ func NewProjectflockService(
|
||||
repo repository.ProjectflockRepository,
|
||||
flockRepo flockRepository.FlockRepository,
|
||||
kandangRepo kandangRepository.KandangRepository,
|
||||
ProjectFlockKandangRepo repository.ProjectFlockKandangRepository,
|
||||
pivotRepo repository.ProjectFlockKandangRepository,
|
||||
warehouseRepo warehouseRepository.WarehouseRepository,
|
||||
productWarehouseRepo productWarehouseRepository.ProductWarehouseRepository,
|
||||
approvalSvc commonSvc.ApprovalService,
|
||||
@@ -72,7 +74,7 @@ func NewProjectflockService(
|
||||
KandangRepo: kandangRepo,
|
||||
WarehouseRepo: warehouseRepo,
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
ProjectFlockKandangRepo: ProjectFlockKandangRepo,
|
||||
PivotRepo: pivotRepo,
|
||||
ApprovalSvc: approvalSvc,
|
||||
approvalWorkflow: utils.ApprovalWorkflowProjectFlock,
|
||||
}
|
||||
@@ -104,74 +106,11 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
|
||||
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
|
||||
projectflocks, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||
db = s.withRelations(db)
|
||||
|
||||
if params.AreaId > 0 {
|
||||
db = db.Where("project_flocks.area_id = ?", params.AreaId)
|
||||
}
|
||||
if params.LocationId > 0 {
|
||||
db = db.Where("project_flocks.location_id = ?", params.LocationId)
|
||||
}
|
||||
if params.Period > 0 {
|
||||
db = db.Where("project_flocks.period = ?", params.Period)
|
||||
}
|
||||
if len(params.KandangIds) > 0 {
|
||||
db = db.Where("EXISTS (SELECT 1 FROM kandangs WHERE kandangs.project_flock_id = project_flocks.id AND kandangs.id IN ?)", params.KandangIds)
|
||||
}
|
||||
|
||||
if params.Search != "" {
|
||||
normalizedSearch := strings.ToLower(strings.TrimSpace(params.Search))
|
||||
if normalizedSearch == "" {
|
||||
for _, expr := range s.buildOrderExpressions(params.SortBy, params.SortOrder) {
|
||||
db = db.Order(expr)
|
||||
}
|
||||
return db
|
||||
}
|
||||
likeQuery := "%" + normalizedSearch + "%"
|
||||
db = db.
|
||||
Joins("LEFT JOIN flocks ON flocks.id = project_flocks.flock_id").
|
||||
Joins("LEFT JOIN areas ON areas.id = project_flocks.area_id").
|
||||
Joins("LEFT JOIN fcrs ON fcrs.id = project_flocks.fcr_id").
|
||||
Joins("LEFT JOIN locations ON locations.id = project_flocks.location_id").
|
||||
Joins("LEFT JOIN users AS created_users ON created_users.id = project_flocks.created_by").
|
||||
Where(`
|
||||
LOWER(flocks.name) LIKE ?
|
||||
OR LOWER(areas.name) LIKE ?
|
||||
OR LOWER(project_flocks.category) LIKE ?
|
||||
OR LOWER(fcrs.name) LIKE ?
|
||||
OR LOWER(locations.name) LIKE ?
|
||||
OR LOWER(locations.address) LIKE ?
|
||||
OR LOWER(created_users.name) LIKE ?
|
||||
OR LOWER(created_users.email) LIKE ?
|
||||
OR LOWER(CAST(project_flocks.period AS TEXT)) LIKE ?
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM kandangs
|
||||
WHERE kandangs.project_flock_id = project_flocks.id
|
||||
AND LOWER(kandangs.name) LIKE ?
|
||||
)
|
||||
`,
|
||||
likeQuery,
|
||||
likeQuery,
|
||||
likeQuery,
|
||||
likeQuery,
|
||||
likeQuery,
|
||||
likeQuery,
|
||||
likeQuery,
|
||||
likeQuery,
|
||||
likeQuery,
|
||||
likeQuery,
|
||||
)
|
||||
}
|
||||
for _, expr := range s.buildOrderExpressions(params.SortBy, params.SortOrder) {
|
||||
db = db.Order(expr)
|
||||
}
|
||||
return db
|
||||
})
|
||||
projectflocks, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params)
|
||||
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to get projectflocks: %+v", err)
|
||||
return nil, 0, err
|
||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flocks")
|
||||
}
|
||||
|
||||
if s.ApprovalSvc != nil && len(projectflocks) > 0 {
|
||||
@@ -198,13 +137,13 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
|
||||
}
|
||||
|
||||
func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) {
|
||||
projectflock, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
||||
projectflock, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
|
||||
}
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed get projectflock by id: %+v", err)
|
||||
return nil, err
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
||||
}
|
||||
|
||||
if s.ApprovalSvc != nil {
|
||||
@@ -240,15 +179,28 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids is required")
|
||||
}
|
||||
|
||||
baseName := strings.TrimSpace(req.FlockName)
|
||||
if baseName == "" {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Flock name cannot be empty")
|
||||
}
|
||||
|
||||
if err := commonSvc.EnsureRelations(c.Context(),
|
||||
commonSvc.RelationCheck{Name: "Flock", ID: &req.FlockId, Exists: relationExistsChecker[entity.Flock](s.Repository.DB())},
|
||||
commonSvc.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: relationExistsChecker[entity.Area](s.Repository.DB())},
|
||||
commonSvc.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: relationExistsChecker[entity.Fcr](s.Repository.DB())},
|
||||
commonSvc.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: relationExistsChecker[entity.Location](s.Repository.DB())},
|
||||
commonSvc.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: s.Repository.AreaExists},
|
||||
commonSvc.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: s.Repository.FcrExists},
|
||||
commonSvc.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: s.Repository.LocationExists},
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
canonicalBase := baseName
|
||||
if s.FlockRepo != nil {
|
||||
baseFlock, err := s.ensureFlockByName(c.Context(), baseName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
canonicalBase = baseFlock.Name
|
||||
}
|
||||
|
||||
kandangIDs := uniqueUintSlice(req.KandangIds)
|
||||
kandangs, err := s.KandangRepo.GetByIDs(c.Context(), kandangIDs, nil)
|
||||
if err != nil {
|
||||
@@ -260,14 +212,14 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
||||
if len(kandangs) != len(kandangIDs) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
|
||||
}
|
||||
for _, kandang := range kandangs {
|
||||
if kandang.ProjectFlockId != nil {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Kandang %s sudah memiliki project flock", kandang.Name))
|
||||
}
|
||||
// larang kalau ada yg sudah terikat ke project lain
|
||||
if linked, err := s.pivotRepo().HasKandangsLinkedToOtherProject(c.Context(), kandangIDs, nil); err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage")
|
||||
} else if linked {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terikat dengan project flock lain")
|
||||
}
|
||||
|
||||
createBody := &entity.ProjectFlock{
|
||||
FlockId: req.FlockId,
|
||||
AreaId: req.AreaId,
|
||||
Category: cat,
|
||||
FcrId: req.FcrId,
|
||||
@@ -278,11 +230,16 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
projectRepo := repository.NewProjectflockRepository(dbTransaction)
|
||||
|
||||
period, err := projectRepo.GetNextPeriodForFlock(c.Context(), req.FlockId)
|
||||
nextSeq, err := projectRepo.GetNextSequenceForBase(c.Context(), canonicalBase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
createBody.Period = period
|
||||
generatedName, seq, err := s.generateSequentialFlockName(c.Context(), projectRepo, canonicalBase, nextSeq, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
createBody.FlockName = generatedName
|
||||
createBody.Period = seq
|
||||
|
||||
if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil {
|
||||
return err
|
||||
@@ -308,11 +265,14 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||
return nil, fiberErr
|
||||
}
|
||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, "Project flock period already exists")
|
||||
}
|
||||
s.Log.Errorf("Failed to create projectflock: %+v", err)
|
||||
return nil, err
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create project flock")
|
||||
}
|
||||
|
||||
return s.GetOne(c, createBody.Id)
|
||||
@@ -323,7 +283,7 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
|
||||
return nil, err
|
||||
}
|
||||
|
||||
existing, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
||||
existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
|
||||
}
|
||||
@@ -334,15 +294,28 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
|
||||
updateBody := make(map[string]any)
|
||||
hasBodyChanges := false
|
||||
var relationChecks []commonSvc.RelationCheck
|
||||
existingBase := pfutils.DeriveBaseName(existing.FlockName)
|
||||
targetBaseName := existingBase
|
||||
needFlockNameRegenerate := false
|
||||
|
||||
if req.FlockId != nil {
|
||||
updateBody["flock_id"] = *req.FlockId
|
||||
if req.FlockName != nil {
|
||||
trimmed := strings.TrimSpace(*req.FlockName)
|
||||
if trimmed == "" {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Flock name cannot be empty")
|
||||
}
|
||||
canonicalBase := trimmed
|
||||
if s.FlockRepo != nil {
|
||||
flockEntity, err := s.ensureFlockByName(c.Context(), trimmed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
canonicalBase = flockEntity.Name
|
||||
}
|
||||
if !strings.EqualFold(canonicalBase, existingBase) {
|
||||
needFlockNameRegenerate = true
|
||||
targetBaseName = canonicalBase
|
||||
hasBodyChanges = true
|
||||
relationChecks = append(relationChecks, commonSvc.RelationCheck{
|
||||
Name: "Flock",
|
||||
ID: req.FlockId,
|
||||
Exists: relationExistsChecker[entity.Flock](s.Repository.DB()),
|
||||
})
|
||||
}
|
||||
}
|
||||
if req.AreaId != nil {
|
||||
updateBody["area_id"] = *req.AreaId
|
||||
@@ -350,7 +323,7 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
|
||||
relationChecks = append(relationChecks, commonSvc.RelationCheck{
|
||||
Name: "Area",
|
||||
ID: req.AreaId,
|
||||
Exists: relationExistsChecker[entity.Area](s.Repository.DB()),
|
||||
Exists: s.Repository.AreaExists,
|
||||
})
|
||||
}
|
||||
if req.Category != nil {
|
||||
@@ -367,7 +340,7 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
|
||||
relationChecks = append(relationChecks, commonSvc.RelationCheck{
|
||||
Name: "FCR",
|
||||
ID: req.FcrId,
|
||||
Exists: relationExistsChecker[entity.Fcr](s.Repository.DB()),
|
||||
Exists: s.Repository.FcrExists,
|
||||
})
|
||||
}
|
||||
if req.LocationId != nil {
|
||||
@@ -376,7 +349,7 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
|
||||
relationChecks = append(relationChecks, commonSvc.RelationCheck{
|
||||
Name: "Location",
|
||||
ID: req.LocationId,
|
||||
Exists: relationExistsChecker[entity.Location](s.Repository.DB()),
|
||||
Exists: s.Repository.LocationExists,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -404,11 +377,12 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
|
||||
if len(kandangs) != len(newKandangIDs) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
|
||||
}
|
||||
for _, k := range kandangs {
|
||||
if k.ProjectFlockId != nil && *k.ProjectFlockId != id {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Kandang %s sudah terikat dengan project flock lain", k.Name))
|
||||
}
|
||||
if linked, err := s.pivotRepo().HasKandangsLinkedToOtherProject(c.Context(), newKandangIDs, &id); err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage")
|
||||
} else if linked {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terikat dengan project flock lain")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
hasChanges := hasBodyChanges || hasKandangChanges
|
||||
@@ -419,6 +393,29 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
projectRepo := repository.NewProjectflockRepository(dbTransaction)
|
||||
|
||||
baseForGeneration := targetBaseName
|
||||
if strings.TrimSpace(baseForGeneration) == "" {
|
||||
baseForGeneration = existingBase
|
||||
}
|
||||
if strings.TrimSpace(baseForGeneration) == "" {
|
||||
baseForGeneration = strings.TrimSpace(existing.FlockName)
|
||||
}
|
||||
|
||||
if needFlockNameRegenerate {
|
||||
nextSeq, err := projectRepo.GetNextSequenceForBase(c.Context(), baseForGeneration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newName, seq, err := s.generateSequentialFlockName(c.Context(), projectRepo, baseForGeneration, nextSeq, &id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
updateBody["flock_name"] = newName
|
||||
if seq != existing.Period {
|
||||
updateBody["period"] = seq
|
||||
}
|
||||
}
|
||||
|
||||
if len(updateBody) > 0 {
|
||||
if err := projectRepo.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
||||
return err
|
||||
@@ -507,7 +504,10 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to update projectflock %d: %+v", id, err)
|
||||
return nil, err
|
||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||
return nil, fiber.NewError(fiber.StatusConflict, "Project flock period already exists")
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock")
|
||||
}
|
||||
|
||||
return s.GetOne(c, id)
|
||||
@@ -611,7 +611,7 @@ func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([]
|
||||
}
|
||||
|
||||
func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
existing, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
|
||||
existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
|
||||
}
|
||||
@@ -645,28 +645,70 @@ func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
return fiberErr
|
||||
}
|
||||
s.Log.Errorf("Failed to delete projectflock %d: %+v", id, err)
|
||||
return err
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete project flock")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s projectflockService) GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, int, error) {
|
||||
func (s projectflockService) GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error) {
|
||||
|
||||
availableStock, err := s.GetAvailableDocQuantity(ctx, kandangID)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
projectFlockKandang, err := s.ProjectFlockKandangRepo.GetByProjectFlockAndKandang(ctx.Context(), projectFlockID, kandangID)
|
||||
pfk, err := s.PivotRepo.GetByProjectFlockAndKandang(ctx.Context(), projectFlockID, kandangID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, 0, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to fetch project_flock_kandang by project %d and kandang %d: %+v", projectFlockID, kandangID, err)
|
||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
|
||||
}
|
||||
|
||||
availableQuantity, err := s.GetAvailableDocQuantity(ctx, pfk.KandangId)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return projectFlockKandang, int(availableStock), nil
|
||||
return pfk, availableQuantity, nil
|
||||
}
|
||||
|
||||
func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, float64, error) {
|
||||
idStr = strings.TrimSpace(idStr)
|
||||
projectFlockIdStr = strings.TrimSpace(projectFlockIdStr)
|
||||
kandangIdStr = strings.TrimSpace(kandangIdStr)
|
||||
|
||||
if idStr != "" {
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil || id <= 0 {
|
||||
return nil, 0, 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, 0, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to fetch project_flock_kandang %d: %+v", id, err)
|
||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
|
||||
}
|
||||
|
||||
availableQuantity, err := s.GetAvailableDocQuantity(ctx, pfk.KandangId)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return pfk, availableQuantity, nil
|
||||
}
|
||||
|
||||
if projectFlockIdStr == "" || kandangIdStr == "" {
|
||||
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Missing lookup parameters")
|
||||
}
|
||||
pfid, err := strconv.Atoi(projectFlockIdStr)
|
||||
if err != nil || pfid <= 0 {
|
||||
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
|
||||
}
|
||||
kid, err := strconv.Atoi(kandangIdStr)
|
||||
if err != nil || kid <= 0 {
|
||||
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
|
||||
}
|
||||
return s.GetProjectFlockKandangByProjectAndKandang(ctx, uint(pfid), uint(kid))
|
||||
}
|
||||
|
||||
func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) {
|
||||
@@ -676,14 +718,7 @@ func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID u
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var productWarehouses []entity.ProductWarehouse
|
||||
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
|
||||
productWarehouses, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(ctx.Context(), "DOC", wh.Id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -695,26 +730,55 @@ func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID u
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error) {
|
||||
flock, err := s.FlockRepo.GetByID(c.Context(), flockID, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("CreatedUser")
|
||||
})
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Flock not found")
|
||||
}
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed get flock %d for period summary: %+v", flockID, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch flock")
|
||||
func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, projectFlockKandangID uint) (*FlockPeriodSummary, error) {
|
||||
if projectFlockKandangID == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required")
|
||||
}
|
||||
|
||||
maxPeriod, err := s.Repository.GetMaxPeriodByFlock(c.Context(), flockID)
|
||||
pivot, err := s.pivotRepo().GetByID(c.Context(), projectFlockKandangID)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found")
|
||||
}
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to compute next period for flock %d: %+v", flockID, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to compute next period")
|
||||
s.Log.Errorf("Failed to fetch project_flock_kandang %d: %+v", projectFlockKandangID, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
|
||||
}
|
||||
|
||||
var baseName string
|
||||
var referenceFlock *entity.Flock
|
||||
if pivot.ProjectFlock.Id != 0 {
|
||||
baseName = pfutils.DeriveBaseName(pivot.ProjectFlock.FlockName)
|
||||
}
|
||||
|
||||
if strings.TrimSpace(baseName) != "" {
|
||||
referenceFlock, err = s.FlockRepo.GetByName(c.Context(), baseName)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.Log.Errorf("Failed to fetch flock %q: %+v", baseName, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch flock")
|
||||
}
|
||||
}
|
||||
|
||||
if referenceFlock == nil {
|
||||
referenceFlock = &entity.Flock{Name: pivot.ProjectFlock.FlockName}
|
||||
}
|
||||
|
||||
maxPeriod := pivot.ProjectFlock.Period
|
||||
if strings.TrimSpace(baseName) != "" {
|
||||
if headerMax, err := s.Repository.GetMaxPeriodByBaseName(c.Context(), baseName); err != nil {
|
||||
s.Log.Warnf("Unable to compute header period for base %q: %+v", baseName, err)
|
||||
} else if headerMax > maxPeriod {
|
||||
maxPeriod = headerMax
|
||||
}
|
||||
|
||||
if pivotMax, err := s.pivotRepo().MaxPeriodByBaseName(c.Context(), baseName); err != nil {
|
||||
s.Log.Warnf("Unable to compute pivot period for base %q: %+v", baseName, err)
|
||||
} else if pivotMax > maxPeriod {
|
||||
maxPeriod = pivotMax
|
||||
}
|
||||
}
|
||||
|
||||
return &FlockPeriodSummary{
|
||||
Flock: *flock,
|
||||
Flock: *referenceFlock,
|
||||
NextPeriod: maxPeriod + 1,
|
||||
}, nil
|
||||
}
|
||||
@@ -732,45 +796,64 @@ func uniqueUintSlice(values []uint) []uint {
|
||||
return result
|
||||
}
|
||||
|
||||
func relationExistsChecker[T any](db *gorm.DB) func(context.Context, uint) (bool, error) {
|
||||
return func(ctx context.Context, id uint) (bool, error) {
|
||||
return commonRepo.Exists[T](ctx, db, id)
|
||||
func (s projectflockService) generateSequentialFlockName(ctx context.Context, repo repository.ProjectflockRepository, baseName string, startNumber int, excludeID *uint) (string, int, error) {
|
||||
name := strings.TrimSpace(baseName)
|
||||
if name == "" {
|
||||
return "", 0, fiber.NewError(fiber.StatusBadRequest, "Base flock name cannot be empty")
|
||||
}
|
||||
|
||||
number := startNumber
|
||||
if number <= 0 {
|
||||
number = 1
|
||||
}
|
||||
|
||||
attempts := 0
|
||||
for {
|
||||
candidate := fmt.Sprintf("%s %03d", name, number)
|
||||
exists, err := repo.ExistsByFlockName(ctx, candidate, excludeID)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed checking project flock name uniqueness for %q: %+v", candidate, err)
|
||||
return "", 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate flock name")
|
||||
}
|
||||
if !exists {
|
||||
return candidate, number, nil
|
||||
}
|
||||
number++
|
||||
attempts++
|
||||
if attempts > 9999 {
|
||||
return "", 0, fiber.NewError(fiber.StatusInternalServerError, "Unable to generate unique flock name")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s projectflockService) buildOrderExpressions(sortBy, sortOrder string) []string {
|
||||
direction := "ASC"
|
||||
if strings.ToLower(sortOrder) == "desc" {
|
||||
direction = "DESC"
|
||||
func (s projectflockService) ensureFlockByName(ctx context.Context, name string) (*entity.Flock, error) {
|
||||
trimmed := strings.TrimSpace(name)
|
||||
if trimmed == "" {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Flock name cannot be empty")
|
||||
}
|
||||
|
||||
switch sortBy {
|
||||
case "area":
|
||||
return []string{
|
||||
fmt.Sprintf("(SELECT name FROM areas WHERE areas.id = project_flocks.area_id) %s", direction),
|
||||
fmt.Sprintf("project_flocks.id %s", direction),
|
||||
flock, err := s.FlockRepo.GetByName(ctx, trimmed)
|
||||
if err == nil {
|
||||
return flock, nil
|
||||
}
|
||||
case "location":
|
||||
return []string{
|
||||
fmt.Sprintf("(SELECT name FROM locations WHERE locations.id = project_flocks.location_id) %s", direction),
|
||||
fmt.Sprintf("project_flocks.id %s", direction),
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.Log.Errorf("Failed to fetch flock by name %q: %+v", trimmed, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to prepare flock data")
|
||||
}
|
||||
case "kandangs":
|
||||
return []string{
|
||||
fmt.Sprintf("(SELECT COUNT(*) FROM kandangs WHERE kandangs.project_flock_id = project_flocks.id) %s", direction),
|
||||
fmt.Sprintf("project_flocks.id %s", direction),
|
||||
|
||||
newFlock := &entity.Flock{
|
||||
Name: trimmed,
|
||||
CreatedBy: 1, // TODO: replace with authenticated user
|
||||
}
|
||||
case "period":
|
||||
return []string{
|
||||
fmt.Sprintf("project_flocks.period %s", direction),
|
||||
fmt.Sprintf("project_flocks.id %s", direction),
|
||||
}
|
||||
default:
|
||||
return []string{
|
||||
"project_flocks.created_at DESC",
|
||||
"project_flocks.updated_at DESC",
|
||||
if err := s.FlockRepo.CreateOne(ctx, newFlock, nil); err != nil {
|
||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||
return s.FlockRepo.GetByName(ctx, trimmed)
|
||||
}
|
||||
s.Log.Errorf("Failed to create flock %q: %+v", trimmed, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to prepare flock data")
|
||||
}
|
||||
|
||||
return newFlock, nil
|
||||
}
|
||||
|
||||
func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint) error {
|
||||
@@ -778,24 +861,45 @@ func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := dbTransaction.Model(&entity.Kandang{}).
|
||||
Where("id IN ?", kandangIDs).
|
||||
Updates(map[string]any{
|
||||
"project_flock_id": projectFlockID,
|
||||
"status": string(utils.KandangStatusPengajuan),
|
||||
}).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
|
||||
if err := s.kandangRepoWithTx(dbTransaction).UpdateStatusByIDs(ctx, kandangIDs, utils.KandangStatusPengajuan); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status")
|
||||
}
|
||||
|
||||
ProjectFlockKandangRepo := s.ProjectFlockKandangRepoWithTx(dbTransaction)
|
||||
records := make([]*entity.ProjectFlockKandang, len(kandangIDs))
|
||||
for i, id := range kandangIDs {
|
||||
records[i] = &entity.ProjectFlockKandang{
|
||||
already, err := s.pivotRepoWithTx(dbTransaction).ListExistingKandangIDs(ctx, projectFlockID, kandangIDs)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing pivot")
|
||||
}
|
||||
exists := make(map[uint]struct{}, len(already))
|
||||
for _, id := range already {
|
||||
exists[id] = struct{}{}
|
||||
}
|
||||
|
||||
var toAttach []uint
|
||||
seen := make(map[uint]struct{}, len(kandangIDs))
|
||||
for _, id := range kandangIDs {
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
if _, ok := exists[id]; !ok {
|
||||
toAttach = append(toAttach, id)
|
||||
}
|
||||
}
|
||||
if len(toAttach) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
records := make([]*entity.ProjectFlockKandang, 0, len(toAttach))
|
||||
for _, id := range toAttach {
|
||||
records = append(records, &entity.ProjectFlockKandang{
|
||||
ProjectFlockId: projectFlockID,
|
||||
KandangId: id,
|
||||
})
|
||||
}
|
||||
if err := s.pivotRepoWithTx(dbTransaction).CreateMany(ctx, records); err != nil {
|
||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
||||
return fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terhubung dengan project flock ini")
|
||||
}
|
||||
if err := ProjectFlockKandangRepo.CreateMany(ctx, records); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history")
|
||||
}
|
||||
return nil
|
||||
@@ -806,26 +910,55 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *
|
||||
return nil
|
||||
}
|
||||
|
||||
updates := map[string]any{"project_flock_id": nil}
|
||||
blocked, err := s.pivotRepoWithTx(dbTransaction).FindKandangsWithRecordings(ctx, projectFlockID, kandangIDs)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to check recordings before detaching kandangs: %+v", err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandang detachment")
|
||||
}
|
||||
if len(blocked) > 0 {
|
||||
names := make([]string, 0, len(blocked))
|
||||
for _, item := range blocked {
|
||||
label := fmt.Sprintf("ID %d", item.Id)
|
||||
if strings.TrimSpace(item.Name) != "" {
|
||||
label = fmt.Sprintf("%s (%s)", label, item.Name)
|
||||
}
|
||||
names = append(names, label)
|
||||
}
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tidak dapat melepas kandang karena sudah memiliki recording: %s", strings.Join(names, ", ")))
|
||||
}
|
||||
|
||||
if resetStatus {
|
||||
updates["status"] = string(utils.KandangStatusNonActive)
|
||||
if err := s.kandangRepoWithTx(dbTransaction).UpdateStatusByIDs(ctx, kandangIDs, utils.KandangStatusNonActive); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status")
|
||||
}
|
||||
}
|
||||
|
||||
if err := dbTransaction.Model(&entity.Kandang{}).
|
||||
Where("id IN ?", kandangIDs).
|
||||
Updates(updates).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs")
|
||||
}
|
||||
|
||||
if err := s.ProjectFlockKandangRepoWithTx(dbTransaction).DeleteMany(ctx, projectFlockID, kandangIDs); err != nil {
|
||||
if err := s.pivotRepoWithTx(dbTransaction).DeleteMany(ctx, projectFlockID, kandangIDs); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s projectflockService) ProjectFlockKandangRepoWithTx(dbTransaction *gorm.DB) repository.ProjectFlockKandangRepository {
|
||||
if s.ProjectFlockKandangRepo == nil {
|
||||
return repository.NewProjectFlockKandangRepository(dbTransaction)
|
||||
func (s projectflockService) pivotRepoWithTx(dbTransaction *gorm.DB) repository.ProjectFlockKandangRepository {
|
||||
if dbTransaction == nil {
|
||||
return s.pivotRepo()
|
||||
}
|
||||
return s.ProjectFlockKandangRepo.WithTx(dbTransaction)
|
||||
return s.pivotRepo().WithTx(dbTransaction)
|
||||
}
|
||||
|
||||
func (s projectflockService) pivotRepo() repository.ProjectFlockKandangRepository {
|
||||
if s.PivotRepo != nil {
|
||||
return s.PivotRepo
|
||||
}
|
||||
return repository.NewProjectFlockKandangRepository(s.Repository.DB())
|
||||
}
|
||||
|
||||
func (s projectflockService) kandangRepoWithTx(tx *gorm.DB) kandangRepository.KandangRepository {
|
||||
if tx != nil {
|
||||
return kandangRepository.NewKandangRepository(tx)
|
||||
}
|
||||
if s.KandangRepo != nil {
|
||||
return s.KandangRepo
|
||||
}
|
||||
return kandangRepository.NewKandangRepository(s.Repository.DB())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DeriveBaseName removes trailing numeric tokens from the flock name.
|
||||
func DeriveBaseName(name string) string {
|
||||
trimmed := strings.TrimSpace(name)
|
||||
if trimmed == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
parts := strings.Fields(trimmed)
|
||||
for len(parts) > 0 {
|
||||
if _, err := strconv.Atoi(parts[len(parts)-1]); err == nil {
|
||||
parts = parts[:len(parts)-1]
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return strings.TrimSpace(strings.Join(parts, " "))
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package validation
|
||||
|
||||
type Create struct {
|
||||
FlockId uint `json:"flock_id" validate:"required_strict,number,gt=0"`
|
||||
FlockName string `json:"flock_name" validate:"required_strict"`
|
||||
AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"`
|
||||
Category string `json:"category" validate:"required_strict"`
|
||||
FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"`
|
||||
@@ -10,7 +10,7 @@ type Create struct {
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
FlockId *uint `json:"flock_id,omitempty" validate:"omitempty,number,gt=0"`
|
||||
FlockName *string `json:"flock_name,omitempty" validate:"omitempty"`
|
||||
AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"`
|
||||
Category *string `json:"category,omitempty" validate:"omitempty"`
|
||||
FcrId *uint `json:"fcr_id,omitempty" validate:"omitempty,number,gt=0"`
|
||||
|
||||
@@ -146,6 +146,60 @@ func (u *RecordingController) UpdateOne(c *fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (u *RecordingController) SubmitGrading(c *fiber.Ctx) error {
|
||||
req := new(validation.SubmitGrading)
|
||||
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
result, err := u.RecordingService.SubmitGrading(c, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Submit grading eggs successfully",
|
||||
Data: dto.ToRecordingDetailDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (u *RecordingController) Approve(c *fiber.Ctx) error {
|
||||
req := new(validation.Approve)
|
||||
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
results, err := u.RecordingService.Approval(c, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
data interface{}
|
||||
message = "Submit recording approvals successfully"
|
||||
)
|
||||
|
||||
if len(results) == 1 {
|
||||
message = "Submit recording approval successfully"
|
||||
data = dto.ToRecordingDetailDTO(results[0])
|
||||
} else {
|
||||
data = dto.ToRecordingListDTOs(results)
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: message,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *RecordingController) DeleteOne(c *fiber.Ctx) error {
|
||||
param := c.Params("id")
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto"
|
||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
||||
)
|
||||
|
||||
// === DTO Structs ===
|
||||
@@ -13,18 +17,19 @@ type RecordingBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
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"`
|
||||
ProjectFlockCategory *string `json:"project_flock_category,omitempty"`
|
||||
TotalDepletionQty *float64 `json:"total_depletion_qty,omitempty"`
|
||||
CumDepletionRate *float64 `json:"cum_depletion_rate,omitempty"`
|
||||
DailyGain *float64 `json:"daily_gain,omitempty"`
|
||||
AvgDailyGain *float64 `json:"avg_daily_gain,omitempty"`
|
||||
CumIntake *int64 `json:"cum_intake,omitempty"`
|
||||
CumIntake *int `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"`
|
||||
TotalChickQty *float64 `json:"total_chick_qty,omitempty"`
|
||||
Approval approvalDTO.ApprovalBaseDTO `json:"approval"`
|
||||
EggGradingStatus *string `json:"egg_grading_status,omitempty"`
|
||||
EggGradingPendingQty *int `json:"egg_grading_pending_qty,omitempty"`
|
||||
EggGradingCompletedQty *int `json:"egg_grading_completed_qty,omitempty"`
|
||||
}
|
||||
|
||||
type RecordingListDTO struct {
|
||||
@@ -39,58 +44,84 @@ type RecordingDetailDTO struct {
|
||||
BodyWeights []RecordingBodyWeightDTO `json:"body_weights"`
|
||||
Depletions []RecordingDepletionDTO `json:"depletions"`
|
||||
Stocks []RecordingStockDTO `json:"stocks"`
|
||||
Eggs []RecordingEggDTO `json:"eggs"`
|
||||
}
|
||||
|
||||
type RecordingBodyWeightDTO struct {
|
||||
Weight float64 `json:"weight"`
|
||||
Qty int `json:"qty"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
AvgWeight float64 `json:"avg_weight"`
|
||||
Qty float64 `json:"qty"`
|
||||
TotalWeight float64 `json:"total_weight"`
|
||||
}
|
||||
|
||||
type RecordingDepletionDTO struct {
|
||||
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||
Total int64 `json:"total"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
Qty float64 `json:"qty"`
|
||||
ProductWarehouse *RecordingProductWarehouseDTO `json:"product_warehouse,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"`
|
||||
UsageAmount *float64 `json:"usage_amount,omitempty"`
|
||||
PendingQty *float64 `json:"pending_qty,omitempty"`
|
||||
ProductWarehouse *RecordingProductWarehouseDTO `json:"product_warehouse,omitempty"`
|
||||
}
|
||||
|
||||
type RecordingEggDTO struct {
|
||||
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||
Qty int `json:"qty"`
|
||||
ProductWarehouse *RecordingProductWarehouseDTO `json:"product_warehouse,omitempty"`
|
||||
Gradings []RecordingEggGradingDTO `json:"gradings,omitempty"`
|
||||
}
|
||||
|
||||
type RecordingProductWarehouseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
ProductId uint `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
WarehouseId uint `json:"warehouse_id"`
|
||||
WarehouseName string `json:"warehouse_name"`
|
||||
}
|
||||
|
||||
type RecordingEggGradingDTO struct {
|
||||
Grade string `json:"grade,omitempty"`
|
||||
Qty float64 `json:"qty"`
|
||||
}
|
||||
|
||||
// === Mapper Functions ===
|
||||
|
||||
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
|
||||
var projectFlockCategory *string
|
||||
if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.ProjectFlock.Id != 0 {
|
||||
category := e.ProjectFlockKandang.ProjectFlock.Category
|
||||
if category != "" {
|
||||
projectFlockCategory = &category
|
||||
}
|
||||
}
|
||||
|
||||
latestApproval := defaultRecordingLatestApproval(e)
|
||||
if e.LatestApproval != nil {
|
||||
snapshot := approvalDTO.ToApprovalDTO(*e.LatestApproval)
|
||||
latestApproval = snapshot
|
||||
}
|
||||
|
||||
gradingStatus, gradingPending, gradingCompleted := computeEggGradingStatus(e)
|
||||
|
||||
return RecordingBaseDTO{
|
||||
Id: e.Id,
|
||||
ProjectFlockKandangId: e.ProjectFlockKandangId,
|
||||
RecordDatetime: e.RecordDatetime,
|
||||
RecordDate: recordDate,
|
||||
Ontime: e.Ontime == 1,
|
||||
Day: e.Day,
|
||||
TotalDepletion: e.TotalDepletion,
|
||||
ProjectFlockCategory: projectFlockCategory,
|
||||
TotalDepletionQty: e.TotalDepletionQty,
|
||||
CumDepletionRate: e.CumDepletionRate,
|
||||
DailyGain: e.DailyGain,
|
||||
AvgDailyGain: e.AvgDailyGain,
|
||||
CumIntake: e.CumIntake,
|
||||
FcrValue: e.FcrValue,
|
||||
TotalChick: e.TotalChick,
|
||||
DailyDepletionRate: e.DailyDepletionRate,
|
||||
CumDepletion: e.CumDepletion,
|
||||
TotalChickQty: e.TotalChickQty,
|
||||
Approval: latestApproval,
|
||||
EggGradingStatus: gradingStatus,
|
||||
EggGradingPendingQty: gradingPending,
|
||||
EggGradingCompletedQty: gradingCompleted,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +154,7 @@ func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO {
|
||||
BodyWeights: ToRecordingBodyWeightDTOs(e.BodyWeights),
|
||||
Depletions: ToRecordingDepletionDTOs(e.Depletions),
|
||||
Stocks: ToRecordingStockDTOs(e.Stocks),
|
||||
Eggs: ToRecordingEggDTOs(e.Eggs),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,9 +162,9 @@ func ToRecordingBodyWeightDTOs(bodyWeights []entity.RecordingBW) []RecordingBody
|
||||
result := make([]RecordingBodyWeightDTO, len(bodyWeights))
|
||||
for i, bw := range bodyWeights {
|
||||
result[i] = RecordingBodyWeightDTO{
|
||||
Weight: bw.Weight,
|
||||
AvgWeight: bw.AvgWeight,
|
||||
Qty: bw.Qty,
|
||||
Notes: bw.Notes,
|
||||
TotalWeight: bw.TotalWeight,
|
||||
}
|
||||
}
|
||||
return result
|
||||
@@ -143,8 +175,8 @@ func ToRecordingDepletionDTOs(depletions []entity.RecordingDepletion) []Recordin
|
||||
for i, d := range depletions {
|
||||
result[i] = RecordingDepletionDTO{
|
||||
ProductWarehouseId: d.ProductWarehouseId,
|
||||
Total: d.Total,
|
||||
Notes: d.Notes,
|
||||
Qty: d.Qty,
|
||||
ProductWarehouse: toRecordingProductWarehouseDTO(&d.ProductWarehouse),
|
||||
}
|
||||
}
|
||||
return result
|
||||
@@ -155,11 +187,138 @@ func ToRecordingStockDTOs(stocks []entity.RecordingStock) []RecordingStockDTO {
|
||||
for i, s := range stocks {
|
||||
result[i] = RecordingStockDTO{
|
||||
ProductWarehouseId: s.ProductWarehouseId,
|
||||
Increase: s.Increase,
|
||||
Decrease: s.Decrease,
|
||||
UsageAmount: s.UsageAmount,
|
||||
Notes: s.Notes,
|
||||
UsageAmount: s.UsageQty,
|
||||
PendingQty: s.PendingQty,
|
||||
ProductWarehouse: toRecordingProductWarehouseDTO(&s.ProductWarehouse),
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ToRecordingEggDTOs(eggs []entity.RecordingEgg) []RecordingEggDTO {
|
||||
result := make([]RecordingEggDTO, len(eggs))
|
||||
for i, egg := range eggs {
|
||||
result[i] = RecordingEggDTO{
|
||||
ProductWarehouseId: egg.ProductWarehouseId,
|
||||
Qty: egg.Qty,
|
||||
ProductWarehouse: toRecordingProductWarehouseDTO(&egg.ProductWarehouse),
|
||||
Gradings: ToRecordingEggGradingDTOs(egg.GradingEggs),
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ToRecordingEggGradingDTOs(gradings []entity.GradingEgg) []RecordingEggGradingDTO {
|
||||
if len(gradings) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]RecordingEggGradingDTO, len(gradings))
|
||||
for i, grading := range gradings {
|
||||
result[i] = RecordingEggGradingDTO{
|
||||
Grade: grading.Grade,
|
||||
Qty: grading.Qty,
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func toRecordingProductWarehouseDTO(pw *entity.ProductWarehouse) *RecordingProductWarehouseDTO {
|
||||
if pw == nil || pw.Id == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
dto := RecordingProductWarehouseDTO{
|
||||
Id: pw.Id,
|
||||
ProductId: pw.ProductId,
|
||||
WarehouseId: pw.WarehouseId,
|
||||
}
|
||||
|
||||
if pw.Product.Id != 0 {
|
||||
dto.ProductName = pw.Product.Name
|
||||
}
|
||||
if pw.Warehouse.Id != 0 {
|
||||
dto.WarehouseName = pw.Warehouse.Name
|
||||
}
|
||||
|
||||
return &dto
|
||||
}
|
||||
|
||||
const goodEggProductWarehouseID uint = 5
|
||||
|
||||
func computeEggGradingStatus(e entity.Recording) (*string, *int, *int) {
|
||||
goodEggs := filterGoodEggs(e.Eggs)
|
||||
if len(goodEggs) == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
totalEggs := 0
|
||||
totalGraded := 0.0
|
||||
for _, egg := range goodEggs {
|
||||
totalEggs += egg.Qty
|
||||
for _, grading := range egg.GradingEggs {
|
||||
totalGraded += grading.Qty
|
||||
}
|
||||
}
|
||||
|
||||
if totalEggs == 0 {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
pendingFloat := float64(totalEggs) - totalGraded
|
||||
if pendingFloat < 0 {
|
||||
pendingFloat = 0
|
||||
}
|
||||
pendingInt := int(math.Round(pendingFloat))
|
||||
completedInt := int(math.Round(totalGraded))
|
||||
if completedInt < 0 {
|
||||
completedInt = 0
|
||||
}
|
||||
|
||||
if pendingInt > 0 {
|
||||
status := "GRADING_TELUR"
|
||||
return &status, &pendingInt, &completedInt
|
||||
}
|
||||
|
||||
status := "GRADING_SELESAI"
|
||||
zero := 0
|
||||
return &status, &zero, &completedInt
|
||||
}
|
||||
|
||||
func filterGoodEggs(eggs []entity.RecordingEgg) []entity.RecordingEgg {
|
||||
if len(eggs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]entity.RecordingEgg, 0, len(eggs))
|
||||
for _, egg := range eggs {
|
||||
if egg.ProductWarehouseId == goodEggProductWarehouseID {
|
||||
result = append(result, egg)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func defaultRecordingLatestApproval(e entity.Recording) approvalDTO.ApprovalBaseDTO {
|
||||
result := approvalDTO.ApprovalBaseDTO{}
|
||||
|
||||
step := utils.RecordingStepPengajuan
|
||||
result.StepNumber = uint16(step)
|
||||
if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowRecording, step); ok {
|
||||
result.StepName = label
|
||||
} else if label, ok := utils.RecordingApprovalSteps[step]; ok {
|
||||
result.StepName = label
|
||||
}
|
||||
|
||||
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
|
||||
result.ActionBy = userDTO.ToUserBaseDTO(*e.CreatedUser)
|
||||
} else if e.CreatedBy != 0 {
|
||||
result.ActionBy = userDTO.UserBaseDTO{
|
||||
Id: e.CreatedBy,
|
||||
IdUser: int64(e.CreatedBy),
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
package recordings
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
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"
|
||||
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/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"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
@@ -18,11 +23,26 @@ type RecordingModule struct{}
|
||||
|
||||
func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
recordingRepo := rRecording.NewRecordingRepository(db)
|
||||
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
|
||||
projectFlockPopulationRepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowRecording, utils.RecordingApprovalSteps); err != nil {
|
||||
panic(fmt.Sprintf("failed to register recording approval workflow: %v", err))
|
||||
}
|
||||
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
|
||||
recordingService := sRecording.NewRecordingService(recordingRepo, projectFlockKandangRepo, productWarehouseRepo, validate)
|
||||
recordingService := sRecording.NewRecordingService(
|
||||
recordingRepo,
|
||||
projectFlockKandangRepo,
|
||||
productWarehouseRepo,
|
||||
projectFlockPopulationRepo,
|
||||
approvalRepo,
|
||||
approvalService,
|
||||
validate,
|
||||
)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
RecordingRoutes(router, userService, recordingService)
|
||||
|
||||
@@ -1,13 +1,51 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"context"
|
||||
"errors"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type RecordingRepository interface {
|
||||
repository.BaseRepository[entity.Recording]
|
||||
|
||||
WithRelations(db *gorm.DB) *gorm.DB
|
||||
GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error)
|
||||
|
||||
CreateBodyWeights(tx *gorm.DB, bodyWeights []entity.RecordingBW) error
|
||||
DeleteBodyWeights(tx *gorm.DB, recordingID uint) error
|
||||
|
||||
CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error
|
||||
DeleteStocks(tx *gorm.DB, recordingID uint) error
|
||||
ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error)
|
||||
|
||||
CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error
|
||||
DeleteDepletions(tx *gorm.DB, recordingID uint) error
|
||||
ListDepletions(tx *gorm.DB, recordingID uint) ([]entity.RecordingDepletion, error)
|
||||
|
||||
CreateEggs(tx *gorm.DB, eggs []entity.RecordingEgg) error
|
||||
DeleteEggs(tx *gorm.DB, recordingID uint) error
|
||||
ListEggs(tx *gorm.DB, recordingID uint) ([]entity.RecordingEgg, error)
|
||||
GetRecordingEggByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.RecordingEgg, error)
|
||||
CreateGradingEggs(tx *gorm.DB, gradings []entity.GradingEgg) error
|
||||
DeleteGradingEggs(tx *gorm.DB, recordingEggID uint) error
|
||||
|
||||
ExistsOnDate(ctx context.Context, projectFlockKandangId uint, recordTime time.Time) (bool, error)
|
||||
|
||||
SumRecordingDepletions(tx *gorm.DB, recordingID uint) (float64, error)
|
||||
FindPreviousRecording(tx *gorm.DB, projectFlockKandangId uint, currentDay int) (*entity.Recording, error)
|
||||
GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error)
|
||||
GetAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error)
|
||||
GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error)
|
||||
GetFcrID(tx *gorm.DB, projectFlockKandangId uint) (uint, error)
|
||||
GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error)
|
||||
}
|
||||
|
||||
type RecordingRepositoryImpl struct {
|
||||
@@ -19,3 +57,337 @@ func NewRecordingRepository(db *gorm.DB) RecordingRepository {
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.Recording](db),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB {
|
||||
return db.
|
||||
Preload("CreatedUser").
|
||||
Preload("ProjectFlockKandang").
|
||||
Preload("ProjectFlockKandang.ProjectFlock").
|
||||
Preload("BodyWeights").
|
||||
Preload("Depletions").
|
||||
Preload("Depletions.ProductWarehouse").
|
||||
Preload("Depletions.ProductWarehouse.Product").
|
||||
Preload("Depletions.ProductWarehouse.Warehouse").
|
||||
Preload("Stocks").
|
||||
Preload("Stocks.ProductWarehouse").
|
||||
Preload("Stocks.ProductWarehouse.Product").
|
||||
Preload("Stocks.ProductWarehouse.Warehouse").
|
||||
Preload("Eggs").
|
||||
Preload("Eggs.ProductWarehouse").
|
||||
Preload("Eggs.ProductWarehouse.Product").
|
||||
Preload("Eggs.ProductWarehouse.Warehouse").
|
||||
Preload("Eggs.GradingEggs")
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error) {
|
||||
var days []int
|
||||
if err := tx.Model(&entity.Recording{}).
|
||||
Where("project_flock_kandangs_id = ?", projectFlockKandangId).
|
||||
Where("day IS NOT NULL").
|
||||
Pluck("day", &days).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return nextRecordingDay(days), nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) CreateBodyWeights(tx *gorm.DB, bodyWeights []entity.RecordingBW) error {
|
||||
if len(bodyWeights) == 0 {
|
||||
return nil
|
||||
}
|
||||
return tx.Create(&bodyWeights).Error
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) DeleteBodyWeights(tx *gorm.DB, recordingID uint) error {
|
||||
return tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingBW{}).Error
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error {
|
||||
if len(stocks) == 0 {
|
||||
return nil
|
||||
}
|
||||
return tx.Create(&stocks).Error
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) DeleteStocks(tx *gorm.DB, recordingID uint) error {
|
||||
return tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingStock{}).Error
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error) {
|
||||
var items []entity.RecordingStock
|
||||
if err := tx.Where("recording_id = ?", recordingID).Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error {
|
||||
if len(depletions) == 0 {
|
||||
return nil
|
||||
}
|
||||
return tx.Create(&depletions).Error
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) DeleteDepletions(tx *gorm.DB, recordingID uint) error {
|
||||
return tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingDepletion{}).Error
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) ListDepletions(tx *gorm.DB, recordingID uint) ([]entity.RecordingDepletion, error) {
|
||||
var items []entity.RecordingDepletion
|
||||
if err := tx.Where("recording_id = ?", recordingID).Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) CreateEggs(tx *gorm.DB, eggs []entity.RecordingEgg) error {
|
||||
if len(eggs) == 0 {
|
||||
return nil
|
||||
}
|
||||
return tx.Create(&eggs).Error
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) DeleteEggs(tx *gorm.DB, recordingID uint) error {
|
||||
return tx.Where("recording_id = ?", recordingID).Delete(&entity.RecordingEgg{}).Error
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) ListEggs(tx *gorm.DB, recordingID uint) ([]entity.RecordingEgg, error) {
|
||||
var items []entity.RecordingEgg
|
||||
if err := tx.Where("recording_id = ?", recordingID).Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetRecordingEggByID(
|
||||
ctx context.Context,
|
||||
id uint,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) (*entity.RecordingEgg, error) {
|
||||
if id == 0 {
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
db := r.DB()
|
||||
if modifier != nil {
|
||||
db = modifier(db)
|
||||
}
|
||||
|
||||
var egg entity.RecordingEgg
|
||||
query := db.WithContext(ctx).
|
||||
Preload("Recording").
|
||||
Preload("Recording.ProjectFlockKandang").
|
||||
Preload("Recording.ProjectFlockKandang.ProjectFlock").
|
||||
Preload("ProductWarehouse").
|
||||
Preload("GradingEggs").
|
||||
Where("id = ?", id)
|
||||
|
||||
if err := query.First(&egg).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &egg, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) CreateGradingEggs(tx *gorm.DB, gradings []entity.GradingEgg) error {
|
||||
if len(gradings) == 0 {
|
||||
return nil
|
||||
}
|
||||
return tx.Create(&gradings).Error
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) DeleteGradingEggs(tx *gorm.DB, recordingEggID uint) error {
|
||||
return tx.Where("recording_egg_id = ?", recordingEggID).Delete(&entity.GradingEgg{}).Error
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) ExistsOnDate(ctx context.Context, projectFlockKandangId uint, recordTime time.Time) (bool, error) {
|
||||
if projectFlockKandangId == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
ref := recordTime.In(time.UTC)
|
||||
startOfDay := time.Date(ref.Year(), ref.Month(), ref.Day(), 0, 0, 0, 0, time.UTC)
|
||||
endOfDay := startOfDay.Add(24 * time.Hour)
|
||||
|
||||
var count int64
|
||||
err := r.DB().
|
||||
WithContext(ctx).
|
||||
Model(&entity.Recording{}).
|
||||
Where("project_flock_kandangs_id = ?", projectFlockKandangId).
|
||||
Where("record_datetime >= ? AND record_datetime < ?", startOfDay, endOfDay).
|
||||
Count(&count).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) SumRecordingDepletions(tx *gorm.DB, recordingID uint) (float64, error) {
|
||||
var result float64
|
||||
if err := tx.Model(&entity.RecordingDepletion{}).
|
||||
Where("recording_id = ?", recordingID).
|
||||
Select("COALESCE(SUM(qty), 0)").
|
||||
Scan(&result).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) FindPreviousRecording(tx *gorm.DB, projectFlockKandangId uint, currentDay int) (*entity.Recording, error) {
|
||||
if currentDay <= 1 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var prev entity.Recording
|
||||
err := tx.
|
||||
Where("project_flock_kandangs_id = ? AND day < ?", projectFlockKandangId, currentDay).
|
||||
Where("day IS NOT NULL").
|
||||
Order("day DESC").
|
||||
Limit(1).
|
||||
Find(&prev).Error
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) || prev.Id == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &prev, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error) {
|
||||
var population entity.ProjectFlockPopulation
|
||||
err := tx.
|
||||
Where("project_flock_kandang_id = ?", projectFlockKandangId).
|
||||
Order("created_at DESC").
|
||||
First(&population).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int64(math.Round(population.TotalQty)), nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error) {
|
||||
var result struct {
|
||||
TotalWeight float64
|
||||
TotalQty float64
|
||||
}
|
||||
if err := tx.Model(&entity.RecordingBW{}).
|
||||
Select("COALESCE(SUM(total_weight), 0) AS total_weight, COALESCE(SUM(qty), 0) AS total_qty").
|
||||
Where("recording_id = ?", recordingID).
|
||||
Scan(&result).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if result.TotalQty == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return result.TotalWeight / result.TotalQty, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) {
|
||||
var rows []struct {
|
||||
UsageQty float64
|
||||
UomName string
|
||||
}
|
||||
|
||||
if err := tx.
|
||||
Table("recording_stocks").
|
||||
Select("COALESCE(recording_stocks.usage_qty, 0) AS usage_qty, 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.UsageQty <= 0 {
|
||||
continue
|
||||
}
|
||||
switch strings.TrimSpace(row.UomName) {
|
||||
case "kilogram", "kg", "kilograms", "kilo":
|
||||
total += row.UsageQty * 1000
|
||||
case "gram", "g", "grams":
|
||||
total += row.UsageQty
|
||||
default:
|
||||
total += row.UsageQty
|
||||
}
|
||||
}
|
||||
return total, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetFcrID(tx *gorm.DB, projectFlockKandangId uint) (uint, error) {
|
||||
var result struct {
|
||||
FcrID uint
|
||||
}
|
||||
if err := tx.Table("project_flock_kandangs").
|
||||
Select("project_flocks.fcr_id AS fcr_id").
|
||||
Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id").
|
||||
Where("project_flock_kandangs.id = ?", projectFlockKandangId).
|
||||
Scan(&result).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.FcrID, nil
|
||||
}
|
||||
|
||||
func (r *RecordingRepositoryImpl) GetFcrStandardWeightKg(tx *gorm.DB, fcrId uint, currentWeightKg float64) (float64, bool, error) {
|
||||
if fcrId == 0 {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
var standard entity.FcrStandard
|
||||
err := tx.
|
||||
Where("fcr_id = ? AND weight >= ?", fcrId, currentWeightKg).
|
||||
Order("weight ASC").
|
||||
First(&standard).Error
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
err = tx.
|
||||
Where("fcr_id = ?", fcrId).
|
||||
Order("weight DESC").
|
||||
First(&standard).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, false, nil
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return 0, false, err
|
||||
}
|
||||
|
||||
weight := standard.Weight
|
||||
if weight > 10 {
|
||||
return weight / 1000, true, nil
|
||||
}
|
||||
return weight, true, nil
|
||||
}
|
||||
|
||||
func nextRecordingDay(days []int) int {
|
||||
if len(days) == 0 {
|
||||
return 1
|
||||
}
|
||||
|
||||
unique := make(map[int]struct{}, len(days))
|
||||
for _, day := range days {
|
||||
if day > 0 {
|
||||
unique[day] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
normalized := make([]int, 0, len(unique))
|
||||
for day := range unique {
|
||||
normalized = append(normalized, day)
|
||||
}
|
||||
sort.Ints(normalized)
|
||||
|
||||
for idx, day := range normalized {
|
||||
expected := idx + 1
|
||||
if day != expected {
|
||||
return expected
|
||||
}
|
||||
}
|
||||
|
||||
return len(normalized) + 1
|
||||
}
|
||||
|
||||
@@ -23,7 +23,9 @@ func RecordingRoutes(v1 fiber.Router, u user.UserService, s recording.RecordingS
|
||||
route.Get("/", ctrl.GetAll)
|
||||
route.Get("/next-day", ctrl.GetNextDay)
|
||||
route.Post("/", ctrl.CreateOne)
|
||||
route.Post("/gradings", ctrl.SubmitGrading)
|
||||
route.Get("/:id", ctrl.GetOne)
|
||||
route.Patch("/:id", ctrl.UpdateOne)
|
||||
route.Post("/approvals", ctrl.Approve)
|
||||
route.Delete("/:id", ctrl.DeleteOne)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,23 +2,25 @@ 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"`
|
||||
AvgWeight float64 `json:"avg_weight" validate:"required"`
|
||||
Qty float64 `json:"qty" validate:"required,gt=0"`
|
||||
TotalWeight *float64 `json:"total_weight,omitempty" validate:"omitempty,gt=0"`
|
||||
}
|
||||
|
||||
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"`
|
||||
Qty *float64 `json:"qty,omitempty" validate:"required_without=UsageAmount,gte=0"`
|
||||
PendingQty *float64 `json:"pending_qty,omitempty" validate:"omitempty,gte=0"`
|
||||
}
|
||||
|
||||
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"`
|
||||
Qty float64 `json:"qty" validate:"required,gte=0"`
|
||||
}
|
||||
|
||||
Egg struct {
|
||||
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"`
|
||||
Qty int `json:"qty" validate:"required,number,min=0"`
|
||||
}
|
||||
)
|
||||
|
||||
@@ -27,12 +29,14 @@ type Create struct {
|
||||
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"`
|
||||
Eggs []Egg `json:"eggs,omitempty" validate:"omitempty,dive"`
|
||||
}
|
||||
|
||||
type Update struct {
|
||||
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"`
|
||||
Eggs []Egg `json:"eggs,omitempty" validate:"omitempty,dive"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
@@ -40,3 +44,19 @@ type Query struct {
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
|
||||
}
|
||||
|
||||
type EggGrading struct {
|
||||
RecordingEggId uint `json:"recording_egg_id" validate:"required,number,min=1"`
|
||||
Grade string `json:"grade" validate:"required"`
|
||||
Qty float64 `json:"qty" validate:"required,gte=0"`
|
||||
}
|
||||
|
||||
type SubmitGrading struct {
|
||||
EggsGrading []EggGrading `json:"eggs_grading" validate:"required,dive"`
|
||||
}
|
||||
|
||||
type Approve struct {
|
||||
Action string `json:"action" validate:"required_strict"`
|
||||
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
|
||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||
}
|
||||
|
||||
@@ -108,7 +108,6 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([
|
||||
db = db.Where("transfer_number ILIKE ?", "%"+params.TransferNumber+"%")
|
||||
}
|
||||
|
||||
// Handle sort
|
||||
sortField := "created_at"
|
||||
if params.Sort != "" {
|
||||
sortField = params.Sort
|
||||
@@ -127,7 +126,6 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Filter by approval status if requested
|
||||
if params.ApprovalStatus != "" {
|
||||
var filtered []entity.LayingTransfer
|
||||
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
|
||||
@@ -156,7 +154,6 @@ func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.LayingTran
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Fetch and populate latest approval
|
||||
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
|
||||
latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transferLaying.Id, nil)
|
||||
if err == nil && latestApproval != nil {
|
||||
@@ -172,7 +169,6 @@ func (s transferLayingService) GetOneWithApproval(c *fiber.Ctx, id uint) (*entit
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Return the LatestApproval that was populated in GetOne
|
||||
return transferLaying, transferLaying.LatestApproval, nil
|
||||
}
|
||||
|
||||
@@ -181,11 +177,18 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := commonSvc.EnsureRelations(c.Context(),
|
||||
commonSvc.RelationCheck{Name: "Source Project Flock", ID: &req.SourceProjectFlockId, Exists: s.ProjectFlockRepo.IdExists},
|
||||
commonSvc.RelationCheck{Name: "Target Project Flock", ID: &req.TargetProjectFlockId, Exists: s.ProjectFlockRepo.IdExists},
|
||||
); err != nil {
|
||||
return nil, err
|
||||
if _, err := s.ProjectFlockRepo.GetByID(c.Context(), req.SourceProjectFlockId, nil); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Source Project Flock not found")
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate source project flock")
|
||||
}
|
||||
|
||||
if _, err := s.ProjectFlockRepo.GetByID(c.Context(), req.TargetProjectFlockId, nil); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Target Project Flock not found")
|
||||
}
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate target project flock")
|
||||
}
|
||||
|
||||
for _, detail := range req.SourceKandangs {
|
||||
@@ -288,7 +291,7 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer laying record")
|
||||
}
|
||||
|
||||
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTxRepo(dbTransaction)
|
||||
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
||||
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction)
|
||||
|
||||
for _, sourceDetail := range req.SourceKandangs {
|
||||
@@ -320,7 +323,6 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang")
|
||||
}
|
||||
|
||||
// Get warehouse for this kandang
|
||||
targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetPFK.KandangId)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -359,7 +361,6 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if transfer laying exists
|
||||
_, err := s.Repository.GetByID(c.Context(), id, nil)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
@@ -368,14 +369,12 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer laying")
|
||||
}
|
||||
|
||||
// Check if latest approval is PENDING (not approved)
|
||||
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
|
||||
latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), id, nil)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
|
||||
}
|
||||
|
||||
// If latest approval exists and is APPROVED or REJECTED, cannot update
|
||||
if latestApproval != nil && latestApproval.Action != nil {
|
||||
action := string(*latestApproval.Action)
|
||||
if action == string(entity.ApprovalActionApproved) || action == string(entity.ApprovalActionRejected) {
|
||||
@@ -409,7 +408,7 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i
|
||||
}
|
||||
|
||||
func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
// Verify transfer laying exists
|
||||
|
||||
_, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||
return db.Preload("Sources.ProductWarehouse").Preload("Targets")
|
||||
})
|
||||
@@ -420,14 +419,12 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer laying")
|
||||
}
|
||||
|
||||
// Check if latest approval is PENDING (not approved/rejected)
|
||||
approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB())
|
||||
latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), id, nil)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
|
||||
}
|
||||
|
||||
// If latest approval exists and is APPROVED or REJECTED, cannot delete
|
||||
if latestApproval != nil && latestApproval.Action != nil {
|
||||
action := string(*latestApproval.Action)
|
||||
if action == string(entity.ApprovalActionApproved) || action == string(entity.ApprovalActionRejected) {
|
||||
@@ -435,22 +432,19 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Delete in transaction to handle cascades and qty restoration
|
||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||
// Restore source warehouse quantities
|
||||
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTxRepo(dbTransaction)
|
||||
|
||||
// Get source repository for detail info
|
||||
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
||||
|
||||
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
|
||||
sources, err := sourceRepoTx.GetByLayingTransferId(c.Context(), id)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer sources")
|
||||
}
|
||||
|
||||
// Restore quantity for each source that was reduced
|
||||
for _, source := range sources {
|
||||
if source.ProductWarehouseId != nil && source.Qty > 0 {
|
||||
// Add back the quantity that was transferred
|
||||
|
||||
if err := productWarehouseRepoTx.PatchOne(c.Context(), *source.ProductWarehouseId, map[string]any{
|
||||
"quantity": gorm.Expr("quantity + ?", source.Qty),
|
||||
}, nil); err != nil {
|
||||
@@ -459,7 +453,6 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Restore project flock population that was reduced
|
||||
projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction)
|
||||
for _, source := range sources {
|
||||
populations, err := projectFlockPopulationRepoTx.GetByProjectFlockKandangID(c.Context(), source.SourceProjectFlockKandangId)
|
||||
@@ -467,13 +460,12 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get populations for restoration")
|
||||
}
|
||||
|
||||
// Restore to latest populations first
|
||||
remainingToRestore := source.Qty
|
||||
for i := len(populations) - 1; i >= 0 && remainingToRestore > 0; i-- {
|
||||
pop := populations[i]
|
||||
restoreAmount := remainingToRestore
|
||||
if remainingToRestore < pop.TotalQty {
|
||||
// Cap restore to what can fit in this population
|
||||
|
||||
restoreAmount = remainingToRestore
|
||||
}
|
||||
|
||||
@@ -486,7 +478,6 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the transfer laying (cascade will delete sources and targets)
|
||||
if err := s.Repository.WithTx(dbTransaction).DeleteOne(c.Context(), id); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete transfer laying")
|
||||
}
|
||||
@@ -536,7 +527,7 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
|
||||
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
|
||||
targetRepoTx := repository.NewLayingTransferTargetRepository(dbTransaction)
|
||||
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTxRepo(dbTransaction)
|
||||
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
|
||||
|
||||
for _, approvableID := range approvableIDs {
|
||||
transfer, err := s.Repository.GetByID(c.Context(), approvableID, nil)
|
||||
@@ -606,6 +597,7 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
|
||||
sourceWarehouse.ProductId,
|
||||
targetWarehouse.Id,
|
||||
target.Qty,
|
||||
actorID,
|
||||
); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create or update product warehouse")
|
||||
}
|
||||
@@ -665,9 +657,9 @@ func createApprovalTransferLaying(ctx context.Context, tx *gorm.DB, transferLayi
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context, tx *gorm.DB, productID uint, warehouseID uint, quantity float64) (*entity.ProductWarehouse, error) {
|
||||
func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context, tx *gorm.DB, productID uint, warehouseID uint, quantity float64, actorID uint) (*entity.ProductWarehouse, error) {
|
||||
|
||||
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTxRepo(tx)
|
||||
productWarehouseRepoTx := rInventory.NewProductWarehouseRepository(tx)
|
||||
|
||||
existing, err := productWarehouseRepoTx.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID)
|
||||
if err == nil && existing != nil {
|
||||
@@ -685,6 +677,7 @@ func (s *transferLayingService) getOrCreateProductWarehouse(ctx context.Context,
|
||||
ProductId: productID,
|
||||
WarehouseId: warehouseID,
|
||||
Quantity: quantity,
|
||||
CreatedBy: actorID,
|
||||
}
|
||||
|
||||
if err := productWarehouseRepoTx.CreateOne(ctx, newWarehouse, nil); err != nil {
|
||||
|
||||
@@ -13,6 +13,7 @@ type StockLogRepository interface {
|
||||
GetByFlaggable(ctx context.Context, logType string, logId uint) ([]*entity.StockLog, error)
|
||||
GetByProductWarehouse(ctx context.Context, productWarehouseId uint, limit int) ([]*entity.StockLog, error)
|
||||
GetByTransactionType(ctx context.Context, transactionType string, limit int) ([]*entity.StockLog, error)
|
||||
ApplyProductWarehouseFilters(db *gorm.DB, productID, warehouseID uint) *gorm.DB
|
||||
}
|
||||
|
||||
type StockLogRepositoryImpl struct {
|
||||
@@ -86,3 +87,20 @@ func (r *StockLogRepositoryImpl) GetByTransactionType(ctx context.Context, trans
|
||||
|
||||
return stockLogs, nil
|
||||
}
|
||||
|
||||
func (r *StockLogRepositoryImpl) ApplyProductWarehouseFilters(db *gorm.DB, productID, warehouseID uint) *gorm.DB {
|
||||
if productID == 0 && warehouseID == 0 {
|
||||
return db
|
||||
}
|
||||
|
||||
db = db.Joins("JOIN product_warehouses ON product_warehouses.id = stock_logs.product_warehouse_id")
|
||||
|
||||
if productID > 0 {
|
||||
db = db.Where("product_warehouses.product_id = ?", productID)
|
||||
}
|
||||
if warehouseID > 0 {
|
||||
db = db.Where("product_warehouses.warehouse_id = ?", warehouseID)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
@@ -192,6 +192,23 @@ var TransferToLayingApprovalSteps = map[approvalutils.ApprovalStep]string{
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Recording Approval
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const (
|
||||
ApprovalWorkflowRecording approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("RECORDINGS")
|
||||
RecordingStepGradingTelur approvalutils.ApprovalStep = 1
|
||||
RecordingStepPengajuan approvalutils.ApprovalStep = 2
|
||||
RecordingStepDisetujui approvalutils.ApprovalStep = 3
|
||||
)
|
||||
|
||||
var RecordingApprovalSteps = map[approvalutils.ApprovalStep]string{
|
||||
RecordingStepGradingTelur: "Grading-Telur",
|
||||
RecordingStepPengajuan: "Pengajuan",
|
||||
RecordingStepDisetujui: "Disetujui",
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// Validators
|
||||
// -------------------------------------------------------------------
|
||||
@@ -320,6 +337,8 @@ func IsValidSupplierCategory(v string) bool {
|
||||
|
||||
// example use
|
||||
|
||||
// Recording helper
|
||||
|
||||
/**
|
||||
if !utils.IsValidFlagType(req.FlagName) {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid flag type")
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package recording
|
||||
|
||||
import (
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
|
||||
)
|
||||
|
||||
func MapBodyWeights(recordingID uint, items []validation.BodyWeight) []entity.RecordingBW {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]entity.RecordingBW, 0, len(items))
|
||||
for _, item := range items {
|
||||
totalWeight := item.TotalWeight
|
||||
if totalWeight == nil {
|
||||
calculated := item.AvgWeight * item.Qty
|
||||
totalWeight = &calculated
|
||||
}
|
||||
|
||||
result = append(result, entity.RecordingBW{
|
||||
RecordingId: recordingID,
|
||||
AvgWeight: item.AvgWeight,
|
||||
Qty: item.Qty,
|
||||
TotalWeight: *totalWeight,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func MapStocks(recordingID uint, items []validation.Stock) []entity.RecordingStock {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]entity.RecordingStock, 0, len(items))
|
||||
for _, item := range items {
|
||||
var usageAmount float64
|
||||
if item.Qty != nil {
|
||||
usageAmount = *item.Qty
|
||||
}
|
||||
usagePtr := new(float64)
|
||||
*usagePtr = usageAmount
|
||||
pending := item.PendingQty
|
||||
if pending == nil {
|
||||
pending = new(float64)
|
||||
}
|
||||
result = append(result, entity.RecordingStock{
|
||||
RecordingId: recordingID,
|
||||
ProductWarehouseId: item.ProductWarehouseId,
|
||||
UsageQty: usagePtr,
|
||||
PendingQty: pending,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func MapDepletions(recordingID uint, items []validation.Depletion) []entity.RecordingDepletion {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]entity.RecordingDepletion, 0, len(items))
|
||||
for _, item := range items {
|
||||
result = append(result, entity.RecordingDepletion{
|
||||
RecordingId: recordingID,
|
||||
ProductWarehouseId: item.ProductWarehouseId,
|
||||
Qty: item.Qty,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func MapEggs(recordingID uint, createdBy uint, items []validation.Egg) []entity.RecordingEgg {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]entity.RecordingEgg, 0, len(items))
|
||||
for _, item := range items {
|
||||
result = append(result, entity.RecordingEgg{
|
||||
RecordingId: recordingID,
|
||||
ProductWarehouseId: item.ProductWarehouseId,
|
||||
Qty: item.Qty,
|
||||
CreatedBy: createdBy,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ToGrams(weight float64) float64 {
|
||||
if weight <= 0 {
|
||||
return 0
|
||||
}
|
||||
if weight < 10 {
|
||||
return weight * 1000
|
||||
}
|
||||
return weight
|
||||
}
|
||||
|
||||
func GramsToKg(grams float64) float64 {
|
||||
if grams <= 0 {
|
||||
return 0
|
||||
}
|
||||
return grams / 1000
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
@@ -58,7 +59,7 @@ func TestKandangIntegration(t *testing.T) {
|
||||
flocID := createFlock(t, app, "Floc Test")
|
||||
|
||||
projectFloc := entities.ProjectFlock{
|
||||
FlockId: flocID,
|
||||
FlockName: fmt.Sprintf("Project Flock %d", flocID),
|
||||
AreaId: areaID,
|
||||
Category: string(utils.ProjectFlockCategoryGrowing),
|
||||
FcrId: fcrID,
|
||||
|
||||
@@ -1,417 +1,417 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
// import (
|
||||
// "encoding/json"
|
||||
// "fmt"
|
||||
// "net/http"
|
||||
// "net/url"
|
||||
// "testing"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
// "github.com/gofiber/fiber/v2"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
)
|
||||
// "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
// "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
// )
|
||||
|
||||
func TestProjectFlockSummary(t *testing.T) {
|
||||
app, db := setupIntegrationApp(t)
|
||||
// func TestProjectFlockSummary(t *testing.T) {
|
||||
// app, db := setupIntegrationApp(t)
|
||||
|
||||
areaID := createArea(t, app, "Area Project")
|
||||
locationID := createLocation(t, app, "Location Project", "Address", areaID)
|
||||
flockID := createFlock(t, app, "Flock Summary")
|
||||
fcrID := createFcr(t, app, "FCR Summary", []map[string]any{
|
||||
{"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0},
|
||||
})
|
||||
kandangID := createKandang(t, app, "Kandang Summary", locationID, 1)
|
||||
// areaID := createArea(t, app, "Area Project")
|
||||
// locationID := createLocation(t, app, "Location Project", "Address", areaID)
|
||||
// flockID := createFlock(t, app, "Flock Summary")
|
||||
// fcrID := createFcr(t, app, "FCR Summary", []map[string]any{
|
||||
// {"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0},
|
||||
// })
|
||||
// kandangID := createKandang(t, app, "Kandang Summary", locationID, 1)
|
||||
|
||||
createPayload := map[string]any{
|
||||
"flock_id": flockID,
|
||||
"area_id": areaID,
|
||||
"category": "growing",
|
||||
"fcr_id": fcrID,
|
||||
"location_id": locationID,
|
||||
"kandang_ids": []uint{kandangID},
|
||||
}
|
||||
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", createPayload)
|
||||
if resp.StatusCode != fiber.StatusCreated {
|
||||
t.Fatalf("expected 201 when creating project flock, got %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
// createPayload := map[string]any{
|
||||
// "flock_id": flockID,
|
||||
// "area_id": areaID,
|
||||
// "category": "growing",
|
||||
// "fcr_id": fcrID,
|
||||
// "location_id": locationID,
|
||||
// "kandang_ids": []uint{kandangID},
|
||||
// }
|
||||
// resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", createPayload)
|
||||
// if resp.StatusCode != fiber.StatusCreated {
|
||||
// t.Fatalf("expected 201 when creating project flock, got %d: %s", resp.StatusCode, string(body))
|
||||
// }
|
||||
|
||||
var createResp struct {
|
||||
Data struct {
|
||||
Id uint `json:"id"`
|
||||
Period int `json:"period"`
|
||||
Category string `json:"category"`
|
||||
Flock struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
} `json:"flock"`
|
||||
Area struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
} `json:"area"`
|
||||
Fcr struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
} `json:"fcr"`
|
||||
Location struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Address string `json:"address"`
|
||||
} `json:"location"`
|
||||
Kandangs []struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
} `json:"kandangs"`
|
||||
CreatedUser struct {
|
||||
Id uint `json:"id"`
|
||||
IdUser uint `json:"id_user"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
} `json:"created_user"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &createResp); err != nil {
|
||||
t.Fatalf("failed to parse create response: %v", err)
|
||||
}
|
||||
if createResp.Data.Flock.Id != flockID || createResp.Data.Flock.Name == "" {
|
||||
t.Fatalf("expected flock detail to be present, got %+v", createResp.Data.Flock)
|
||||
}
|
||||
if createResp.Data.Area.Id != areaID || createResp.Data.Area.Name == "" {
|
||||
t.Fatalf("expected area detail to be present, got %+v", createResp.Data.Area)
|
||||
}
|
||||
if createResp.Data.Category != string(utils.ProjectFlockCategoryGrowing) {
|
||||
t.Fatalf("expected category to be %s, got %s", utils.ProjectFlockCategoryGrowing, createResp.Data.Category)
|
||||
}
|
||||
if createResp.Data.Location.Id != locationID || createResp.Data.Location.Name == "" {
|
||||
t.Fatalf("expected location detail to be present, got %+v", createResp.Data.Location)
|
||||
}
|
||||
if len(createResp.Data.Kandangs) != 1 || createResp.Data.Kandangs[0].Id != kandangID {
|
||||
t.Fatalf("expected kandang detail to be present, got %+v", createResp.Data.Kandangs)
|
||||
}
|
||||
if createResp.Data.Kandangs[0].Status != string(utils.KandangStatusPengajuan) {
|
||||
t.Fatalf("expected kandang status to be PENGAJUAN, got %s", createResp.Data.Kandangs[0].Status)
|
||||
}
|
||||
if createResp.Data.Period != 1 {
|
||||
t.Fatalf("expected period 1 to be assigned automatically, got %d", createResp.Data.Period)
|
||||
}
|
||||
// var createResp struct {
|
||||
// Data struct {
|
||||
// Id uint `json:"id"`
|
||||
// Period int `json:"period"`
|
||||
// Category string `json:"category"`
|
||||
// Flock struct {
|
||||
// Id uint `json:"id"`
|
||||
// Name string `json:"name"`
|
||||
// } `json:"flock"`
|
||||
// Area struct {
|
||||
// Id uint `json:"id"`
|
||||
// Name string `json:"name"`
|
||||
// } `json:"area"`
|
||||
// Fcr struct {
|
||||
// Id uint `json:"id"`
|
||||
// Name string `json:"name"`
|
||||
// } `json:"fcr"`
|
||||
// Location struct {
|
||||
// Id uint `json:"id"`
|
||||
// Name string `json:"name"`
|
||||
// Address string `json:"address"`
|
||||
// } `json:"location"`
|
||||
// Kandangs []struct {
|
||||
// Id uint `json:"id"`
|
||||
// Name string `json:"name"`
|
||||
// Status string `json:"status"`
|
||||
// } `json:"kandangs"`
|
||||
// CreatedUser struct {
|
||||
// Id uint `json:"id"`
|
||||
// IdUser uint `json:"id_user"`
|
||||
// Email string `json:"email"`
|
||||
// Name string `json:"name"`
|
||||
// } `json:"created_user"`
|
||||
// } `json:"data"`
|
||||
// }
|
||||
// if err := json.Unmarshal(body, &createResp); err != nil {
|
||||
// t.Fatalf("failed to parse create response: %v", err)
|
||||
// }
|
||||
// if createResp.Data.Flock.Id != flockID || createResp.Data.Flock.Name == "" {
|
||||
// t.Fatalf("expected flock detail to be present, got %+v", createResp.Data.Flock)
|
||||
// }
|
||||
// if createResp.Data.Area.Id != areaID || createResp.Data.Area.Name == "" {
|
||||
// t.Fatalf("expected area detail to be present, got %+v", createResp.Data.Area)
|
||||
// }
|
||||
// if createResp.Data.Category != string(utils.ProjectFlockCategoryGrowing) {
|
||||
// t.Fatalf("expected category to be %s, got %s", utils.ProjectFlockCategoryGrowing, createResp.Data.Category)
|
||||
// }
|
||||
// if createResp.Data.Location.Id != locationID || createResp.Data.Location.Name == "" {
|
||||
// t.Fatalf("expected location detail to be present, got %+v", createResp.Data.Location)
|
||||
// }
|
||||
// if len(createResp.Data.Kandangs) != 1 || createResp.Data.Kandangs[0].Id != kandangID {
|
||||
// t.Fatalf("expected kandang detail to be present, got %+v", createResp.Data.Kandangs)
|
||||
// }
|
||||
// if createResp.Data.Kandangs[0].Status != string(utils.KandangStatusPengajuan) {
|
||||
// t.Fatalf("expected kandang status to be PENGAJUAN, got %s", createResp.Data.Kandangs[0].Status)
|
||||
// }
|
||||
// if createResp.Data.Period != 1 {
|
||||
// t.Fatalf("expected period 1 to be assigned automatically, got %d", createResp.Data.Period)
|
||||
// }
|
||||
|
||||
createdKandang := fetchKandang(t, db, kandangID)
|
||||
if createdKandang.Status != string(utils.KandangStatusPengajuan) {
|
||||
t.Fatalf("expected kandang status in DB to be PENGAJUAN, got %s", createdKandang.Status)
|
||||
}
|
||||
// createdKandang := fetchKandang(t, db, kandangID)
|
||||
// if createdKandang.Status != string(utils.KandangStatusPengajuan) {
|
||||
// t.Fatalf("expected kandang status in DB to be PENGAJUAN, got %s", createdKandang.Status)
|
||||
// }
|
||||
|
||||
var pivotRecords []entities.ProjectFlockKandang
|
||||
if err := db.Where("project_flock_id = ?", createResp.Data.Id).Find(&pivotRecords).Error; err != nil {
|
||||
t.Fatalf("failed to fetch pivot records: %v", err)
|
||||
}
|
||||
if len(pivotRecords) != 1 {
|
||||
t.Fatalf("expected 1 pivot record, got %d", len(pivotRecords))
|
||||
}
|
||||
firstPivotRecord := pivotRecords[0]
|
||||
if firstPivotRecord.KandangId != kandangID {
|
||||
t.Fatalf("expected pivot kandang id %d, got %d", kandangID, firstPivotRecord.KandangId)
|
||||
}
|
||||
// var pivotRecords []entities.ProjectFlockKandang
|
||||
// if err := db.Where("project_flock_id = ?", createResp.Data.Id).Find(&pivotRecords).Error; err != nil {
|
||||
// t.Fatalf("failed to fetch pivot records: %v", err)
|
||||
// }
|
||||
// if len(pivotRecords) != 1 {
|
||||
// t.Fatalf("expected 1 pivot record, got %d", len(pivotRecords))
|
||||
// }
|
||||
// firstPivotRecord := pivotRecords[0]
|
||||
// if firstPivotRecord.KandangId != kandangID {
|
||||
// t.Fatalf("expected pivot kandang id %d, got %d", kandangID, firstPivotRecord.KandangId)
|
||||
// }
|
||||
|
||||
secondKandangID := createKandang(t, app, "Kandang Summary 2", locationID, 1)
|
||||
secondPayload := map[string]any{
|
||||
"flock_id": flockID,
|
||||
"area_id": areaID,
|
||||
"category": "laying",
|
||||
"fcr_id": fcrID,
|
||||
"location_id": locationID,
|
||||
"kandang_ids": []uint{secondKandangID},
|
||||
}
|
||||
resp, body = doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", secondPayload)
|
||||
if resp.StatusCode != fiber.StatusCreated {
|
||||
t.Fatalf("expected 201 when creating second project flock, got %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
var createRespSecond struct {
|
||||
Data struct {
|
||||
Id uint `json:"id"`
|
||||
Period int `json:"period"`
|
||||
Category string `json:"category"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &createRespSecond); err != nil {
|
||||
t.Fatalf("failed to parse second create response: %v", err)
|
||||
}
|
||||
if createRespSecond.Data.Period != 2 {
|
||||
t.Fatalf("expected second period to be 2, got %d", createRespSecond.Data.Period)
|
||||
}
|
||||
if createRespSecond.Data.Category != string(utils.ProjectFlockCategoryLaying) {
|
||||
t.Fatalf("expected category to be %s, got %s", utils.ProjectFlockCategoryLaying, createRespSecond.Data.Category)
|
||||
}
|
||||
// secondKandangID := createKandang(t, app, "Kandang Summary 2", locationID, 1)
|
||||
// secondPayload := map[string]any{
|
||||
// "flock_id": flockID,
|
||||
// "area_id": areaID,
|
||||
// "category": "laying",
|
||||
// "fcr_id": fcrID,
|
||||
// "location_id": locationID,
|
||||
// "kandang_ids": []uint{secondKandangID},
|
||||
// }
|
||||
// resp, body = doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", secondPayload)
|
||||
// if resp.StatusCode != fiber.StatusCreated {
|
||||
// t.Fatalf("expected 201 when creating second project flock, got %d: %s", resp.StatusCode, string(body))
|
||||
// }
|
||||
// var createRespSecond struct {
|
||||
// Data struct {
|
||||
// Id uint `json:"id"`
|
||||
// Period int `json:"period"`
|
||||
// Category string `json:"category"`
|
||||
// } `json:"data"`
|
||||
// }
|
||||
// if err := json.Unmarshal(body, &createRespSecond); err != nil {
|
||||
// t.Fatalf("failed to parse second create response: %v", err)
|
||||
// }
|
||||
// if createRespSecond.Data.Period != 2 {
|
||||
// t.Fatalf("expected second period to be 2, got %d", createRespSecond.Data.Period)
|
||||
// }
|
||||
// if createRespSecond.Data.Category != string(utils.ProjectFlockCategoryLaying) {
|
||||
// t.Fatalf("expected category to be %s, got %s", utils.ProjectFlockCategoryLaying, createRespSecond.Data.Category)
|
||||
// }
|
||||
|
||||
pivotRecords = nil
|
||||
if err := db.Where("project_flock_id = ?", createRespSecond.Data.Id).Find(&pivotRecords).Error; err != nil {
|
||||
t.Fatalf("failed to fetch second pivot records: %v", err)
|
||||
}
|
||||
if len(pivotRecords) != 1 {
|
||||
t.Fatalf("expected 1 pivot record for second project, got %d", len(pivotRecords))
|
||||
}
|
||||
secondPivotRecord := pivotRecords[0]
|
||||
if secondPivotRecord.KandangId != secondKandangID {
|
||||
t.Fatalf("expected second pivot kandang id %d, got %d", secondKandangID, secondPivotRecord.KandangId)
|
||||
}
|
||||
// pivotRecords = nil
|
||||
// if err := db.Where("project_flock_id = ?", createRespSecond.Data.Id).Find(&pivotRecords).Error; err != nil {
|
||||
// t.Fatalf("failed to fetch second pivot records: %v", err)
|
||||
// }
|
||||
// if len(pivotRecords) != 1 {
|
||||
// t.Fatalf("expected 1 pivot record for second project, got %d", len(pivotRecords))
|
||||
// }
|
||||
// secondPivotRecord := pivotRecords[0]
|
||||
// if secondPivotRecord.KandangId != secondKandangID {
|
||||
// t.Fatalf("expected second pivot kandang id %d, got %d", secondKandangID, secondPivotRecord.KandangId)
|
||||
// }
|
||||
|
||||
secondKandang := fetchKandang(t, db, secondKandangID)
|
||||
if secondKandang.Status != string(utils.KandangStatusPengajuan) {
|
||||
t.Fatalf("expected second kandang status in DB to be PENGAJUAN, got %s", secondKandang.Status)
|
||||
}
|
||||
// secondKandang := fetchKandang(t, db, secondKandangID)
|
||||
// if secondKandang.Status != string(utils.KandangStatusPengajuan) {
|
||||
// t.Fatalf("expected second kandang status in DB to be PENGAJUAN, got %s", secondKandang.Status)
|
||||
// }
|
||||
|
||||
resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil)
|
||||
if resp.StatusCode != fiber.StatusOK {
|
||||
t.Fatalf("expected 200 when fetching summary, got %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
// resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil)
|
||||
// if resp.StatusCode != fiber.StatusOK {
|
||||
// t.Fatalf("expected 200 when fetching summary, got %d: %s", resp.StatusCode, string(body))
|
||||
// }
|
||||
|
||||
var summary struct {
|
||||
Data struct {
|
||||
NextPeriod int `json:"next_period"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &summary); err != nil {
|
||||
t.Fatalf("failed to parse summary response: %v", err)
|
||||
}
|
||||
// var summary struct {
|
||||
// Data struct {
|
||||
// NextPeriod int `json:"next_period"`
|
||||
// } `json:"data"`
|
||||
// }
|
||||
// if err := json.Unmarshal(body, &summary); err != nil {
|
||||
// t.Fatalf("failed to parse summary response: %v", err)
|
||||
// }
|
||||
|
||||
if summary.Data.NextPeriod != 3 {
|
||||
t.Fatalf("expected next_period 3, got %d", summary.Data.NextPeriod)
|
||||
}
|
||||
// if summary.Data.NextPeriod != 3 {
|
||||
// t.Fatalf("expected next_period 3, got %d", summary.Data.NextPeriod)
|
||||
// }
|
||||
|
||||
resp, body = doJSONRequest(t, app, http.MethodDelete, "/api/production/project_flocks/"+uintToString(createResp.Data.Id), nil)
|
||||
if resp.StatusCode != fiber.StatusOK {
|
||||
t.Fatalf("expected 200 when deleting first project flock, got %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
// resp, body = doJSONRequest(t, app, http.MethodDelete, "/api/production/project_flocks/"+uintToString(createResp.Data.Id), nil)
|
||||
// if resp.StatusCode != fiber.StatusOK {
|
||||
// t.Fatalf("expected 200 when deleting first project flock, got %d: %s", resp.StatusCode, string(body))
|
||||
// }
|
||||
|
||||
firstKandang := fetchKandang(t, db, kandangID)
|
||||
if firstKandang.ProjectFlockId != nil {
|
||||
t.Fatalf("expected project_flock_id to be nil after delete, got %v", *firstKandang.ProjectFlockId)
|
||||
}
|
||||
if firstKandang.Status != string(utils.KandangStatusNonActive) {
|
||||
t.Fatalf("expected kandang status to revert to NON_ACTIVE, got %s", firstKandang.Status)
|
||||
}
|
||||
// firstKandang := fetchKandang(t, db, kandangID)
|
||||
// if firstKandang.ProjectFlockId != nil {
|
||||
// t.Fatalf("expected project_flock_id to be nil after delete, got %v", *firstKandang.ProjectFlockId)
|
||||
// }
|
||||
// if firstKandang.Status != string(utils.KandangStatusNonActive) {
|
||||
// t.Fatalf("expected kandang status to revert to NON_ACTIVE, got %s", firstKandang.Status)
|
||||
// }
|
||||
|
||||
var remainingFirst int64
|
||||
if err := db.Model(&entities.ProjectFlockKandang{}).
|
||||
Where("project_flock_id = ? AND kandang_id = ?", createResp.Data.Id, kandangID).
|
||||
Count(&remainingFirst).Error; err != nil {
|
||||
t.Fatalf("failed to count first pivot records after delete: %v", err)
|
||||
}
|
||||
if remainingFirst != 0 {
|
||||
t.Fatalf("expected no pivot records remaining after delete, found %d", remainingFirst)
|
||||
}
|
||||
// var remainingFirst int64
|
||||
// if err := db.Model(&entities.ProjectFlockKandang{}).
|
||||
// Where("project_flock_id = ? AND kandang_id = ?", createResp.Data.Id, kandangID).
|
||||
// Count(&remainingFirst).Error; err != nil {
|
||||
// t.Fatalf("failed to count first pivot records after delete: %v", err)
|
||||
// }
|
||||
// if remainingFirst != 0 {
|
||||
// t.Fatalf("expected no pivot records remaining after delete, found %d", remainingFirst)
|
||||
// }
|
||||
|
||||
resp, body = doJSONRequest(t, app, http.MethodDelete, "/api/production/project_flocks/"+uintToString(createRespSecond.Data.Id), nil)
|
||||
if resp.StatusCode != fiber.StatusOK {
|
||||
t.Fatalf("expected 200 when deleting second project flock, got %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
// resp, body = doJSONRequest(t, app, http.MethodDelete, "/api/production/project_flocks/"+uintToString(createRespSecond.Data.Id), nil)
|
||||
// if resp.StatusCode != fiber.StatusOK {
|
||||
// t.Fatalf("expected 200 when deleting second project flock, got %d: %s", resp.StatusCode, string(body))
|
||||
// }
|
||||
|
||||
secondKandang = fetchKandang(t, db, secondKandangID)
|
||||
if secondKandang.ProjectFlockId != nil {
|
||||
t.Fatalf("expected second project_flock_id to be nil after delete, got %v", *secondKandang.ProjectFlockId)
|
||||
}
|
||||
if secondKandang.Status != string(utils.KandangStatusNonActive) {
|
||||
t.Fatalf("expected second kandang status to revert to NON_ACTIVE, got %s", secondKandang.Status)
|
||||
}
|
||||
// secondKandang = fetchKandang(t, db, secondKandangID)
|
||||
// if secondKandang.ProjectFlockId != nil {
|
||||
// t.Fatalf("expected second project_flock_id to be nil after delete, got %v", *secondKandang.ProjectFlockId)
|
||||
// }
|
||||
// if secondKandang.Status != string(utils.KandangStatusNonActive) {
|
||||
// t.Fatalf("expected second kandang status to revert to NON_ACTIVE, got %s", secondKandang.Status)
|
||||
// }
|
||||
|
||||
var remainingSecond int64
|
||||
if err := db.Model(&entities.ProjectFlockKandang{}).
|
||||
Where("project_flock_id = ? AND kandang_id = ?", createRespSecond.Data.Id, secondKandangID).
|
||||
Count(&remainingSecond).Error; err != nil {
|
||||
t.Fatalf("failed to count second pivot records after delete: %v", err)
|
||||
}
|
||||
if remainingSecond != 0 {
|
||||
t.Fatalf("expected no second pivot records remaining after delete, found %d", remainingSecond)
|
||||
}
|
||||
// var remainingSecond int64
|
||||
// if err := db.Model(&entities.ProjectFlockKandang{}).
|
||||
// Where("project_flock_id = ? AND kandang_id = ?", createRespSecond.Data.Id, secondKandangID).
|
||||
// Count(&remainingSecond).Error; err != nil {
|
||||
// t.Fatalf("failed to count second pivot records after delete: %v", err)
|
||||
// }
|
||||
// if remainingSecond != 0 {
|
||||
// t.Fatalf("expected no second pivot records remaining after delete, found %d", remainingSecond)
|
||||
// }
|
||||
|
||||
resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil)
|
||||
if resp.StatusCode != fiber.StatusOK {
|
||||
t.Fatalf("expected 200 when fetching summary after delete, got %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
// resp, body = doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks/flocks/"+uintToString(flockID)+"/periods", nil)
|
||||
// if resp.StatusCode != fiber.StatusOK {
|
||||
// t.Fatalf("expected 200 when fetching summary after delete, got %d: %s", resp.StatusCode, string(body))
|
||||
// }
|
||||
|
||||
if err := json.Unmarshal(body, &summary); err != nil {
|
||||
t.Fatalf("failed to parse summary response after delete: %v", err)
|
||||
}
|
||||
// if err := json.Unmarshal(body, &summary); err != nil {
|
||||
// t.Fatalf("failed to parse summary response after delete: %v", err)
|
||||
// }
|
||||
|
||||
if summary.Data.NextPeriod != 1 {
|
||||
t.Fatalf("expected next_period 1 after soft deletes, got %d", summary.Data.NextPeriod)
|
||||
}
|
||||
}
|
||||
// if summary.Data.NextPeriod != 1 {
|
||||
// t.Fatalf("expected next_period 1 after soft deletes, got %d", summary.Data.NextPeriod)
|
||||
// }
|
||||
// }
|
||||
|
||||
func uintToString(v uint) string {
|
||||
return fmt.Sprintf("%d", v)
|
||||
}
|
||||
// func uintToString(v uint) string {
|
||||
// return fmt.Sprintf("%d", v)
|
||||
// }
|
||||
|
||||
func TestProjectFlockSearchByRelatedFields(t *testing.T) {
|
||||
app, _ := setupIntegrationApp(t)
|
||||
// func TestProjectFlockSearchByRelatedFields(t *testing.T) {
|
||||
// app, _ := setupIntegrationApp(t)
|
||||
|
||||
areaID := createArea(t, app, "Area Search Target")
|
||||
locationID := createLocation(t, app, "Location Search Target", "Location Address Target", areaID)
|
||||
flockID := createFlock(t, app, "Flock Search Target")
|
||||
fcrID := createFcr(t, app, "FCR Search Target", []map[string]any{
|
||||
{"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0},
|
||||
})
|
||||
kandangID := createKandang(t, app, "Kandang Search Target", locationID, 1)
|
||||
// areaID := createArea(t, app, "Area Search Target")
|
||||
// locationID := createLocation(t, app, "Location Search Target", "Location Address Target", areaID)
|
||||
// flockID := createFlock(t, app, "Flock Search Target")
|
||||
// fcrID := createFcr(t, app, "FCR Search Target", []map[string]any{
|
||||
// {"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0},
|
||||
// })
|
||||
// kandangID := createKandang(t, app, "Kandang Search Target", locationID, 1)
|
||||
|
||||
createPayload := map[string]any{
|
||||
"flock_id": flockID,
|
||||
"area_id": areaID,
|
||||
"category": "growing",
|
||||
"fcr_id": fcrID,
|
||||
"location_id": locationID,
|
||||
"kandang_ids": []uint{kandangID},
|
||||
}
|
||||
// createPayload := map[string]any{
|
||||
// "flock_id": flockID,
|
||||
// "area_id": areaID,
|
||||
// "category": "growing",
|
||||
// "fcr_id": fcrID,
|
||||
// "location_id": locationID,
|
||||
// "kandang_ids": []uint{kandangID},
|
||||
// }
|
||||
|
||||
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", createPayload)
|
||||
if resp.StatusCode != fiber.StatusCreated {
|
||||
t.Fatalf("expected 201 when creating project flock, got %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
// resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", createPayload)
|
||||
// if resp.StatusCode != fiber.StatusCreated {
|
||||
// t.Fatalf("expected 201 when creating project flock, got %d: %s", resp.StatusCode, string(body))
|
||||
// }
|
||||
|
||||
var createResp struct {
|
||||
Data struct {
|
||||
Id uint `json:"id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &createResp); err != nil {
|
||||
t.Fatalf("failed to parse create response: %v", err)
|
||||
}
|
||||
// var createResp struct {
|
||||
// Data struct {
|
||||
// Id uint `json:"id"`
|
||||
// } `json:"data"`
|
||||
// }
|
||||
// if err := json.Unmarshal(body, &createResp); err != nil {
|
||||
// t.Fatalf("failed to parse create response: %v", err)
|
||||
// }
|
||||
|
||||
searchTerms := []string{
|
||||
"Flock Search Target",
|
||||
"Area Search Target",
|
||||
string(utils.ProjectFlockCategoryGrowing),
|
||||
"growing",
|
||||
"FCR Search Target",
|
||||
"Kandang Search Target",
|
||||
"Location Search Target",
|
||||
"Location Address Target",
|
||||
"Tester",
|
||||
"1",
|
||||
}
|
||||
// searchTerms := []string{
|
||||
// "Flock Search Target",
|
||||
// "Area Search Target",
|
||||
// string(utils.ProjectFlockCategoryGrowing),
|
||||
// "growing",
|
||||
// "FCR Search Target",
|
||||
// "Kandang Search Target",
|
||||
// "Location Search Target",
|
||||
// "Location Address Target",
|
||||
// "Tester",
|
||||
// "1",
|
||||
// }
|
||||
|
||||
for _, term := range searchTerms {
|
||||
path := "/api/production/project_flocks?search=" + url.QueryEscape(term)
|
||||
resp, body := doJSONRequest(t, app, http.MethodGet, path, nil)
|
||||
if resp.StatusCode != fiber.StatusOK {
|
||||
t.Fatalf("expected 200 when searching for %q, got %d: %s", term, resp.StatusCode, string(body))
|
||||
}
|
||||
// for _, term := range searchTerms {
|
||||
// path := "/api/production/project_flocks?search=" + url.QueryEscape(term)
|
||||
// resp, body := doJSONRequest(t, app, http.MethodGet, path, nil)
|
||||
// if resp.StatusCode != fiber.StatusOK {
|
||||
// t.Fatalf("expected 200 when searching for %q, got %d: %s", term, resp.StatusCode, string(body))
|
||||
// }
|
||||
|
||||
var listResp struct {
|
||||
Data []struct {
|
||||
Id uint `json:"id"`
|
||||
} `json:"data"`
|
||||
Meta struct {
|
||||
TotalResults int64 `json:"total_results"`
|
||||
} `json:"meta"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &listResp); err != nil {
|
||||
t.Fatalf("failed to parse list response for %q: %v", term, err)
|
||||
}
|
||||
if listResp.Meta.TotalResults == 0 {
|
||||
t.Fatalf("expected at least one result when searching for %q", term)
|
||||
}
|
||||
if len(listResp.Data) == 0 {
|
||||
t.Fatalf("expected data when searching for %q", term)
|
||||
}
|
||||
if listResp.Data[0].Id != createResp.Data.Id {
|
||||
t.Fatalf("expected project flock id %d for search term %q, got %d", createResp.Data.Id, term, listResp.Data[0].Id)
|
||||
}
|
||||
}
|
||||
}
|
||||
// var listResp struct {
|
||||
// Data []struct {
|
||||
// Id uint `json:"id"`
|
||||
// } `json:"data"`
|
||||
// Meta struct {
|
||||
// TotalResults int64 `json:"total_results"`
|
||||
// } `json:"meta"`
|
||||
// }
|
||||
// if err := json.Unmarshal(body, &listResp); err != nil {
|
||||
// t.Fatalf("failed to parse list response for %q: %v", term, err)
|
||||
// }
|
||||
// if listResp.Meta.TotalResults == 0 {
|
||||
// t.Fatalf("expected at least one result when searching for %q", term)
|
||||
// }
|
||||
// if len(listResp.Data) == 0 {
|
||||
// t.Fatalf("expected data when searching for %q", term)
|
||||
// }
|
||||
// if listResp.Data[0].Id != createResp.Data.Id {
|
||||
// t.Fatalf("expected project flock id %d for search term %q, got %d", createResp.Data.Id, term, listResp.Data[0].Id)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
func TestProjectFlockSorting(t *testing.T) {
|
||||
app, _ := setupIntegrationApp(t)
|
||||
// func TestProjectFlockSorting(t *testing.T) {
|
||||
// app, _ := setupIntegrationApp(t)
|
||||
|
||||
areaA := createArea(t, app, "Area Alpha")
|
||||
areaB := createArea(t, app, "Area Beta")
|
||||
// areaA := createArea(t, app, "Area Alpha")
|
||||
// areaB := createArea(t, app, "Area Beta")
|
||||
|
||||
locationA := createLocation(t, app, "Location Alpha", "Address Alpha", areaA)
|
||||
locationB := createLocation(t, app, "Location Beta", "Address Beta", areaB)
|
||||
// locationA := createLocation(t, app, "Location Alpha", "Address Alpha", areaA)
|
||||
// locationB := createLocation(t, app, "Location Beta", "Address Beta", areaB)
|
||||
|
||||
flockOne := createFlock(t, app, "Flock Sort One")
|
||||
flockTwo := createFlock(t, app, "Flock Sort Two")
|
||||
// flockOne := createFlock(t, app, "Flock Sort One")
|
||||
// flockTwo := createFlock(t, app, "Flock Sort Two")
|
||||
|
||||
fcrID := createFcr(t, app, "FCR Sort", []map[string]any{
|
||||
{"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0},
|
||||
})
|
||||
// fcrID := createFcr(t, app, "FCR Sort", []map[string]any{
|
||||
// {"weight": 1.0, "fcr_number": 1.5, "mortality": 2.0},
|
||||
// })
|
||||
|
||||
kandangOne := createKandang(t, app, "Kandang Sort One", locationA, 1)
|
||||
kandangTwo := createKandang(t, app, "Kandang Sort Two", locationB, 1)
|
||||
kandangThree := createKandang(t, app, "Kandang Sort Three", locationB, 1)
|
||||
// kandangOne := createKandang(t, app, "Kandang Sort One", locationA, 1)
|
||||
// kandangTwo := createKandang(t, app, "Kandang Sort Two", locationB, 1)
|
||||
// kandangThree := createKandang(t, app, "Kandang Sort Three", locationB, 1)
|
||||
|
||||
projectOnePayload := map[string]any{
|
||||
"flock_id": flockOne,
|
||||
"area_id": areaA,
|
||||
"category": "growing",
|
||||
"fcr_id": fcrID,
|
||||
"location_id": locationA,
|
||||
"kandang_ids": []uint{kandangOne},
|
||||
}
|
||||
resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", projectOnePayload)
|
||||
if resp.StatusCode != fiber.StatusCreated {
|
||||
t.Fatalf("expected 201 for project one, got %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
projectOneID := parseProjectFlockID(t, body)
|
||||
// projectOnePayload := map[string]any{
|
||||
// "flock_id": flockOne,
|
||||
// "area_id": areaA,
|
||||
// "category": "growing",
|
||||
// "fcr_id": fcrID,
|
||||
// "location_id": locationA,
|
||||
// "kandang_ids": []uint{kandangOne},
|
||||
// }
|
||||
// resp, body := doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", projectOnePayload)
|
||||
// if resp.StatusCode != fiber.StatusCreated {
|
||||
// t.Fatalf("expected 201 for project one, got %d: %s", resp.StatusCode, string(body))
|
||||
// }
|
||||
// projectOneID := parseProjectFlockID(t, body)
|
||||
|
||||
projectTwoPayload := map[string]any{
|
||||
"flock_id": flockTwo,
|
||||
"area_id": areaB,
|
||||
"category": "laying",
|
||||
"fcr_id": fcrID,
|
||||
"location_id": locationB,
|
||||
"kandang_ids": []uint{kandangTwo, kandangThree},
|
||||
}
|
||||
resp, body = doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", projectTwoPayload)
|
||||
if resp.StatusCode != fiber.StatusCreated {
|
||||
t.Fatalf("expected 201 for project two, got %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
projectTwoID := parseProjectFlockID(t, body)
|
||||
// projectTwoPayload := map[string]any{
|
||||
// "flock_id": flockTwo,
|
||||
// "area_id": areaB,
|
||||
// "category": "laying",
|
||||
// "fcr_id": fcrID,
|
||||
// "location_id": locationB,
|
||||
// "kandang_ids": []uint{kandangTwo, kandangThree},
|
||||
// }
|
||||
// resp, body = doJSONRequest(t, app, http.MethodPost, "/api/production/project_flocks", projectTwoPayload)
|
||||
// if resp.StatusCode != fiber.StatusCreated {
|
||||
// t.Fatalf("expected 201 for project two, got %d: %s", resp.StatusCode, string(body))
|
||||
// }
|
||||
// projectTwoID := parseProjectFlockID(t, body)
|
||||
|
||||
updatePeriodPayload := map[string]any{"period": 5}
|
||||
resp, body = doJSONRequest(t, app, http.MethodPatch, "/api/production/project_flocks/"+uintToString(projectTwoID), updatePeriodPayload)
|
||||
if resp.StatusCode != fiber.StatusOK {
|
||||
t.Fatalf("expected 200 when updating period, got %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
// updatePeriodPayload := map[string]any{"period": 5}
|
||||
// resp, body = doJSONRequest(t, app, http.MethodPatch, "/api/production/project_flocks/"+uintToString(projectTwoID), updatePeriodPayload)
|
||||
// if resp.StatusCode != fiber.StatusOK {
|
||||
// t.Fatalf("expected 200 when updating period, got %d: %s", resp.StatusCode, string(body))
|
||||
// }
|
||||
|
||||
assertOrder := func(t *testing.T, app *fiber.App, query string, expectedFirst uint) {
|
||||
t.Helper()
|
||||
resp, body := doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks?"+query, nil)
|
||||
if resp.StatusCode != fiber.StatusOK {
|
||||
t.Fatalf("expected 200 for query %q, got %d: %s", query, resp.StatusCode, string(body))
|
||||
}
|
||||
var listResp struct {
|
||||
Data []struct {
|
||||
Id uint `json:"id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &listResp); err != nil {
|
||||
t.Fatalf("failed to parse list response for %q: %v", query, err)
|
||||
}
|
||||
if len(listResp.Data) == 0 {
|
||||
t.Fatalf("expected data for query %q", query)
|
||||
}
|
||||
if listResp.Data[0].Id != expectedFirst {
|
||||
t.Fatalf("expected first id %d for query %q, got %d", expectedFirst, query, listResp.Data[0].Id)
|
||||
}
|
||||
}
|
||||
// assertOrder := func(t *testing.T, app *fiber.App, query string, expectedFirst uint) {
|
||||
// t.Helper()
|
||||
// resp, body := doJSONRequest(t, app, http.MethodGet, "/api/production/project_flocks?"+query, nil)
|
||||
// if resp.StatusCode != fiber.StatusOK {
|
||||
// t.Fatalf("expected 200 for query %q, got %d: %s", query, resp.StatusCode, string(body))
|
||||
// }
|
||||
// var listResp struct {
|
||||
// Data []struct {
|
||||
// Id uint `json:"id"`
|
||||
// } `json:"data"`
|
||||
// }
|
||||
// if err := json.Unmarshal(body, &listResp); err != nil {
|
||||
// t.Fatalf("failed to parse list response for %q: %v", query, err)
|
||||
// }
|
||||
// if len(listResp.Data) == 0 {
|
||||
// t.Fatalf("expected data for query %q", query)
|
||||
// }
|
||||
// if listResp.Data[0].Id != expectedFirst {
|
||||
// t.Fatalf("expected first id %d for query %q, got %d", expectedFirst, query, listResp.Data[0].Id)
|
||||
// }
|
||||
// }
|
||||
|
||||
assertOrder(t, app, "sort_by=area&sort_order=asc", projectOneID)
|
||||
assertOrder(t, app, "sort_by=location&sort_order=desc", projectTwoID)
|
||||
assertOrder(t, app, "sort_by=period&sort_order=desc", projectTwoID)
|
||||
assertOrder(t, app, "sort_by=kandangs&sort_order=desc", projectTwoID)
|
||||
assertOrder(t, app, "sort_by=kandangs&sort_order=asc", projectOneID)
|
||||
}
|
||||
// assertOrder(t, app, "sort_by=area&sort_order=asc", projectOneID)
|
||||
// assertOrder(t, app, "sort_by=location&sort_order=desc", projectTwoID)
|
||||
// assertOrder(t, app, "sort_by=period&sort_order=desc", projectTwoID)
|
||||
// assertOrder(t, app, "sort_by=kandangs&sort_order=desc", projectTwoID)
|
||||
// assertOrder(t, app, "sort_by=kandangs&sort_order=asc", projectOneID)
|
||||
// }
|
||||
|
||||
func parseProjectFlockID(t *testing.T, body []byte) uint {
|
||||
t.Helper()
|
||||
var resp struct {
|
||||
Data struct {
|
||||
Id uint `json:"id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &resp); err != nil {
|
||||
t.Fatalf("failed to parse project flock response: %v", err)
|
||||
}
|
||||
return resp.Data.Id
|
||||
}
|
||||
// func parseProjectFlockID(t *testing.T, body []byte) uint {
|
||||
// t.Helper()
|
||||
// var resp struct {
|
||||
// Data struct {
|
||||
// Id uint `json:"id"`
|
||||
// } `json:"data"`
|
||||
// }
|
||||
// if err := json.Unmarshal(body, &resp); err != nil {
|
||||
// t.Fatalf("failed to parse project flock response: %v", err)
|
||||
// }
|
||||
// return resp.Data.Id
|
||||
// }
|
||||
|
||||
Reference in New Issue
Block a user