diff --git a/go.mod b/go.mod index 355f8e5c..abb6d004 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/redis/go-redis/v9 v9.14.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/viper v1.19.0 + github.com/xuri/excelize/v2 v2.9.0 golang.org/x/crypto v0.33.0 gorm.io/driver/postgres v1.5.9 gorm.io/gorm v1.25.11 @@ -71,9 +72,12 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/philhofer/fwd v1.1.2 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/richardlehane/mscfb v1.0.4 // indirect + github.com/richardlehane/msoleps v1.0.4 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect @@ -82,12 +86,15 @@ require ( github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/testify v1.11.1 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tinylib/msgp v1.1.8 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.55.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect + github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d // indirect + github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect diff --git a/go.sum b/go.sum index 188b0dae..73b36464 100644 --- a/go.sum +++ b/go.sum @@ -182,6 +182,8 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= @@ -195,6 +197,11 @@ github.com/redis/go-redis/v9 v9.14.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6 github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= +github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00= +github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -238,8 +245,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= @@ -252,6 +260,16 @@ github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8 github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY= +github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= +github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.9.0 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE= +github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE= +github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A= +github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE= +github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -278,6 +296,8 @@ golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= +golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= diff --git a/internal/database/migrations/20251227234328_create_project_flock_kandang_uniformity.down.sql b/internal/database/migrations/20251227234328_create_project_flock_kandang_uniformity.down.sql new file mode 100644 index 00000000..19eaba80 --- /dev/null +++ b/internal/database/migrations/20251227234328_create_project_flock_kandang_uniformity.down.sql @@ -0,0 +1,6 @@ +DROP INDEX IF EXISTS idx_project_flock_kandang_uniformity_deleted_at; +DROP INDEX IF EXISTS idx_project_flock_kandang_uniformity_created_by; +DROP INDEX IF EXISTS idx_project_flock_kandang_uniformity_project_flock_kandang_week; +DROP INDEX IF EXISTS idx_project_flock_kandang_uniformity_project_flock_kandang_id; + +DROP TABLE IF EXISTS project_flock_kandang_uniformity; diff --git a/internal/database/migrations/20251227234328_create_project_flock_kandang_uniformity.up.sql b/internal/database/migrations/20251227234328_create_project_flock_kandang_uniformity.up.sql new file mode 100644 index 00000000..86bc5ed5 --- /dev/null +++ b/internal/database/migrations/20251227234328_create_project_flock_kandang_uniformity.up.sql @@ -0,0 +1,58 @@ +CREATE TABLE IF NOT EXISTS project_flock_kandang_uniformity ( + id BIGSERIAL PRIMARY KEY, + uniformity NUMERIC(15, 3), + week INT NOT NULL, + cv NUMERIC(15, 3), + chick_qty_of_weight NUMERIC(15, 3), + mean_up NUMERIC(15, 3), + mean_down NUMERIC(15, 3), + project_flock_kandang_id BIGINT NOT NULL, + uniform_qty NUMERIC(15, 3), + not_uniform_qty NUMERIC(15, 3), + uniform_date TIMESTAMPTZ, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ, + created_by BIGINT NOT NULL +); + +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'fk_project_flock_kandang_uniformity_project_flock_kandang' + ) THEN + EXECUTE + 'ALTER TABLE project_flock_kandang_uniformity + ADD CONSTRAINT fk_project_flock_kandang_uniformity_project_flock_kandang + FOREIGN KEY (project_flock_kandang_id) + REFERENCES project_flock_kandangs(id) + ON DELETE RESTRICT ON UPDATE CASCADE'; + END IF; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN + IF NOT EXISTS ( + SELECT 1 FROM pg_constraint WHERE conname = 'fk_project_flock_kandang_uniformity_created_by' + ) THEN + EXECUTE + 'ALTER TABLE project_flock_kandang_uniformity + ADD CONSTRAINT fk_project_flock_kandang_uniformity_created_by + FOREIGN KEY (created_by) + REFERENCES users(id) + ON DELETE RESTRICT ON UPDATE CASCADE'; + END IF; + END IF; +END $$; + +CREATE INDEX IF NOT EXISTS idx_project_flock_kandang_uniformity_project_flock_kandang_id + ON project_flock_kandang_uniformity (project_flock_kandang_id); + +CREATE INDEX IF NOT EXISTS idx_project_flock_kandang_uniformity_project_flock_kandang_week + ON project_flock_kandang_uniformity (project_flock_kandang_id, week); + +CREATE INDEX IF NOT EXISTS idx_project_flock_kandang_uniformity_created_by + ON project_flock_kandang_uniformity (created_by); + +CREATE INDEX IF NOT EXISTS idx_project_flock_kandang_uniformity_deleted_at + ON project_flock_kandang_uniformity (deleted_at); diff --git a/internal/database/seed/seeder.backup b/internal/database/seed/seeder.backup new file mode 100644 index 00000000..c0e3628c --- /dev/null +++ b/internal/database/seed/seeder.backup @@ -0,0 +1,1047 @@ +// package seed + +// import ( +// "errors" +// "fmt" +// "strings" +// "time" + +// entity "gitlab.com/mbugroup/lti-api.git/internal/entities" +// "gitlab.com/mbugroup/lti-api.git/internal/utils" + +// "gorm.io/gorm" +// ) + +// func Run(db *gorm.DB) error { +// return db.Transaction(func(tx *gorm.DB) error { +// users, err := seedUsers(tx) +// if err != nil { +// return err +// } +// adminID := users["admin"] + +// uoms, err := seedUoms(tx, adminID) +// if err != nil { +// return err +// } + +// areas, err := seedAreas(tx, adminID) +// if err != nil { +// return err +// } + +// locations, err := seedLocations(tx, adminID, areas) +// if err != nil { +// return err +// } + +// productCategories, err := seedProductCategories(tx, adminID) +// if err != nil { +// return err +// } + +// if _, err := seedFlocks(tx, adminID); err != nil { +// return err +// } + +// if _, err := seedFcr(tx, adminID); err != nil { +// return err +// } + +// kandangs, err := seedKandangs(tx, adminID, locations, users) +// if err != nil { +// return err +// } + +// if err := seedWarehouses(tx, adminID, areas, locations, kandangs); err != nil { +// return err +// } + +// suppliers, err := seedSuppliers(tx, adminID) +// if err != nil { +// return err +// } + +// if err := seedCustomers(tx, adminID, users); err != nil { +// return err +// } + +// if err := seedProducts(tx, adminID, uoms, productCategories, suppliers); err != nil { +// return err +// } + +// if err := seedNonstocks(tx, adminID, uoms, suppliers); err != nil { +// return err +// } + +// if err := seedBanks(tx, adminID); err != nil { +// return err +// } + +// if err := seedProductWarehouse(tx, adminID); err != nil { +// return err +// } + +// if err := seedTransferStock(tx); err != nil { +// return err +// } +// fmt.Println("✅ Master data seeding completed") +// return nil +// }) +// } + +// func seedUsers(tx *gorm.DB) (map[string]uint, error) { +// seeds := []struct { +// Key string +// Data entity.User +// }{ +// { +// Key: "admin", +// Data: entity.User{Email: "admin@mbugroup.id", IdUser: 1, Name: "Super Admin"}, +// }, +// } + +// result := make(map[string]uint, len(seeds)) + +// for _, seed := range seeds { +// var user entity.User +// err := tx.Where("email = ?", seed.Data.Email).First(&user).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// user = seed.Data +// if err := tx.Create(&user).Error; err != nil { +// return nil, err +// } +// } else if err != nil { +// return nil, err +// } +// result[seed.Key] = user.Id +// } + +// return result, nil +// } + +// func seedUoms(tx *gorm.DB, createdBy uint) (map[string]uint, error) { +// names := []string{"Kilogram", "Gram", "Liter", "Unit", "Ekor"} +// result := make(map[string]uint, len(names)) + +// for _, name := range names { +// var uom entity.Uom +// err := tx.Where("name = ?", name).First(&uom).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// uom = entity.Uom{Name: name, CreatedBy: createdBy} +// if err := tx.Create(&uom).Error; err != nil { +// return nil, err +// } +// } else if err != nil { +// return nil, err +// } +// result[name] = uom.Id +// } + +// return result, nil +// } + +// func seedAreas(tx *gorm.DB, createdBy uint) (map[string]uint, error) { +// names := []string{"Priangan", "Banten"} +// result := make(map[string]uint, len(names)) + +// for _, name := range names { +// var area entity.Area +// err := tx.Where("name = ?", name).First(&area).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// area = entity.Area{Name: name, CreatedBy: createdBy} +// if err := tx.Create(&area).Error; err != nil { +// return nil, err +// } +// } else if err != nil { +// return nil, err +// } +// result[name] = area.Id +// } + +// return result, nil +// } + +// func seedLocations(tx *gorm.DB, createdBy uint, areas map[string]uint) (map[string]uint, error) { +// seeds := []struct { +// Name string +// Address string +// Area string +// }{ +// {"Singaparna", "Tasik", "Priangan"}, +// {"Cikaum", "Cikaum", "Banten"}, +// } + +// result := make(map[string]uint, len(seeds)) + +// for _, seed := range seeds { +// areaID, ok := areas[seed.Area] +// if !ok { +// return nil, fmt.Errorf("area %s not seeded", seed.Area) +// } + +// var loc entity.Location +// err := tx.Where("name = ?", seed.Name).First(&loc).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// loc = entity.Location{ +// Name: seed.Name, +// Address: seed.Address, +// AreaId: areaID, +// CreatedBy: createdBy, +// } +// if err := tx.Create(&loc).Error; err != nil { +// return nil, err +// } +// } else if err != nil { +// return nil, err +// } +// result[seed.Name] = loc.Id +// } + +// return result, nil +// } + +// func seedFlocks(tx *gorm.DB, createdBy uint) (map[string]uint, error) { +// names := []string{"Flock Priangan", "Flock Banten"} +// result := make(map[string]uint, len(names)) + +// for _, name := range names { +// var flock entity.Flock +// err := tx.Where("name = ?", name).First(&flock).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// flock = entity.Flock{ +// Name: name, +// CreatedBy: createdBy, +// } +// if err := tx.Create(&flock).Error; err != nil { +// return nil, err +// } +// } else if err != nil { +// return nil, err +// } else { +// if err := tx.Model(&entity.Flock{}).Where("id = ?", flock.Id).Updates(map[string]any{ +// "created_by": createdBy, +// }).Error; err != nil { +// return nil, err +// } +// } +// result[name] = flock.Id +// } + +// return result, nil +// } + +// 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 +// Capacity float64 +// Location string +// PicKey string +// }{ +// {Name: "Singaparna 1", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Singaparna", PicKey: "admin"}, +// {Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Singaparna", PicKey: "admin"}, +// {Name: "Cikaum 1", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Cikaum", PicKey: "admin"}, +// {Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Cikaum", PicKey: "admin"}, +// } + +// result := make(map[string]uint, len(seeds)) + +// for _, seed := range seeds { +// locID, ok := locations[seed.Location] +// if !ok { +// return nil, fmt.Errorf("location %s not seeded", seed.Location) +// } +// picID, ok := users[seed.PicKey] +// if !ok { +// return nil, fmt.Errorf("user %s not seeded", seed.PicKey) +// } + +// var kandang entity.Kandang +// err := tx.Where("name = ?", seed.Name).First(&kandang).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// kandang = entity.Kandang{ +// Name: seed.Name, +// Status: string(seed.Status), +// LocationId: locID, +// PicId: picID, +// CreatedBy: createdBy, +// } +// if err := tx.Create(&kandang).Error; err != nil { +// return nil, err +// } +// } else if err != nil { +// return nil, err +// } else { +// updates := map[string]any{ +// "location_id": locID, +// "pic_id": picID, +// "status": string(seed.Status), +// } +// if err := tx.Model(&entity.Kandang{}).Where("id = ?", kandang.Id).Updates(updates).Error; err != nil { +// return nil, err +// } +// } +// result[seed.Name] = kandang.Id +// } + +// return result, nil +// } + +// func seedWarehouses(tx *gorm.DB, createdBy uint, areas map[string]uint, locations map[string]uint, kandangs map[string]uint) error { +// seeds := []struct { +// Name string +// Type string +// Area string +// Location *string +// Kandang *string +// }{ +// {Name: "Gudang Priangan", Type: string(utils.WarehouseTypeArea), Area: "Priangan"}, +// {Name: "Gudang Singaparna", Type: string(utils.WarehouseTypeLokasi), Area: "Priangan", Location: strPtr("Singaparna")}, +// {Name: "Gudang Singaparna 1", Type: string(utils.WarehouseTypeKandang), Area: "Priangan", Location: strPtr("Singaparna"), Kandang: strPtr("Singaparna 1")}, +// {Name: "Gudang Singaparna 2", Type: string(utils.WarehouseTypeKandang), Area: "Priangan", Location: strPtr("Singaparna"), Kandang: strPtr("Singaparna 2")}, +// {Name: "Gudang Banten", Type: string(utils.WarehouseTypeArea), Area: "Banten"}, +// {Name: "Gudang Cikaum", Type: string(utils.WarehouseTypeLokasi), Area: "Banten", Location: strPtr("Cikaum")}, +// {Name: "Gudang Cikaum 1", Type: string(utils.WarehouseTypeKandang), Area: "Banten", Location: strPtr("Cikaum"), Kandang: strPtr("Cikaum 1")}, +// {Name: "Gudang Cikaum 2", Type: string(utils.WarehouseTypeKandang), Area: "Banten", Location: strPtr("Cikaum"), Kandang: strPtr("Cikaum 2")}, +// } + +// for _, seed := range seeds { +// areaID, ok := areas[seed.Area] +// if !ok { +// return fmt.Errorf("area %s not seeded", seed.Area) +// } + +// var warehouse entity.Warehouse +// err := tx.Where("name = ?", seed.Name).First(&warehouse).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// warehouse = entity.Warehouse{ +// Name: seed.Name, +// Type: seed.Type, +// AreaId: areaID, +// CreatedBy: createdBy, +// } +// } else if err != nil { +// return err +// } + +// if seed.Location != nil { +// locID, ok := locations[*seed.Location] +// if !ok { +// return fmt.Errorf("location %s not seeded", *seed.Location) +// } +// warehouse.LocationId = uintPtr(locID) +// } +// if seed.Kandang != nil { +// kandangID, ok := kandangs[*seed.Kandang] +// if !ok { +// return fmt.Errorf("kandang %s not seeded", *seed.Kandang) +// } +// warehouse.KandangId = uintPtr(kandangID) +// } + +// if warehouse.Id == 0 { +// if err := tx.Create(&warehouse).Error; err != nil { +// return err +// } +// } else { +// if err := tx.Model(&entity.Warehouse{}).Where("id = ?", warehouse.Id).Updates(map[string]any{ +// "type": warehouse.Type, +// "area_id": warehouse.AreaId, +// "location_id": warehouse.LocationId, +// "kandang_id": warehouse.KandangId, +// }).Error; err != nil { +// return err +// } +// } +// } + +// return nil +// } + +// func seedProductCategories(tx *gorm.DB, createdBy uint) (map[string]uint, error) { +// seeds := []struct { +// Name string +// Code string +// }{ +// {"Pullet", "PLT"}, +// {"Bahan Baku", "RAW"}, +// {"Day Old Chick", "DOC"}, +// {"Telur", "EGG"}, +// } + +// result := make(map[string]uint, len(seeds)) + +// for _, seed := range seeds { +// var category entity.ProductCategory +// err := tx.Where("name = ?", seed.Name).First(&category).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// category = entity.ProductCategory{Name: seed.Name, Code: seed.Code, CreatedBy: createdBy} +// if err := tx.Create(&category).Error; err != nil { +// return nil, err +// } +// } else if err != nil { +// return nil, err +// } else { +// if err := tx.Model(&entity.ProductCategory{}).Where("id = ?", category.Id).Updates(map[string]any{ +// "code": seed.Code, +// }).Error; err != nil { +// return nil, err +// } +// } +// result[seed.Name] = category.Id +// } + +// return result, nil +// } + +// func seedSuppliers(tx *gorm.DB, createdBy uint) (map[string]uint, error) { +// seeds := []struct { +// Name string +// Alias string +// Category string +// Email string +// Phone string +// Address string +// }{ +// {"PT CHAROEN POKPHAND INDONESIA Tbk", "CPI", string(utils.SupplierCategorySapronak), "cpi@gmail.com", "081200000001", "Jl. Pakan 1, Bekasi"}, +// {"BOP Vendor", "BOP", string(utils.SupplierCategoryBOP), "bop@gmail.com", "081200000002", "Jl. Veteriner 3, Bogor"}, +// {"Ekspedisi", "EKS", string(utils.SupplierCategoryBOP), "bop@gmail.com", "081200000002", "Jl. Veteriner 3, Bogor"}, +// } + +// result := make(map[string]uint, len(seeds)) + +// for idx, seed := range seeds { +// var supplier entity.Supplier +// err := tx.Where("name = ?", seed.Name).First(&supplier).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// supplier = entity.Supplier{ +// Name: seed.Name, +// Alias: seed.Alias, +// Pic: "John Doe", +// Type: string(utils.CustomerSupplierTypeBisnis), +// Category: seed.Category, +// Phone: seed.Phone, +// Email: seed.Email, +// Address: seed.Address, +// DueDate: 30, +// CreatedBy: createdBy, +// AccountNumber: strPtr(fmt.Sprintf("%03d", idx+1)), +// } +// if err := tx.Create(&supplier).Error; err != nil { +// return nil, err +// } +// } else if err != nil { +// return nil, err +// } +// result[seed.Name] = supplier.Id +// } + +// return result, nil +// } + +// func seedCustomers(tx *gorm.DB, createdBy uint, users map[string]uint) error { +// seeds := []struct { +// Name string +// PicKey string +// Address string +// Phone string +// Email string +// }{ +// {"Abdul Azis", "admin", "Jl. Raya Utama 1, Bekasi", "082100000001", "abdul.azis@gmail.com"}, +// } + +// for idx, seed := range seeds { +// picID, ok := users[seed.PicKey] +// if !ok { +// return fmt.Errorf("user %s not seeded", seed.PicKey) +// } + +// var customer entity.Customer +// err := tx.Where("name = ?", seed.Name).First(&customer).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// customer = entity.Customer{ +// Name: seed.Name, +// PicId: picID, +// Type: string(utils.CustomerSupplierTypeBisnis), +// Address: seed.Address, +// Phone: seed.Phone, +// Email: seed.Email, +// AccountNumber: *strPtr(fmt.Sprintf("%03d", idx+1)), +// CreatedBy: createdBy, +// } +// if err := tx.Create(&customer).Error; err != nil { +// return err +// } +// } else if err != nil { +// return err +// } +// } + +// return nil +// } + +// func seedFcr(tx *gorm.DB, createdBy uint) (map[string]uint, error) { +// seeds := []struct { +// Name string +// Standards []struct { +// Weight float64 +// FcrNumber float64 +// Mortality float64 +// } +// }{ +// { +// Name: "FCR Layer", +// Standards: []struct { +// Weight float64 +// FcrNumber float64 +// Mortality float64 +// }{ +// {Weight: 0.8, FcrNumber: 1.60, Mortality: 2.0}, +// {Weight: 1.5, FcrNumber: 1.75, Mortality: 3.5}, +// }, +// }, +// } + +// result := make(map[string]uint, len(seeds)) + +// for _, seed := range seeds { +// var fcr entity.Fcr +// err := tx.Where("name = ?", seed.Name).First(&fcr).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// fcr = entity.Fcr{Name: seed.Name, CreatedBy: createdBy} +// if err := tx.Create(&fcr).Error; err != nil { +// return nil, err +// } +// } else if err != nil { +// return nil, err +// } +// result[seed.Name] = fcr.Id + +// for _, std := range seed.Standards { +// var standard entity.FcrStandard +// err := tx.Where("fcr_id = ? AND weight = ?", fcr.Id, std.Weight).First(&standard).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// standard = entity.FcrStandard{ +// FcrID: fcr.Id, +// Weight: std.Weight, +// FcrNumber: std.FcrNumber, +// Mortality: std.Mortality, +// } +// if err := tx.Create(&standard).Error; err != nil { +// return nil, err +// } +// } else if err != nil { +// return nil, err +// } else { +// if err := tx.Model(&entity.FcrStandard{}).Where("id = ?", standard.Id).Updates(map[string]any{ +// "fcr_number": std.FcrNumber, +// "mortality": std.Mortality, +// }).Error; err != nil { +// return nil, err +// } +// } +// } +// } + +// return result, nil +// } + +// func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories map[string]uint, suppliers map[string]uint) error { +// seeds := []struct { +// Name string +// Brand string +// Sku string +// Uom string +// Category string +// Price float64 +// Selling *float64 +// Tax *float64 +// Expiry *int +// Suppliers []string +// Flags []utils.FlagType +// }{ +// { +// Name: "DOC Broiler", +// Brand: "MBU Broiler", +// Sku: "BRO0001", +// Uom: "Ekor", +// Category: "Day Old Chick", +// Price: 7500, +// 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, +// Flags: []utils.FlagType{utils.FlagAyamAfkir}, +// }, +// { +// Name: "Ayam Mati", +// Brand: "-", +// Sku: "2", +// Uom: "Ekor", +// Category: "Day Old Chick", +// Price: 1, +// Flags: []utils.FlagType{utils.FlagAyamMati}, +// }, +// { +// Name: "Ayam Culling", +// Brand: "-", +// Sku: "3", +// Uom: "Ekor", +// Category: "Day Old Chick", +// Price: 1, +// Flags: []utils.FlagType{utils.FlagAyamCulling}, +// }, +// { +// Name: "Telur Konsumsi Baik", +// Brand: "-", +// Sku: "4", +// Uom: "Unit", +// Category: "Telur", +// Price: 1, +// Flags: []utils.FlagType{utils.FlagTelurUtuh}, +// }, +// { +// Name: "Telur Pecah", +// Brand: "-", +// Sku: "5", +// Uom: "Unit", +// Category: "Telur", +// Price: 1, +// Flags: []utils.FlagType{utils.FlagTelurPecah}, +// }, +// { +// Name: "281 SPECIAL STARTER", +// Brand: "281 STARTER", +// Sku: "281", +// Uom: "Kilogram", +// Category: "Bahan Baku", +// Price: 7850, +// Expiry: intPtr(60), +// Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, +// Flags: []utils.FlagType{utils.FlagPakan, utils.FlagStarter}, +// }, +// { +// Name: "Ayam Layer", +// Brand: "-", +// Sku: "LYR0001", +// Uom: "Ekor", +// Category: "Pullet", +// Price: 20000, +// Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, +// Flags: []utils.FlagType{utils.FlagLayer}, +// }, +// } + +// for _, seed := range seeds { +// uomID, ok := uoms[seed.Uom] +// if !ok { +// return fmt.Errorf("uom %s not seeded", seed.Uom) +// } +// categoryID, ok := categories[seed.Category] +// if !ok { +// return fmt.Errorf("product category %s not seeded", seed.Category) +// } + +// var product entity.Product +// err := tx.Where("name = ?", seed.Name).First(&product).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// selling := seed.Selling +// tax := seed.Tax +// product = entity.Product{ +// Name: seed.Name, +// Brand: seed.Brand, +// Sku: &seed.Sku, +// UomId: uomID, +// ProductCategoryId: categoryID, +// ProductPrice: seed.Price, +// SellingPrice: selling, +// Tax: tax, +// ExpiryPeriod: seed.Expiry, +// CreatedBy: createdBy, +// } +// if err := tx.Create(&product).Error; err != nil { +// return err +// } +// } else if err != nil { +// return err +// } else { +// updates := map[string]any{ +// "brand": seed.Brand, +// "uom_id": uomID, +// "product_category_id": categoryID, +// "product_price": seed.Price, +// "selling_price": seed.Selling, +// "tax": seed.Tax, +// "expiry_period": seed.Expiry, +// } +// if seed.Sku != "" { +// updates["sku"] = seed.Sku +// } +// if err := tx.Model(&entity.Product{}).Where("id = ?", product.Id).Updates(updates).Error; err != nil { +// return err +// } +// } + +// for _, supplierName := range seed.Suppliers { +// supplierID, ok := suppliers[supplierName] +// if !ok { +// return fmt.Errorf("supplier %s not seeded", supplierName) +// } +// var existing entity.ProductSupplier +// err := tx.Where("product_id = ? AND supplier_id = ?", product.Id, supplierID).First(&existing).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// link := entity.ProductSupplier{ProductId: product.Id, SupplierId: supplierID} +// if err := tx.Create(&link).Error; err != nil { +// return err +// } +// } else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { +// return err +// } +// } + +// if err := seedFlags(tx, product.Id, entity.FlagableTypeProduct, seed.Flags); err != nil { +// return err +// } +// } + +// return nil +// } + +// func seedNonstocks(tx *gorm.DB, createdBy uint, uoms map[string]uint, suppliers map[string]uint) error { +// seeds := []struct { +// Name string +// Uom string +// Suppliers []string +// Flags []utils.FlagType +// }{ +// { +// Name: "Expedisi DOC", +// Uom: "Ekor", +// Suppliers: []string{"Ekspedisi"}, +// Flags: []utils.FlagType{utils.FlagEkspedisi}, +// }, +// { +// Name: "Solar", +// Uom: "Liter", +// Suppliers: []string{"BOP Vendor"}, +// Flags: []utils.FlagType{}, +// }, +// } + +// for _, seed := range seeds { +// uomID, ok := uoms[seed.Uom] +// if !ok { +// return fmt.Errorf("uom %s not seeded", seed.Uom) +// } + +// var nonstock entity.Nonstock +// err := tx.Where("name = ?", seed.Name).First(&nonstock).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// nonstock = entity.Nonstock{ +// Name: seed.Name, +// UomId: uomID, +// CreatedBy: createdBy, +// } +// if err := tx.Create(&nonstock).Error; err != nil { +// return err +// } +// } else if err != nil { +// return err +// } else { +// if err := tx.Model(&entity.Nonstock{}).Where("id = ?", nonstock.Id).Updates(map[string]any{ +// "uom_id": uomID, +// }).Error; err != nil { +// return err +// } +// } + +// for _, supplierName := range seed.Suppliers { +// supplierID, ok := suppliers[supplierName] +// if !ok { +// return fmt.Errorf("supplier %s not seeded", supplierName) +// } +// var existing entity.NonstockSupplier +// err := tx.Where("nonstock_id = ? AND supplier_id = ?", nonstock.Id, supplierID).First(&existing).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// link := entity.NonstockSupplier{NonstockId: nonstock.Id, SupplierId: supplierID} +// if err := tx.Create(&link).Error; err != nil { +// return err +// } +// } else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { +// return err +// } +// } + +// if err := seedFlags(tx, nonstock.Id, entity.FlagableTypeNonstock, seed.Flags); err != nil { +// return err +// } +// } + +// return nil +// } + +// // nanti saya isi + +// func seedFlags(tx *gorm.DB, flagableID uint, flagableType string, flags []utils.FlagType) error { +// if len(flags) == 0 { +// return nil +// } +// for _, flag := range flags { +// name := strings.ToUpper(string(flag)) +// var existing entity.Flag +// err := tx.Where("name = ? AND flagable_id = ? AND flagable_type = ?", name, flagableID, flagableType).First(&existing).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// record := entity.Flag{ +// Name: name, +// FlagableID: flagableID, +// FlagableType: flagableType, +// } +// if err := tx.Create(&record).Error; err != nil { +// return err +// } +// } else if err != nil { +// return err +// } +// } +// return nil +// } + +// func seedBanks(tx *gorm.DB, createdBy uint) error { +// seeds := []struct { +// Name string +// Alias string +// Owner *string +// AccountNumber string +// }{ +// { +// Name: "Bank Central Asia", +// Alias: "BCA", +// AccountNumber: "1234567890", +// Owner: ptr("PT MBU Group"), +// }, +// { +// Name: "Bank Rakyat Indonesia", +// Alias: "BRI", +// AccountNumber: "9876543210", +// Owner: ptr("PT MBU Group"), +// }, +// { +// Name: "Bank Mandiri", +// Alias: "MAND", +// AccountNumber: "1122334455", +// Owner: ptr("PT MBU Group"), +// }, +// } + +// for _, seed := range seeds { +// var bank entity.Bank +// err := tx.Where("name = ?", seed.Name).First(&bank).Error + +// if errors.Is(err, gorm.ErrRecordNotFound) { +// bank = entity.Bank{ +// Name: seed.Name, +// Alias: seed.Alias, +// Owner: seed.Owner, +// AccountNumber: seed.AccountNumber, +// CreatedBy: createdBy, +// CreatedAt: time.Now(), +// UpdatedAt: time.Now(), +// } +// if err := tx.Create(&bank).Error; err != nil { +// return err +// } +// } else if err != nil { +// return err +// } else { +// // update data jika sudah ada +// if err := tx.Model(&entity.Bank{}).Where("id = ?", bank.Id).Updates(map[string]any{ +// "alias": seed.Alias, +// "owner": seed.Owner, +// "account_number": seed.AccountNumber, +// "updated_at": time.Now(), +// }).Error; err != nil { +// return err +// } +// } +// } + +// return nil +// } + +// func seedProductWarehouse(tx *gorm.DB, createdBy uint) error { +// seeds := []struct { +// ProductName string +// WarehouseName string +// Quantity float64 +// }{ +// {ProductName: "DOC Broiler", WarehouseName: "Gudang Priangan", Quantity: 0}, +// {ProductName: "281 SPECIAL STARTER", WarehouseName: "Gudang Singaparna", Quantity: 0}, +// {ProductName: "281 SPECIAL STARTER", WarehouseName: "Gudang Banten", Quantity: 0}, +// {ProductName: "DOC Broiler", WarehouseName: "Gudang Singaparna 1", Quantity: 0}, +// {ProductName: "Telur Konsumsi Baik", WarehouseName: "Gudang Singaparna 1", Quantity: 0}, +// {ProductName: "Telur Pecah", WarehouseName: "Gudang Singaparna 1", Quantity: 0}, +// {ProductName: "Telur Konsumsi Baik", WarehouseName: "Gudang Cikaum 1", Quantity: 0}, +// {ProductName: "Telur Pecah", WarehouseName: "Gudang Cikaum 1", Quantity: 0}, +// } + +// 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 = ?", product.Id, warehouse.Id).First(&productWarehouse).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// productWarehouse = entity.ProductWarehouse{ +// ProductId: product.Id, +// WarehouseId: warehouse.Id, +// Quantity: seed.Quantity, +// // CreatedBy: createdBy, +// } +// if err := tx.Create(&productWarehouse).Error; err != nil { +// return err +// } +// } else if err != nil { +// return err +// } else { +// if err := tx.Model(&productWarehouse).Updates(map[string]any{ +// "quantity": seed.Quantity, +// }).Error; err != nil { +// return err +// } +// } +// } + +// return nil +// } + +// func seedTransferStock(tx *gorm.DB) error { + +// transfer := entity.StockTransfer{ +// FromWarehouseId: 1, +// ToWarehouseId: 2, +// Reason: "Seed transfer stock", +// TransferDate: time.Now(), +// MovementNumber: "SEED-TRF-00001", +// CreatedBy: 1, +// } +// if err := tx.Create(&transfer).Error; err != nil { +// return err +// } + +// details := []entity.StockTransferDetail{ +// { +// StockTransferId: transfer.Id, +// ProductId: 1, + +// SourceProductWarehouseID: func() *uint64 { id := uint64(1); return &id }(), +// DestProductWarehouseID: func() *uint64 { id := uint64(2); return &id }(), +// UsageQty: 10, +// PendingQty: 0, +// TotalQty: 10, +// TotalUsed: 0, +// }, +// { +// StockTransferId: transfer.Id, +// ProductId: 2, + +// SourceProductWarehouseID: func() *uint64 { id := uint64(1); return &id }(), +// DestProductWarehouseID: func() *uint64 { id := uint64(2); return &id }(), +// UsageQty: 5, +// PendingQty: 0, +// TotalQty: 5, +// TotalUsed: 0, +// }, +// } +// for i := range details { +// if err := tx.Create(&details[i]).Error; err != nil { +// return err +// } +// } + +// deliveries := []entity.StockTransferDelivery{ +// { +// StockTransferId: transfer.Id, +// SupplierId: 1, +// VehiclePlate: "B 1234 XYZ", +// DriverName: "Driver Seed", +// DocumentPath: "seed.pdf", +// ShippingCostItem: 1000, +// ShippingCostTotal: 2000, +// }, +// } +// for i := range deliveries { +// if err := tx.Create(&deliveries[i]).Error; err != nil { +// return err +// } +// } + +// detailMap := make(map[uint64]uint64) +// for _, d := range details { +// detailMap[d.ProductId] = d.Id +// } + +// deliveryItems := []entity.StockTransferDeliveryItem{ +// { +// StockTransferDeliveryId: deliveries[0].Id, +// StockTransferDetailId: detailMap[1], +// Quantity: 50, +// }, +// { +// StockTransferDeliveryId: deliveries[0].Id, +// StockTransferDetailId: detailMap[2], +// Quantity: 30, +// }, +// } +// for i := range deliveryItems { +// if err := tx.Create(&deliveryItems[i]).Error; err != nil { +// return err +// } +// } + +// return nil +// } +// func ptr[T any](v T) *T { +// return &v +// } + +// func strPtr(s string) *string { +// return &s +// } + +// func intPtr(v int) *int { +// return &v +// } + +// func uintPtr(v uint) *uint { +// return &v +// } diff --git a/internal/database/seed/seeder.go b/internal/database/seed/seeder.go index 26c3f6e8..b4f6886e 100644 --- a/internal/database/seed/seeder.go +++ b/internal/database/seed/seeder.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "strings" - "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -25,66 +24,20 @@ func Run(db *gorm.DB) error { return err } - areas, err := seedAreas(tx, adminID) - if err != nil { - return err - } - - locations, err := seedLocations(tx, adminID, areas) - if err != nil { - return err - } - productCategories, err := seedProductCategories(tx, adminID) if err != nil { return err } - if _, err := seedFlocks(tx, adminID); err != nil { - return err - } - - if _, err := seedFcr(tx, adminID); err != nil { - return err - } - - kandangs, err := seedKandangs(tx, adminID, locations, users) - if err != nil { - return err - } - - if err := seedWarehouses(tx, adminID, areas, locations, kandangs); err != nil { - return err - } - suppliers, err := seedSuppliers(tx, adminID) if err != nil { return err } - if err := seedCustomers(tx, adminID, users); err != nil { - return err - } - if err := seedProducts(tx, adminID, uoms, productCategories, suppliers); err != nil { return err } - if err := seedNonstocks(tx, adminID, uoms, suppliers); err != nil { - return err - } - - if err := seedBanks(tx, adminID); err != nil { - return err - } - - if err := seedProductWarehouse(tx, adminID); err != nil { - return err - } - - if err := seedTransferStock(tx); err != nil { - return err - } fmt.Println("✅ Master data seeding completed") return nil }) @@ -141,224 +94,6 @@ func seedUoms(tx *gorm.DB, createdBy uint) (map[string]uint, error) { return result, nil } -func seedAreas(tx *gorm.DB, createdBy uint) (map[string]uint, error) { - names := []string{"Priangan", "Banten"} - result := make(map[string]uint, len(names)) - - for _, name := range names { - var area entity.Area - err := tx.Where("name = ?", name).First(&area).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - area = entity.Area{Name: name, CreatedBy: createdBy} - if err := tx.Create(&area).Error; err != nil { - return nil, err - } - } else if err != nil { - return nil, err - } - result[name] = area.Id - } - - return result, nil -} - -func seedLocations(tx *gorm.DB, createdBy uint, areas map[string]uint) (map[string]uint, error) { - seeds := []struct { - Name string - Address string - Area string - }{ - {"Singaparna", "Tasik", "Priangan"}, - {"Cikaum", "Cikaum", "Banten"}, - } - - result := make(map[string]uint, len(seeds)) - - for _, seed := range seeds { - areaID, ok := areas[seed.Area] - if !ok { - return nil, fmt.Errorf("area %s not seeded", seed.Area) - } - - var loc entity.Location - err := tx.Where("name = ?", seed.Name).First(&loc).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - loc = entity.Location{ - Name: seed.Name, - Address: seed.Address, - AreaId: areaID, - CreatedBy: createdBy, - } - if err := tx.Create(&loc).Error; err != nil { - return nil, err - } - } else if err != nil { - return nil, err - } - result[seed.Name] = loc.Id - } - - return result, nil -} - -func seedFlocks(tx *gorm.DB, createdBy uint) (map[string]uint, error) { - names := []string{"Flock Priangan", "Flock Banten"} - result := make(map[string]uint, len(names)) - - for _, name := range names { - var flock entity.Flock - err := tx.Where("name = ?", name).First(&flock).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - flock = entity.Flock{ - Name: name, - CreatedBy: createdBy, - } - if err := tx.Create(&flock).Error; err != nil { - return nil, err - } - } else if err != nil { - return nil, err - } else { - if err := tx.Model(&entity.Flock{}).Where("id = ?", flock.Id).Updates(map[string]any{ - "created_by": createdBy, - }).Error; err != nil { - return nil, err - } - } - result[name] = flock.Id - } - - return result, nil -} - -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 - Capacity float64 - Location string - PicKey string - }{ - {Name: "Singaparna 1", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Singaparna", PicKey: "admin"}, - {Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Singaparna", PicKey: "admin"}, - {Name: "Cikaum 1", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Cikaum", PicKey: "admin"}, - {Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Capacity: 50000, Location: "Cikaum", PicKey: "admin"}, - } - - result := make(map[string]uint, len(seeds)) - - for _, seed := range seeds { - locID, ok := locations[seed.Location] - if !ok { - return nil, fmt.Errorf("location %s not seeded", seed.Location) - } - picID, ok := users[seed.PicKey] - if !ok { - return nil, fmt.Errorf("user %s not seeded", seed.PicKey) - } - - var kandang entity.Kandang - err := tx.Where("name = ?", seed.Name).First(&kandang).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - kandang = entity.Kandang{ - Name: seed.Name, - Status: string(seed.Status), - LocationId: locID, - PicId: picID, - CreatedBy: createdBy, - } - if err := tx.Create(&kandang).Error; err != nil { - return nil, err - } - } else if err != nil { - return nil, err - } else { - updates := map[string]any{ - "location_id": locID, - "pic_id": picID, - "status": string(seed.Status), - } - if err := tx.Model(&entity.Kandang{}).Where("id = ?", kandang.Id).Updates(updates).Error; err != nil { - return nil, err - } - } - result[seed.Name] = kandang.Id - } - - return result, nil -} - -func seedWarehouses(tx *gorm.DB, createdBy uint, areas map[string]uint, locations map[string]uint, kandangs map[string]uint) error { - seeds := []struct { - Name string - Type string - Area string - Location *string - Kandang *string - }{ - {Name: "Gudang Priangan", Type: string(utils.WarehouseTypeArea), Area: "Priangan"}, - {Name: "Gudang Singaparna", Type: string(utils.WarehouseTypeLokasi), Area: "Priangan", Location: strPtr("Singaparna")}, - {Name: "Gudang Singaparna 1", Type: string(utils.WarehouseTypeKandang), Area: "Priangan", Location: strPtr("Singaparna"), Kandang: strPtr("Singaparna 1")}, - {Name: "Gudang Singaparna 2", Type: string(utils.WarehouseTypeKandang), Area: "Priangan", Location: strPtr("Singaparna"), Kandang: strPtr("Singaparna 2")}, - {Name: "Gudang Banten", Type: string(utils.WarehouseTypeArea), Area: "Banten"}, - {Name: "Gudang Cikaum", Type: string(utils.WarehouseTypeLokasi), Area: "Banten", Location: strPtr("Cikaum")}, - {Name: "Gudang Cikaum 1", Type: string(utils.WarehouseTypeKandang), Area: "Banten", Location: strPtr("Cikaum"), Kandang: strPtr("Cikaum 1")}, - {Name: "Gudang Cikaum 2", Type: string(utils.WarehouseTypeKandang), Area: "Banten", Location: strPtr("Cikaum"), Kandang: strPtr("Cikaum 2")}, - } - - for _, seed := range seeds { - areaID, ok := areas[seed.Area] - if !ok { - return fmt.Errorf("area %s not seeded", seed.Area) - } - - var warehouse entity.Warehouse - err := tx.Where("name = ?", seed.Name).First(&warehouse).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - warehouse = entity.Warehouse{ - Name: seed.Name, - Type: seed.Type, - AreaId: areaID, - CreatedBy: createdBy, - } - } else if err != nil { - return err - } - - if seed.Location != nil { - locID, ok := locations[*seed.Location] - if !ok { - return fmt.Errorf("location %s not seeded", *seed.Location) - } - warehouse.LocationId = uintPtr(locID) - } - if seed.Kandang != nil { - kandangID, ok := kandangs[*seed.Kandang] - if !ok { - return fmt.Errorf("kandang %s not seeded", *seed.Kandang) - } - warehouse.KandangId = uintPtr(kandangID) - } - - if warehouse.Id == 0 { - if err := tx.Create(&warehouse).Error; err != nil { - return err - } - } else { - if err := tx.Model(&entity.Warehouse{}).Where("id = ?", warehouse.Id).Updates(map[string]any{ - "type": warehouse.Type, - "area_id": warehouse.AreaId, - "location_id": warehouse.LocationId, - "kandang_id": warehouse.KandangId, - }).Error; err != nil { - return err - } - } - } - - return nil -} - func seedProductCategories(tx *gorm.DB, createdBy uint) (map[string]uint, error) { seeds := []struct { Name string @@ -440,113 +175,6 @@ func seedSuppliers(tx *gorm.DB, createdBy uint) (map[string]uint, error) { return result, nil } -func seedCustomers(tx *gorm.DB, createdBy uint, users map[string]uint) error { - seeds := []struct { - Name string - PicKey string - Address string - Phone string - Email string - }{ - {"Abdul Azis", "admin", "Jl. Raya Utama 1, Bekasi", "082100000001", "abdul.azis@gmail.com"}, - } - - for idx, seed := range seeds { - picID, ok := users[seed.PicKey] - if !ok { - return fmt.Errorf("user %s not seeded", seed.PicKey) - } - - var customer entity.Customer - err := tx.Where("name = ?", seed.Name).First(&customer).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - customer = entity.Customer{ - Name: seed.Name, - PicId: picID, - Type: string(utils.CustomerSupplierTypeBisnis), - Address: seed.Address, - Phone: seed.Phone, - Email: seed.Email, - AccountNumber: *strPtr(fmt.Sprintf("%03d", idx+1)), - CreatedBy: createdBy, - } - if err := tx.Create(&customer).Error; err != nil { - return err - } - } else if err != nil { - return err - } - } - - return nil -} - -func seedFcr(tx *gorm.DB, createdBy uint) (map[string]uint, error) { - seeds := []struct { - Name string - Standards []struct { - Weight float64 - FcrNumber float64 - Mortality float64 - } - }{ - { - Name: "FCR Layer", - Standards: []struct { - Weight float64 - FcrNumber float64 - Mortality float64 - }{ - {Weight: 0.8, FcrNumber: 1.60, Mortality: 2.0}, - {Weight: 1.5, FcrNumber: 1.75, Mortality: 3.5}, - }, - }, - } - - result := make(map[string]uint, len(seeds)) - - for _, seed := range seeds { - var fcr entity.Fcr - err := tx.Where("name = ?", seed.Name).First(&fcr).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - fcr = entity.Fcr{Name: seed.Name, CreatedBy: createdBy} - if err := tx.Create(&fcr).Error; err != nil { - return nil, err - } - } else if err != nil { - return nil, err - } - result[seed.Name] = fcr.Id - - for _, std := range seed.Standards { - var standard entity.FcrStandard - err := tx.Where("fcr_id = ? AND weight = ?", fcr.Id, std.Weight).First(&standard).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - standard = entity.FcrStandard{ - FcrID: fcr.Id, - Weight: std.Weight, - FcrNumber: std.FcrNumber, - Mortality: std.Mortality, - } - if err := tx.Create(&standard).Error; err != nil { - return nil, err - } - } else if err != nil { - return nil, err - } else { - if err := tx.Model(&entity.FcrStandard{}).Where("id = ?", standard.Id).Updates(map[string]any{ - "fcr_number": std.FcrNumber, - "mortality": std.Mortality, - }).Error; err != nil { - return nil, err - } - } - } - } - - return result, nil -} - func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories map[string]uint, suppliers map[string]uint) error { seeds := []struct { Name string @@ -560,92 +188,88 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories Expiry *int Suppliers []string Flags []utils.FlagType + IsVisible bool }{ { - Name: "DOC Broiler", - Brand: "MBU Broiler", - Sku: "BRO0001", + Name: "ISA Brown", + Brand: "ISA Brown", + Sku: "ISA0001", Uom: "Ekor", Category: "Day Old Chick", Price: 7500, Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, - Flags: []utils.FlagType{utils.FlagDOC}, + Flags: []utils.FlagType{utils.FlagDOC, utils.FlagPullet, utils.FlagLayer}, + IsVisible: true, }, { - 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, - Flags: []utils.FlagType{utils.FlagAyamAfkir}, - }, - { - Name: "Ayam Mati", - Brand: "-", - Sku: "2", - Uom: "Ekor", - Category: "Day Old Chick", - Price: 1, - Flags: []utils.FlagType{utils.FlagAyamMati}, - }, - { - Name: "Ayam Culling", - Brand: "-", - Sku: "3", - Uom: "Ekor", - Category: "Day Old Chick", - Price: 1, - Flags: []utils.FlagType{utils.FlagAyamCulling}, - }, - { - Name: "Telur Konsumsi Baik", - Brand: "-", - Sku: "4", - Uom: "Unit", - Category: "Telur", - Price: 1, - Flags: []utils.FlagType{utils.FlagTelurUtuh}, - }, - { - Name: "Telur Pecah", - Brand: "-", - Sku: "5", - Uom: "Unit", - Category: "Telur", - Price: 1, - Flags: []utils.FlagType{utils.FlagTelurPecah}, - }, - { - Name: "281 SPECIAL STARTER", - Brand: "281 STARTER", - Sku: "281", - Uom: "Kilogram", - Category: "Bahan Baku", - Price: 7850, - Expiry: intPtr(60), - Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, - Flags: []utils.FlagType{utils.FlagPakan, utils.FlagStarter}, - }, - { - Name: "Ayam Layer", + Name: "Ayam Afkir", Brand: "-", - Sku: "LYR0001", + Sku: "1", Uom: "Ekor", - Category: "Pullet", - Price: 20000, - Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, - Flags: []utils.FlagType{utils.FlagLayer}, + Category: "Day Old Chick", + Price: 1, + Flags: []utils.FlagType{utils.FlagAyamAfkir}, + IsVisible: false, + }, + { + Name: "Ayam Mati", + Brand: "-", + Sku: "2", + Uom: "Ekor", + Category: "Day Old Chick", + Price: 1, + Flags: []utils.FlagType{utils.FlagAyamMati}, + IsVisible: false, + }, + { + Name: "Ayam Culling", + Brand: "-", + Sku: "3", + Uom: "Ekor", + Category: "Day Old Chick", + Price: 1, + Flags: []utils.FlagType{utils.FlagAyamCulling}, + IsVisible: false, + }, + { + Name: "Telur Utuh", + Brand: "-", + Sku: "4", + Uom: "Gram", + Category: "Telur", + Price: 1, + Flags: []utils.FlagType{utils.FlagTelurUtuh}, + IsVisible: false, + }, + { + Name: "Telur Pecah", + Brand: "-", + Sku: "5", + Uom: "Gram", + Category: "Telur", + Price: 1, + Flags: []utils.FlagType{utils.FlagTelurPecah}, + IsVisible: false, + }, + { + Name: "Telur Putih", + Brand: "-", + Sku: "6", + Uom: "Gram", + Category: "Telur", + Price: 1, + Flags: []utils.FlagType{utils.FlagTelurPutih}, + IsVisible: false, + }, + { + Name: "Telur Retak", + Brand: "-", + Sku: "7", + Uom: "Gram", + Category: "Telur", + Price: 1, + Flags: []utils.FlagType{utils.FlagTelurRetak}, + IsVisible: false, }, } @@ -724,78 +348,78 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories return nil } -func seedNonstocks(tx *gorm.DB, createdBy uint, uoms map[string]uint, suppliers map[string]uint) error { - seeds := []struct { - Name string - Uom string - Suppliers []string - Flags []utils.FlagType - }{ - { - Name: "Expedisi DOC", - Uom: "Ekor", - Suppliers: []string{"Ekspedisi"}, - Flags: []utils.FlagType{utils.FlagEkspedisi}, - }, - { - Name: "Solar", - Uom: "Liter", - Suppliers: []string{"BOP Vendor"}, - Flags: []utils.FlagType{}, - }, - } +// func seedNonstocks(tx *gorm.DB, createdBy uint, uoms map[string]uint, suppliers map[string]uint) error { +// seeds := []struct { +// Name string +// Uom string +// Suppliers []string +// Flags []utils.FlagType +// }{ +// { +// Name: "LAJ", +// Uom: "Unit", +// Suppliers: []string{"Ekspedisi"}, +// Flags: []utils.FlagType{utils.FlagEkspedisi}, +// }, +// { +// Name: "Solar", +// Uom: "Liter", +// Suppliers: []string{"BOP Vendor"}, +// Flags: []utils.FlagType{}, +// }, +// } - for _, seed := range seeds { - uomID, ok := uoms[seed.Uom] - if !ok { - return fmt.Errorf("uom %s not seeded", seed.Uom) - } +// for _, seed := range seeds { +// uomID, ok := uoms[seed.Uom] +// if !ok { +// return fmt.Errorf("uom %s not seeded", seed.Uom) +// } - var nonstock entity.Nonstock - err := tx.Where("name = ?", seed.Name).First(&nonstock).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - nonstock = entity.Nonstock{ - Name: seed.Name, - UomId: uomID, - CreatedBy: createdBy, - } - if err := tx.Create(&nonstock).Error; err != nil { - return err - } - } else if err != nil { - return err - } else { - if err := tx.Model(&entity.Nonstock{}).Where("id = ?", nonstock.Id).Updates(map[string]any{ - "uom_id": uomID, - }).Error; err != nil { - return err - } - } +// var nonstock entity.Nonstock +// err := tx.Where("name = ?", seed.Name).First(&nonstock).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// nonstock = entity.Nonstock{ +// Name: seed.Name, +// UomId: uomID, +// CreatedBy: createdBy, +// } +// if err := tx.Create(&nonstock).Error; err != nil { +// return err +// } +// } else if err != nil { +// return err +// } else { +// if err := tx.Model(&entity.Nonstock{}).Where("id = ?", nonstock.Id).Updates(map[string]any{ +// "uom_id": uomID, +// }).Error; err != nil { +// return err +// } +// } - for _, supplierName := range seed.Suppliers { - supplierID, ok := suppliers[supplierName] - if !ok { - return fmt.Errorf("supplier %s not seeded", supplierName) - } - var existing entity.NonstockSupplier - err := tx.Where("nonstock_id = ? AND supplier_id = ?", nonstock.Id, supplierID).First(&existing).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - link := entity.NonstockSupplier{NonstockId: nonstock.Id, SupplierId: supplierID} - if err := tx.Create(&link).Error; err != nil { - return err - } - } else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { - return err - } - } +// for _, supplierName := range seed.Suppliers { +// supplierID, ok := suppliers[supplierName] +// if !ok { +// return fmt.Errorf("supplier %s not seeded", supplierName) +// } +// var existing entity.NonstockSupplier +// err := tx.Where("nonstock_id = ? AND supplier_id = ?", nonstock.Id, supplierID).First(&existing).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// link := entity.NonstockSupplier{NonstockId: nonstock.Id, SupplierId: supplierID} +// if err := tx.Create(&link).Error; err != nil { +// return err +// } +// } else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { +// return err +// } +// } - if err := seedFlags(tx, nonstock.Id, entity.FlagableTypeNonstock, seed.Flags); err != nil { - return err - } - } +// if err := seedFlags(tx, nonstock.Id, entity.FlagableTypeNonstock, seed.Flags); err != nil { +// return err +// } +// } - return nil -} +// return nil +// } // nanti saya isi @@ -823,225 +447,6 @@ func seedFlags(tx *gorm.DB, flagableID uint, flagableType string, flags []utils. return nil } -func seedBanks(tx *gorm.DB, createdBy uint) error { - seeds := []struct { - Name string - Alias string - Owner *string - AccountNumber string - }{ - { - Name: "Bank Central Asia", - Alias: "BCA", - AccountNumber: "1234567890", - Owner: ptr("PT MBU Group"), - }, - { - Name: "Bank Rakyat Indonesia", - Alias: "BRI", - AccountNumber: "9876543210", - Owner: ptr("PT MBU Group"), - }, - { - Name: "Bank Mandiri", - Alias: "MAND", - AccountNumber: "1122334455", - Owner: ptr("PT MBU Group"), - }, - } - - for _, seed := range seeds { - var bank entity.Bank - err := tx.Where("name = ?", seed.Name).First(&bank).Error - - if errors.Is(err, gorm.ErrRecordNotFound) { - bank = entity.Bank{ - Name: seed.Name, - Alias: seed.Alias, - Owner: seed.Owner, - AccountNumber: seed.AccountNumber, - CreatedBy: createdBy, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - if err := tx.Create(&bank).Error; err != nil { - return err - } - } else if err != nil { - return err - } else { - // update data jika sudah ada - if err := tx.Model(&entity.Bank{}).Where("id = ?", bank.Id).Updates(map[string]any{ - "alias": seed.Alias, - "owner": seed.Owner, - "account_number": seed.AccountNumber, - "updated_at": time.Now(), - }).Error; err != nil { - return err - } - } - } - - return nil -} - -func seedProductWarehouse(tx *gorm.DB, createdBy uint) error { - seeds := []struct { - ProductName string - WarehouseName string - Quantity float64 - }{ - {ProductName: "DOC Broiler", WarehouseName: "Gudang Priangan", Quantity: 0}, - {ProductName: "281 SPECIAL STARTER", WarehouseName: "Gudang Singaparna", Quantity: 0}, - {ProductName: "281 SPECIAL STARTER", WarehouseName: "Gudang Banten", Quantity: 0}, - {ProductName: "DOC Broiler", WarehouseName: "Gudang Singaparna 1", Quantity: 0}, - {ProductName: "Telur Konsumsi Baik", WarehouseName: "Gudang Singaparna 1", Quantity: 0}, - {ProductName: "Telur Pecah", WarehouseName: "Gudang Singaparna 1", Quantity: 0}, - {ProductName: "Telur Konsumsi Baik", WarehouseName: "Gudang Cikaum 1", Quantity: 0}, - {ProductName: "Telur Pecah", WarehouseName: "Gudang Cikaum 1", Quantity: 0}, - } - - 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 = ?", product.Id, warehouse.Id).First(&productWarehouse).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - productWarehouse = entity.ProductWarehouse{ - ProductId: product.Id, - WarehouseId: warehouse.Id, - Quantity: seed.Quantity, - // CreatedBy: createdBy, - } - if err := tx.Create(&productWarehouse).Error; err != nil { - return err - } - } else if err != nil { - return err - } else { - if err := tx.Model(&productWarehouse).Updates(map[string]any{ - "quantity": seed.Quantity, - }).Error; err != nil { - return err - } - } - } - - return nil -} - -func seedTransferStock(tx *gorm.DB) error { - - transfer := entity.StockTransfer{ - FromWarehouseId: 1, - ToWarehouseId: 2, - Reason: "Seed transfer stock", - TransferDate: time.Now(), - MovementNumber: "SEED-TRF-00001", - CreatedBy: 1, - } - if err := tx.Create(&transfer).Error; err != nil { - return err - } - - details := []entity.StockTransferDetail{ - { - StockTransferId: transfer.Id, - ProductId: 1, - - SourceProductWarehouseID: func() *uint64 { id := uint64(1); return &id }(), - DestProductWarehouseID: func() *uint64 { id := uint64(2); return &id }(), - UsageQty: 10, - PendingQty: 0, - TotalQty: 10, - TotalUsed: 0, - }, - { - StockTransferId: transfer.Id, - ProductId: 2, - - SourceProductWarehouseID: func() *uint64 { id := uint64(1); return &id }(), - DestProductWarehouseID: func() *uint64 { id := uint64(2); return &id }(), - UsageQty: 5, - PendingQty: 0, - TotalQty: 5, - TotalUsed: 0, - }, - } - for i := range details { - if err := tx.Create(&details[i]).Error; err != nil { - return err - } - } - - deliveries := []entity.StockTransferDelivery{ - { - StockTransferId: transfer.Id, - SupplierId: 1, - VehiclePlate: "B 1234 XYZ", - DriverName: "Driver Seed", - DocumentPath: "seed.pdf", - ShippingCostItem: 1000, - ShippingCostTotal: 2000, - }, - } - for i := range deliveries { - if err := tx.Create(&deliveries[i]).Error; err != nil { - return err - } - } - - detailMap := make(map[uint64]uint64) - for _, d := range details { - detailMap[d.ProductId] = d.Id - } - - deliveryItems := []entity.StockTransferDeliveryItem{ - { - StockTransferDeliveryId: deliveries[0].Id, - StockTransferDetailId: detailMap[1], - Quantity: 50, - }, - { - StockTransferDeliveryId: deliveries[0].Id, - StockTransferDetailId: detailMap[2], - Quantity: 30, - }, - } - for i := range deliveryItems { - if err := tx.Create(&deliveryItems[i]).Error; err != nil { - return err - } - } - - return nil -} -func ptr[T any](v T) *T { - return &v -} - func strPtr(s string) *string { return &s } - -func intPtr(v int) *int { - return &v -} - -func uintPtr(v uint) *uint { - return &v -} diff --git a/internal/entities/project_flock_kandang_uniformity.go b/internal/entities/project_flock_kandang_uniformity.go new file mode 100644 index 00000000..ecf90d19 --- /dev/null +++ b/internal/entities/project_flock_kandang_uniformity.go @@ -0,0 +1,33 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type ProjectFlockKandangUniformity struct { + Id uint `gorm:"primaryKey"` + Uniformity float64 `gorm:"type:numeric(15,3)"` + Week int `gorm:"not null"` + Cv float64 `gorm:"type:numeric(15,3)"` + ChickQtyOfWeight float64 `gorm:"type:numeric(15,3)"` + MeanUp float64 `gorm:"type:numeric(15,3)"` + MeanDown float64 `gorm:"type:numeric(15,3)"` + ProjectFlockKandangId uint `gorm:"not null"` + UniformQty float64 `gorm:"type:numeric(15,3)"` + NotUniformQty float64 `gorm:"type:numeric(15,3)"` + UniformDate *time.Time `gorm:"type:timestamptz"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + CreatedBy uint `gorm:"not null"` + + ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + LatestApproval *Approval `gorm:"-" json:"-"` +} + +func (ProjectFlockKandangUniformity) TableName() string { + return "project_flock_kandang_uniformity" +} diff --git a/internal/entities/uniformity.go b/internal/entities/uniformity.go new file mode 100644 index 00000000..8402ad3b --- /dev/null +++ b/internal/entities/uniformity.go @@ -0,0 +1,18 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type Uniformity struct { + Id uint `gorm:"primaryKey"` + Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS NULL"` + CreatedBy uint `gorm:"not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` +} diff --git a/internal/middleware/permissions.go b/internal/middleware/permissions.go index f0056149..18a0e713 100644 --- a/internal/middleware/permissions.go +++ b/internal/middleware/permissions.go @@ -218,12 +218,13 @@ const ( ) const ( - P_FinanceGetAll = "lti.finance.list" - P_FinanceGetOne = "lti.finance.detail" - P_FinanceCreateOne = "lti.finance.create" - P_FinanceUpdateOne = "lti.finance.update" - P_FinanceDeleteOne = "lti.finance.delete" - P_FinanceApproval = "lti.finance.approve" + P_Uniformities_GetAll = "lti.production.uniformity.list" + P_Uniformities_GetOne = "lti.production.uniformity.detail" + P_Uniformities_Verify = "lti.production.uniformity.verify" + P_Uniformities_CreateOne = "lti.production.uniformity.create" + P_Uniformities_UpdateOne = "lti.production.uniformity.update" + P_Uniformities_DeleteOne = "lti.production.uniformity.delete" + P_Uniformities_Approval = "lti.production.uniformity.approve" ) const ( diff --git a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go index 92330f26..503709b2 100644 --- a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go +++ b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go @@ -29,6 +29,8 @@ type ProductWarehouseRepository interface { IdExists(ctx context.Context, id uint) (bool, error) CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error EnsureProductWarehouse(ctx context.Context, productID, warehouseID uint, projectFlockKandangID *uint, createdBy uint) (uint, error) + GetByProductWarehouseAndProjectFlockKandang(ctx context.Context, productId, warehouseId, projectFlockKandangId uint) (*entity.ProductWarehouse, error) + DeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error } type ProductWarehouseRepositoryImpl struct { @@ -272,6 +274,30 @@ func (r *ProductWarehouseRepositoryImpl) EnsureProductWarehouse( return entity.Id, nil } +func (r *ProductWarehouseRepositoryImpl) GetByProductWarehouseAndProjectFlockKandang( + ctx context.Context, + productId uint, + warehouseId uint, + projectFlockKandangId uint, +) (*entity.ProductWarehouse, error) { + var productWarehouse entity.ProductWarehouse + if err := r.DB().WithContext(ctx). + Where("product_id = ? AND warehouse_id = ? AND project_flock_kandang_id = ?", productId, warehouseId, projectFlockKandangId). + First(&productWarehouse).Error; err != nil { + return nil, err + } + return &productWarehouse, nil +} + +func (r *ProductWarehouseRepositoryImpl) DeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error { + if len(projectFlockKandangIDs) == 0 { + return nil + } + return r.DB().WithContext(ctx). + Where("project_flock_kandang_id IN ?", projectFlockKandangIDs). + Delete(&entity.ProductWarehouse{}).Error +} + func (r *ProductWarehouseRepositoryImpl) GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error) { var productWarehouse entity.ProductWarehouse err := r.DB().WithContext(ctx). diff --git a/internal/modules/master/nonstocks/dto/nonstock.dto.go b/internal/modules/master/nonstocks/dto/nonstock.dto.go index b2af526c..9954ee76 100644 --- a/internal/modules/master/nonstocks/dto/nonstock.dto.go +++ b/internal/modules/master/nonstocks/dto/nonstock.dto.go @@ -1,11 +1,12 @@ package dto import ( + "time" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto" uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" - "time" ) // === DTO Structs === @@ -22,7 +23,7 @@ type NonstockListDTO struct { Name string `json:"name"` Flags []string `json:"flags"` Uom *uomDTO.UomRelationDTO `json:"uom"` - Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers,omitempty"` + Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"` CreatedUser *userDTO.UserRelationDTO `json:"created_user"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -100,7 +101,7 @@ func ToNonstockDetailDTO(e entity.Nonstock) NonstockDetailDTO { func toNonstockSupplierDTOs(relations []entity.NonstockSupplier) []supplierDTO.SupplierRelationDTO { if len(relations) == 0 { - return nil + return make([]supplierDTO.SupplierRelationDTO, 0) } result := make([]supplierDTO.SupplierRelationDTO, 0, len(relations)) @@ -112,7 +113,7 @@ func toNonstockSupplierDTOs(relations []entity.NonstockSupplier) []supplierDTO.S } if len(result) == 0 { - return nil + return make([]supplierDTO.SupplierRelationDTO, 0) } return result diff --git a/internal/modules/master/nonstocks/validations/nonstock.validation.go b/internal/modules/master/nonstocks/validations/nonstock.validation.go index c421b7ec..f3a298ef 100644 --- a/internal/modules/master/nonstocks/validations/nonstock.validation.go +++ b/internal/modules/master/nonstocks/validations/nonstock.validation.go @@ -3,8 +3,8 @@ package validation type Create struct { Name string `json:"name" validate:"required_strict,min=3,max=50"` UomID uint `json:"uom_id" validate:"required,gt=0"` - SupplierIDs []uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"` - Flags []string `json:"flags,omitempty" validate:"omitempty,dive,max=50"` + SupplierIDs []uint `json:"supplier_ids" validate:"dive,gt=0"` + Flags []string `json:"flags" validate:"dive,max=50"` } type Update struct { diff --git a/internal/modules/production/project_flocks/controllers/projectflock.controller.go b/internal/modules/production/project_flocks/controllers/projectflock.controller.go index c48e1e2a..4315b948 100644 --- a/internal/modules/production/project_flocks/controllers/projectflock.controller.go +++ b/internal/modules/production/project_flocks/controllers/projectflock.controller.go @@ -268,6 +268,7 @@ func (u *ProjectflockController) GetPeriodSummary(c *fiber.Ctx) error { func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error { projectFlockId := c.QueryInt("project_flock_id", 0) kandangId := c.QueryInt("kandang_id", 0) + withPopulation := c.QueryBool("withpopulation", false) if projectFlockId == 0 || kandangId == 0 { return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id or kandang_id") @@ -280,6 +281,13 @@ func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error { dtoResult := dto.ToProjectFlockKandangDTO(*result) dtoResult.AvailableQuantity = float64(availableStock) + if withPopulation { + population, err := u.ProjectflockService.GetProjectFlockKandangPopulation(c, result.Id) + if err != nil { + return err + } + dtoResult.Population = &population + } if dtoResult.ProjectFlock != nil { for i := range dtoResult.ProjectFlock.Kandangs { diff --git a/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go b/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go index d1f0d40b..8dedaf15 100644 --- a/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go @@ -34,6 +34,7 @@ type ProjectFlockKandangDTO struct { Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"` ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"` AvailableQuantity float64 `json:"available_quantity"` + Population *float64 `json:"population,omitempty"` } func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO { diff --git a/internal/modules/production/project_flocks/module.go b/internal/modules/production/project_flocks/module.go index acd77338..98e4a630 100644 --- a/internal/modules/production/project_flocks/module.go +++ b/internal/modules/production/project_flocks/module.go @@ -16,6 +16,7 @@ import ( rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" rProjectBudget "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" sProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services" utils "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -32,6 +33,8 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid nonstockRepo := rNonstock.NewNonstockRepository(db) projectflockRepo := rProjectflock.NewProjectflockRepository(db) projectflockKandangRepo := rProjectflock.NewProjectFlockKandangRepository(db) + projectFlockPopulationRepo := rProjectflock.NewProjectFlockPopulationRepository(db) + recordingRepo := rRecording.NewRecordingRepository(db) warehouseRepo := rWarehouse.NewWarehouseRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) projectBudgetRepo := rProjectBudget.NewProjectBudgetRepository(db) @@ -43,7 +46,7 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid panic(fmt.Sprintf("failed to register project flock approval workflow: %v", err)) } - projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, warehouseRepo, productWarehouseRepo, projectBudgetRepo, nonstockRepo, approvalService, validate) + projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, warehouseRepo, productWarehouseRepo, projectBudgetRepo, nonstockRepo, projectFlockPopulationRepo, recordingRepo, approvalService, validate) userService := sUser.NewUserService(userRepo, validate) ProjectflockRoutes(router, userService, projectflockService) diff --git a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go index a2b56dce..fd263b27 100644 --- a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go +++ b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go @@ -15,6 +15,7 @@ type ProjectFlockPopulationRepository interface { GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) + GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) // subset of base repository methods used by services CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error @@ -106,3 +107,20 @@ func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProjectFlockKandangI } return total, nil } + +func (r *projectFlockPopulationRepositoryImpl) GetAvailableQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) { + var total float64 + err := r.DB().WithContext(ctx). + Table("project_flock_populations"). + Select("COALESCE(SUM(total_qty - total_used_qty), 0) AS total_qty"). + Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id"). + Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID). + Scan(&total).Error + if err != nil { + return 0, err + } + if total < 0 { + total = 0 + } + return total, nil +} diff --git a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go index 911c8b0b..42dcafd9 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -27,6 +27,7 @@ type ProjectFlockKandangRepository interface { MaxPeriodByBaseName(ctx context.Context, baseName string) (int, error) ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error) HasOpenNewerPeriod(ctx context.Context, kandangID uint, currentPeriod int, excludeID *uint) (bool, error) + ListIDsByProjectAndKandang(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) WithTx(tx *gorm.DB) ProjectFlockKandangRepository DB() *gorm.DB IdExists(ctx context.Context, id uint) (bool, error) @@ -89,6 +90,20 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockID(ctx context.Cont return records, nil } +func (r *projectFlockKandangRepositoryImpl) ListIDsByProjectAndKandang(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) { + if len(kandangIDs) == 0 { + return []uint{}, nil + } + var ids []uint + if err := r.db.WithContext(ctx). + Model(&entity.ProjectFlockKandang{}). + Where("project_flock_id = ? AND kandang_id IN ?", projectFlockID, kandangIDs). + Pluck("id", &ids).Error; err != nil { + return nil, err + } + return ids, nil +} + func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error) { var records []entity.ProjectFlockKandang var total int64 diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index 9431729f..1e859e47 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -21,6 +21,7 @@ import ( 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" + recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" utils "gitlab.com/mbugroup/lti-api.git/internal/utils" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" @@ -37,6 +38,7 @@ type ProjectflockService interface { 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) + GetProjectFlockKandangPopulation(ctx *fiber.Ctx, projectFlockKandangID uint) (float64, error) GetPeriodSummary(ctx *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error) GetProjectPeriods(ctx *fiber.Ctx, projectIDs []uint) (map[uint]int, error) Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error) @@ -54,6 +56,8 @@ type projectflockService struct { ProductWarehouseRepo productWarehouseRepository.ProductWarehouseRepository ProjectBudgetRepo projectBudgetRepository.ProjectBudgetRepository PivotRepo repository.ProjectFlockKandangRepository + PopulationRepo repository.ProjectFlockPopulationRepository + RecordingRepo recordingRepo.RecordingRepository ApprovalSvc commonSvc.ApprovalService approvalWorkflow approvalutils.ApprovalWorkflowKey } @@ -73,6 +77,8 @@ func NewProjectflockService( productWarehouseRepo productWarehouseRepository.ProductWarehouseRepository, projectBudgetRepo projectBudgetRepository.ProjectBudgetRepository, nonstockRepo nonstockRepository.NonstockRepository, + populationRepo repository.ProjectFlockPopulationRepository, + recordingRepo recordingRepo.RecordingRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate, @@ -86,7 +92,10 @@ func NewProjectflockService( NonstockRepo: nonstockRepo, WarehouseRepo: warehouseRepo, ProductWarehouseRepo: productWarehouseRepo, + ProjectBudgetRepo: projectBudgetRepo, PivotRepo: pivotRepo, + PopulationRepo: populationRepo, + RecordingRepo: recordingRepo, ApprovalSvc: approvalSvc, approvalWorkflow: utils.ApprovalWorkflowProjectFlock, } @@ -419,6 +428,34 @@ func (s projectflockService) GetProjectFlockKandangByProjectAndKandang(ctx *fibe return pfk, availableQuantity, nil } +func (s projectflockService) GetProjectFlockKandangPopulation(ctx *fiber.Ctx, projectFlockKandangID uint) (float64, error) { + if s.PopulationRepo == nil { + return 0, fiber.NewError(fiber.StatusInternalServerError, "Project flock population repository is not configured") + } + if projectFlockKandangID == 0 { + return 0, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required") + } + + if s.RecordingRepo != nil { + latest, err := s.RecordingRepo.GetLatestByProjectFlockKandangID(ctx.Context(), projectFlockKandangID) + if err != nil { + s.Log.Errorf("Failed to fetch latest recording for project flock kandang %d: %+v", projectFlockKandangID, err) + return 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang population") + } + if latest != nil && latest.TotalChickQty != nil && *latest.TotalChickQty > 0 { + return *latest.TotalChickQty, nil + } + } + + total, err := s.PopulationRepo.GetAvailableQtyByProjectFlockKandangID(ctx.Context(), projectFlockKandangID) + if err != nil { + s.Log.Errorf("Failed to fetch project flock kandang population %d: %+v", projectFlockKandangID, err) + return 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang population") + } + + return total, 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) @@ -795,6 +832,9 @@ func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction * } return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history") } + if err := s.ensureProjectFlockKandangProductWarehouses(ctx, dbTransaction, records); err != nil { + return err + } return nil } @@ -820,6 +860,23 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction * return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Tidak dapat melepas kandang karena sudah memiliki recording: %s", strings.Join(names, ", "))) } + pfkIDs, err := s.pivotRepoWithTx(dbTransaction).ListIDsByProjectAndKandang(ctx, projectFlockID, kandangIDs) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to load project flock kandang ids") + } + + if len(pfkIDs) > 0 { + pwRepo := s.ProductWarehouseRepo + if dbTransaction != nil { + pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction) + } else if pwRepo == nil { + pwRepo = productWarehouseRepository.NewProductWarehouseRepository(s.Repository.DB()) + } + if err := pwRepo.DeleteByProjectFlockKandangIDs(ctx, pfkIDs); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to remove product warehouses for project flock kandang") + } + } + if resetStatus { if err := s.kandangRepoWithTx(dbTransaction).UpdateStatusByIDs(ctx, kandangIDs, utils.KandangStatusNonActive); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs status") @@ -856,6 +913,81 @@ func (s projectflockService) kandangRepoWithTx(tx *gorm.DB) kandangRepository.Ka return kandangRepository.NewKandangRepository(s.Repository.DB()) } +func (s projectflockService) ensureProjectFlockKandangProductWarehouses(ctx context.Context, dbTransaction *gorm.DB, records []*entity.ProjectFlockKandang) error { + if len(records) == 0 { + return nil + } + + pwRepo := s.ProductWarehouseRepo + if dbTransaction != nil { + pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction) + } else if pwRepo == nil { + pwRepo = productWarehouseRepository.NewProductWarehouseRepository(s.Repository.DB()) + } + + warehouseRepo := s.WarehouseRepo + if dbTransaction != nil { + warehouseRepo = warehouseRepository.NewWarehouseRepository(dbTransaction) + } else if warehouseRepo == nil { + warehouseRepo = warehouseRepository.NewWarehouseRepository(s.Repository.DB()) + } + + flags := []utils.FlagType{ + utils.FlagAyamAfkir, + utils.FlagAyamCulling, + utils.FlagAyamMati, + utils.FlagTelurPecah, + utils.FlagTelurUtuh, + } + + productIDs := make(map[utils.FlagType]uint, len(flags)) + for _, flag := range flags { + product, err := pwRepo.GetFirstProductByFlag(ctx, string(flag)) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product untuk flag %s tidak ditemukan", flag)) + } + return err + } + productIDs[flag] = product.Id + } + + for _, record := range records { + if record == nil || record.Id == 0 { + continue + } + + warehouse, err := warehouseRepo.GetByKandangID(ctx, record.KandangId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse untuk kandang %d belum tersedia", record.KandangId)) + } + return err + } + + for _, flag := range flags { + productID := productIDs[flag] + if _, err := pwRepo.GetByProductWarehouseAndProjectFlockKandang(ctx, productID, warehouse.Id, record.Id); err == nil { + continue + } else if !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + + newPW := entity.ProductWarehouse{ + ProductId: productID, + WarehouseId: warehouse.Id, + ProjectFlockKandangId: &record.Id, + Quantity: 0, + } + if err := pwRepo.CreateOne(ctx, &newPW, nil); err != nil { + return err + } + } + } + + return nil +} + func (s projectflockService) Resubmit(c *fiber.Ctx, req *validation.Resubmit, id uint) (*entity.ProjectFlock, error) { if err := s.Validate.Struct(req); err != nil { return nil, err diff --git a/internal/modules/production/recordings/repositories/recording.repository.go b/internal/modules/production/recordings/repositories/recording.repository.go index 6e362ba7..a615692f 100644 --- a/internal/modules/production/recordings/repositories/recording.repository.go +++ b/internal/modules/production/recordings/repositories/recording.repository.go @@ -17,6 +17,7 @@ type RecordingRepository interface { repository.BaseRepository[entity.Recording] WithRelations(db *gorm.DB) *gorm.DB + GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error) GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error) CreateBodyWeights(tx *gorm.DB, bodyWeights []entity.RecordingBW) error @@ -81,6 +82,27 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB { Preload("Eggs.ProductWarehouse.Warehouse") } +func (r *RecordingRepositoryImpl) GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error) { + if projectFlockKandangId == 0 { + return nil, errors.New("project_flock_kandang_id is required") + } + + var record entity.Recording + err := r.DB().WithContext(ctx). + Where("project_flock_kandangs_id = ?", projectFlockKandangId). + Order("record_datetime DESC"). + Order("created_at DESC"). + Limit(1). + Find(&record).Error + if errors.Is(err, gorm.ErrRecordNotFound) || record.Id == 0 { + return nil, nil + } + if err != nil { + return nil, err + } + return &record, nil +} + func (r *RecordingRepositoryImpl) GenerateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error) { var days []int if err := tx.Model(&entity.Recording{}). diff --git a/internal/modules/production/route.go b/internal/modules/production/route.go index d1425b7c..4066121a 100644 --- a/internal/modules/production/route.go +++ b/internal/modules/production/route.go @@ -8,10 +8,11 @@ import ( "gorm.io/gorm" chickins "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins" + projectFlockKandangs "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs" projectflocks "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks" recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings" transferLayings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings" - projectFlockKandangs "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs" + uniformitys "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities" // MODULE IMPORTS ) @@ -24,8 +25,9 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida chickins.ChickinModule{}, transferLayings.TransferLayingModule{}, projectFlockKandangs.ProjectFlockKandangModule{}, + uniformitys.UniformityModule{}, // MODULE REGISTRY -} + } for _, m := range allModules { m.RegisterRoutes(group, db, validate) diff --git a/internal/modules/production/uniformities/controllers/uniformity.controller.go b/internal/modules/production/uniformities/controllers/uniformity.controller.go new file mode 100644 index 00000000..12cc3739 --- /dev/null +++ b/internal/modules/production/uniformities/controllers/uniformity.controller.go @@ -0,0 +1,292 @@ +package controller + +import ( + "math" + + "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/dto" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/services" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/validations" + "gitlab.com/mbugroup/lti-api.git/internal/response" + + "github.com/gofiber/fiber/v2" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" +) + +type UniformityController struct { + UniformityService service.UniformityService +} + +func NewUniformityController(uniformityService service.UniformityService) *UniformityController { + return &UniformityController{ + UniformityService: uniformityService, + } +} + +func (u *UniformityController) GetAll(c *fiber.Ctx) error { + query, err := validation.ParseQuery(c) + if err != nil { + return err + } + + result, totalResults, err := u.UniformityService.GetAll(c, query) + if err != nil { + return err + } + standards, err := u.UniformityService.MapStandards(c, result) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.UniformityListDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get all production uniformities successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + Filters: fiber.Map{ + "location_id": "", + "project_flock_id": "", + "status": "Pengajuan", + }, + }, + Data: dto.ToUniformityListDTOsWithStandard(result, standards), + }) +} + +func (u *UniformityController) GetOne(c *fiber.Ctx) error { + id, err := validation.ParseIDParam(c, "id") + if err != nil { + return err + } + + result, err := u.UniformityService.GetOne(c, id) + if err != nil { + return err + } + + withDetails := c.QueryBool("with_details", false) + calculation := service.UniformityCalculation{} + var document *entity.Document + var meanWeight float64 + if result.MeanUp > 0 { + meanWeight = math.Round(result.MeanUp / 1.10) + } + if withDetails { + var err error + calculation, document, err = u.UniformityService.CalculateUniformityFromDocument(c, id) + if err != nil { + return err + } + } else { + calculation = service.UniformityCalculation{ + ChickQtyOfWeight: result.ChickQtyOfWeight, + MeanWeight: meanWeight, + MeanDown: result.MeanDown, + MeanUp: result.MeanUp, + UniformQty: result.UniformQty, + OutsideQty: result.NotUniformQty, + Uniformity: result.Uniformity, + Cv: result.Cv, + } + } + + standard, err := u.UniformityService.GetStandard(c, result) + if err != nil { + return err + } + var standardDTO *dto.UniformityStandardDTO + if standard != nil { + standardDTO = &dto.UniformityStandardDTO{ + MeanWeight: standard.MeanWeight, + Uniformity: standard.Uniformity, + } + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get production uniformity successfully", + Data: dto.ToUniformityDetailDTO(*result, calculation, document, standardDTO), + }) +} + +func (u *UniformityController) CreateOne(c *fiber.Ctx) error { + req, file, err := validation.ParseCreate(c) + if err != nil { + return err + } + + rows, err := u.UniformityService.ParseBodyWeightExcel(c, file) + if err != nil { + return err + } + + calculation, err := u.UniformityService.ComputeUniformity(rows) + if err != nil { + return err + } + + result, err := u.UniformityService.CreateOne(c, req, file, rows) + if err != nil { + return err + } + + document := dto.NewDocumentForResponse(file.Filename) + standard, err := u.UniformityService.GetStandard(c, result) + if err != nil { + return err + } + var standardDTO *dto.UniformityStandardDTO + if standard != nil { + standardDTO = &dto.UniformityStandardDTO{ + MeanWeight: standard.MeanWeight, + Uniformity: standard.Uniformity, + } + } + + return c.Status(fiber.StatusCreated). + JSON(response.Success{ + Code: fiber.StatusCreated, + Status: "success", + Message: "Create uniformity successfully", + Data: dto.ToUniformityDetailDTO(*result, calculation, document, standardDTO), + }) +} + +func (u *UniformityController) UploadBodyWeightExcel(c *fiber.Ctx) error { + files, err := validation.ParseUploadFiles(c) + if err != nil { + return err + } + + rows, err := u.UniformityService.ParseBodyWeightExcel(c, files[0]) + if err != nil { + return err + } + + calculation, err := u.UniformityService.ComputeUniformity(rows) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Uniformity verified successfully", + Data: dto.ToUniformityVerificationDTO(calculation), + }) +} + +func (u *UniformityController) UpdateOne(c *fiber.Ctx) error { + id, err := validation.ParseIDParam(c, "id") + if err != nil { + return err + } + + req, file, err := validation.ParseUpdate(c) + if err != nil { + return err + } + + var rows []service.BodyWeightExcelRow + if file != nil { + parsed, err := u.UniformityService.ParseBodyWeightExcel(c, file) + if err != nil { + return err + } + rows = parsed + } + + result, err := u.UniformityService.UpdateOne(c, req, id, file, rows) + if err != nil { + return err + } + + standard, err := u.UniformityService.GetStandard(c, result) + if err != nil { + return err + } + var standardDTO *dto.UniformityStandardDTO + if standard != nil { + standardDTO = &dto.UniformityStandardDTO{ + MeanWeight: standard.MeanWeight, + Uniformity: standard.Uniformity, + } + } + + calculation := service.UniformityCalculation{ + ChickQtyOfWeight: result.ChickQtyOfWeight, + MeanWeight: math.Round(result.MeanUp / 1.10), + MeanDown: result.MeanDown, + MeanUp: result.MeanUp, + UniformQty: result.UniformQty, + OutsideQty: result.NotUniformQty, + Uniformity: result.Uniformity, + Cv: result.Cv, + } + var document *entity.Document + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Update uniformity successfully", + Data: dto.ToUniformityDetailDTO(*result, calculation, document, standardDTO), + }) +} + +func (u *UniformityController) DeleteOne(c *fiber.Ctx) error { + id, err := validation.ParseIDParam(c, "id") + if err != nil { + return err + } + + if err := u.UniformityService.DeleteOne(c, id); err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Common{ + Code: fiber.StatusOK, + Status: "success", + Message: "Delete uniformity successfully", + }) +} + +func (u *UniformityController) Approve(c *fiber.Ctx) error { + req, err := validation.ParseApprove(c) + if err != nil { + return err + } + + results, err := u.UniformityService.Approval(c, req) + if err != nil { + return err + } + + var ( + data interface{} + message = "Submit uniformity approvals successfully" + ) + + if len(results) == 1 { + message = "Submit uniformity approval successfully" + data = dto.ToUniformityListDTOs(results)[0] + } else { + data = dto.ToUniformityListDTOs(results) + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: message, + Data: data, + }) +} diff --git a/internal/modules/production/uniformities/dto/uniformity.dto.go b/internal/modules/production/uniformities/dto/uniformity.dto.go new file mode 100644 index 00000000..1324d805 --- /dev/null +++ b/internal/modules/production/uniformities/dto/uniformity.dto.go @@ -0,0 +1,236 @@ +package dto + +import ( + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/services" +) + +type UniformitySamplingDTO struct { + ChickQtyOfWeight float64 `json:"chick_qty_of_weight"` + MeanWeight float64 `json:"mean_weight"` + MeanDown float64 `json:"mean_down"` + MeanUp float64 `json:"mean_up"` +} + +type UniformityResultDTO struct { + UniformQty float64 `json:"uniform_qty"` + OutsideQty float64 `json:"outside_qty"` + Uniformity float64 `json:"uniformity"` + Cv float64 `json:"cv"` +} + +type UniformityStandardDTO struct { + MeanWeight *float64 `json:"mean_weight"` + Uniformity *float64 `json:"uniformity"` +} + +type UniformityDetailItemDTO struct { + Id int `json:"id"` + Weight float64 `json:"weight"` + Range string `json:"range"` +} + +type UniformityVerificationDTO struct { + Sampling UniformitySamplingDTO `json:"sampling"` + Result UniformityResultDTO `json:"result"` + UniformityDetails []UniformityDetailItemDTO `json:"uniformity_details"` +} + +type UniformityInfoDTO struct { + Tanggal string `json:"tanggal"` + LokasiFarm string `json:"lokasi_farm"` + ProjectFlock string `json:"project_flock"` + Kandang string `json:"kandang"` + FileName string `json:"file_name"` +} + +type UniformityDetailDTO struct { + Id uint `json:"id"` + InfoUmum UniformityInfoDTO `json:"info_umum"` + Sampling UniformitySamplingDTO `json:"sampling"` + Result UniformityResultDTO `json:"result"` + Standard *UniformityStandardDTO `json:"standard"` + UniformityDetails []UniformityDetailItemDTO `json:"uniformity_details"` +} + +type UniformityListDTO struct { + Id uint `json:"id"` + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` + LocationName string `json:"location_name"` + FlockName string `json:"flock_name"` + KandangName string `json:"kandang_name"` + AppliedAt *time.Time `json:"applied_at"` + Week int `json:"week"` + Status string `json:"status"` + Uniformity float64 `json:"uniformity"` + Cv float64 `json:"cv"` + ChickQtyOfWeight float64 `json:"chick_qty_of_weight"` + UniformQty float64 `json:"uniform_qty"` + MeanUp float64 `json:"mean_up"` + MeanDown float64 `json:"mean_down"` + StandardMeanWeight *float64 `json:"standard_mean_weight"` + StandardUniformity *float64 `json:"standard_uniformity"` + CreatedAt time.Time `json:"created_at"` + CreatedBy uint `json:"created_by"` + LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"` +} + +func NewDocumentForResponse(name string) *entity.Document { + if name == "" { + return nil + } + return &entity.Document{Name: name} +} + +func ToUniformityVerificationDTO(calc service.UniformityCalculation) UniformityVerificationDTO { + return UniformityVerificationDTO{ + Sampling: toUniformitySamplingDTO(calc), + Result: toUniformityResultDTO(calc), + UniformityDetails: toUniformityDetailItemsDTO(calc), + } +} + +func ToUniformityDetailDTO( + entityData entity.ProjectFlockKandangUniformity, + calc service.UniformityCalculation, + document *entity.Document, + standard *UniformityStandardDTO, +) UniformityDetailDTO { + info := UniformityInfoDTO{ + Tanggal: formatUniformityDate(entityData.UniformDate), + LokasiFarm: resolveLocationName(entityData.ProjectFlockKandang), + ProjectFlock: resolveProjectFlockName(entityData.ProjectFlockKandang), + Kandang: resolveKandangName(entityData.ProjectFlockKandang), + FileName: "", + } + if document != nil { + info.FileName = document.Name + } + + return UniformityDetailDTO{ + Id: entityData.Id, + InfoUmum: info, + Sampling: toUniformitySamplingDTO(calc), + Result: toUniformityResultDTO(calc), + Standard: standard, + UniformityDetails: toUniformityDetailItemsDTO(calc), + } +} + +func ToUniformityListDTOs(items []entity.ProjectFlockKandangUniformity) []UniformityListDTO { + result := make([]UniformityListDTO, len(items)) + for i, item := range items { + var latestApproval *approvalDTO.ApprovalRelationDTO + status := "Pengajuan" + if item.LatestApproval != nil { + mapped := approvalDTO.ToApprovalDTO(*item.LatestApproval) + latestApproval = &mapped + if mapped.StepName != "" { + status = mapped.StepName + } + } + + result[i] = UniformityListDTO{ + Id: item.Id, + ProjectFlockKandangId: item.ProjectFlockKandangId, + LocationName: resolveLocationName(item.ProjectFlockKandang), + FlockName: resolveProjectFlockName(item.ProjectFlockKandang), + KandangName: resolveKandangName(item.ProjectFlockKandang), + AppliedAt: item.UniformDate, + Week: item.Week, + Status: status, + Uniformity: item.Uniformity, + Cv: item.Cv, + ChickQtyOfWeight: item.ChickQtyOfWeight, + UniformQty: item.UniformQty, + MeanUp: item.MeanUp, + MeanDown: item.MeanDown, + CreatedAt: item.CreatedAt, + CreatedBy: item.CreatedBy, + LatestApproval: latestApproval, + } + } + return result +} + +func ToUniformityListDTOsWithStandard( + items []entity.ProjectFlockKandangUniformity, + standards map[uint]service.UniformityStandard, +) []UniformityListDTO { + result := ToUniformityListDTOs(items) + if len(result) == 0 || len(standards) == 0 { + return result + } + + for i := range result { + if std, ok := standards[result[i].Id]; ok { + result[i].StandardMeanWeight = std.MeanWeight + result[i].StandardUniformity = std.Uniformity + } + } + return result +} + +func toUniformitySamplingDTO(calc service.UniformityCalculation) UniformitySamplingDTO { + return UniformitySamplingDTO{ + ChickQtyOfWeight: calc.ChickQtyOfWeight, + MeanWeight: calc.MeanWeight, + MeanDown: calc.MeanDown, + MeanUp: calc.MeanUp, + } +} + +func toUniformityResultDTO(calc service.UniformityCalculation) UniformityResultDTO { + return UniformityResultDTO{ + UniformQty: calc.UniformQty, + OutsideQty: calc.OutsideQty, + Uniformity: calc.Uniformity, + Cv: calc.Cv, + } +} + +func toUniformityDetailItemsDTO(calc service.UniformityCalculation) []UniformityDetailItemDTO { + result := make([]UniformityDetailItemDTO, len(calc.Details)) + for i, item := range calc.Details { + result[i] = UniformityDetailItemDTO{ + Id: item.Id, + Weight: item.Weight, + Range: item.Range, + } + } + return result +} + +func resolveLocationName(pfk entity.ProjectFlockKandang) string { + if pfk.Kandang.Id != 0 && pfk.Kandang.Location.Id != 0 { + return pfk.Kandang.Location.Name + } + if pfk.ProjectFlock.Id != 0 && pfk.ProjectFlock.Location.Id != 0 { + return pfk.ProjectFlock.Location.Name + } + return "" +} + +func resolveProjectFlockName(pfk entity.ProjectFlockKandang) string { + if pfk.ProjectFlock.Id != 0 { + return pfk.ProjectFlock.FlockName + } + return "" +} + +func resolveKandangName(pfk entity.ProjectFlockKandang) string { + if pfk.Kandang.Id != 0 { + return pfk.Kandang.Name + } + return "" +} + +func formatUniformityDate(date *time.Time) string { + if date == nil || date.IsZero() { + return "" + } + return date.Format("2006-01-02") +} diff --git a/internal/modules/production/uniformities/module.go b/internal/modules/production/uniformities/module.go new file mode 100644 index 00000000..b3162940 --- /dev/null +++ b/internal/modules/production/uniformities/module.go @@ -0,0 +1,57 @@ +package uniformitys + +import ( + "context" + "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" + rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories" + rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" + rUniformity "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/repositories" + sUniformity "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/services" + + rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + "gitlab.com/mbugroup/lti-api.git/internal/utils" +) + +type UniformityModule struct{} + +func (UniformityModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + uniformityRepo := rUniformity.NewUniformityRepository(db) + documentRepo := commonRepo.NewDocumentRepository(db) + approvalRepo := commonRepo.NewApprovalRepository(db) + projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db) + productionStandardRepo := rProductionStandard.NewProductionStandardRepository(db) + standardGrowthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db) + userRepo := rUser.NewUserRepository(db) + + documentSvc, err := commonSvc.NewDocumentServiceFromConfig(context.Background(), documentRepo) + if err != nil { + panic(fmt.Sprintf("failed to create document service: %v", err)) + } + + approvalSvc := commonSvc.NewApprovalService(approvalRepo) + if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowUniformity, utils.UniformityApprovalSteps); err != nil { + panic(fmt.Sprintf("failed to register uniformity approval workflow: %v", err)) + } + + uniformityService := sUniformity.NewUniformityService( + uniformityRepo, + documentSvc, + approvalRepo, + approvalSvc, + projectFlockKandangRepo, + productionStandardRepo, + standardGrowthDetailRepo, + validate, + ) + userService := sUser.NewUserService(userRepo, validate) + + UniformityRoutes(router, userService, uniformityService) +} diff --git a/internal/modules/production/uniformities/repositories/uniformity.repository.go b/internal/modules/production/uniformities/repositories/uniformity.repository.go new file mode 100644 index 00000000..3bc66f4f --- /dev/null +++ b/internal/modules/production/uniformities/repositories/uniformity.repository.go @@ -0,0 +1,21 @@ +package repository + +import ( + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gorm.io/gorm" +) + +type UniformityRepository interface { + repository.BaseRepository[entity.ProjectFlockKandangUniformity] +} + +type UniformityRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.ProjectFlockKandangUniformity] +} + +func NewUniformityRepository(db *gorm.DB) UniformityRepository { + return &UniformityRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlockKandangUniformity](db), + } +} diff --git a/internal/modules/production/uniformities/route.go b/internal/modules/production/uniformities/route.go new file mode 100644 index 00000000..ff2b1805 --- /dev/null +++ b/internal/modules/production/uniformities/route.go @@ -0,0 +1,25 @@ +package uniformitys + +import ( + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/controllers" + uniformity "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + "github.com/gofiber/fiber/v2" +) + +func UniformityRoutes(v1 fiber.Router, u user.UserService, s uniformity.UniformityService) { + ctrl := controller.NewUniformityController(s) + + route := v1.Group("/uniformities") + route.Use(m.Auth(u)) + + route.Get("/", m.RequirePermissions(m.P_Uniformities_GetAll), ctrl.GetAll) + route.Post("/", m.RequirePermissions(m.P_Uniformities_CreateOne), ctrl.CreateOne) + route.Post("/verify", m.RequirePermissions(m.P_Uniformities_Verify), ctrl.UploadBodyWeightExcel) + route.Post("/approvals", m.RequirePermissions(m.P_Uniformities_Approval), ctrl.Approve) + route.Get("/:id", m.RequirePermissions(m.P_Uniformities_GetOne), ctrl.GetOne) + route.Patch("/:id", m.RequirePermissions(m.P_Uniformities_UpdateOne), ctrl.UpdateOne) + route.Delete("/:id", m.RequirePermissions(m.P_Uniformities_DeleteOne), ctrl.DeleteOne) +} diff --git a/internal/modules/production/uniformities/services/uniformity.body_weight_excel.go b/internal/modules/production/uniformities/services/uniformity.body_weight_excel.go new file mode 100644 index 00000000..4e87f0cc --- /dev/null +++ b/internal/modules/production/uniformities/services/uniformity.body_weight_excel.go @@ -0,0 +1,200 @@ +package service + +import ( + "io" + "mime/multipart" + "strconv" + "strings" + + "github.com/gofiber/fiber/v2" + "github.com/xuri/excelize/v2" +) + +type BodyWeightExcelRow struct { + No int `json:"no"` + Weight float64 `json:"weight"` + Range string `json:"range,omitempty"` +} + +func (s uniformityService) ParseBodyWeightExcel(_ *fiber.Ctx, file *multipart.FileHeader) ([]BodyWeightExcelRow, error) { + if file == nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "file is required") + } + + reader, err := file.Open() + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "failed to open file") + } + defer reader.Close() + + rows, err := parseBodyWeightExcelReader(reader) + if err != nil { + return nil, err + } + + return rows, nil +} + +func parseBodyWeightExcelReader(reader io.Reader) ([]BodyWeightExcelRow, error) { + xlsx, err := excelize.OpenReader(reader) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "failed to read excel file") + } + defer func() { + _ = xlsx.Close() + }() + + sheets := xlsx.GetSheetList() + if len(sheets) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "no sheets found in file") + } + + sheetName := sheets[0] + if len(sheets) > 1 { + sheetName = sheets[1] + } + + rows, err := xlsx.GetRows(sheetName, excelize.Options{RawCellValue: true}) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "failed to read sheet rows") + } + + return parseBodyWeightRows(rows) +} + +func parseBodyWeightRows(rows [][]string) ([]BodyWeightExcelRow, error) { + headerRowIdx, noCol, bwCol, rangeCol := findBodyWeightHeader(rows) + if headerRowIdx < 0 || bwCol < 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "header BW not found") + } + + result := make([]BodyWeightExcelRow, 0) + lastNo := 0 + + for i := headerRowIdx + 1; i < len(rows); i++ { + row := rows[i] + weightStr := cellAt(row, bwCol) + weightVal, ok := parseNumber(weightStr) + if !ok { + continue + } + + noVal := 0 + if noCol >= 0 { + if parsed, ok := parseNumber(cellAt(row, noCol)); ok { + noVal = int(parsed) + } + } + if noVal <= 0 { + noVal = lastNo + 1 + } + if noVal > lastNo { + lastNo = noVal + } + + rangeVal := "" + if rangeCol >= 0 { + rangeVal = strings.TrimSpace(cellAt(row, rangeCol)) + } + + rowPayload := BodyWeightExcelRow{ + No: noVal, + Weight: weightVal, + Range: rangeVal, + } + if rowPayload.No <= 0 || rowPayload.Weight <= 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "invalid body weight row data") + } + + result = append(result, rowPayload) + } + + if len(result) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "no body weight data found") + } + + return result, nil +} + +func findBodyWeightHeader(rows [][]string) (rowIdx int, noCol int, bwCol int, rangeCol int) { + rowIdx = -1 + noCol = -1 + bwCol = -1 + rangeCol = -1 + + for i, row := range rows { + tempNo := -1 + tempBW := -1 + tempRange := -1 + for j, cell := range row { + label := normalizeHeader(cell) + switch label { + case "no": + tempNo = j + case "bw": + tempBW = j + case "outsiderange": + tempRange = j + default: + if strings.HasPrefix(label, "bw") { + tempBW = j + } else if strings.HasPrefix(label, "no") { + tempNo = j + } else if strings.Contains(label, "range") { + tempRange = j + } + } + } + if tempBW >= 0 { + rowIdx = i + bwCol = tempBW + noCol = tempNo + rangeCol = tempRange + break + } + } + + return rowIdx, noCol, bwCol, rangeCol +} + +func cellAt(row []string, idx int) string { + if idx < 0 || idx >= len(row) { + return "" + } + return strings.TrimSpace(row[idx]) +} + +func normalizeHeader(value string) string { + trimmed := strings.ToLower(strings.TrimSpace(value)) + if trimmed == "" { + return "" + } + var b strings.Builder + for _, r := range trimmed { + if r >= 'a' && r <= 'z' { + b.WriteRune(r) + } + } + return b.String() +} + +func parseNumber(value string) (float64, bool) { + trimmed := strings.TrimSpace(value) + if trimmed == "" { + return 0, false + } + + if strings.Contains(trimmed, ",") { + if strings.Contains(trimmed, ".") { + trimmed = strings.ReplaceAll(trimmed, ",", "") + } else { + trimmed = strings.ReplaceAll(trimmed, ",", ".") + } + } + + parsed, err := strconv.ParseFloat(trimmed, 64) + if err != nil { + return 0, false + } + return parsed, true +} diff --git a/internal/modules/production/uniformities/services/uniformity.service.go b/internal/modules/production/uniformities/services/uniformity.service.go new file mode 100644 index 00000000..2e76e48f --- /dev/null +++ b/internal/modules/production/uniformities/services/uniformity.service.go @@ -0,0 +1,959 @@ +package service + +import ( + "context" + "errors" + "fmt" + "math" + "mime/multipart" + "net/http" + "strings" + "time" + + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories" + rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" + repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/repositories" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/validations" + "gitlab.com/mbugroup/lti-api.git/internal/utils" + approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +type UniformityService interface { + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandangUniformity, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error) + GetSummary(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error) + GetStandard(ctx *fiber.Ctx, uniformity *entity.ProjectFlockKandangUniformity) (*UniformityStandard, error) + MapStandards(ctx *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]UniformityStandard, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create, file *multipart.FileHeader, rows []BodyWeightExcelRow) (*entity.ProjectFlockKandangUniformity, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint, file *multipart.FileHeader, rows []BodyWeightExcelRow) (*entity.ProjectFlockKandangUniformity, error) + DeleteOne(ctx *fiber.Ctx, id uint) error + Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlockKandangUniformity, error) + ParseBodyWeightExcel(ctx *fiber.Ctx, file *multipart.FileHeader) ([]BodyWeightExcelRow, error) + ComputeUniformity(rows []BodyWeightExcelRow) (UniformityCalculation, error) + CalculateUniformityFromDocument(ctx *fiber.Ctx, uniformityID uint) (UniformityCalculation, *entity.Document, error) +} + +type uniformityService struct { + Log *logrus.Logger + Validate *validator.Validate + Repository repository.UniformityRepository + DocumentSvc commonSvc.DocumentService + ApprovalRepo commonRepo.ApprovalRepository + ApprovalSvc commonSvc.ApprovalService + ProjectFlockKandangRepo rProjectFlock.ProjectFlockKandangRepository + ProductionStandardRepo rProductionStandard.ProductionStandardRepository + StandardGrowthDetailRepo rProductionStandard.StandardGrowthDetailRepository +} + +func NewUniformityService( + repo repository.UniformityRepository, + documentSvc commonSvc.DocumentService, + approvalRepo commonRepo.ApprovalRepository, + approvalSvc commonSvc.ApprovalService, + projectFlockKandangRepo rProjectFlock.ProjectFlockKandangRepository, + productionStandardRepo rProductionStandard.ProductionStandardRepository, + standardGrowthDetailRepo rProductionStandard.StandardGrowthDetailRepository, + validate *validator.Validate, +) UniformityService { + return &uniformityService{ + Log: utils.Log, + Validate: validate, + Repository: repo, + DocumentSvc: documentSvc, + ApprovalRepo: approvalRepo, + ApprovalSvc: approvalSvc, + ProjectFlockKandangRepo: projectFlockKandangRepo, + ProductionStandardRepo: productionStandardRepo, + StandardGrowthDetailRepo: standardGrowthDetailRepo, + } +} + +func (s uniformityService) withRelations(db *gorm.DB) *gorm.DB { + return db. + Preload("ProjectFlockKandang.ProjectFlock.Location"). + Preload("ProjectFlockKandang.Kandang.Location") +} + +func (s uniformityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandangUniformity, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + offset := (params.Page - 1) * params.Limit + + uniformitys, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + if params.ProjectFlockKandangId != 0 { + db = db.Where("project_flock_kandang_id = ?", params.ProjectFlockKandangId) + } + if params.Week != 0 { + db = db.Where("week = ?", params.Week) + } + return db.Order("uniform_date DESC").Order("created_at DESC") + }) + + if err != nil { + s.Log.Errorf("Failed to get uniformitys: %+v", err) + return nil, 0, err + } + if err := s.attachLatestApprovals(c.Context(), uniformitys); err != nil { + return nil, 0, err + } + return uniformitys, total, nil +} + +func (s uniformityService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error) { + uniformity, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Uniformity not found") + } + if err != nil { + s.Log.Errorf("Failed get uniformity by id: %+v", err) + return nil, err + } + if err := s.attachLatestApproval(c.Context(), uniformity); err != nil { + return nil, err + } + return uniformity, nil +} + +func (s uniformityService) GetSummary(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandangUniformity, error) { + return s.GetOne(c, id) +} + +func (s uniformityService) GetStandard(c *fiber.Ctx, uniformity *entity.ProjectFlockKandangUniformity) (*UniformityStandard, error) { + if uniformity == nil { + return nil, nil + } + return s.resolveUniformityStandard(c.Context(), *uniformity) +} + +func (s uniformityService) MapStandards(c *fiber.Ctx, items []entity.ProjectFlockKandangUniformity) (map[uint]UniformityStandard, error) { + if len(items) == 0 { + return nil, nil + } + if s.ProductionStandardRepo == nil || s.StandardGrowthDetailRepo == nil { + return nil, nil + } + + categoryStandard := make(map[string]*entity.ProductionStandard) + detailCache := make(map[uint]map[int]entity.StandardGrowthDetail) + result := make(map[uint]UniformityStandard, len(items)) + + for _, item := range items { + if item.Id == 0 { + continue + } + standard, err := s.resolveCategoryStandard(c.Context(), item.ProjectFlockKandang.ProjectFlock.Category, categoryStandard) + if err != nil { + return nil, err + } + if standard == nil { + continue + } + + weekMap, ok := detailCache[standard.Id] + if !ok { + details, err := s.StandardGrowthDetailRepo.GetByProductionStandardID(c.Context(), standard.Id) + if err != nil { + return nil, err + } + weekMap = make(map[int]entity.StandardGrowthDetail, len(details)) + for _, detail := range details { + weekMap[detail.Week] = detail + } + detailCache[standard.Id] = weekMap + } + + detail, ok := weekMap[item.Week] + if !ok { + continue + } + standardDTO := UniformityStandard{ + MeanWeight: cloneFloat64(detail.TargetMeanBw), + Uniformity: float64Ptr(detail.MinUniformity), + } + result[item.Id] = standardDTO + } + + return result, nil +} + +func (s *uniformityService) CreateOne(c *fiber.Ctx, req *validation.Create, file *multipart.FileHeader, rows []BodyWeightExcelRow) (*entity.ProjectFlockKandangUniformity, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + if s.ProjectFlockKandangRepo == nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Project flock kandang repository not available") + } + + if file == nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "document is required") + } + + uniformDate, err := time.Parse("2006-01-02", req.Date) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "date must be in YYYY-MM-DD format") + } + + if err := commonSvc.EnsureRelations( + c.Context(), + commonSvc.RelationCheck{Name: "Project Flock Kandang", ID: &req.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists}, + ); err != nil { + return nil, err + } + if err := s.ensureUniqueUniformity(c.Context(), 0, req.ProjectFlockKandangId, req.Week, &uniformDate); err != nil { + return nil, err + } + + if len(rows) == 0 { + parsedRows, err := s.ParseBodyWeightExcel(c, file) + if err != nil { + return nil, err + } + rows = parsedRows + } + + calculation, err := s.ComputeUniformity(rows) + if err != nil { + return nil, err + } + actorID, err := m.ActorIDFromContext(c) + if err != nil { + return nil, err + } + + createBody := &entity.ProjectFlockKandangUniformity{ + Uniformity: calculation.Uniformity, + Week: req.Week, + Cv: calculation.Cv, + ChickQtyOfWeight: calculation.ChickQtyOfWeight, + MeanUp: calculation.MeanUp, + MeanDown: calculation.MeanDown, + ProjectFlockKandangId: req.ProjectFlockKandangId, + UniformQty: calculation.UniformQty, + NotUniformQty: calculation.OutsideQty, + UniformDate: &uniformDate, + CreatedBy: actorID, + } + + if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { + repoTx := s.Repository.WithTx(tx) + if err := repoTx.CreateOne(c.Context(), createBody, nil); err != nil { + return err + } + if err := s.createUniformityApproval( + c.Context(), + tx, + createBody.Id, + utils.UniformityStepPengajuan, + entity.ApprovalActionCreated, + actorID, + nil, + ); err != nil { + return err + } + return nil + }); err != nil { + s.Log.Errorf("Failed to create uniformity: %+v", err) + return nil, err + } + + if s.DocumentSvc != nil { + actorIDCopy := actorID + _, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{ + DocumentableType: "UNIFORMITY", + DocumentableID: uint64(createBody.Id), + CreatedBy: &actorIDCopy, + Files: []commonSvc.DocumentFile{ + { + File: file, + Type: "UNIFORMITY", + }, + }, + }) + if err != nil { + s.rollbackUniformityCreate(c.Context(), createBody.Id) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to upload uniformity document") + } + } + + return s.GetOne(c, createBody.Id) +} + +func (s uniformityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint, file *multipart.FileHeader, rows []BodyWeightExcelRow) (*entity.ProjectFlockKandangUniformity, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + updateBody := make(map[string]any) + var uniformDate *time.Time + + if req.Date != nil { + parsed, err := time.Parse("2006-01-02", *req.Date) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "date must be in YYYY-MM-DD format") + } + updateBody["uniform_date"] = parsed + uniformDate = &parsed + } + if req.ProjectFlockKandangId != nil { + if s.ProjectFlockKandangRepo == nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Project flock kandang repository not available") + } + if err := commonSvc.EnsureRelations( + c.Context(), + commonSvc.RelationCheck{Name: "Project Flock Kandang", ID: req.ProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists}, + ); err != nil { + return nil, err + } + updateBody["project_flock_kandang_id"] = *req.ProjectFlockKandangId + } + if req.Week != nil { + updateBody["week"] = *req.Week + } + + if req.Date != nil || req.ProjectFlockKandangId != nil || req.Week != nil { + current, err := s.Repository.GetByID(c.Context(), id, nil) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Uniformity not found") + } + return nil, err + } + targetDate := uniformDate + if targetDate == nil { + targetDate = current.UniformDate + } + targetWeek := current.Week + if req.Week != nil { + targetWeek = *req.Week + } + targetPFKID := current.ProjectFlockKandangId + if req.ProjectFlockKandangId != nil { + targetPFKID = *req.ProjectFlockKandangId + } + if targetDate != nil { + if err := s.ensureUniqueUniformity(c.Context(), id, targetPFKID, targetWeek, targetDate); err != nil { + return nil, err + } + } + } + + if file != nil { + if s.DocumentSvc == nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Document service not available") + } + + if len(rows) == 0 { + parsedRows, err := s.ParseBodyWeightExcel(c, file) + if err != nil { + return nil, err + } + rows = parsedRows + } + + calculation, err := s.ComputeUniformity(rows) + if err != nil { + return nil, err + } + + updateBody["uniformity"] = calculation.Uniformity + updateBody["cv"] = calculation.Cv + updateBody["chick_qty_of_weight"] = calculation.ChickQtyOfWeight + updateBody["mean_up"] = calculation.MeanUp + updateBody["mean_down"] = calculation.MeanDown + updateBody["uniform_qty"] = calculation.UniformQty + updateBody["not_uniform_qty"] = calculation.OutsideQty + } + + if len(updateBody) == 0 { + return s.GetOne(c, id) + } + + if file == nil { + if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Uniformity not found") + } + s.Log.Errorf("Failed to update uniformity: %+v", err) + return nil, err + } + + return s.GetOne(c, id) + } + + existingDocs, err := s.DocumentSvc.ListByTarget(c.Context(), "UNIFORMITY", uint64(id)) + if err != nil { + return nil, err + } + + actorID, err := m.ActorIDFromContext(c) + if err != nil { + return nil, err + } + + actorIDCopy := actorID + uploadResults, err := s.DocumentSvc.UploadDocuments(c.Context(), commonSvc.DocumentUploadRequest{ + DocumentableType: "UNIFORMITY", + DocumentableID: uint64(id), + CreatedBy: &actorIDCopy, + Files: []commonSvc.DocumentFile{ + { + File: file, + Type: "UNIFORMITY", + }, + }, + }) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to upload uniformity document") + } + + if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { + if len(uploadResults) > 0 { + ids := make([]uint, 0, len(uploadResults)) + for _, result := range uploadResults { + if result.Document.Id != 0 { + ids = append(ids, result.Document.Id) + } + } + if len(ids) > 0 { + _ = s.DocumentSvc.DeleteDocuments(c.Context(), ids, true) + } + } + + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Uniformity not found") + } + s.Log.Errorf("Failed to update uniformity: %+v", err) + return nil, err + } + + if len(existingDocs) > 0 { + oldIDs := make([]uint, 0, len(existingDocs)) + for _, doc := range existingDocs { + if doc.Id != 0 { + oldIDs = append(oldIDs, doc.Id) + } + } + if len(oldIDs) > 0 { + _ = s.DocumentSvc.DeleteDocuments(c.Context(), oldIDs, true) + } + } + + return s.GetOne(c, id) +} + +func (s *uniformityService) ensureUniqueUniformity(ctx context.Context, id uint, projectFlockKandangID uint, week int, uniformDate *time.Time) error { + if projectFlockKandangID == 0 || week == 0 || uniformDate == nil || uniformDate.IsZero() { + return nil + } + + query := s.Repository.DB().WithContext(ctx). + Model(&entity.ProjectFlockKandangUniformity{}). + Where("project_flock_kandang_id = ? AND week = ? AND uniform_date = ?", projectFlockKandangID, week, *uniformDate) + if id != 0 { + query = query.Where("id <> ?", id) + } + + var count int64 + if err := query.Count(&count).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate uniformity uniqueness") + } + if count > 0 { + return fiber.NewError(fiber.StatusConflict, "Uniformity already exists for the same project flock kandang, week, and date") + } + return nil +} + +func (s uniformityService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := s.Repository.DeleteOne(c.Context(), id); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Uniformity not found") + } + s.Log.Errorf("Failed to delete uniformity: %+v", err) + return err + } + return nil +} + +func (s uniformityService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlockKandangUniformity, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + actionValue := strings.ToUpper(strings.TrimSpace(req.Action)) + var action entity.ApprovalAction + switch actionValue { + case string(entity.ApprovalActionApproved): + action = entity.ApprovalActionApproved + case string(entity.ApprovalActionRejected): + action = entity.ApprovalActionRejected + default: + return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED") + } + + ids := uniqueUintSlice(req.ApprovableIds) + if len(ids) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") + } + + step := utils.UniformityStepPengajuan + if action == entity.ApprovalActionApproved { + step = utils.UniformityStepDisetujui + } + + actorID, err := m.ActorIDFromContext(c) + if err != nil { + return nil, err + } + + ctx := c.Context() + transactionErr := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { + repoTx := s.Repository.WithTx(tx) + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) + + for _, id := range ids { + if _, err := repoTx.GetByID(ctx, id, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Uniformity %d not found", id)) + } + return err + } + + if _, err := approvalSvc.CreateApproval( + ctx, + utils.ApprovalWorkflowUniformity, + id, + step, + &action, + actorID, + req.Notes, + ); err != nil { + return err + } + } + + return nil + }) + if transactionErr != nil { + if fiberErr, ok := transactionErr.(*fiber.Error); ok { + return nil, fiberErr + } + s.Log.Errorf("Failed to record approvals for uniformities %+v: %+v", ids, transactionErr) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to submit uniformity approval") + } + + results := make([]entity.ProjectFlockKandangUniformity, 0, len(ids)) + for _, id := range ids { + loaded, err := s.GetOne(c, id) + if err != nil { + return nil, err + } + results = append(results, *loaded) + } + + return results, nil +} + +type UniformityDetailItem struct { + Id int + Weight float64 + Range string +} + +type UniformityCalculation struct { + ChickQtyOfWeight float64 + MeanWeight float64 + MeanDown float64 + MeanUp float64 + UniformQty float64 + OutsideQty float64 + Uniformity float64 + Cv float64 + Details []UniformityDetailItem +} + +type UniformityStandard struct { + MeanWeight *float64 + Uniformity *float64 +} + +func (s uniformityService) ComputeUniformity(rows []BodyWeightExcelRow) (UniformityCalculation, error) { + return computeUniformity(rows) +} + +func (s uniformityService) CalculateUniformityFromDocument(c *fiber.Ctx, uniformityID uint) (UniformityCalculation, *entity.Document, error) { + if s.DocumentSvc == nil { + return UniformityCalculation{}, nil, fiber.NewError(fiber.StatusInternalServerError, "Document service not available") + } + + documents, err := s.DocumentSvc.ListByTarget(c.Context(), "UNIFORMITY", uint64(uniformityID)) + if err != nil { + return UniformityCalculation{}, nil, err + } + if len(documents) == 0 { + return UniformityCalculation{}, nil, fiber.NewError(fiber.StatusNotFound, "Uniformity document not found") + } + + document := documents[0] + url := s.DocumentSvc.PublicURL(document) + if url == "" { + return UniformityCalculation{}, nil, fiber.NewError(fiber.StatusBadRequest, "Uniformity document URL not available") + } + + req, err := http.NewRequestWithContext(c.Context(), http.MethodGet, url, nil) + if err != nil { + return UniformityCalculation{}, nil, err + } + resp, err := http.DefaultClient.Do(req) + if err != nil { + return UniformityCalculation{}, nil, err + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return UniformityCalculation{}, nil, fiber.NewError(fiber.StatusBadRequest, "Failed to download uniformity document") + } + + rows, err := parseBodyWeightExcelReader(resp.Body) + if err != nil { + return UniformityCalculation{}, nil, err + } + + calculation, err := computeUniformity(rows) + if err != nil { + return UniformityCalculation{}, nil, err + } + + return calculation, &document, nil +} + +func (s *uniformityService) createUniformityApproval( + ctx context.Context, + db *gorm.DB, + uniformityID uint, + step approvalutils.ApprovalStep, + action entity.ApprovalAction, + actorID uint, + notes *string, +) error { + if uniformityID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Uniformity tidak valid untuk approval") + } + if actorID == 0 { + return fiber.NewError(fiber.StatusBadRequest, "Actor Id tidak valid untuk approval") + } + + var svc commonSvc.ApprovalService + if db != nil { + svc = commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(db)) + } else if s.ApprovalSvc != nil { + svc = s.ApprovalSvc + } else { + svc = commonSvc.NewApprovalService(s.ApprovalRepo) + } + + _, err := svc.CreateApproval(ctx, utils.ApprovalWorkflowUniformity, uniformityID, step, &action, actorID, notes) + return err +} + +func (s *uniformityService) attachLatestApprovals(ctx context.Context, items []entity.ProjectFlockKandangUniformity) error { + if len(items) == 0 || s.ApprovalSvc == nil { + return nil + } + + ids := make([]uint, 0, len(items)) + visited := make(map[uint]struct{}, len(items)) + for _, item := range items { + if item.Id == 0 { + continue + } + if _, ok := visited[item.Id]; ok { + continue + } + visited[item.Id] = struct{}{} + ids = append(ids, item.Id) + } + + if len(ids) == 0 { + return nil + } + + latestMap, err := s.ApprovalSvc.LatestByTargets(ctx, utils.ApprovalWorkflowUniformity, ids, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err != nil { + s.Log.Warnf("Unable to load latest approvals for uniformities: %+v", err) + return nil + } + + if len(latestMap) == 0 { + return nil + } + + for i := range items { + if items[i].Id == 0 { + continue + } + if approval, ok := latestMap[items[i].Id]; ok { + items[i].LatestApproval = approval + } + } + + return nil +} + +func (s *uniformityService) attachLatestApproval(ctx context.Context, item *entity.ProjectFlockKandangUniformity) error { + if item == nil || item.Id == 0 || s.ApprovalSvc == nil { + return nil + } + + approvals, err := s.ApprovalSvc.ListByTarget(ctx, utils.ApprovalWorkflowUniformity, item.Id, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err != nil { + s.Log.Warnf("Unable to load approvals for uniformity %d: %+v", item.Id, err) + return nil + } + + if len(approvals) == 0 { + item.LatestApproval = nil + return nil + } + + latest := approvals[len(approvals)-1] + item.LatestApproval = &latest + return nil +} + +func (s *uniformityService) resolveUniformityStandard(ctx context.Context, item entity.ProjectFlockKandangUniformity) (*UniformityStandard, error) { + if s.ProductionStandardRepo == nil || s.StandardGrowthDetailRepo == nil { + return nil, nil + } + + standard, err := s.resolveCategoryStandard(ctx, item.ProjectFlockKandang.ProjectFlock.Category, nil) + if err != nil || standard == nil { + return nil, err + } + + detail, err := s.StandardGrowthDetailRepo.GetByStandardIDAndWeek(ctx, standard.Id, item.Week) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + + return &UniformityStandard{ + MeanWeight: cloneFloat64(detail.TargetMeanBw), + Uniformity: float64Ptr(detail.MinUniformity), + }, nil +} + +func (s *uniformityService) resolveCategoryStandard( + ctx context.Context, + category string, + cache map[string]*entity.ProductionStandard, +) (*entity.ProductionStandard, error) { + category = strings.TrimSpace(category) + if category == "" { + return nil, nil + } + if cache != nil { + if cached, ok := cache[category]; ok { + return cached, nil + } + } + + var standard entity.ProductionStandard + err := s.ProductionStandardRepo.DB().WithContext(ctx). + Where("project_category = ?", category). + Where("deleted_at IS NULL"). + Order("created_at DESC"). + First(&standard).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + if cache != nil { + cache[category] = nil + } + return nil, nil + } + return nil, err + } + + standardCopy := standard + if cache != nil { + cache[category] = &standardCopy + } + return &standardCopy, nil +} + +func cloneFloat64(value *float64) *float64 { + if value == nil { + return nil + } + copy := *value + return © +} + +func float64Ptr(value float64) *float64 { + copy := value + return © +} + +func (s *uniformityService) rollbackUniformityCreate(ctx context.Context, uniformityID uint) { + if uniformityID == 0 { + return + } + + if s.ApprovalRepo != nil { + if err := s.ApprovalRepo.DeleteByTarget(ctx, utils.ApprovalWorkflowUniformity.String(), uniformityID); err != nil { + s.Log.WithError(err).Warnf("Failed to rollback uniformity approvals for %d", uniformityID) + } + } + + if err := s.Repository.DeleteOne(ctx, uniformityID); err != nil { + s.Log.WithError(err).Warnf("Failed to rollback uniformity %d", uniformityID) + } +} + +func uniqueUintSlice(values []uint) []uint { + if len(values) == 0 { + return nil + } + + seen := make(map[uint]struct{}, len(values)) + result := make([]uint, 0, len(values)) + for _, v := range values { + if v == 0 { + continue + } + if _, ok := seen[v]; ok { + continue + } + seen[v] = struct{}{} + result = append(result, v) + } + return result +} + +func computeUniformity(rows []BodyWeightExcelRow) (UniformityCalculation, error) { + weights := make([]float64, 0, len(rows)) + details := make([]UniformityDetailItem, 0, len(rows)) + hasRangeLabels := false + for idx, row := range rows { + if row.Weight <= 0 { + continue + } + id := row.No + if id <= 0 { + id = idx + 1 + } + weights = append(weights, row.Weight) + rangeLabel := strings.TrimSpace(row.Range) + if rangeLabel != "" { + upper := strings.ToUpper(rangeLabel) + if upper == "HIGH" || upper == "LOW" { + hasRangeLabels = true + } + rangeLabel = upper + } + details = append(details, UniformityDetailItem{ + Id: id, + Weight: row.Weight, + Range: rangeLabel, + }) + } + + total := float64(len(weights)) + if total == 0 { + return UniformityCalculation{}, fiber.NewError(fiber.StatusBadRequest, "no body weight data found") + } + + var sum float64 + for _, w := range weights { + sum += w + } + mean := sum / total + meanUpThreshold := roundToPrecision(mean*1.10, 3) + meanDownThreshold := roundToPrecision(mean*0.90, 3) + + var uniformCount float64 + for i := range details { + if hasRangeLabels { + if details[i].Range == "HIGH" || details[i].Range == "LOW" { + details[i].Range = "Outside" + continue + } + details[i].Range = "Ideal" + uniformCount++ + continue + } + + w := details[i].Weight + if w > meanUpThreshold || w < meanDownThreshold { + details[i].Range = "Outside" + continue + } + details[i].Range = "Ideal" + uniformCount++ + } + outsideCount := total - uniformCount + + var cv float64 + if mean > 0 && total > 1 { + stddevWeights := weights + stddevCount := float64(len(stddevWeights)) + if stddevCount > 1 { + var stddevSum float64 + for _, w := range stddevWeights { + stddevSum += w + } + stddevMean := stddevSum / stddevCount + var sumSquares float64 + for _, w := range stddevWeights { + diff := w - stddevMean + sumSquares += diff * diff + } + stddev := math.Sqrt(sumSquares / (stddevCount - 1)) + cv = (stddev / mean) * 100 + } + } + + uniformity := (uniformCount / total) * 100 + + return UniformityCalculation{ + ChickQtyOfWeight: total, + MeanWeight: roundToPrecision(mean, 0), + MeanDown: roundToPrecision(mean*0.90, 0), + MeanUp: roundToPrecision(mean*1.10, 0), + UniformQty: uniformCount, + OutsideQty: outsideCount, + Uniformity: roundToPrecision(uniformity, 0), + Cv: roundToPrecision(cv, 1), + Details: details, + }, nil +} + +func roundToPrecision(value float64, precision int) float64 { + if precision < 0 { + return value + } + scale := math.Pow10(precision) + scaled := value * scale + fraction := scaled - math.Floor(scaled) + if fraction >= 0.5 { + return math.Ceil(scaled) / scale + } + return math.Floor(scaled) / scale +} diff --git a/internal/modules/production/uniformities/validations/uniformity.validation.go b/internal/modules/production/uniformities/validations/uniformity.validation.go new file mode 100644 index 00000000..b2aeaf26 --- /dev/null +++ b/internal/modules/production/uniformities/validations/uniformity.validation.go @@ -0,0 +1,164 @@ +package validation + +import ( + "mime/multipart" + "strconv" + "strings" + + "github.com/gofiber/fiber/v2" +) + +type Create struct { + Date string `form:"date" validate:"required"` + ProjectFlockKandangId uint `form:"project_flock_kandang_id" validate:"required,number,min=1"` + Week int `form:"week" validate:"required,min=1"` +} + +type Update struct { + Date *string `json:"date,omitempty" form:"date" validate:"omitempty"` + ProjectFlockKandangId *uint `json:"project_flock_kandang_id,omitempty" form:"project_flock_kandang_id" validate:"omitempty,number,min=1"` + Week *int `json:"week,omitempty" form:"week" validate:"omitempty,min=1"` +} + +type Query struct { + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"` + Week int `query:"week" validate:"omitempty,min=1"` +} + +type UploadExcelRequest struct { + Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,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"` +} + +func ParseIDParam(c *fiber.Ctx, name string) (uint, error) { + raw := strings.TrimSpace(c.Params(name)) + if raw == "" { + return 0, fiber.NewError(fiber.StatusBadRequest, "Invalid Id") + } + id, err := strconv.Atoi(raw) + if err != nil || id <= 0 { + return 0, fiber.NewError(fiber.StatusBadRequest, "Invalid Id") + } + return uint(id), nil +} + +func ParseQuery(c *fiber.Ctx) (*Query, error) { + query := &Query{ + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + ProjectFlockKandangId: uint(c.QueryInt("project_flock_kandang_id", 0)), + Week: c.QueryInt("week", 0), + } + + if query.Page < 1 || query.Limit < 1 { + return nil, fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") + } + + return query, nil +} + +func ParseCreate(c *fiber.Ctx) (*Create, *multipart.FileHeader, error) { + date := strings.TrimSpace(c.FormValue("date")) + if date == "" { + return nil, nil, fiber.NewError(fiber.StatusBadRequest, "date is required") + } + + projectFlockKandangIDStr := strings.TrimSpace(c.FormValue("project_flock_kandang_id")) + if projectFlockKandangIDStr == "" { + return nil, nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required") + } + + projectFlockKandangID, err := strconv.Atoi(projectFlockKandangIDStr) + if err != nil || projectFlockKandangID <= 0 { + return nil, nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is required") + } + + weekStr := strings.TrimSpace(c.FormValue("week")) + if weekStr == "" { + return nil, nil, fiber.NewError(fiber.StatusBadRequest, "week is required") + } + + week, err := strconv.Atoi(weekStr) + if err != nil || week <= 0 { + return nil, nil, fiber.NewError(fiber.StatusBadRequest, "week is required") + } + + file, err := c.FormFile("document") + if err != nil { + return nil, nil, fiber.NewError(fiber.StatusBadRequest, "document is required") + } + + return &Create{ + Date: date, + ProjectFlockKandangId: uint(projectFlockKandangID), + Week: week, + }, file, nil +} + +func ParseUpdate(c *fiber.Ctx) (*Update, *multipart.FileHeader, error) { + contentType := strings.ToLower(c.Get("Content-Type")) + if strings.Contains(contentType, "multipart/form-data") { + req := &Update{} + + date := strings.TrimSpace(c.FormValue("date")) + if date != "" { + req.Date = &date + } + + projectFlockKandangIDStr := strings.TrimSpace(c.FormValue("project_flock_kandang_id")) + if projectFlockKandangIDStr != "" { + projectFlockKandangID, err := strconv.Atoi(projectFlockKandangIDStr) + if err != nil || projectFlockKandangID <= 0 { + return nil, nil, fiber.NewError(fiber.StatusBadRequest, "project_flock_kandang_id is invalid") + } + idCopy := uint(projectFlockKandangID) + req.ProjectFlockKandangId = &idCopy + } + + weekStr := strings.TrimSpace(c.FormValue("week")) + if weekStr != "" { + week, err := strconv.Atoi(weekStr) + if err != nil || week <= 0 { + return nil, nil, fiber.NewError(fiber.StatusBadRequest, "week is invalid") + } + req.Week = &week + } + + file, err := c.FormFile("document") + if err != nil { + file = nil + } + + return req, file, nil + } + + req := new(Update) + if err := c.BodyParser(req); err != nil { + return nil, nil, fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + return req, nil, nil +} + +func ParseUploadFiles(c *fiber.Ctx) ([]*multipart.FileHeader, error) { + file, err := c.FormFile("document") + if err != nil || file == nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "document is required") + } + + return []*multipart.FileHeader{file}, nil +} + +func ParseApprove(c *fiber.Ctx) (*Approve, error) { + req := new(Approve) + if err := c.BodyParser(req); err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + return req, nil +} diff --git a/internal/utils/constant.go b/internal/utils/constant.go index 85b0cc91..c94ac7bc 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -289,6 +289,21 @@ var RecordingApprovalSteps = map[approvalutils.ApprovalStep]string{ RecordingStepDisetujui: "Disetujui", } +// ------------------------------------------------------------------- +// Uniformity Approval +// ------------------------------------------------------------------- + +const ( + ApprovalWorkflowUniformity approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("UNIFORMITIES") + UniformityStepPengajuan approvalutils.ApprovalStep = 1 + UniformityStepDisetujui approvalutils.ApprovalStep = 2 +) + +var UniformityApprovalSteps = map[approvalutils.ApprovalStep]string{ + UniformityStepPengajuan: "Pengajuan", + UniformityStepDisetujui: "Disetujui", +} + // ------------------------------------------------------------------- // Purchase Approval // ------------------------------------------------------------------- @@ -408,12 +423,12 @@ type DocumentType string type DocumentableType string const ( - DocumentTypeTransfer DocumentType = "STOCK_TRANSFER_DOCUMENT" - DocumentTypeExpense DocumentType = "EXPENSE_DOCUMENT" + DocumentTypeTransfer DocumentType = "STOCK_TRANSFER_DOCUMENT" + DocumentTypeExpense DocumentType = "EXPENSE_DOCUMENT" DocumentTypeExpenseRealization DocumentType = "EXPENSE_REALIZATION_DOCUMENT" - DocumentableTypeTransfer DocumentableType = "STOCK_TRANSFER" - DocumentableTypeExpense DocumentableType = "EXPENSE" + DocumentableTypeTransfer DocumentableType = "STOCK_TRANSFER" + DocumentableTypeExpense DocumentableType = "EXPENSE" DocumentableTypeExpenseRealization DocumentableType = "EXPENSE_REALIZATION" )