Compare commits

..

69 Commits

Author SHA1 Message Date
MacBook Air M1 c729067ab5 adjust validate pactch daily checklist 2026-01-08 15:08:40 +07:00
MacBook Air M1 f079bee92a adjust get all phase activity 2026-01-08 13:37:53 +07:00
MacBook Air M1 f1f7edb9ab Adjust limit for get all employee 2026-01-08 11:50:34 +07:00
MacBook Air M1 a4840fc98a add master data config checklist 2026-01-07 21:37:51 +07:00
MacBook Air M1 c3f8ae5887 add api daily checklist report 2026-01-07 17:39:17 +07:00
MacBook Air M1 e545047165 add api summary and update status 2026-01-07 12:02:44 +07:00
MacBook Air M1 42aa6829c5 add api detail daily checklist 2026-01-07 10:36:40 +07:00
MacBook Air M1 dded9e807b add api check uncheck assignment 2026-01-06 23:59:16 +07:00
MacBook Air M1 3bd0602525 add daily checklist module;adjust master data;adjust migration 2026-01-06 17:03:55 +07:00
MacBook Air M1 b1996be24c add module master phase activity 2026-01-05 22:25:46 +07:00
MacBook Air M1 4a08be1f55 add module master data phases 2026-01-05 19:59:03 +07:00
MacBook Air M1 9f840f2650 adjust patch employee 2026-01-05 17:49:44 +07:00
MacBook Air M1 80109b77db adjust api get all employees 2026-01-05 17:32:41 +07:00
MacBook Air M1 df504e3ff0 add migration;add api create employee 2026-01-05 17:17:25 +07:00
Hafizh A. Y. c1a162b4d4 Merge branch 'fix/sapronak-dev' into 'development'
adjust api closing tap sapronak

See merge request mbugroup/lti-api!130
2026-01-03 18:04:50 +00:00
Hafizh A. Y. 1101879039 Merge branch 'fix/BE-Document_s3' into 'development'
feat(BE): fix fifo system recording and uniformity dto

See merge request mbugroup/lti-api!129
2026-01-03 18:04:24 +00:00
ragilap 8de33a0f24 feat(BE): fix delete project flock budget and uniformity, and fix uniformity with update purchase document 2026-01-02 20:43:57 +07:00
MacBook Air M1 1348483b1c adjust api closing tap sapronak 2026-01-02 13:19:11 +07:00
ragilap 2f8f84cb0d Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into fix/BE-Document_s3 2026-01-02 12:05:28 +07:00
ragilap cc5a58b6d1 feat(BE): sso delete and fix response too many request 2026-01-02 12:04:50 +07:00
ragilap fe51f33ab4 feat(BE): fixing fifo system recording 2025-12-31 19:30:04 +07:00
ragilap e0dd2799fc feat(BE): fix fifo system recording and uniformity dto 2025-12-31 15:10:06 +07:00
Hafizh A. Y. 394eb0f363 Merge branch 'dev/fix-route' into 'development'
fix rename route api closing data production

See merge request mbugroup/lti-api!127
2025-12-31 06:16:55 +00:00
MacBook Air M1 47d497d6b0 fix rename route api closing data production 2025-12-31 13:15:02 +07:00
Hafizh A. Y. 3a1a2b436d Merge branch 'fix/BE-Document_s3' into 'development'
feat(BE): add function read and download in document

See merge request mbugroup/lti-api!126
2025-12-31 04:41:53 +00:00
ragilap 9d285869f5 feat(BE): add function read and download in document 2025-12-31 11:39:53 +07:00
Hafizh A. Y. 4e2724a702 Merge branch 'dev/teguh' into 'development'
feat[BE]: add get all nonstock by supplier id on nonstock get all

See merge request mbugroup/lti-api!125
2025-12-31 03:02:48 +00:00
Hafizh A. Y. 953756c15c Merge branch 'fix/BE/US-278-purchase_adjustment_createOne' into 'development'
Fix/be/us 278 purchase adjustment createone

See merge request mbugroup/lti-api!123
2025-12-31 03:01:35 +00:00
Hafizh A. Y. 2749e44439 Merge branch 'feat/BE/US-281-adjustment_recording' into 'development'
feat(BE-281): adjustment recording table with handhouse and deleting weight...

See merge request mbugroup/lti-api!122
2025-12-31 03:01:09 +00:00
ragilap b8c0b0c37d feat(BE-278): add std for max_depletion 2025-12-31 09:44:20 +07:00
Hafizh A. Y. acbf52a5e1 Merge branch 'feat/BE/US-281-uniformity' into 'development'
feat(BE-281): adjustment bug erorr 500 if 404 record projectflock

See merge request mbugroup/lti-api!124
2025-12-31 02:41:42 +00:00
aguhh18 0fc560b91c fix(be): update nonstock query to use SupplierID as a non-pointer type 2025-12-31 09:40:05 +07:00
Hafizh A. Y. d35d0bbe6b Merge branch 'dev/teguh' into 'development'
fix(BE): fix error get location id when only attach to location in expense

See merge request mbugroup/lti-api!119
2025-12-31 02:18:27 +00:00
ragilap d9afd2913e feat(BE-278): adjustment_recording dto 2025-12-31 09:13:55 +07:00
ragilap dbaee73134 feat(BE-278): fix error purchase product warehouse 2025-12-31 07:50:13 +07:00
ragilap 709e304f7f feat(BE-281): adjustment bug erorr 500 if 404 record projectflock 2025-12-31 07:39:20 +07:00
ragilap d994cfdce7 productstock 2025-12-31 07:08:03 +07:00
Hafizh A. Y. d3bb00a06a Merge branch 'fix/nonstock-undefined-field' into 'development'
fix(be): nonstock response supplier null to empty array

See merge request mbugroup/lti-api!120
2025-12-30 23:57:54 +00:00
Hafizh A. Y 5302713811 fix(be): nonstock response supplier null to empty array 2025-12-31 06:52:38 +07:00
Hafizh A. Y. f698ca070c Merge branch 'fix/nonstock-undefined-field' into 'development'
fix(be): remove omitempty in dto and validation nonstock

See merge request mbugroup/lti-api!118
2025-12-30 23:44:27 +00:00
Hafizh A. Y 6c42119f4d fix(be): remove omitempty in dto and validation nonstock 2025-12-31 06:43:34 +07:00
ragilap bc03c469f2 feat(BE-278): add delete document s3 2025-12-31 04:00:41 +07:00
ragilap fd5f83ca58 feat(BE-278): unrestrict feat warehouse purchase,adding purchase upload document 2025-12-31 03:50:58 +07:00
Hafizh A. Y. 299c8c7177 Merge branch 'fix/setup-seeder' into 'development'
fix: setup seeder for development

See merge request mbugroup/lti-api!117
2025-12-30 16:54:23 +00:00
Hafizh A. Y 78359db880 fix: setup seeder for development 2025-12-30 23:52:37 +07:00
aguhh18 91fd8a253b feat(BE): update foreign key constraints for project_chickins and adjust service logic for project flock kandang retrieval 2025-12-30 20:16:40 +07:00
aguhh18 d91ff7a4c2 feat(BE): add supplier_id filter to GetAll method and update validation for query parameters 2025-12-30 20:03:23 +07:00
aguhh18 3ecea6741f feat(BE): update DeleteOne method to use uint64 for ID and implement soft delete logic 2025-12-30 19:39:10 +07:00
aguhh18 b988f45a0b feat(BE): update expense DTO and service to directly use location from expense 2025-12-30 19:30:42 +07:00
Hafizh A. Y. 10799cc1ed Merge branch 'feat/BE/Sprint-8' into 'development'
[FEAT/BE] Partial Merge #3

See merge request mbugroup/lti-api!116
2025-12-30 12:06:19 +00:00
Hafizh A. Y. c9c581ef30 Merge branch 'sprint-8/gio' into 'feat/BE/Sprint-8'
fix api get all closing; fix get closing sapronak; fix get all maste data product

See merge request mbugroup/lti-api!115
2025-12-30 09:36:48 +00:00
Hafizh A. Y. 6ee795cf2a Merge branch 'dev/teguh' into 'feat/BE/Sprint-8'
feat(BE US#386): add standard_fcr column to production_standard_details and update existing API

See merge request mbugroup/lti-api!114
2025-12-30 09:36:00 +00:00
aguhh18 471fd1dbbf feat(BE): enhance product warehouse handling and automatic calculations for delivery and sales orders 2025-12-30 16:30:44 +07:00
ragilap 4e5caa8cba feat(BE-281): add rbac for uniformity 2025-12-30 15:23:34 +07:00
ragilap 0396aa0255 feat(BE-287):adjustment purchase restrict unfinished 2025-12-30 14:27:50 +07:00
ragilap 756ba223ed feat(BE-281):add standart production into response recording get one 2025-12-30 13:17:01 +07:00
ragilap 0c776e8332 feat(BE-281): uncoment auth 2025-12-30 12:08:49 +07:00
ragilap 90125ffe1a feat(BE-281):add dto standart mean bw and uniformity 2025-12-30 12:07:28 +07:00
aguhh18 c36719cc1a Merge branch 'feat/BE/Sprint-8' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-12-30 10:42:04 +07:00
aguhh18 e4acd9a21e feat(BE): add standard_fcr column to production_standard_details and update related services and validations 2025-12-30 10:27:12 +07:00
ragilap 9a094b8bfe feat(BE-281):fix document payload 2025-12-30 09:56:48 +07:00
ragilap 16ef73fce3 Merge branch 'feat/BE/Sprint-8' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-281-adjustment_recording 2025-12-30 09:41:42 +07:00
Hafizh A. Y. 49af2d6448 Merge branch 'feat/BE/Sprint-8' into 'development'
Feat[BE]: Partial Merge

See merge request mbugroup/lti-api!112
2025-12-29 14:39:58 +00:00
ragilap 6523290aaf feat(BE-281): change template excel 2025-12-29 19:44:10 +07:00
ragilap a2066979c1 feat(BE-281): adjustment uniformity for make unique for week,projectflockandang, and date 2025-12-29 19:04:10 +07:00
ragilap 8e7e976946 feat(BE-281): adjustment recording table with handhouse and deleting weight unfinished dto:standart fcr,hand house and others 2025-12-29 16:25:08 +07:00
ragilap 8dfb224614 feat(BE-281): changes std deviasi first 100 data to all 2025-12-29 10:13:29 +07:00
ragilap 411d6fe6a9 feat(BE-281): deleting bw in recording 2025-12-29 09:38:49 +07:00
ragilap 644896edfa feat(BE-281): unfinished uniformity and create project flock triger productwarehouse and add new filtering lookup 2025-12-29 00:21:26 +07:00
134 changed files with 9929 additions and 1278 deletions
+7
View File
@@ -19,6 +19,7 @@ require (
github.com/redis/go-redis/v9 v9.14.0 github.com/redis/go-redis/v9 v9.14.0
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/spf13/viper v1.19.0 github.com/spf13/viper v1.19.0
github.com/xuri/excelize/v2 v2.9.0
golang.org/x/crypto v0.33.0 golang.org/x/crypto v0.33.0
gorm.io/driver/postgres v1.5.9 gorm.io/driver/postgres v1.5.9
gorm.io/gorm v1.25.11 gorm.io/gorm v1.25.11
@@ -71,9 +72,12 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/mapstructure v1.5.0 // 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/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/philhofer/fwd v1.1.2 // indirect github.com/philhofer/fwd v1.1.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // 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/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/sagikazarmark/locafero v0.4.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/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // 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/subosito/gotenv v1.6.0 // indirect
github.com/tinylib/msgp v1.1.8 // indirect github.com/tinylib/msgp v1.1.8 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.55.0 // indirect github.com/valyala/fasthttp v1.55.0 // indirect
github.com/valyala/tcplisten v1.0.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/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
+21 -1
View File
@@ -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/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 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 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 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= 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-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 h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 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.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 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.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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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.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 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0= 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/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 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= 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/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 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= 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/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 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 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.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/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -187,10 +187,11 @@ func (r *BaseRepositoryImpl[T]) PatchOne(
updates map[string]any, updates map[string]any,
modifier func(*gorm.DB) *gorm.DB, modifier func(*gorm.DB) *gorm.DB,
) error { ) error {
q := r.db.WithContext(ctx).Model(new(T)).Where("id = ?", id) q := r.db.WithContext(ctx)
if modifier != nil { if modifier != nil {
q = modifier(q) q = modifier(q)
} }
q = q.Model(new(T)).Where("id = ?", id)
result := q.Updates(updates) result := q.Updates(updates)
if result.Error != nil { if result.Error != nil {
@@ -6,8 +6,10 @@ import (
"fmt" "fmt"
"mime" "mime"
"mime/multipart" "mime/multipart"
"net/url"
"path/filepath" "path/filepath"
"strings" "strings"
"time"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
"gitlab.com/mbugroup/lti-api.git/internal/config" "gitlab.com/mbugroup/lti-api.git/internal/config"
@@ -29,6 +31,7 @@ type DocumentService interface {
DeleteDocuments(ctx context.Context, ids []uint, removeFromStorage bool) error DeleteDocuments(ctx context.Context, ids []uint, removeFromStorage bool) error
DeleteByTarget(ctx context.Context, documentableType string, documentableID uint64, removeFromStorage bool) error DeleteByTarget(ctx context.Context, documentableType string, documentableID uint64, removeFromStorage bool) error
PublicURL(document entity.Document) string PublicURL(document entity.Document) string
PresignURL(ctx context.Context, document entity.Document, expires time.Duration) (string, error)
} }
type DocumentUploadRequest struct { type DocumentUploadRequest struct {
@@ -293,6 +296,66 @@ func (s *documentService) PublicURL(document entity.Document) string {
return s.storage.URL(document.Path) return s.storage.URL(document.Path)
} }
func (s *documentService) PresignURL(ctx context.Context, document entity.Document, expires time.Duration) (string, error) {
if s.storage == nil {
return "", errors.New("document storage not configured")
}
if strings.TrimSpace(document.Path) == "" {
return "", errors.New("document path is required")
}
return s.storage.PresignURL(ctx, document.Path, expires)
}
// ResolveDocumentURL normalizes a stored path or URL into a presigned URL.
func ResolveDocumentURL(
ctx context.Context,
svc DocumentService,
rawPath string,
expires time.Duration,
) (string, error) {
if svc == nil {
return "", nil
}
rawPath = strings.TrimSpace(rawPath)
if rawPath == "" {
return "", nil
}
key := rawPath
lower := strings.ToLower(rawPath)
if strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") {
key = extractS3KeyFromURL(rawPath)
if key == "" {
return "", nil
}
}
return svc.PresignURL(ctx, entity.Document{Path: key}, expires)
}
func extractS3KeyFromURL(raw string) string {
parsed, err := url.Parse(strings.TrimSpace(raw))
if err != nil {
return ""
}
path := strings.TrimPrefix(parsed.Path, "/")
if path == "" {
return ""
}
host := strings.ToLower(strings.TrimSpace(parsed.Host))
if strings.HasPrefix(host, "s3.") || strings.HasPrefix(host, "s3-") {
parts := strings.SplitN(path, "/", 2)
if len(parts) == 2 {
return parts[1]
}
return ""
}
return path
}
func (s *documentService) generateObjectKey(ext string) (string, error) { func (s *documentService) generateObjectKey(ext string) (string, error) {
normalizedExt := strings.TrimSpace(ext) normalizedExt := strings.TrimSpace(ext)
if normalizedExt != "" && !strings.HasPrefix(normalizedExt, ".") { if normalizedExt != "" && !strings.HasPrefix(normalizedExt, ".") {
@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io" "io"
"strings" "strings"
"time"
"github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws"
awsconfig "github.com/aws/aws-sdk-go-v2/config" awsconfig "github.com/aws/aws-sdk-go-v2/config"
@@ -17,6 +18,7 @@ type DocumentStorage interface {
Upload(ctx context.Context, key string, body io.Reader, size int64, contentType string) (DocumentStorageUploadResult, error) Upload(ctx context.Context, key string, body io.Reader, size int64, contentType string) (DocumentStorageUploadResult, error)
Delete(ctx context.Context, key string) error Delete(ctx context.Context, key string) error
URL(key string) string URL(key string) string
PresignURL(ctx context.Context, key string, expires time.Duration) (string, error)
} }
type DocumentStorageUploadResult struct { type DocumentStorageUploadResult struct {
@@ -36,9 +38,10 @@ type S3DocumentStorageConfig struct {
} }
type s3DocumentStorage struct { type s3DocumentStorage struct {
client *s3.Client client *s3.Client
bucket string presignClient *s3.PresignClient
base string bucket string
base string
} }
func NewS3DocumentStorage(ctx context.Context, cfg S3DocumentStorageConfig) (DocumentStorage, error) { func NewS3DocumentStorage(ctx context.Context, cfg S3DocumentStorageConfig) (DocumentStorage, error) {
@@ -86,6 +89,7 @@ func NewS3DocumentStorage(ctx context.Context, cfg S3DocumentStorageConfig) (Doc
client := s3.NewFromConfig(awsCfg, func(o *s3.Options) { client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
o.UsePathStyle = cfg.ForcePathStyle o.UsePathStyle = cfg.ForcePathStyle
}) })
presignClient := s3.NewPresignClient(client)
baseURL := strings.TrimSuffix(strings.TrimSpace(cfg.BaseURL), "/") baseURL := strings.TrimSuffix(strings.TrimSpace(cfg.BaseURL), "/")
if baseURL == "" { if baseURL == "" {
@@ -97,9 +101,10 @@ func NewS3DocumentStorage(ctx context.Context, cfg S3DocumentStorageConfig) (Doc
} }
return &s3DocumentStorage{ return &s3DocumentStorage{
client: client, client: client,
bucket: bucket, presignClient: presignClient,
base: baseURL, bucket: bucket,
base: baseURL,
}, nil }, nil
} }
@@ -158,3 +163,23 @@ func (s *s3DocumentStorage) URL(key string) string {
} }
return fmt.Sprintf("%s/%s", s.base, key) return fmt.Sprintf("%s/%s", s.base, key)
} }
func (s *s3DocumentStorage) PresignURL(ctx context.Context, key string, expires time.Duration) (string, error) {
key = strings.TrimPrefix(strings.TrimSpace(key), "/")
if key == "" {
return "", errors.New("storage key is required")
}
if expires <= 0 {
expires = 15 * time.Minute
}
out, err := s.presignClient.PresignGetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
}, s3.WithPresignExpires(expires))
if err != nil {
return "", err
}
return out.URL, nil
}
@@ -192,7 +192,6 @@ func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*St
if req.Quantity < 0 { if req.Quantity < 0 {
return nil, errors.New("quantity must be zero or greater") return nil, errors.New("quantity must be zero or greater")
} }
cfg, ok := fifo.Usable(req.UsableKey) cfg, ok := fifo.Usable(req.UsableKey)
if !ok { if !ok {
return nil, fmt.Errorf("usable %q is not registered", req.UsableKey) return nil, fmt.Errorf("usable %q is not registered", req.UsableKey)
@@ -220,7 +219,6 @@ func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*St
currentPending := ctxRow.PendingQty currentPending := ctxRow.PendingQty
currentTotal := currentUsage + currentPending currentTotal := currentUsage + currentPending
delta := req.Quantity - currentTotal delta := req.Quantity - currentTotal
var ( var (
usageDelta float64 usageDelta float64
pendingDelta float64 pendingDelta float64
@@ -285,7 +283,6 @@ func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*St
result.ReleasedQuantity = releasedAmount result.ReleasedQuantity = releasedAmount
result.UsageQuantity = currentUsage + usageDelta result.UsageQuantity = currentUsage + usageDelta
result.PendingQuantity = currentPending + pendingDelta result.PendingQuantity = currentPending + pendingDelta
return nil return nil
}) })
if err != nil { if err != nil {
@@ -299,7 +296,6 @@ func (s *fifoService) ReleaseUsage(ctx context.Context, req StockReleaseRequest)
if req.UsableID == 0 || strings.TrimSpace(req.UsableKey.String()) == "" { if req.UsableID == 0 || strings.TrimSpace(req.UsableKey.String()) == "" {
return errors.New("usable key and id are required") return errors.New("usable key and id are required")
} }
return s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error { return s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
cfg, ok := fifo.Usable(req.UsableKey) cfg, ok := fifo.Usable(req.UsableKey)
if !ok { if !ok {
@@ -310,7 +306,6 @@ func (s *fifoService) ReleaseUsage(ctx context.Context, req StockReleaseRequest)
if err != nil { if err != nil {
return err return err
} }
var usageDelta, pendingDelta float64 var usageDelta, pendingDelta float64
if ctxRow.UsageQty > 0 { if ctxRow.UsageQty > 0 {
if _, err := s.releaseUsagePortion(ctx, tx, req.UsableKey, req.UsableID, ctxRow.UsageQty); err != nil { if _, err := s.releaseUsagePortion(ctx, tx, req.UsableKey, req.UsableID, ctxRow.UsageQty); err != nil {
@@ -715,7 +710,7 @@ func (s *fifoService) releaseUsagePortion(
} }
} else { } else {
if err := s.allocations.PatchOne(ctx, allocation.Id, map[string]any{ if err := s.allocations.PatchOne(ctx, allocation.Id, map[string]any{
"quantity": allocation.Qty - releaseAmt, "qty": allocation.Qty - releaseAmt,
}, func(db *gorm.DB) *gorm.DB { }, func(db *gorm.DB) *gorm.DB {
return s.txOrDB(tx, db) return s.txOrDB(tx, db)
}); err != nil { }); err != nil {
@@ -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;
@@ -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);
@@ -0,0 +1,54 @@
BEGIN;
CREATE TABLE IF NOT EXISTS recording_bws (
id BIGSERIAL PRIMARY KEY,
recording_id BIGINT NOT NULL,
avg_weight NUMERIC(8,2) NOT NULL,
qty NUMERIC(15,3) NOT NULL DEFAULT 1,
total_weight NUMERIC(10,3) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT fk_recording_bws_recording
FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE,
CONSTRAINT chk_recording_bws_nonneg
CHECK (avg_weight >= 0 AND qty >= 0 AND total_weight >= 0)
);
CREATE INDEX IF NOT EXISTS idx_recording_bws_recording
ON recording_bws (recording_id);
ALTER TABLE recordings
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v3;
ALTER TABLE recordings
DROP COLUMN IF EXISTS hand_day,
DROP COLUMN IF EXISTS hand_house,
DROP COLUMN IF EXISTS feed_intake,
DROP COLUMN IF EXISTS egg_mesh,
DROP COLUMN IF EXISTS egg_weight;
ALTER TABLE recordings
ADD CONSTRAINT chk_recordings_nonnegatives_v2 CHECK (
(total_depletion_qty IS NULL OR total_depletion_qty >= 0) AND
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0) AND
(daily_gain IS NULL OR daily_gain >= 0) AND
(avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND
(cum_intake IS NULL OR cum_intake >= 0) AND
(fcr_value IS NULL OR fcr_value >= 0) AND
(total_chick_qty IS NULL OR total_chick_qty >= 0)
);
ALTER TABLE recording_eggs
DROP CONSTRAINT IF EXISTS chk_recording_eggs_qty;
ALTER TABLE recording_eggs
ALTER COLUMN weight TYPE NUMERIC(10,3) USING weight::NUMERIC(10,3);
ALTER TABLE recording_eggs
ADD CONSTRAINT chk_recording_eggs_qty CHECK (
qty >= 0 AND (weight IS NULL OR weight >= 0)
);
COMMIT;
@@ -0,0 +1,44 @@
BEGIN;
ALTER TABLE recordings
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v2;
ALTER TABLE recordings
ADD COLUMN IF NOT EXISTS hand_day NUMERIC(15,3),
ADD COLUMN IF NOT EXISTS hand_house NUMERIC(15,3),
ADD COLUMN IF NOT EXISTS feed_intake NUMERIC(15,3),
ADD COLUMN IF NOT EXISTS egg_mesh NUMERIC(15,3),
ADD COLUMN IF NOT EXISTS egg_weight NUMERIC(15,3);
ALTER TABLE recordings
ADD CONSTRAINT chk_recordings_nonnegatives_v3 CHECK (
(total_depletion_qty IS NULL OR total_depletion_qty >= 0) AND
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 0) AND
(daily_gain IS NULL OR daily_gain >= 0) AND
(avg_daily_gain IS NULL OR avg_daily_gain >= 0) AND
(cum_intake IS NULL OR cum_intake >= 0) AND
(fcr_value IS NULL OR fcr_value >= 0) AND
(total_chick_qty IS NULL OR total_chick_qty >= 0) AND
(hand_day IS NULL OR hand_day >= 0) AND
(hand_house IS NULL OR hand_house >= 0) AND
(feed_intake IS NULL OR feed_intake >= 0) AND
(egg_mesh IS NULL OR egg_mesh >= 0) AND
(egg_weight IS NULL OR egg_weight >= 0)
);
ALTER TABLE recording_eggs
ALTER COLUMN weight TYPE NUMERIC(15,3) USING weight::NUMERIC(15,3);
ALTER TABLE recording_eggs
DROP CONSTRAINT IF EXISTS chk_recording_eggs_qty;
ALTER TABLE recording_eggs
ADD CONSTRAINT chk_recording_eggs_qty CHECK (
qty >= 0 AND
(weight IS NULL OR weight >= 0)
);
DROP INDEX IF EXISTS idx_recording_bws_recording;
DROP TABLE IF EXISTS recording_bws;
COMMIT;
@@ -0,0 +1,3 @@
-- Remove standard_fcr column from production_standard_details table
ALTER TABLE production_standard_details
DROP COLUMN IF EXISTS standard_fcr;
@@ -0,0 +1,3 @@
-- Add standard_fcr column to production_standard_details table
ALTER TABLE production_standard_details
ADD COLUMN standard_fcr NUMERIC(15, 3);
@@ -0,0 +1,20 @@
-- Drop CASCADE constraint
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_project_chickins_kandang'
AND conrelid = 'project_chickins'::regclass
) THEN
ALTER TABLE project_chickins
DROP CONSTRAINT fk_project_chickins_kandang;
END IF;
END $$;
-- Recreate foreign key constraint with RESTRICT (original behavior)
ALTER TABLE project_chickins
ADD CONSTRAINT fk_project_chickins_kandang
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,20 @@
-- Drop existing foreign key constraint with RESTRICT
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_project_chickins_kandang'
AND conrelid = 'project_chickins'::regclass
) THEN
ALTER TABLE project_chickins
DROP CONSTRAINT fk_project_chickins_kandang;
END IF;
END $$;
-- Add new foreign key constraint with CASCADE delete
ALTER TABLE project_chickins
ADD CONSTRAINT fk_project_chickins_kandang
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,126 @@
CREATE OR REPLACE FUNCTION soft_delete_handle_fk() RETURNS TRIGGER AS $$
DECLARE
fk record;
child_column text;
parent_column text;
parent_value text;
child_has_deleted_at boolean;
ref_exists boolean;
sql text;
BEGIN
IF OLD.deleted_at IS NULL AND NEW.deleted_at IS NOT NULL THEN
FOR fk IN
SELECT conrelid::regclass AS child_table,
conkey AS child_cols,
confkey AS parent_cols,
confdeltype
FROM pg_constraint
WHERE contype = 'f'
AND confrelid = TG_RELID
LOOP
IF array_length(fk.child_cols, 1) IS DISTINCT FROM 1
OR array_length(fk.parent_cols, 1) IS DISTINCT FROM 1 THEN
RAISE NOTICE 'soft_delete_handle_fk skipped composite fk on %', fk.child_table;
CONTINUE;
END IF;
SELECT attname INTO child_column
FROM pg_attribute
WHERE attrelid = fk.child_table
AND attnum = fk.child_cols[1]
AND NOT attisdropped;
SELECT attname INTO parent_column
FROM pg_attribute
WHERE attrelid = TG_RELID
AND attnum = fk.parent_cols[1]
AND NOT attisdropped;
EXECUTE format('SELECT ($1).%I', parent_column)
INTO parent_value
USING OLD;
SELECT EXISTS (
SELECT 1
FROM pg_attribute
WHERE attrelid = fk.child_table
AND attname = 'deleted_at'
AND NOT attisdropped
) INTO child_has_deleted_at;
IF fk.confdeltype IN ('r', 'a') THEN
sql := format(
'SELECT EXISTS (SELECT 1 FROM %s WHERE %I = $1 %s)',
fk.child_table,
child_column,
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
);
EXECUTE sql INTO ref_exists USING parent_value;
IF ref_exists THEN
RAISE EXCEPTION 'Cannot soft delete %, still referenced by %',
TG_TABLE_NAME, fk.child_table;
END IF;
ELSIF fk.confdeltype = 'n' THEN
sql := format(
'UPDATE %s SET %I = NULL WHERE %I = $1 %s',
fk.child_table,
child_column,
child_column,
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
);
EXECUTE sql USING parent_value;
ELSIF fk.confdeltype = 'c' THEN
IF child_has_deleted_at THEN
sql := format(
'UPDATE %s SET deleted_at = NOW() WHERE %I = $1 AND deleted_at IS NULL',
fk.child_table,
child_column
);
EXECUTE sql USING parent_value;
ELSE
sql := format(
'DELETE FROM %s WHERE %I = $1',
fk.child_table,
child_column
);
EXECUTE sql USING parent_value;
END IF;
ELSIF fk.confdeltype = 'd' THEN
sql := format(
'UPDATE %s SET %I = DEFAULT WHERE %I = $1 %s',
fk.child_table,
child_column,
child_column,
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
);
EXECUTE sql USING parent_value;
END IF;
END LOOP;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DO $$
DECLARE
r record;
trigger_name text;
BEGIN
FOR r IN
SELECT table_schema, table_name
FROM information_schema.columns
WHERE column_name = 'deleted_at'
AND table_schema = 'public'
GROUP BY table_schema, table_name
LOOP
trigger_name := format('trg_soft_delete_fk_%s', r.table_name);
EXECUTE format('DROP TRIGGER IF EXISTS %I ON %I.%I', trigger_name, r.table_schema, r.table_name);
EXECUTE format(
'CREATE TRIGGER %I BEFORE UPDATE OF deleted_at ON %I.%I FOR EACH ROW EXECUTE FUNCTION soft_delete_handle_fk()',
trigger_name,
r.table_schema,
r.table_name
);
END LOOP;
END $$;
@@ -0,0 +1,142 @@
CREATE OR REPLACE FUNCTION soft_delete_handle_fk() RETURNS TRIGGER AS $$
DECLARE
fk record;
child_column text;
parent_column text;
parent_value text;
child_has_deleted_at boolean;
ref_exists boolean;
sql text;
child_type text;
BEGIN
IF OLD.deleted_at IS NULL AND NEW.deleted_at IS NOT NULL THEN
FOR fk IN
SELECT conrelid::regclass AS child_table,
conkey AS child_cols,
confkey AS parent_cols,
confdeltype
FROM pg_constraint
WHERE contype = 'f'
AND confrelid = TG_RELID
LOOP
IF array_length(fk.child_cols, 1) IS DISTINCT FROM 1
OR array_length(fk.parent_cols, 1) IS DISTINCT FROM 1 THEN
RAISE NOTICE 'soft_delete_handle_fk skipped composite fk on %', fk.child_table;
CONTINUE;
END IF;
SELECT attname INTO child_column
FROM pg_attribute
WHERE attrelid = fk.child_table
AND attnum = fk.child_cols[1]
AND NOT attisdropped;
SELECT attname INTO parent_column
FROM pg_attribute
WHERE attrelid = TG_RELID
AND attnum = fk.parent_cols[1]
AND NOT attisdropped;
SELECT format_type(atttypid, atttypmod) INTO child_type
FROM pg_attribute
WHERE attrelid = fk.child_table
AND attname = child_column
AND NOT attisdropped;
IF child_type IS NULL THEN
child_type := 'text';
END IF;
EXECUTE format('SELECT ($1).%I', parent_column)
INTO parent_value
USING OLD;
SELECT EXISTS (
SELECT 1
FROM pg_attribute
WHERE attrelid = fk.child_table
AND attname = 'deleted_at'
AND NOT attisdropped
) INTO child_has_deleted_at;
IF fk.confdeltype IN ('r', 'a') THEN
sql := format(
'SELECT EXISTS (SELECT 1 FROM %s WHERE %I = $1::%s %s)',
fk.child_table,
child_column,
child_type,
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
);
EXECUTE sql INTO ref_exists USING parent_value;
IF ref_exists THEN
RAISE EXCEPTION 'Cannot soft delete %, still referenced by %',
TG_TABLE_NAME, fk.child_table;
END IF;
ELSIF fk.confdeltype = 'n' THEN
sql := format(
'UPDATE %s SET %I = NULL WHERE %I = $1::%s %s',
fk.child_table,
child_column,
child_column,
child_type,
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
);
EXECUTE sql USING parent_value;
ELSIF fk.confdeltype = 'c' THEN
IF child_has_deleted_at THEN
sql := format(
'UPDATE %s SET deleted_at = NOW() WHERE %I = $1::%s AND deleted_at IS NULL',
fk.child_table,
child_column,
child_type
);
EXECUTE sql USING parent_value;
ELSE
sql := format(
'DELETE FROM %s WHERE %I = $1::%s',
fk.child_table,
child_column,
child_type
);
EXECUTE sql USING parent_value;
END IF;
ELSIF fk.confdeltype = 'd' THEN
sql := format(
'UPDATE %s SET %I = DEFAULT WHERE %I = $1::%s %s',
fk.child_table,
child_column,
child_column,
child_type,
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
);
EXECUTE sql USING parent_value;
END IF;
END LOOP;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DO $$
DECLARE
r record;
trigger_name text;
BEGIN
FOR r IN
SELECT table_schema, table_name
FROM information_schema.columns
WHERE column_name = 'deleted_at'
AND table_schema = 'public'
GROUP BY table_schema, table_name
LOOP
trigger_name := format('trg_soft_delete_fk_%s', r.table_name);
EXECUTE format('DROP TRIGGER IF EXISTS %I ON %I.%I', trigger_name, r.table_schema, r.table_name);
EXECUTE format(
'CREATE TRIGGER %I BEFORE UPDATE OF deleted_at ON %I.%I FOR EACH ROW EXECUTE FUNCTION soft_delete_handle_fk()',
trigger_name,
r.table_schema,
r.table_name
);
END LOOP;
END $$;
@@ -0,0 +1,86 @@
BEGIN;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_project_flock_kandang_uniformity_project_flock_kandang'
) THEN
ALTER TABLE project_flock_kandang_uniformity
DROP CONSTRAINT fk_project_flock_kandang_uniformity_project_flock_kandang;
END IF;
END $$;
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;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_tables
WHERE tablename = 'project_budgets'
) THEN
IF EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_project_budgets_project_flock_id'
) THEN
ALTER TABLE project_budgets
DROP CONSTRAINT fk_project_budgets_project_flock_id;
END IF;
ALTER TABLE project_budgets
ADD CONSTRAINT fk_project_budgets_project_flock_id
FOREIGN KEY (project_flock_id)
REFERENCES project_flocks(id);
END IF;
END $$;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_tables
WHERE tablename = 'project_flock_kandang_uniformity'
) THEN
IF NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'project_flock_kandang_uniformity'
AND column_name = 'created_at'
) THEN
ALTER TABLE project_flock_kandang_uniformity
ADD COLUMN created_at TIMESTAMPTZ DEFAULT NOW();
END IF;
IF NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'project_flock_kandang_uniformity'
AND column_name = 'updated_at'
) THEN
ALTER TABLE project_flock_kandang_uniformity
ADD COLUMN updated_at TIMESTAMPTZ DEFAULT NOW();
END IF;
IF NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'project_flock_kandang_uniformity'
AND column_name = 'deleted_at'
) THEN
ALTER TABLE project_flock_kandang_uniformity
ADD COLUMN deleted_at TIMESTAMPTZ;
END IF;
END IF;
END $$;
COMMIT;
@@ -0,0 +1,90 @@
BEGIN;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_project_flock_kandang_uniformity_project_flock_kandang'
) THEN
ALTER TABLE project_flock_kandang_uniformity
DROP CONSTRAINT fk_project_flock_kandang_uniformity_project_flock_kandang;
END IF;
END $$;
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 CASCADE ON UPDATE CASCADE;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_tables
WHERE tablename = 'project_budgets'
) THEN
IF EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_project_budgets_project_flock_id'
) THEN
ALTER TABLE project_budgets
DROP CONSTRAINT fk_project_budgets_project_flock_id;
END IF;
ALTER TABLE project_budgets
ADD CONSTRAINT fk_project_budgets_project_flock_id
FOREIGN KEY (project_flock_id)
REFERENCES project_flocks(id)
ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
END $$;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_trigger
WHERE tgname = 'trg_soft_delete_fk_project_flock_kandang_uniformity'
) THEN
DROP TRIGGER trg_soft_delete_fk_project_flock_kandang_uniformity
ON project_flock_kandang_uniformity;
END IF;
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'project_flock_kandang_uniformity'
AND column_name = 'created_at'
) THEN
ALTER TABLE project_flock_kandang_uniformity
DROP COLUMN created_at;
END IF;
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'project_flock_kandang_uniformity'
AND column_name = 'updated_at'
) THEN
ALTER TABLE project_flock_kandang_uniformity
DROP COLUMN updated_at;
END IF;
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'project_flock_kandang_uniformity'
AND column_name = 'deleted_at'
) THEN
ALTER TABLE project_flock_kandang_uniformity
DROP COLUMN deleted_at;
END IF;
END $$;
COMMIT;
@@ -0,0 +1,12 @@
DROP TABLE IF EXISTS daily_checklist_tasks;
DROP TABLE IF EXISTS daily_checklist_activity_task_assignees;
DROP TABLE IF EXISTS daily_checklist_activity_tasks;
DROP TABLE IF EXISTS daily_checklist_phases;
DROP TABLE IF EXISTS daily_checklists;
DROP TABLE IF EXISTS checklists;
DROP TABLE IF EXISTS phase_activities;
DROP TABLE IF EXISTS phases;
DROP TABLE IF EXISTS employee_kandangs;
DROP TABLE IF EXISTS employees;
DROP TYPE IF EXISTS category_code;
@@ -0,0 +1,194 @@
CREATE TYPE category_code AS ENUM (
'pullet_open',
'pullet_close',
'produksi_open',
'produksi_close'
);
-- MASTER TABLES
CREATE TABLE employees (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name varchar NOT NULL,
is_active boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE employee_kandangs (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
employee_id bigint NOT NULL,
kandang_id bigint NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT fk_employee_kandangs_employee
FOREIGN KEY (employee_id) REFERENCES employees(id)
ON DELETE CASCADE,
CONSTRAINT fk_employee_kandangs_kandang
FOREIGN KEY (kandang_id) REFERENCES kandangs(id)
ON DELETE CASCADE,
CONSTRAINT uq_employee_kandangs UNIQUE (employee_id, kandang_id)
);
-- PHASE & CHECKLIST
CREATE TABLE phases (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name varchar NOT NULL,
is_active boolean NOT NULL DEFAULT true,
category category_code NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE phase_activities (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
phase_id bigint NOT NULL,
name varchar NOT NULL,
description text,
time_type text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT fk_phase_activities_phase
FOREIGN KEY (phase_id) REFERENCES phases(id)
ON DELETE CASCADE
);
CREATE TABLE checklists (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name varchar NOT NULL,
description text,
phase_id bigint,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz,
CONSTRAINT fk_checklists_phase
FOREIGN KEY (phase_id) REFERENCES phases(id)
ON DELETE SET NULL
);
-- DAILY CHECKLISTS
CREATE TABLE daily_checklists (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
kandang_id bigint NOT NULL,
checklist_id bigint NOT NULL,
date date NOT NULL,
name varchar,
status varchar,
category category_code NOT NULL,
total_score integer,
document_path varchar,
reject_reason text,
created_by bigint,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT fk_daily_checklists_kandang
FOREIGN KEY (kandang_id) REFERENCES kandangs(id)
ON DELETE CASCADE,
CONSTRAINT fk_daily_checklists_checklist
FOREIGN KEY (checklist_id) REFERENCES checklists(id)
ON DELETE RESTRICT,
CONSTRAINT fk_daily_checklists_created_by
FOREIGN KEY (created_by) REFERENCES users(id)
ON DELETE SET NULL
);
--RELASI CHECKLIST ⇄ PHASE
CREATE TABLE daily_checklist_phases (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
checklist_id bigint NOT NULL,
phase_id bigint NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT fk_dcp_checklist
FOREIGN KEY (checklist_id) REFERENCES checklists(id)
ON DELETE CASCADE,
CONSTRAINT fk_dcp_phase
FOREIGN KEY (phase_id) REFERENCES phases(id)
ON DELETE CASCADE,
CONSTRAINT uq_daily_checklist_phases UNIQUE (checklist_id, phase_id)
);
--ACTIVITY TASKS & ASSIGNMENT
CREATE TABLE daily_checklist_activity_tasks (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
checklist_id bigint NOT NULL,
phase_id bigint NOT NULL,
phase_activity_id bigint NOT NULL,
time_type text,
notes text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT fk_dcat_checklist
FOREIGN KEY (checklist_id) REFERENCES checklists(id)
ON DELETE CASCADE,
CONSTRAINT fk_dcat_phase
FOREIGN KEY (phase_id) REFERENCES phases(id)
ON DELETE CASCADE,
CONSTRAINT fk_dcat_phase_activity
FOREIGN KEY (phase_activity_id) REFERENCES phase_activities(id)
ON DELETE CASCADE
);
CREATE TABLE daily_checklist_activity_task_assignments (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
task_id bigint NOT NULL,
employee_id bigint NOT NULL,
checked boolean NOT NULL DEFAULT false,
note text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT fk_assignment_task
FOREIGN KEY (task_id) REFERENCES daily_checklist_activity_tasks(id)
ON DELETE CASCADE,
CONSTRAINT fk_assignment_employee
FOREIGN KEY (employee_id) REFERENCES employees(id)
ON DELETE CASCADE
);
--DAILY CHECKLIST TASK RESULT
CREATE TABLE daily_checklist_tasks (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
daily_checklist_id bigint NOT NULL,
checklist_id bigint NOT NULL,
checklist_item_id bigint,
is_completed boolean NOT NULL DEFAULT false,
score_value integer,
notes text,
photo_proof varchar,
status varchar,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT fk_dct_daily
FOREIGN KEY (daily_checklist_id) REFERENCES daily_checklists(id)
ON DELETE CASCADE,
CONSTRAINT fk_dct_checklist
FOREIGN KEY (checklist_id) REFERENCES checklists(id)
ON DELETE CASCADE,
CONSTRAINT fk_dct_checklist_item
FOREIGN KEY (checklist_item_id) REFERENCES phase_activities(id)
ON DELETE SET NULL
);
@@ -0,0 +1,2 @@
ALTER TABLE daily_checklists
DROP CONSTRAINT IF EXISTS daily_checklists_date_kandang_category_key;
@@ -0,0 +1,3 @@
ALTER TABLE daily_checklists
ADD CONSTRAINT daily_checklists_date_kandang_category_key
UNIQUE (date, kandang_id, category);
@@ -0,0 +1,2 @@
ALTER TABLE daily_checklists
ALTER COLUMN checklist_id SET NOT NULL;
@@ -0,0 +1,2 @@
ALTER TABLE daily_checklists
ALTER COLUMN checklist_id DROP NOT NULL;
@@ -0,0 +1,4 @@
ALTER TABLE daily_checklist_phases
DROP CONSTRAINT IF EXISTS fk_dcp_daily_checklist,
ADD CONSTRAINT fk_dcp_checklist
FOREIGN KEY (checklist_id) REFERENCES checklists(id) ON DELETE CASCADE;
@@ -0,0 +1,4 @@
ALTER TABLE daily_checklist_phases
DROP CONSTRAINT IF EXISTS fk_dcp_checklist,
ADD CONSTRAINT fk_dcp_daily_checklist
FOREIGN KEY (checklist_id) REFERENCES daily_checklists(id) ON DELETE CASCADE;
@@ -0,0 +1,4 @@
ALTER TABLE daily_checklist_activity_tasks
DROP CONSTRAINT IF EXISTS fk_dcat_daily_checklist,
ADD CONSTRAINT fk_dcat_checklist
FOREIGN KEY (checklist_id) REFERENCES checklists(id) ON DELETE CASCADE;
@@ -0,0 +1,4 @@
ALTER TABLE daily_checklist_activity_tasks
DROP CONSTRAINT IF EXISTS fk_dcat_checklist,
ADD CONSTRAINT fk_dcat_daily_checklist
FOREIGN KEY (checklist_id) REFERENCES daily_checklists(id) ON DELETE CASCADE;
@@ -0,0 +1,2 @@
ALTER TABLE daily_checklist_activity_task_assignments
DROP CONSTRAINT IF EXISTS daily_checklist_activity_task_assignments_task_employee_key;
@@ -0,0 +1,3 @@
ALTER TABLE daily_checklist_activity_task_assignments
ADD CONSTRAINT daily_checklist_activity_task_assignments_task_employee_key
UNIQUE (task_id, employee_id);
@@ -0,0 +1,8 @@
ALTER TABLE phase_activities
DROP COLUMN IF EXISTS deleted_at;
ALTER TABLE phases
DROP COLUMN IF EXISTS deleted_at;
ALTER TABLE employees
DROP COLUMN IF EXISTS deleted_at;
@@ -0,0 +1,8 @@
ALTER TABLE employees
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
ALTER TABLE phases
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
ALTER TABLE phase_activities
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS config_checklists;
@@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS config_checklists (
id BIGSERIAL PRIMARY KEY,
date DATE NOT NULL,
percentage_threshold_bad INTEGER NOT NULL,
percentage_threshold_enough INTEGER NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
File diff suppressed because it is too large Load Diff
+139 -722
View File
@@ -4,7 +4,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -25,66 +24,20 @@ func Run(db *gorm.DB) error {
return err 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) productCategories, err := seedProductCategories(tx, adminID)
if err != nil { if err != nil {
return err 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) suppliers, err := seedSuppliers(tx, adminID)
if err != nil { if err != nil {
return err return err
} }
if err := seedCustomers(tx, adminID, users); err != nil {
return err
}
if err := seedProducts(tx, adminID, uoms, productCategories, suppliers); err != nil { if err := seedProducts(tx, adminID, uoms, productCategories, suppliers); err != nil {
return err 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") fmt.Println("✅ Master data seeding completed")
return nil return nil
}) })
@@ -141,224 +94,6 @@ func seedUoms(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
return result, nil 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) { func seedProductCategories(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
seeds := []struct { seeds := []struct {
Name string Name string
@@ -440,113 +175,6 @@ func seedSuppliers(tx *gorm.DB, createdBy uint) (map[string]uint, error) {
return result, nil 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 { func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories map[string]uint, suppliers map[string]uint) error {
seeds := []struct { seeds := []struct {
Name string Name string
@@ -560,92 +188,88 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
Expiry *int Expiry *int
Suppliers []string Suppliers []string
Flags []utils.FlagType Flags []utils.FlagType
IsVisible bool
}{ }{
{ {
Name: "DOC Broiler", Name: "ISA Brown",
Brand: "MBU Broiler", Brand: "ISA Brown",
Sku: "BRO0001", Sku: "ISA0001",
Uom: "Ekor", Uom: "Ekor",
Category: "Day Old Chick", Category: "Day Old Chick",
Price: 7500, Price: 7500,
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, 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", Name: "Ayam Afkir",
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: "-", Brand: "-",
Sku: "LYR0001", Sku: "1",
Uom: "Ekor", Uom: "Ekor",
Category: "Pullet", Category: "Day Old Chick",
Price: 20000, Price: 1,
Suppliers: []string{"PT CHAROEN POKPHAND INDONESIA Tbk"}, Flags: []utils.FlagType{utils.FlagAyamAfkir},
Flags: []utils.FlagType{utils.FlagLayer}, 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 return nil
} }
func seedNonstocks(tx *gorm.DB, createdBy uint, uoms map[string]uint, suppliers map[string]uint) error { // func seedNonstocks(tx *gorm.DB, createdBy uint, uoms map[string]uint, suppliers map[string]uint) error {
seeds := []struct { // seeds := []struct {
Name string // Name string
Uom string // Uom string
Suppliers []string // Suppliers []string
Flags []utils.FlagType // Flags []utils.FlagType
}{ // }{
{ // {
Name: "Expedisi DOC", // Name: "LAJ",
Uom: "Ekor", // Uom: "Unit",
Suppliers: []string{"Ekspedisi"}, // Suppliers: []string{"Ekspedisi"},
Flags: []utils.FlagType{utils.FlagEkspedisi}, // Flags: []utils.FlagType{utils.FlagEkspedisi},
}, // },
{ // {
Name: "Solar", // Name: "Solar",
Uom: "Liter", // Uom: "Liter",
Suppliers: []string{"BOP Vendor"}, // Suppliers: []string{"BOP Vendor"},
Flags: []utils.FlagType{}, // Flags: []utils.FlagType{},
}, // },
} // }
for _, seed := range seeds { // for _, seed := range seeds {
uomID, ok := uoms[seed.Uom] // uomID, ok := uoms[seed.Uom]
if !ok { // if !ok {
return fmt.Errorf("uom %s not seeded", seed.Uom) // return fmt.Errorf("uom %s not seeded", seed.Uom)
} // }
var nonstock entity.Nonstock // var nonstock entity.Nonstock
err := tx.Where("name = ?", seed.Name).First(&nonstock).Error // err := tx.Where("name = ?", seed.Name).First(&nonstock).Error
if errors.Is(err, gorm.ErrRecordNotFound) { // if errors.Is(err, gorm.ErrRecordNotFound) {
nonstock = entity.Nonstock{ // nonstock = entity.Nonstock{
Name: seed.Name, // Name: seed.Name,
UomId: uomID, // UomId: uomID,
CreatedBy: createdBy, // CreatedBy: createdBy,
} // }
if err := tx.Create(&nonstock).Error; err != nil { // if err := tx.Create(&nonstock).Error; err != nil {
return err // return err
} // }
} else if err != nil { // } else if err != nil {
return err // return err
} else { // } else {
if err := tx.Model(&entity.Nonstock{}).Where("id = ?", nonstock.Id).Updates(map[string]any{ // if err := tx.Model(&entity.Nonstock{}).Where("id = ?", nonstock.Id).Updates(map[string]any{
"uom_id": uomID, // "uom_id": uomID,
}).Error; err != nil { // }).Error; err != nil {
return err // return err
} // }
} // }
for _, supplierName := range seed.Suppliers { // for _, supplierName := range seed.Suppliers {
supplierID, ok := suppliers[supplierName] // supplierID, ok := suppliers[supplierName]
if !ok { // if !ok {
return fmt.Errorf("supplier %s not seeded", supplierName) // return fmt.Errorf("supplier %s not seeded", supplierName)
} // }
var existing entity.NonstockSupplier // var existing entity.NonstockSupplier
err := tx.Where("nonstock_id = ? AND supplier_id = ?", nonstock.Id, supplierID).First(&existing).Error // err := tx.Where("nonstock_id = ? AND supplier_id = ?", nonstock.Id, supplierID).First(&existing).Error
if errors.Is(err, gorm.ErrRecordNotFound) { // if errors.Is(err, gorm.ErrRecordNotFound) {
link := entity.NonstockSupplier{NonstockId: nonstock.Id, SupplierId: supplierID} // link := entity.NonstockSupplier{NonstockId: nonstock.Id, SupplierId: supplierID}
if err := tx.Create(&link).Error; err != nil { // if err := tx.Create(&link).Error; err != nil {
return err // return err
} // }
} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { // } else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err // return err
} // }
} // }
if err := seedFlags(tx, nonstock.Id, entity.FlagableTypeNonstock, seed.Flags); err != nil { // if err := seedFlags(tx, nonstock.Id, entity.FlagableTypeNonstock, seed.Flags); err != nil {
return err // return err
} // }
} // }
return nil // return nil
} // }
// nanti saya isi // nanti saya isi
@@ -823,213 +447,6 @@ func seedFlags(tx *gorm.DB, flagableID uint, flagableType string, flags []utils.
return nil 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: 100},
{ProductName: "281 SPECIAL STARTER", WarehouseName: "Gudang Singaparna", Quantity: 200},
{ProductName: "281 SPECIAL STARTER", WarehouseName: "Gudang Banten", Quantity: 300},
{ProductName: "DOC Broiler", WarehouseName: "Gudang Singaparna 1", Quantity: 5000},
{ProductName: "Telur Konsumsi Baik", WarehouseName: "Gudang Singaparna 1", Quantity: 600},
{ProductName: "Telur Pecah", WarehouseName: "Gudang Singaparna 1", Quantity: 80},
{ProductName: "Telur Konsumsi Baik", WarehouseName: "Gudang Cikaum 1", Quantity: 450},
{ProductName: "Telur Pecah", WarehouseName: "Gudang Cikaum 1", Quantity: 60},
}
for _, seed := range seeds {
var product entity.Product
if err := tx.Where("name = ?", seed.ProductName).First(&product).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("product %q not found for product warehouse seeding", seed.ProductName)
}
return err
}
var warehouse entity.Warehouse
if err := tx.Where("name = ?", seed.WarehouseName).First(&warehouse).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("warehouse %q not found for product warehouse seeding", seed.WarehouseName)
}
return err
}
var productWarehouse entity.ProductWarehouse
err := tx.Where("product_id = ? AND warehouse_id = ?", 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,
// Quantity: 10,
},
{
StockTransferId: transfer.Id,
ProductId: 2,
// Quantity: 5,
},
}
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 { func strPtr(s string) *string {
return &s return &s
} }
func intPtr(v int) *int {
return &v
}
func uintPtr(v uint) *uint {
return &v
}
+17
View File
@@ -0,0 +1,17 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type ConfigChecklist struct {
Id uint `gorm:"primaryKey"`
Date time.Time `gorm:"type:date;not null"`
PercentageThresholdBad int `gorm:"not null"`
PercentageThresholdEnough int `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
+81
View File
@@ -0,0 +1,81 @@
package entities
import "time"
type DailyChecklist struct {
Id uint `gorm:"primaryKey"`
KandangId uint `gorm:"not null"`
ChecklistId *uint
Date time.Time `gorm:"type:date;not null"`
Name *string `gorm:"type:varchar(255)"`
Status *string `gorm:"type:varchar(255)"`
Category string `gorm:"type:category_code;not null"`
TotalScore *int
DocumentPath *string
RejectReason *string
CreatedBy *uint
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
Checklist *Checklist `gorm:"foreignKey:ChecklistId;references:Id"`
Creator *User `gorm:"foreignKey:CreatedBy;references:Id"`
Tasks []DailyChecklistTask `gorm:"foreignKey:DailyChecklistId;references:Id"`
}
type DailyChecklistPhase struct {
Id uint `gorm:"primaryKey"`
ChecklistId uint `gorm:"not null"`
PhaseId uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
Checklist Checklist `gorm:"foreignKey:ChecklistId;references:Id"`
Phase Phases `gorm:"foreignKey:PhaseId;references:Id"`
}
type DailyChecklistActivityTask struct {
Id uint `gorm:"primaryKey"`
ChecklistId uint `gorm:"not null"`
PhaseId uint `gorm:"not null"`
PhaseActivityId uint `gorm:"not null"`
TimeType *string `gorm:"type:text"`
Notes *string `gorm:"type:text"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
Checklist DailyChecklist `gorm:"foreignKey:ChecklistId;references:Id"`
Phase Phases `gorm:"foreignKey:PhaseId;references:Id"`
PhaseActivity PhaseActivity `gorm:"foreignKey:PhaseActivityId;references:Id"`
Assignments []DailyChecklistActivityTaskAssignment `gorm:"foreignKey:TaskId;references:Id"`
}
type DailyChecklistActivityTaskAssignment struct {
Id uint `gorm:"primaryKey"`
TaskId uint `gorm:"not null"`
EmployeeId uint `gorm:"not null"`
Checked bool `gorm:"not null;default:false"`
Note *string `gorm:"type:text"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
Task DailyChecklistActivityTask `gorm:"foreignKey:TaskId;references:Id"`
Employee Employee `gorm:"foreignKey:EmployeeId;references:Id"`
}
type DailyChecklistTask struct {
Id uint `gorm:"primaryKey"`
DailyChecklistId uint `gorm:"not null"`
ChecklistId uint `gorm:"not null"`
ChecklistItemId *uint
IsCompleted bool `gorm:"not null;default:false"`
ScoreValue *int
Notes *string `gorm:"type:text"`
PhotoProof *string
Status *string
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DailyChecklist *DailyChecklist `gorm:"foreignKey:DailyChecklistId;references:Id"`
Checklist Checklist `gorm:"foreignKey:ChecklistId;references:Id"`
ChecklistItem *PhaseActivity `gorm:"foreignKey:ChecklistItemId;references:Id"`
}
+31
View File
@@ -0,0 +1,31 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Employee struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
IsActive bool `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
EmployeeKandangs []EmployeeKandang `gorm:"foreignKey:EmployeeId;references:Id"`
}
type Employees = Employee
type EmployeeKandang struct {
Id uint `gorm:"primaryKey"`
EmployeeId uint `gorm:"not null"`
KandangId uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
Employee Employee `gorm:"foreignKey:EmployeeId;references:Id"`
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
}
+43
View File
@@ -0,0 +1,43 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Phases struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
IsActive bool `gorm:"not null;default:true"`
Category string `gorm:"type:category_code;not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Activities []PhaseActivity `gorm:"foreignKey:PhaseId;references:Id"`
}
type PhaseActivity struct {
Id uint `gorm:"primaryKey"`
PhaseId uint `gorm:"not null"`
Name string `gorm:"not null"`
Description *string `gorm:"type:text"`
TimeType *string `gorm:"type:text"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Phase Phases `gorm:"foreignKey:PhaseId;references:Id"`
}
type Checklist struct {
Id uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
Description *string `gorm:"type:text"`
PhaseId *uint
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Phase *Phases `gorm:"foreignKey:PhaseId;references:Id"`
}
@@ -12,6 +12,7 @@ type ProductionStandardDetail struct {
TargetHenHouseProduction *float64 `gorm:"type:numeric(15,3)"` TargetHenHouseProduction *float64 `gorm:"type:numeric(15,3)"`
TargetEggWeight *float64 `gorm:"type:numeric(15,3)"` TargetEggWeight *float64 `gorm:"type:numeric(15,3)"`
TargetEggMass *float64 `gorm:"type:numeric(15,3)"` TargetEggMass *float64 `gorm:"type:numeric(15,3)"`
StandardFCR *float64 `gorm:"type:numeric(15,3)"`
CreatedAt time.Time `gorm:"type:timestamptz;not null"` CreatedAt time.Time `gorm:"type:timestamptz;not null"`
UpdatedAt time.Time `gorm:"type:timestamptz;not null"` UpdatedAt time.Time `gorm:"type:timestamptz;not null"`
@@ -0,0 +1,26 @@
package entities
import "time"
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"`
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"
}
+13 -3
View File
@@ -13,11 +13,14 @@ type Recording struct {
Day *int `gorm:"column:day"` Day *int `gorm:"column:day"`
TotalDepletionQty *float64 `gorm:"column:total_depletion_qty"` TotalDepletionQty *float64 `gorm:"column:total_depletion_qty"`
CumDepletionRate *float64 `gorm:"column:cum_depletion_rate"` CumDepletionRate *float64 `gorm:"column:cum_depletion_rate"`
DailyGain *float64 `gorm:"column:daily_gain"`
AvgDailyGain *float64 `gorm:"column:avg_daily_gain"`
CumIntake *int `gorm:"column:cum_intake"` CumIntake *int `gorm:"column:cum_intake"`
FcrValue *float64 `gorm:"column:fcr_value"` FcrValue *float64 `gorm:"column:fcr_value"`
TotalChickQty *float64 `gorm:"column:total_chick_qty"` TotalChickQty *float64 `gorm:"column:total_chick_qty"`
HandDay *float64 `gorm:"column:hand_day"`
HandHouse *float64 `gorm:"column:hand_house"`
FeedIntake *float64 `gorm:"column:feed_intake"`
EggMesh *float64 `gorm:"column:egg_mesh"`
EggWeight *float64 `gorm:"column:egg_weight"`
CreatedBy uint `gorm:"column:created_by"` CreatedBy uint `gorm:"column:created_by"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
@@ -25,10 +28,17 @@ type Recording struct {
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
BodyWeights []RecordingBW `gorm:"foreignKey:RecordingId;references:Id"`
Depletions []RecordingDepletion `gorm:"foreignKey:RecordingId;references:Id"` Depletions []RecordingDepletion `gorm:"foreignKey:RecordingId;references:Id"`
Stocks []RecordingStock `gorm:"foreignKey:RecordingId;references:Id"` Stocks []RecordingStock `gorm:"foreignKey:RecordingId;references:Id"`
Eggs []RecordingEgg `gorm:"foreignKey:RecordingId;references:Id"` Eggs []RecordingEgg `gorm:"foreignKey:RecordingId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"-"` LatestApproval *Approval `gorm:"-" json:"-"`
StandardHandDay *float64 `gorm:"-"`
StandardHandHouse *float64 `gorm:"-"`
StandardFeedIntake *float64 `gorm:"-"`
StandardMaxDepletion *float64 `gorm:"-"`
StandardEggMesh *float64 `gorm:"-"`
StandardEggWeight *float64 `gorm:"-"`
StandardFcr *float64 `gorm:"-"`
} }
-15
View File
@@ -1,15 +0,0 @@
package entities
import "time"
type RecordingBW struct {
Id uint `gorm:"primaryKey"`
RecordingId uint `gorm:"column:recording_id;not null;index"`
AvgWeight float64 `gorm:"column:avg_weight;not null"`
Qty float64 `gorm:"column:qty;not null"`
TotalWeight float64 `gorm:"column:total_weight;not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
}
+18
View File
@@ -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"`
}
+8 -6
View File
@@ -44,6 +44,7 @@ const (
P_ReportExpenseGetAll = "lti.repport.expense.list" P_ReportExpenseGetAll = "lti.repport.expense.list"
P_ReportDeliveryGetAll = "lti.repport.delivery.list" P_ReportDeliveryGetAll = "lti.repport.delivery.list"
P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list" P_ReportPurchaseSupplierGetAll = "lti.repport.purchasesupplier.list"
P_ReportHppPerKandangGetAll = "lti.repport.gethppperkandang.list"
) )
const ( const (
@@ -218,12 +219,13 @@ const (
) )
const ( const (
P_FinanceGetAll = "lti.finance.list" P_Uniformities_GetAll = "lti.production.uniformity.list"
P_FinanceGetOne = "lti.finance.detail" P_Uniformities_GetOne = "lti.production.uniformity.detail"
P_FinanceCreateOne = "lti.finance.create" P_Uniformities_Verify = "lti.production.uniformity.verify"
P_FinanceUpdateOne = "lti.finance.update" P_Uniformities_CreateOne = "lti.production.uniformity.create"
P_FinanceDeleteOne = "lti.finance.delete" P_Uniformities_UpdateOne = "lti.production.uniformity.update"
P_FinanceApproval = "lti.finance.approve" P_Uniformities_DeleteOne = "lti.production.uniformity.delete"
P_Uniformities_Approval = "lti.production.uniformity.approve"
) )
const ( const (
@@ -407,7 +407,7 @@ SELECT
COALESCE(fw.name, '') AS source_warehouse, COALESCE(fw.name, '') AS source_warehouse,
COALESCE(tw.name, '') AS destination_warehouse, COALESCE(tw.name, '') AS destination_warehouse,
'' AS destination, '' AS destination,
std.quantity AS quantity, std.usage_qty AS quantity,
u.name AS unit, u.name AS unit,
'Stock Refill' AS notes 'Stock Refill' AS notes
FROM stock_transfer_details std FROM stock_transfer_details std
@@ -456,7 +456,7 @@ SELECT
COALESCE(fw.name, '') AS source_warehouse, COALESCE(fw.name, '') AS source_warehouse,
'' AS destination_warehouse, '' AS destination_warehouse,
COALESCE(tw.name, '') AS destination, COALESCE(tw.name, '') AS destination,
std.quantity AS quantity, std.usage_qty AS quantity,
u.name AS unit, u.name AS unit,
'Transfer to other unit' AS notes 'Transfer to other unit' AS notes
FROM stock_transfer_details std FROM stock_transfer_details std
@@ -927,34 +927,34 @@ func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.C
COALESCE(SUM( COALESCE(SUM(
CASE CASE
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0)
WHEN sa.stockable_type = ? THEN COALESCE(std.quantity, 0) WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0)
ELSE 0 ELSE 0
END END
), 0) AS total_qty, ), 0) AS total_qty,
COALESCE(SUM( COALESCE(SUM(
CASE CASE
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0) WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0) WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0) * COALESCE(tpi.price, 0)
ELSE 0 ELSE 0
END END
), 0) AS total_price, ), 0) AS total_price,
COALESCE(SUM( COALESCE(SUM(
CASE CASE
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0)
WHEN sa.stockable_type = ? THEN COALESCE(std.quantity, 0) WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0)
ELSE 0 ELSE 0
END END
), 0) AS qty_divisor, ), 0) AS qty_divisor,
COALESCE(SUM( COALESCE(SUM(
CASE CASE
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0) WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) * COALESCE(pi.price, 0)
WHEN sa.stockable_type = ? THEN COALESCE(std.quantity, 0) * COALESCE(tpi.price, 0) WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0) * COALESCE(tpi.price, 0)
ELSE 0 ELSE 0
END END
), 0) / NULLIF(COALESCE(SUM( ), 0) / NULLIF(COALESCE(SUM(
CASE CASE
WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0) WHEN sa.stockable_type = ? THEN COALESCE(sa.qty, 0)
WHEN sa.stockable_type = ? THEN COALESCE(std.quantity, 0) WHEN sa.stockable_type = ? THEN COALESCE(std.usage_qty, 0)
ELSE 0 ELSE 0
END END
), 0), 0) AS average_price`, ), 0), 0) AS average_price`,
+1 -1
View File
@@ -30,6 +30,6 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
route.Get("/:projectFlockId/sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronak) route.Get("/:projectFlockId/sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSapronak)
route.Get("/:project_flock_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPP) route.Get("/:project_flock_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPP)
route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPPByKandang) route.Get("/:project_flock_id/:project_flock_kandang_id/expedition-hpp", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetExpeditionHPPByKandang)
route.Get("/:projectFlockId/data-produksi", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingDataProduksi) route.Get("/:projectFlockId/production-data", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingDataProduksi)
route.Get("/:projectFlockId/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuangan) route.Get("/:projectFlockId/keuangan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingKeuangan)
} }
@@ -0,0 +1,524 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/validations"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type DailyChecklistController struct {
DailyChecklistService service.DailyChecklistService
}
func NewDailyChecklistController(dailyChecklistService service.DailyChecklistService) *DailyChecklistController {
return &DailyChecklistController{
DailyChecklistService: dailyChecklistService,
}
}
func (u *DailyChecklistController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
query.DateFrom = c.Query("date_from", "")
query.DateTo = c.Query("date_to", "")
query.Status = c.Query("status", "")
if kandangParam := c.Query("kandang_id", ""); kandangParam != "" {
kandangID, err := strconv.ParseUint(kandangParam, 10, 64)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
}
value := uint(kandangID)
query.KandangID = &value
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.DailyChecklistService.GetAll(c, query)
if err != nil {
return err
}
responseData := make([]dto.DailyChecklistListDTO, len(result))
for i, item := range result {
var name string
if item.Name != nil {
name = *item.Name
}
var status string
if item.Status != nil {
status = *item.Status
}
var kandang *kandangDTO.KandangRelationDTO
if item.Kandang.Id != 0 {
mapped := kandangDTO.ToKandangRelationDTO(item.Kandang)
kandang = &mapped
}
responseData[i] = dto.DailyChecklistListDTO{
Id: item.ID,
Name: name,
Status: status,
Category: item.Category,
Date: item.Date,
Kandang: kandang,
CreatedUser: nil,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
TotalPhase: item.TotalPhase,
TotalActivity: item.TotalActivity,
Progress: item.Progress,
}
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.DailyChecklistListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all dailyChecklists successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: responseData,
})
}
func (u *DailyChecklistController) GetSummary(c *fiber.Ctx) error {
query := &validation.SummaryQuery{
DateFrom: c.Query("date_from"),
DateTo: c.Query("date_to"),
Category: c.Query("category"),
}
if query.DateFrom == "" || query.DateTo == "" {
return fiber.NewError(fiber.StatusBadRequest, "date_from and date_to are required")
}
if kandangParam := c.Query("kandang_id"); kandangParam != "" {
kandangID, err := strconv.ParseUint(kandangParam, 10, 64)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
}
value := uint(kandangID)
query.KandangID = &value
}
result, err := u.DailyChecklistService.GetSummary(c, query)
if err != nil {
return err
}
type summaryResponse struct {
PerformanceOverview []dto.DailyChecklistPerformanceOverviewDTO `json:"performance_overview"`
TrackingABK []dto.DailyChecklistSummaryDTO `json:"tracking_abk"`
}
performanceMap := make(map[uint]*dto.DailyChecklistPerformanceOverviewDTO)
tracking := make([]dto.DailyChecklistSummaryDTO, len(result))
for i, summary := range result {
tracking[i] = dto.DailyChecklistSummaryDTO{
EmployeeID: summary.EmployeeID,
EmployeeName: summary.EmployeeName,
KandangID: summary.KandangID,
KandangName: summary.KandangName,
TotalActivity: summary.TotalActivity,
ActivityDone: summary.ActivityDone,
ActivityLeft: summary.ActivityLeft,
CompletionRate: summary.CompletionRate,
LastActivity: summary.LastActivity,
}
if _, ok := performanceMap[summary.EmployeeID]; !ok {
performanceMap[summary.EmployeeID] = &dto.DailyChecklistPerformanceOverviewDTO{
EmployeeID: summary.EmployeeID,
EmployeeName: summary.EmployeeName,
}
}
performanceMap[summary.EmployeeID].TotalActivity += summary.TotalActivity
performanceMap[summary.EmployeeID].ActivityDone += summary.ActivityDone
performanceMap[summary.EmployeeID].ActivityLeft += summary.ActivityLeft
}
performance := make([]dto.DailyChecklistPerformanceOverviewDTO, 0, len(performanceMap))
for _, v := range performanceMap {
performance = append(performance, *v)
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get daily checklist summary successfully",
Data: summaryResponse{
PerformanceOverview: performance,
TrackingABK: tracking,
},
})
}
func (u *DailyChecklistController) GetReport(c *fiber.Ctx) error {
query := &validation.ReportQuery{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Month: c.QueryInt("bulan", 0),
Year: c.QueryInt("tahun", 0),
}
parseUintParam := func(param string) (*uint, error) {
if param == "" {
return nil, nil
}
value, err := strconv.ParseUint(param, 10, 64)
if err != nil {
return nil, err
}
u := uint(value)
return &u, nil
}
if val, err := parseUintParam(c.Query("area_id", "")); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid area_id")
} else {
query.AreaID = val
}
if val, err := parseUintParam(c.Query("location_id", "")); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid location_id")
} else {
query.LocationID = val
}
if val, err := parseUintParam(c.Query("kandang_id", "")); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
} else {
query.KandangID = val
}
if val, err := parseUintParam(c.Query("employee_id", "")); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid employee_id")
} else {
query.EmployeeID = val
}
if val, err := parseUintParam(c.Query("phase_id", "")); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid phase_id")
} else {
query.PhaseID = val
}
if query.Month == 0 || query.Year == 0 {
return fiber.NewError(fiber.StatusBadRequest, "bulan and tahun are required")
}
result, totalResults, err := u.DailyChecklistService.GetReport(c, query)
withoutActivities := func(src map[string]int) map[string]int {
if src == nil {
return map[string]int{}
}
return src
}
if err != nil {
return err
}
responseData := make([]dto.DailyChecklistReportDTO, len(result))
for i, item := range result {
responseData[i] = dto.DailyChecklistReportDTO{
Area: dto.DailyChecklistReportEntityDTO{
Id: item.AreaID,
Name: item.AreaName,
},
Farm: dto.DailyChecklistReportEntityDTO{
Id: item.LocationID,
Name: item.LocationName,
},
Kandang: dto.DailyChecklistReportEntityDTO{
Id: item.KandangID,
Name: item.KandangName,
},
ABK: dto.DailyChecklistReportEntityDTO{
Id: item.EmployeeID,
Name: item.EmployeeName,
},
Phase: item.PhaseName,
DailyActivities: withoutActivities(item.DailyActivities),
Summary: dto.DailyChecklistReportSummaryDTO{
TotalChecklist: item.Summary.TotalChecklist,
JumlahHariEfektif: item.Summary.JumlahHariEfektif,
AbkPercentage: item.Summary.AbkPercentage,
KandangPercentage: item.Summary.KandangPercentage,
Kategori: dto.DailyChecklistReportCategoryDTO{
Kurang: item.Summary.Category.Kurang,
Cukup: item.Summary.Category.Cukup,
Baik: item.Summary.Category.Baik,
},
},
}
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.DailyChecklistReportDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get daily checklist report successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: responseData,
})
}
func (u *DailyChecklistController) GetOne(c *fiber.Ctx) error {
param := c.Params("idDailyChecklist")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
detail, err := u.DailyChecklistService.GetDetail(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get dailyChecklist successfully",
Data: dto.ToDailyChecklistDetailDTO(detail.Checklist, detail.Phases, detail.Tasks, detail.AssignedEmployees, detail.TotalActivities, detail.Progress),
})
}
func (u *DailyChecklistController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.DailyChecklistService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create dailyChecklist successfully",
Data: dto.ToDailyChecklistListDTO(*result),
})
}
func (u *DailyChecklistController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("idDailyChecklist")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.DailyChecklistService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update dailyChecklist successfully",
Data: dto.ToDailyChecklistListDTO(*result),
})
}
func (u *DailyChecklistController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("idDailyChecklist")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.DailyChecklistService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete dailyChecklist successfully",
})
}
func (u *DailyChecklistController) CreateDailyChecklistPhase(c *fiber.Ctx) error {
param := c.Params("idDailyChecklist")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid daily checklist id")
}
req := new(validation.AssignPhases)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
if err := u.DailyChecklistService.AssignPhases(c, uint(id), req); err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Daily checklist phases saved successfully",
})
}
func (u *DailyChecklistController) CreateAssignment(c *fiber.Ctx) error {
param := c.Params("idDailyChecklist")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid daily checklist id")
}
req := new(validation.AssignTask)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
if err := u.DailyChecklistService.AssignTasks(c, uint(id), req); err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Daily checklist assignments saved successfully",
})
}
func (u *DailyChecklistController) RemoveAssignment(c *fiber.Ctx) error {
dailyChecklistParam := c.Params("idDailyChecklist")
employeeParam := c.Params("idEmployee")
dailyChecklistID, err := strconv.Atoi(dailyChecklistParam)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid daily checklist id")
}
employeeID, err := strconv.Atoi(employeeParam)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid employee id")
}
if err := u.DailyChecklistService.RemoveAssignment(c, uint(dailyChecklistID), uint(employeeID)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Assignment removed successfully",
})
}
func (u *DailyChecklistController) GetPhaseByIdChecklist(c *fiber.Ctx) error {
param := c.Params("idDailyChecklist")
id, err := strconv.Atoi(param)
if err != nil || id <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid daily checklist id")
}
phaseIDs, err := u.DailyChecklistService.GetChecklistPhaseIDs(c, uint(id))
if err != nil {
return err
}
responseData := make([]map[string]uint, len(phaseIDs))
for i, phaseID := range phaseIDs {
responseData[i] = map[string]uint{"phase_id": phaseID}
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get phases successfully",
Data: responseData,
})
}
func (u *DailyChecklistController) GetAllTasks(c *fiber.Ctx) error {
checklistParam := c.Query("checklist_id", "")
if checklistParam == "" {
return fiber.NewError(fiber.StatusBadRequest, "checklist_id is required")
}
checklistID, err := strconv.Atoi(checklistParam)
if err != nil || checklistID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid checklist_id")
}
result, err := u.DailyChecklistService.GetTasks(c, uint(checklistID))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get daily checklist tasks successfully",
Data: result,
})
}
func (u *DailyChecklistController) UpdateAssignment(c *fiber.Ctx) error {
req := new(validation.UpdateAssignment)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
if err := u.DailyChecklistService.UpdateAssignment(c, req); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Assignment updated successfully",
})
}
@@ -0,0 +1,232 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
employeeDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
phaseActivityDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/dto"
phasesDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type DailyChecklistRelationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type DailyChecklistListDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
Category string `json:"category"`
Date time.Time `json:"date"`
Kandang *kandangDTO.KandangRelationDTO `json:"kandang,omitempty"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
TotalPhase int `json:"total_phase"`
TotalActivity int `json:"total_activity"`
Progress int `json:"progress"`
}
type DailyChecklistDetailDTO struct {
DailyChecklistListDTO
Phases []DailyChecklistPhaseDTO `json:"phases"`
Tasks []DailyChecklistActivityTaskDTO `json:"tasks"`
AssignedEmployees []employeeDTO.EmployeesRelationDTO `json:"assigned_employees"`
TotalActivity int `json:"total_activity"`
Progress float64 `json:"progress"`
}
type DailyChecklistSummaryDTO struct {
EmployeeID uint `json:"employee_id"`
EmployeeName string `json:"employee_name"`
KandangID uint `json:"kandang_id"`
KandangName string `json:"kandang_name"`
TotalActivity int `json:"total_activity"`
ActivityDone int `json:"activity_done"`
ActivityLeft int `json:"activity_left"`
CompletionRate int `json:"completion_rate"`
LastActivity *time.Time `json:"last_activity,omitempty"`
}
type DailyChecklistPerformanceOverviewDTO struct {
EmployeeID uint `json:"employee_id"`
EmployeeName string `json:"employee_name"`
TotalActivity int `json:"total_activity"`
ActivityDone int `json:"activity_done"`
ActivityLeft int `json:"activity_left"`
}
type DailyChecklistReportDTO struct {
Area DailyChecklistReportEntityDTO `json:"area"`
Farm DailyChecklistReportEntityDTO `json:"farm"`
Kandang DailyChecklistReportEntityDTO `json:"kandang"`
ABK DailyChecklistReportEntityDTO `json:"abk"`
Phase string `json:"phase"`
DailyActivities map[string]int `json:"daily_activities"`
Summary DailyChecklistReportSummaryDTO `json:"summary"`
}
type DailyChecklistReportEntityDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type DailyChecklistReportSummaryDTO struct {
TotalChecklist int `json:"total_checklist"`
JumlahHariEfektif int `json:"jumlah_hari_efektif"`
AbkPercentage int `json:"abk_percentage"`
KandangPercentage int `json:"kandang_percentage"`
Kategori DailyChecklistReportCategoryDTO `json:"kategori"`
}
type DailyChecklistReportCategoryDTO struct {
Kurang int `json:"kurang"`
Cukup int `json:"cukup"`
Baik int `json:"baik"`
}
type DailyChecklistPhaseDTO struct {
Id uint `json:"id"`
PhaseId uint `json:"phase_id"`
Phase phasesDTO.PhasesListDTO `json:"phase"`
}
type DailyChecklistActivityTaskDTO struct {
Id uint `json:"id"`
ChecklistId uint `json:"checklist_id"`
PhaseId uint `json:"phase_id"`
PhaseActivityId uint `json:"phase_activity_id"`
TimeType *string `json:"time_type"`
Notes *string `json:"notes"`
Phase phasesDTO.PhasesListDTO `json:"phase"`
PhaseActivity phaseActivityDTO.PhaseActivityListDTO `json:"phase_activity"`
Assignments []DailyChecklistAssignmentDTO `json:"assignments"`
}
type DailyChecklistAssignmentDTO struct {
Employee employeeDTO.EmployeesRelationDTO `json:"employee"`
Checked bool `json:"checked"`
Note *string `json:"note"`
}
// === Mapper Functions ===
func ToDailyChecklistRelationDTO(e entity.DailyChecklist) DailyChecklistRelationDTO {
var name string
if e.Name != nil {
name = *e.Name
}
return DailyChecklistRelationDTO{
Id: e.Id,
Name: name,
}
}
func ToDailyChecklistListDTO(e entity.DailyChecklist) DailyChecklistListDTO {
var createdUser *userDTO.UserRelationDTO
// if e.CreatedUser.Id != 0 {
// mapped := userDTO.ToUserRelationDTO(e.CreatedUser)
// createdUser = &mapped
// }
var name string
if e.Name != nil {
name = *e.Name
}
var status string
if e.Status != nil {
status = *e.Status
}
var kandang *kandangDTO.KandangRelationDTO
if e.Kandang.Id != 0 {
mapped := kandangDTO.ToKandangRelationDTO(e.Kandang)
kandang = &mapped
}
return DailyChecklistListDTO{
Id: e.Id,
Name: name,
Status: status,
Category: e.Category,
Date: e.Date,
Kandang: kandang,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
TotalPhase: 0,
TotalActivity: 0,
Progress: 0,
}
}
func ToDailyChecklistDetailDTO(checklist entity.DailyChecklist, phases []entity.DailyChecklistPhase, tasks []entity.DailyChecklistActivityTask, assignedEmployees []entity.Employee, totalActivities int, progress float64) DailyChecklistDetailDTO {
phaseDTOs := make([]DailyChecklistPhaseDTO, 0, len(phases))
for _, phase := range phases {
phaseDTOs = append(phaseDTOs, DailyChecklistPhaseDTO{
Id: phase.Id,
PhaseId: phase.PhaseId,
Phase: phasesDTO.ToPhasesListDTO(phase.Phase),
})
}
taskDTOs := make([]DailyChecklistActivityTaskDTO, 0, len(tasks))
for _, task := range tasks {
mappedAssignments := make([]DailyChecklistAssignmentDTO, 0, len(task.Assignments))
for _, assignment := range task.Assignments {
if assignment.Employee.Id == 0 {
continue
}
mapped := DailyChecklistAssignmentDTO{
Employee: employeeDTO.ToEmployeesRelationDTO(assignment.Employee),
Checked: assignment.Checked,
Note: assignment.Note,
}
mappedAssignments = append(mappedAssignments, mapped)
}
phaseDTO := phasesDTO.PhasesListDTO{}
if task.Phase.Id != 0 {
phaseDTO = phasesDTO.ToPhasesListDTO(task.Phase)
}
activityDTO := phaseActivityDTO.PhaseActivityListDTO{}
if task.PhaseActivity.Id != 0 {
activityDTO = phaseActivityDTO.ToPhaseActivityListDTO(task.PhaseActivity)
}
taskDTOs = append(taskDTOs, DailyChecklistActivityTaskDTO{
Id: task.Id,
ChecklistId: task.ChecklistId,
PhaseId: task.PhaseId,
PhaseActivityId: task.PhaseActivityId,
TimeType: task.TimeType,
Notes: task.Notes,
Phase: phaseDTO,
PhaseActivity: activityDTO,
Assignments: mappedAssignments,
})
}
assignedDTOs := make([]employeeDTO.EmployeesRelationDTO, 0, len(assignedEmployees))
for _, emp := range assignedEmployees {
assignedDTOs = append(assignedDTOs, employeeDTO.ToEmployeesRelationDTO(emp))
}
return DailyChecklistDetailDTO{
DailyChecklistListDTO: ToDailyChecklistListDTO(checklist),
Phases: phaseDTOs,
Tasks: taskDTOs,
AssignedEmployees: assignedDTOs,
TotalActivity: totalActivities,
Progress: progress,
}
}
@@ -0,0 +1,27 @@
package dailyChecklists
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rDailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/repositories"
sDailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
rPhases "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type DailyChecklistModule struct{}
func (DailyChecklistModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
dailyChecklistRepo := rDailyChecklist.NewDailyChecklistRepository(db)
phasesRepo := rPhases.NewPhasesRepository(db)
userRepo := rUser.NewUserRepository(db)
dailyChecklistService := sDailyChecklist.NewDailyChecklistService(dailyChecklistRepo, phasesRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
DailyChecklistRoutes(router, userService, dailyChecklistService)
}
@@ -0,0 +1,21 @@
package repository
import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
"gorm.io/gorm"
)
type DailyChecklistRepository interface {
repository.BaseRepository[entity.DailyChecklist]
}
type DailyChecklistRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.DailyChecklist]
}
func NewDailyChecklistRepository(db *gorm.DB) DailyChecklistRepository {
return &DailyChecklistRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.DailyChecklist](db),
}
}
@@ -0,0 +1,63 @@
package dailyChecklists
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/controllers"
dailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func DailyChecklistRoutes(v1 fiber.Router, u user.UserService, s dailyChecklist.DailyChecklistService) {
ctrl := controller.NewDailyChecklistController(s)
route := v1.Group("/daily-checklists")
// route.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Get("/report", ctrl.GetReport)
route.Get("/summary", ctrl.GetSummary)
route.Get("/report", ctrl.GetReport)
// create daily checklist
route.Post("/", ctrl.CreateOne)
// get detail data daily checklist by id
route.Get("/relation/:idDailyChecklist", ctrl.GetOne)
// get phases by daily checklist id
route.Get("/phase/:idDailyChecklist", ctrl.GetPhaseByIdChecklist)
// create task
/*
ketika add phase
*/
route.Post("/phase/:idDailyChecklist", ctrl.CreateDailyChecklistPhase)
// create assigment
/*
ketika add ABK
*/
route.Post("/assignment/:idDailyChecklist", ctrl.CreateAssignment)
// remove assignment
/*
ketika remove ABK
*/
route.Delete("/:idDailyChecklist/assignments/:idEmployee", ctrl.RemoveAssignment)
//get all tasks
route.Get("/tasks", ctrl.GetAllTasks)
// update assignment
/*
ketika check dan uncheck tugas oleh ABK
*/
route.Post("/assignment", ctrl.UpdateAssignment)
route.Patch("/:idDailyChecklist", ctrl.UpdateOne)
route.Delete("/:idDailyChecklist", ctrl.DeleteOne)
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,57 @@
package validation
type Create struct {
Date string `json:"date" validate:"required"`
KandangId uint `json:"kandang_id" validate:"required"`
Category string `json:"category" validate:"required"`
Status string `json:"status" validate:"required"`
}
type Update struct {
Status string `json:"status" validate:"required"`
RejectReason *string `json:"reject_reason"`
}
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"`
Search string `query:"search" validate:"omitempty,max=50"`
DateFrom string `query:"date_from" validate:"omitempty"`
DateTo string `query:"date_to" validate:"omitempty"`
Status string `query:"status" validate:"omitempty"`
KandangID *uint `query:"kandang_id" validate:"omitempty"`
}
type AssignPhases struct {
PhaseIDs string `json:"phase_ids" validate:"required"`
}
type AssignTask struct {
EmployeeIDs string `json:"employee_ids" validate:"required"`
}
type UpdateAssignment struct {
TaskID uint `json:"task_id" validate:"required"`
EmployeeID uint `json:"employee_id" validate:"required"`
Checked *bool `json:"checked,omitempty"`
Note *string `json:"note,omitempty"`
}
type SummaryQuery struct {
DateFrom string `query:"date_from" validate:"required"`
DateTo string `query:"date_to" validate:"required"`
Category string `query:"category" validate:"omitempty"`
KandangID *uint `query:"kandang_id" validate:"omitempty"`
}
type ReportQuery struct {
Page int `query:"page" validate:"required,number,min=1,gt=0"`
Limit int `query:"limit" validate:"required,number,min=1,max=100,gt=0"`
Month int `query:"bulan" validate:"required,number,min=1,max=12"`
Year int `query:"tahun" validate:"required,number,min=1900"`
AreaID *uint `query:"area_id" validate:"omitempty"`
LocationID *uint `query:"location_id" validate:"omitempty"`
KandangID *uint `query:"kandang_id" validate:"omitempty"`
EmployeeID *uint `query:"employee_id" validate:"omitempty"`
PhaseID *uint `query:"phase_id" validate:"omitempty"`
}
@@ -203,12 +203,12 @@ func (u *ExpenseController) UpdateOne(c *fiber.Ctx) error {
func (u *ExpenseController) DeleteOne(c *fiber.Ctx) error { func (u *ExpenseController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id") param := c.Params("id")
id, err := strconv.Atoi(param) id64, err := strconv.ParseUint(param, 10, 64)
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
} }
if err := u.ExpenseService.DeleteOne(c, uint(id)); err != nil { if err := u.ExpenseService.DeleteOne(c, id64); err != nil {
return err return err
} }
+3 -5
View File
@@ -105,11 +105,9 @@ func ToExpenseBaseDTO(e *entity.Expense) ExpenseBaseDTO {
realizationDate = &e.RealizationDate realizationDate = &e.RealizationDate
} }
if len(e.Nonstocks) > 0 && e.Nonstocks[0].Kandang != nil { if e.Location != nil && e.Location.Id != 0 {
if e.Nonstocks[0].Kandang.Location.Id != 0 { mapped := locationDTO.ToLocationRelationDTO(*e.Location)
mapped := locationDTO.ToLocationRelationDTO(e.Nonstocks[0].Kandang.Location) location = &mapped
location = &mapped
}
} }
if e.Supplier != nil && e.Supplier.Id != 0 { if e.Supplier != nil && e.Supplier.Id != 0 {
@@ -3,6 +3,8 @@ package repository
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"time"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository" "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
@@ -17,6 +19,7 @@ type ExpenseRepository interface {
GetWithSupplier(ctx context.Context, id uint64) (*entity.Expense, error) GetWithSupplier(ctx context.Context, id uint64) (*entity.Expense, error)
WithProjectFlockKandangFilter(pfkID, kandangID uint) func(*gorm.DB) *gorm.DB WithProjectFlockKandangFilter(pfkID, kandangID uint) func(*gorm.DB) *gorm.DB
CountUnfinishedByProjectFlockKandang(ctx context.Context, pfkID, kandangID uint, isFinished func(*entity.Approval) bool) (int64, error) CountUnfinishedByProjectFlockKandang(ctx context.Context, pfkID, kandangID uint, isFinished func(*entity.Approval) bool) (int64, error)
DeleteOne(ctx context.Context, id uint) error
} }
type ExpenseRepositoryImpl struct { type ExpenseRepositoryImpl struct {
@@ -107,3 +110,23 @@ func (r *ExpenseRepositoryImpl) CountUnfinishedByProjectFlockKandang(ctx context
} }
return unfinished, nil return unfinished, nil
} }
func (r *ExpenseRepositoryImpl) DeleteOne(ctx context.Context, id uint) error {
// Cast to uint64 to match entity.Id type
id64 := uint64(id)
deletedAt := time.Now()
// Use raw SQL with interpolated integer to avoid type issues
// Interpolate id directly as integer literal (safe because it's uint64)
result := r.DB().WithContext(ctx).
Exec(`UPDATE "expenses" SET "deleted_at" = $1 WHERE "id" = `+fmt.Sprintf("%d", id64)+` AND "deleted_at" IS NULL`,
deletedAt)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return gorm.ErrRecordNotFound
}
return nil
}
+12 -12
View File
@@ -22,16 +22,16 @@ func ExpenseRoutes(v1 fiber.Router, u user.UserService, s expense.ExpenseService
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/",m.RequirePermissions(m.P_ExpenseGetAll), ctrl.GetAll) route.Get("/", m.RequirePermissions(m.P_ExpenseGetAll), ctrl.GetAll)
route.Post("/",m.RequirePermissions(m.P_ExpenseCreateOne), ctrl.CreateOne) route.Post("/", m.RequirePermissions(m.P_ExpenseCreateOne), ctrl.CreateOne)
route.Get("/:id",m.RequirePermissions(m.P_ExpenseGetOne), ctrl.GetOne) route.Get("/:id", m.RequirePermissions(m.P_ExpenseGetOne), ctrl.GetOne)
route.Patch("/:id",m.RequirePermissions(m.P_ExpenseUpdateOne), ctrl.UpdateOne) route.Patch("/:id", m.RequirePermissions(m.P_ExpenseUpdateOne), ctrl.UpdateOne)
route.Delete("/:id",m.RequirePermissions(m.P_ExpenseDeleteOne), ctrl.DeleteOne) route.Delete("/:id", m.RequirePermissions(m.P_ExpenseDeleteOne), ctrl.DeleteOne)
route.Post("/approvals/manager",m.RequirePermissions(m.P_ExpenseApprovalManager), ctrl.Approval) route.Post("/approvals/manager", m.RequirePermissions(m.P_ExpenseApprovalManager), ctrl.Approval)
route.Post("/approvals/finance",m.RequirePermissions(m.P_ExpenseApprovalFinance), ctrl.Approval) route.Post("/approvals/finance", m.RequirePermissions(m.P_ExpenseApprovalFinance), ctrl.Approval)
route.Post("/:id/realizations",m.RequirePermissions(m.P_ExpenseCreateRealizations), ctrl.CreateRealization) route.Post("/:id/realizations", m.RequirePermissions(m.P_ExpenseCreateRealizations), ctrl.CreateRealization)
route.Patch("/:id/realizations",m.RequirePermissions(m.P_ExpenseUpdateRealizations), ctrl.UpdateRealization) route.Patch("/:id/realizations", m.RequirePermissions(m.P_ExpenseUpdateRealizations), ctrl.UpdateRealization)
route.Post("/:id/complete",m.RequirePermissions(m.P_ExpenseCompleteExpense), ctrl.CompleteExpense) route.Post("/:id/complete", m.RequirePermissions(m.P_ExpenseCompleteExpense), ctrl.CompleteExpense)
route.Delete("/:id/documents/:documentId",m.RequirePermissions(m.P_ExpenseDocument), ctrl.DeleteDocument) route.Delete("/:id/documents/:documentId", m.RequirePermissions(m.P_ExpenseDocument), ctrl.DeleteDocument)
route.Delete("/:id/realization-documents/:documentId",m.RequirePermissions(m.P_ExpenseDocumentRealizations), ctrl.DeleteRealizationDocument) route.Delete("/:id/realization-documents/:documentId", m.RequirePermissions(m.P_ExpenseDocumentRealizations), ctrl.DeleteRealizationDocument)
} }
@@ -30,7 +30,7 @@ type ExpenseService interface {
GetOne(ctx *fiber.Ctx, id uint) (*expenseDto.ExpenseDetailDTO, error) GetOne(ctx *fiber.Ctx, id uint) (*expenseDto.ExpenseDetailDTO, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*expenseDto.ExpenseDetailDTO, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*expenseDto.ExpenseDetailDTO, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*expenseDto.ExpenseDetailDTO, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*expenseDto.ExpenseDetailDTO, error)
DeleteOne(ctx *fiber.Ctx, id uint) error DeleteOne(ctx *fiber.Ctx, id uint64) error
CreateRealization(ctx *fiber.Ctx, expenseID uint, req *validation.CreateRealization) (*expenseDto.ExpenseDetailDTO, error) CreateRealization(ctx *fiber.Ctx, expenseID uint, req *validation.CreateRealization) (*expenseDto.ExpenseDetailDTO, error)
CompleteExpense(ctx *fiber.Ctx, id uint, notes *string) (*expenseDto.ExpenseDetailDTO, error) CompleteExpense(ctx *fiber.Ctx, id uint, notes *string) (*expenseDto.ExpenseDetailDTO, error)
UpdateRealization(ctx *fiber.Ctx, expenseID uint, req *validation.UpdateRealization) (*expenseDto.ExpenseDetailDTO, error) UpdateRealization(ctx *fiber.Ctx, expenseID uint, req *validation.UpdateRealization) (*expenseDto.ExpenseDetailDTO, error)
@@ -68,6 +68,7 @@ func (s expenseService) withRelations(db *gorm.DB) *gorm.DB {
return db. return db.
Preload("CreatedUser"). Preload("CreatedUser").
Preload("Supplier"). Preload("Supplier").
Preload("Location").
Preload("Nonstocks.Nonstock"). Preload("Nonstocks.Nonstock").
Preload("Nonstocks.Realization"). Preload("Nonstocks.Realization").
Preload("Nonstocks.ProjectFlockKandang.Kandang.Location"). Preload("Nonstocks.ProjectFlockKandang.Kandang.Location").
@@ -214,21 +215,19 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get active project flocks for location") return fiber.NewError(fiber.StatusInternalServerError, "Failed to get active project flocks for location")
} }
if len(activeProjectFlocks) == 0 { if len(activeProjectFlocks) > 0 {
return fiber.NewError(fiber.StatusBadRequest, "No active project flocks found for this location") projectFlockIDs := make([]uint64, len(activeProjectFlocks))
} for i, pf := range activeProjectFlocks {
projectFlockIDs[i] = uint64(pf.Id)
}
projectFlockIDs := make([]uint64, len(activeProjectFlocks)) projectFlockIdsJSON, err := json.Marshal(projectFlockIDs)
for i, pf := range activeProjectFlocks { if err != nil {
projectFlockIDs[i] = uint64(pf.Id) return fiber.NewError(fiber.StatusInternalServerError, "Failed to marshal project_flock_ids")
}
jsonStr := string(projectFlockIdsJSON)
projectFlockIdJSON = &jsonStr
} }
projectFlockIdsJSON, err := json.Marshal(projectFlockIDs)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to marshal project_flock_ids")
}
jsonStr := string(projectFlockIdsJSON)
projectFlockIdJSON = &jsonStr
} }
expense = &entity.Expense{ expense = &entity.Expense{
@@ -621,14 +620,15 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
return responseDTO, nil return responseDTO, nil
} }
func (s expenseService) DeleteOne(c *fiber.Ctx, id uint) error { func (s expenseService) DeleteOne(c *fiber.Ctx, id uint64) error {
idUint := uint(id)
if err := commonSvc.EnsureRelations(c.Context(), if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: s.Repository.IdExists}, commonSvc.RelationCheck{Name: "Expense", ID: &idUint, Exists: s.Repository.IdExists},
); err != nil { ); err != nil {
return err return err
} }
expense, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { expense, err := s.Repository.GetByID(c.Context(), idUint, func(db *gorm.DB) *gorm.DB {
return db.Preload("Nonstocks") return db.Preload("Nonstocks")
}) })
if err != nil { if err != nil {
@@ -643,7 +643,7 @@ func (s expenseService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil { if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil {
return err return err
} }
if err := s.Repository.DeleteOne(c.Context(), id); err != nil { if err := s.Repository.DeleteOne(c.Context(), idUint); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Expense not found for ID %d: %+v", id, err) s.Log.Errorf("Expense not found for ID %d: %+v", id, err)
return fiber.NewError(fiber.StatusNotFound, "Expense not found") return fiber.NewError(fiber.StatusNotFound, "Expense not found")
@@ -117,39 +117,36 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
var createdLogId uint var createdLogId uint
isProductWarehouseExist, err := s.ProductWarehouseRepo.ProductWarehouseExistByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID)) var projectFlockKandangID *uint
if err != nil { pfkID, err := s.getActiveProjectFlockKandangID(ctx, uint(req.WarehouseID))
s.Log.Errorf("Failed to check product warehouse existence: %+v", err) if err == nil && pfkID > 0 {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse") projectFlockKandangID = &pfkID
} }
if !isProductWarehouseExist {
projectFlockKandangID, err := s.getActiveProjectFlockKandangID(ctx, uint(req.WarehouseID)) pw, err := s.ProductWarehouseRepo.FindByProductWarehouseAndPfk(
if err != nil { ctx,
return nil, err uint(req.ProductID),
uint(req.WarehouseID),
projectFlockKandangID,
)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to find product warehouse: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
} }
newPW := &entity.ProductWarehouse{ newPW := &entity.ProductWarehouse{
ProductId: uint(req.ProductID), ProductId: uint(req.ProductID),
WarehouseId: uint(req.WarehouseID), WarehouseId: uint(req.WarehouseID),
Quantity: 0, Quantity: 0,
ProjectFlockKandangId: &projectFlockKandangID, ProjectFlockKandangId: projectFlockKandangID,
// CreatedBy: 1, // TODO: should Get from auth middleware
} }
if err := s.ProductWarehouseRepo.CreateOne(ctx, newPW, nil); err != nil { if err := s.ProductWarehouseRepo.CreateOne(ctx, newPW, nil); err != nil {
s.Log.Errorf("Failed to create product warehouse: %+v", err) s.Log.Errorf("Failed to create product warehouse: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create product warehouse") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create product warehouse")
} }
s.Log.Infof("Product warehouse created: %+v", newPW.Id) pw = newPW
}
pw, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
ctx,
uint(req.ProductID),
uint(req.WarehouseID),
)
if err != nil {
s.Log.Errorf("Failed to get product warehouse for project flock check: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse")
} }
if err := common.EnsureProjectFlockNotClosedForProductWarehouses( if err := common.EnsureProjectFlockNotClosedForProductWarehouses(
@@ -197,13 +194,17 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
StockLogId: newLog.Id, StockLogId: newLog.Id,
ProductWarehouseId: productWarehouse.Id, ProductWarehouseId: productWarehouse.Id,
} }
if err := s.AdjustmentStockRepository.WithTx(tx).CreateOne(ctx, adjustmentStock, nil); err != nil {
s.Log.Errorf("Failed to create adjustment stock: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create adjustment stock record")
}
if transactionType == string(utils.StockLogTransactionTypeIncrease) { if transactionType == string(utils.StockLogTransactionTypeIncrease) {
// Adjustment INCREASE → Replenish stock (Stockable) // Adjustment INCREASE → Replenish stock (Stockable)
note := fmt.Sprintf("Stock Adjustment IN #%d", newLog.Id) note := fmt.Sprintf("Stock Adjustment IN #%d", newLog.Id)
replenishResult, err := s.FifoSvc.Replenish(ctx, common.StockReplenishRequest{ _, err := s.FifoSvc.Replenish(ctx, common.StockReplenishRequest{
StockableKey: "ADJUSTMENT_IN", StockableKey: "ADJUSTMENT_IN",
StockableID: newLog.Id, StockableID: adjustmentStock.Id,
ProductWarehouseID: uint(productWarehouse.Id), ProductWarehouseID: uint(productWarehouse.Id),
Quantity: req.Quantity, Quantity: req.Quantity,
Note: &note, Note: &note,
@@ -213,15 +214,11 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to replenish stock via FIFO: %v", err)) return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to replenish stock via FIFO: %v", err))
} }
// Update stockable tracking fields
adjustmentStock.TotalQty = replenishResult.AddedQuantity
adjustmentStock.TotalUsed = 0
} else { } else {
// Adjustment DECREASE → Consume stock (Usable) // Adjustment DECREASE → Consume stock (Usable)
consumeResult, err := s.FifoSvc.Consume(ctx, common.StockConsumeRequest{ _, err := s.FifoSvc.Consume(ctx, common.StockConsumeRequest{
UsableKey: "ADJUSTMENT_OUT", UsableKey: "ADJUSTMENT_OUT",
UsableID: newLog.Id, UsableID: adjustmentStock.Id,
ProductWarehouseID: uint(productWarehouse.Id), ProductWarehouseID: uint(productWarehouse.Id),
Quantity: req.Quantity, Quantity: req.Quantity,
AllowPending: false, // Don't allow pending for adjustment AllowPending: false, // Don't allow pending for adjustment
@@ -230,16 +227,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to consume stock via FIFO: %v", err)) return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to consume stock via FIFO: %v", err))
} }
// Update usable tracking fields
adjustmentStock.UsageQty = consumeResult.UsageQuantity
adjustmentStock.PendingQty = consumeResult.PendingQuantity
}
// Save AdjustmentStock record
if err := s.AdjustmentStockRepository.WithTx(tx).CreateOne(ctx, adjustmentStock, nil); err != nil {
s.Log.Errorf("Failed to create adjustment stock: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create adjustment stock record")
} }
// Update ProductWarehouse quantity (for backward compatibility/reporting) // Update ProductWarehouse quantity (for backward compatibility/reporting)
@@ -18,6 +18,7 @@ type ProductWarehouseRepository interface {
ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error) ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error)
ExistsByID(ctx context.Context, id uint) (bool, error) ExistsByID(ctx context.Context, id uint) (bool, error)
GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error) GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error)
FindByProductWarehouseAndPfk(ctx context.Context, productID uint, warehouseID uint, projectFlockKandangID *uint) (*entity.ProductWarehouse, error)
GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error)
GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error) GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error)
GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error) GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error)
@@ -28,6 +29,8 @@ type ProductWarehouseRepository interface {
IdExists(ctx context.Context, id uint) (bool, error) IdExists(ctx context.Context, id uint) (bool, error)
CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error
EnsureProductWarehouse(ctx context.Context, productID, warehouseID uint, projectFlockKandangID *uint, createdBy uint) (uint, 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 { type ProductWarehouseRepositoryImpl struct {
@@ -107,6 +110,20 @@ func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehous
return &productWarehouse, nil return &productWarehouse, nil
} }
func (r *ProductWarehouseRepositoryImpl) FindByProductWarehouseAndPfk(ctx context.Context, productID uint, warehouseID uint, projectFlockKandangID *uint) (*entity.ProductWarehouse, error) {
var productWarehouse entity.ProductWarehouse
err := r.DB().WithContext(ctx).
Where("product_id = ? AND warehouse_id = ? AND project_flock_kandang_id IS NOT DISTINCT FROM ?", productID, warehouseID, projectFlockKandangID).
First(&productWarehouse).Error
if err != nil {
return nil, err
}
return &productWarehouse, nil
}
func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) { func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) {
var productWarehouses []entity.ProductWarehouse var productWarehouses []entity.ProductWarehouse
q := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}). q := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}).
@@ -199,6 +216,31 @@ func (r *ProductWarehouseRepositoryImpl) CleanupEmpty(ctx context.Context, affec
return nil return nil
} }
var inUseIDs []uint
if err := r.DB().WithContext(ctx).
Model(&entity.PurchaseItem{}).
Where("product_warehouse_id IN ?", emptyIDs).
Distinct().
Pluck("product_warehouse_id", &inUseIDs).Error; err != nil {
return err
}
if len(inUseIDs) > 0 {
inUse := make(map[uint]struct{}, len(inUseIDs))
for _, id := range inUseIDs {
inUse[id] = struct{}{}
}
filtered := make([]uint, 0, len(emptyIDs))
for _, id := range emptyIDs {
if _, exists := inUse[id]; !exists {
filtered = append(filtered, id)
}
}
emptyIDs = filtered
}
if len(emptyIDs) == 0 {
return nil
}
if err := r.DB().WithContext(ctx). if err := r.DB().WithContext(ctx).
Model(&entity.PurchaseItem{}). Model(&entity.PurchaseItem{}).
Where("product_warehouse_id IN ?", emptyIDs). Where("product_warehouse_id IN ?", emptyIDs).
@@ -257,6 +299,30 @@ func (r *ProductWarehouseRepositoryImpl) EnsureProductWarehouse(
return entity.Id, nil 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) { func (r *ProductWarehouseRepositoryImpl) GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error) {
var productWarehouse entity.ProductWarehouse var productWarehouse entity.ProductWarehouse
err := r.DB().WithContext(ctx). err := r.DB().WithContext(ctx).
@@ -106,23 +106,17 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
} }
func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) { func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) {
s.Log.Infof("Attempting to get StockTransfer with ID: %d", id)
transferPtr, err := s.StockTransferRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { transferPtr, err := s.StockTransferRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
return s.withRelations(db) return s.withRelations(db)
}) })
if err != nil { if err != nil {
s.Log.Errorf("Error getting StockTransfer ID %d: %+v", id, err)
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found") return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found")
} }
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer")
} }
if transferPtr != nil {
s.Log.Infof("StockTransfer %d has %d documents", transferPtr.Id, len(transferPtr.Documents))
}
return transferPtr, nil return transferPtr, nil
} }
@@ -336,7 +330,9 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
Files: documentFiles, Files: documentFiles,
}) })
if err != nil { if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to upload document for delivery %d", idx+1)) s.Log.WithError(err).Errorf("Failed to upload document for delivery %d (delivery_id: %d, filename: %s)",
idx+1, deliveries[idx].Id, file.Filename)
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to upload document for delivery %d: %v", idx+1, err))
} }
} }
} }
@@ -396,7 +392,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
}) })
if err != nil { if err != nil {
s.Log.Errorf("Transaction failed in CreateOne: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to process transfer transaction: %v", err)) return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to process transfer transaction: %v", err))
} }
@@ -247,11 +247,15 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery
itemDeliveryDate = &parsedDate itemDeliveryDate = &parsedDate
} }
// Hitung total_weight dan total_price otomatis
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
totalPrice := requestedProduct.UnitPrice * requestedProduct.Qty
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
deliveryProduct.UnitPrice = requestedProduct.UnitPrice deliveryProduct.UnitPrice = requestedProduct.UnitPrice
deliveryProduct.AvgWeight = requestedProduct.AvgWeight deliveryProduct.AvgWeight = requestedProduct.AvgWeight
deliveryProduct.TotalWeight = requestedProduct.TotalWeight deliveryProduct.TotalWeight = totalWeight
deliveryProduct.TotalPrice = requestedProduct.TotalPrice deliveryProduct.TotalPrice = totalPrice
deliveryProduct.DeliveryDate = itemDeliveryDate deliveryProduct.DeliveryDate = itemDeliveryDate
deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber
@@ -357,11 +361,15 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
oldRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty oldRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty
// Hitung total_weight dan total_price otomatis
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
totalPrice := requestedProduct.UnitPrice * requestedProduct.Qty
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
deliveryProduct.UnitPrice = requestedProduct.UnitPrice deliveryProduct.UnitPrice = requestedProduct.UnitPrice
deliveryProduct.AvgWeight = requestedProduct.AvgWeight deliveryProduct.AvgWeight = requestedProduct.AvgWeight
deliveryProduct.TotalWeight = requestedProduct.TotalWeight deliveryProduct.TotalWeight = totalWeight
deliveryProduct.TotalPrice = requestedProduct.TotalPrice deliveryProduct.TotalPrice = totalPrice
deliveryProduct.DeliveryDate = itemDeliveryDate deliveryProduct.DeliveryDate = itemDeliveryDate
deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber
@@ -75,7 +75,6 @@ func (s salesOrdersService) getOne(c *fiber.Ctx, id uint) (*entity.Marketing, er
return nil, fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") return nil, fiber.NewError(fiber.StatusNotFound, "SalesOrders not found")
} }
if err != nil { if err != nil {
s.Log.Errorf("Failed get marketing by id: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order")
} }
@@ -293,13 +292,17 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
for _, rp := range req.MarketingProducts { for _, rp := range req.MarketingProducts {
if old, ok := oldByPW[rp.ProductWarehouseId]; ok { if old, ok := oldByPW[rp.ProductWarehouseId]; ok {
// Hitung total_weight dan total_price otomatis
totalWeight := rp.Qty * rp.AvgWeight
totalPrice := rp.UnitPrice * rp.Qty
updateBody := map[string]any{ updateBody := map[string]any{
"product_warehouse_id": rp.ProductWarehouseId, "product_warehouse_id": rp.ProductWarehouseId,
"qty": rp.Qty, "qty": rp.Qty,
"unit_price": rp.UnitPrice, "unit_price": rp.UnitPrice,
"avg_weight": rp.AvgWeight, "avg_weight": rp.AvgWeight,
"total_weight": rp.TotalWeight, "total_weight": totalWeight,
"total_price": rp.TotalPrice, "total_price": totalPrice,
} }
if err := marketingProductRepoTx.PatchOne(c.Context(), old.Id, updateBody, nil); err != nil { if err := marketingProductRepoTx.PatchOne(c.Context(), old.Id, updateBody, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing product") return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing product")
@@ -589,30 +592,34 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e
func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo repository.MarketingDeliveryProductRepository) error { func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo repository.MarketingDeliveryProductRepository) error {
// Hitung total_weight dan total_price otomatis
totalWeight := rp.Qty * rp.AvgWeight
totalPrice := rp.UnitPrice * rp.Qty
marketingProduct := &entity.MarketingProduct{ marketingProduct := &entity.MarketingProduct{
MarketingId: marketingId, MarketingId: marketingId,
ProductWarehouseId: rp.ProductWarehouseId, ProductWarehouseId: rp.ProductWarehouseId,
Qty: rp.Qty, Qty: rp.Qty,
UnitPrice: rp.UnitPrice, UnitPrice: rp.UnitPrice,
AvgWeight: rp.AvgWeight, AvgWeight: rp.AvgWeight,
TotalWeight: rp.TotalWeight, TotalWeight: totalWeight,
TotalPrice: rp.TotalPrice, TotalPrice: totalPrice,
} }
if err := marketingProductRepo.CreateOne(ctx, marketingProduct, nil); err != nil { if err := marketingProductRepo.CreateOne(ctx, marketingProduct, nil); err != nil {
return err return err
} }
marketingDeliveryProduct := &entity.MarketingDeliveryProduct{ marketingDeliveryProduct := &entity.MarketingDeliveryProduct{
MarketingProductId: marketingProduct.Id, MarketingProductId: marketingProduct.Id,
ProductWarehouseId: marketingProduct.ProductWarehouseId, ProductWarehouseId: marketingProduct.ProductWarehouseId,
UnitPrice: 0, UnitPrice: 0,
TotalWeight: 0, TotalWeight: 0,
AvgWeight: 0, AvgWeight: 0,
TotalPrice: 0, TotalPrice: 0,
DeliveryDate: nil, DeliveryDate: nil,
VehicleNumber: rp.VehicleNumber, VehicleNumber: rp.VehicleNumber,
UsageQty: 0, UsageQty: 0,
PendingQty: 0, PendingQty: 0,
} }
if err := invDeliveryRepo.CreateOne(ctx, marketingDeliveryProduct, nil); err != nil { if err := invDeliveryRepo.CreateOne(ctx, marketingDeliveryProduct, nil); err != nil {
return err return err
@@ -5,8 +5,6 @@ type DeliveryProduct struct {
Qty float64 `json:"qty" validate:"omitempty,gte=0"` Qty float64 `json:"qty" validate:"omitempty,gte=0"`
UnitPrice float64 `json:"unit_price" validate:"omitempty,gte=0"` UnitPrice float64 `json:"unit_price" validate:"omitempty,gte=0"`
AvgWeight float64 `json:"avg_weight" validate:"omitempty,gte=0"` AvgWeight float64 `json:"avg_weight" validate:"omitempty,gte=0"`
TotalWeight float64 `json:"total_weight" validate:"omitempty,gte=0"`
TotalPrice float64 `json:"total_price" validate:"omitempty,gte=0"`
DeliveryDate string `json:"delivery_date" validate:"omitempty,datetime=2006-01-02"` DeliveryDate string `json:"delivery_date" validate:"omitempty,datetime=2006-01-02"`
VehicleNumber string `json:"vehicle_number" validate:"omitempty,max=50"` VehicleNumber string `json:"vehicle_number" validate:"omitempty,max=50"`
} }
@@ -12,10 +12,8 @@ type CreateMarketingProduct struct {
VehicleNumber string `json:"vehicle_number" validate:"required,min=1,max=50"` VehicleNumber string `json:"vehicle_number" validate:"required,min=1,max=50"`
ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,gt=0"` ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,gt=0"`
UnitPrice float64 `json:"unit_price" validate:"required,gt=0"` UnitPrice float64 `json:"unit_price" validate:"required,gt=0"`
TotalWeight float64 `json:"total_weight" validate:"required,gt=0"`
Qty float64 `json:"qty" validate:"required,gt=0"` Qty float64 `json:"qty" validate:"required,gt=0"`
AvgWeight float64 `json:"avg_weight" validate:"required,gt=0"` AvgWeight float64 `json:"avg_weight" validate:"required,gt=0"`
TotalPrice float64 `json:"total_price" validate:"required,gt=0"`
} }
type Update struct { type Update struct {
@@ -0,0 +1,144 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type ConfigChecklistController struct {
ConfigChecklistService service.ConfigChecklistService
}
func NewConfigChecklistController(configChecklistService service.ConfigChecklistService) *ConfigChecklistController {
return &ConfigChecklistController{
ConfigChecklistService: configChecklistService,
}
}
func (u *ConfigChecklistController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.ConfigChecklistService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ConfigChecklistListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all configChecklists successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToConfigChecklistListDTOs(result),
})
}
func (u *ConfigChecklistController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.ConfigChecklistService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get configChecklist successfully",
Data: dto.ToConfigChecklistListDTO(*result),
})
}
func (u *ConfigChecklistController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.ConfigChecklistService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create configChecklist successfully",
Data: dto.ToConfigChecklistListDTO(*result),
})
}
func (u *ConfigChecklistController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.ConfigChecklistService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update configChecklist successfully",
Data: dto.ToConfigChecklistListDTO(*result),
})
}
func (u *ConfigChecklistController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.ConfigChecklistService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete configChecklist successfully",
})
}
@@ -0,0 +1,61 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
)
// === DTO Structs ===
type ConfigChecklistRelationDTO struct {
Id uint `json:"id"`
Date time.Time `json:"date"`
}
type ConfigChecklistListDTO struct {
Id uint `json:"id"`
Date time.Time `json:"date"`
PercentageThresholdBad int `json:"percentage_threshold_bad"`
PercentageThresholdEnough int `json:"percentage_threshold_enough"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ConfigChecklistDetailDTO struct {
ConfigChecklistListDTO
}
// === Mapper Functions ===
func ToConfigChecklistRelationDTO(e entity.ConfigChecklist) ConfigChecklistRelationDTO {
return ConfigChecklistRelationDTO{
Id: e.Id,
Date: e.Date,
}
}
func ToConfigChecklistListDTO(e entity.ConfigChecklist) ConfigChecklistListDTO {
return ConfigChecklistListDTO{
Id: e.Id,
Date: e.Date,
PercentageThresholdBad: e.PercentageThresholdBad,
PercentageThresholdEnough: e.PercentageThresholdEnough,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
}
}
func ToConfigChecklistListDTOs(e []entity.ConfigChecklist) []ConfigChecklistListDTO {
result := make([]ConfigChecklistListDTO, len(e))
for i, r := range e {
result[i] = ToConfigChecklistListDTO(r)
}
return result
}
func ToConfigChecklistDetailDTO(e entity.ConfigChecklist) ConfigChecklistDetailDTO {
return ConfigChecklistDetailDTO{
ConfigChecklistListDTO: ToConfigChecklistListDTO(e),
}
}
@@ -0,0 +1,25 @@
package configChecklists
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rConfigChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/repositories"
sConfigChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type ConfigChecklistModule struct{}
func (ConfigChecklistModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
configChecklistRepo := rConfigChecklist.NewConfigChecklistRepository(db)
userRepo := rUser.NewUserRepository(db)
configChecklistService := sConfigChecklist.NewConfigChecklistService(configChecklistRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
ConfigChecklistRoutes(router, userService, configChecklistService)
}
@@ -0,0 +1,21 @@
package repository
import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
"gorm.io/gorm"
)
type ConfigChecklistRepository interface {
repository.BaseRepository[entity.ConfigChecklist]
}
type ConfigChecklistRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.ConfigChecklist]
}
func NewConfigChecklistRepository(db *gorm.DB) ConfigChecklistRepository {
return &ConfigChecklistRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ConfigChecklist](db),
}
}
@@ -0,0 +1,23 @@
package configChecklists
import (
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/controllers"
configChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func ConfigChecklistRoutes(v1 fiber.Router, u user.UserService, s configChecklist.ConfigChecklistService) {
ctrl := controller.NewConfigChecklistController(s)
route := v1.Group("/config-checklists")
route.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -0,0 +1,146 @@
package service
import (
"errors"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/config-checklists/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type ConfigChecklistService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ConfigChecklist, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.ConfigChecklist, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ConfigChecklist, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ConfigChecklist, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type configChecklistService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.ConfigChecklistRepository
}
func NewConfigChecklistService(repo repository.ConfigChecklistRepository, validate *validator.Validate) ConfigChecklistService {
return &configChecklistService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s configChecklistService) withRelations(db *gorm.DB) *gorm.DB {
return db
}
func (s configChecklistService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ConfigChecklist, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
configChecklists, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
return db.Order("date DESC").Order("created_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get configChecklists: %+v", err)
return nil, 0, err
}
return configChecklists, total, nil
}
func (s configChecklistService) GetOne(c *fiber.Ctx, id uint) (*entity.ConfigChecklist, error) {
configChecklist, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "ConfigChecklist not found")
}
if err != nil {
s.Log.Errorf("Failed get configChecklist by id: %+v", err)
return nil, err
}
return configChecklist, nil
}
func (s *configChecklistService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ConfigChecklist, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
date, err := time.Parse("2006-01-02", req.Date)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid date format, use YYYY-MM-DD")
}
createBody := &entity.ConfigChecklist{
Date: date,
PercentageThresholdBad: req.PercentageThresholdBad,
PercentageThresholdEnough: req.PercentageThresholdEnough,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
s.Log.Errorf("Failed to create configChecklist: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s configChecklistService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ConfigChecklist, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
if req.Date != nil {
date, err := time.Parse("2006-01-02", *req.Date)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "invalid date format, use YYYY-MM-DD")
}
updateBody["date"] = date
}
if req.PercentageThresholdBad != nil {
updateBody["percentage_threshold_bad"] = *req.PercentageThresholdBad
}
if req.PercentageThresholdEnough != nil {
updateBody["percentage_threshold_enough"] = *req.PercentageThresholdEnough
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "ConfigChecklist not found")
}
s.Log.Errorf("Failed to update configChecklist: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s configChecklistService) 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, "ConfigChecklist not found")
}
s.Log.Errorf("Failed to delete configChecklist: %+v", err)
return err
}
return nil
}
@@ -0,0 +1,19 @@
package validation
type Create struct {
Date string `json:"date" validate:"required"`
PercentageThresholdBad int `json:"percentage_threshold_bad" validate:"required"`
PercentageThresholdEnough int `json:"percentage_threshold_enough" validate:"required"`
}
type Update struct {
Date *string `json:"date,omitempty" validate:"omitempty"`
PercentageThresholdBad *int `json:"percentage_threshold_bad,omitempty" validate:"omitempty"`
PercentageThresholdEnough *int `json:"percentage_threshold_enough,omitempty" validate:"omitempty"`
}
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"`
Search string `query:"search" validate:"omitempty,max=50"`
}
@@ -0,0 +1,161 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type EmployeesController struct {
EmployeesService service.EmployeesService
}
func NewEmployeesController(employeesService service.EmployeesService) *EmployeesController {
return &EmployeesController{
EmployeesService: employeesService,
}
}
func (u *EmployeesController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
if kandangParam := c.Query("kandang_id", ""); kandangParam != "" {
id, err := strconv.Atoi(kandangParam)
if err != nil || id <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "invalid kandang_id")
}
temp := uint(id)
query.KandangId = &temp
}
if activeParam := c.Query("is_active", ""); activeParam != "" {
value, err := strconv.ParseBool(activeParam)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "invalid is_active value")
}
query.IsActive = &value
}
result, totalResults, err := u.EmployeesService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.EmployeesListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all employeess successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToEmployeesListDTOs(result),
})
}
func (u *EmployeesController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.EmployeesService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get employees successfully",
Data: dto.ToEmployeesListDTO(*result),
})
}
func (u *EmployeesController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.EmployeesService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create employees successfully",
Data: dto.ToEmployeesListDTO(*result),
})
}
func (u *EmployeesController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.EmployeesService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update employees successfully",
Data: dto.ToEmployeesListDTO(*result),
})
}
func (u *EmployeesController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.EmployeesService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete employees successfully",
})
}
@@ -0,0 +1,70 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
)
// === DTO Structs ===
type EmployeesRelationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type EmployeesListDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
IsActive bool `json:"is_active"`
Kandangs []kandangDTO.KandangRelationDTO `json:"kandangs"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type EmployeesDetailDTO struct {
EmployeesListDTO
}
// === Mapper Functions ===
func ToEmployeesRelationDTO(e entity.Employees) EmployeesRelationDTO {
return EmployeesRelationDTO{
Id: e.Id,
Name: e.Name,
}
}
func ToEmployeesListDTO(e entity.Employees) EmployeesListDTO {
kandangs := make([]kandangDTO.KandangRelationDTO, 0, len(e.EmployeeKandangs))
for _, rel := range e.EmployeeKandangs {
if rel.Kandang.Id == 0 {
continue
}
kandangs = append(kandangs, kandangDTO.ToKandangRelationDTO(rel.Kandang))
}
return EmployeesListDTO{
Id: e.Id,
Name: e.Name,
IsActive: e.IsActive,
Kandangs: kandangs,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
}
}
func ToEmployeesListDTOs(e []entity.Employees) []EmployeesListDTO {
result := make([]EmployeesListDTO, len(e))
for i, r := range e {
result[i] = ToEmployeesListDTO(r)
}
return result
}
func ToEmployeesDetailDTO(e entity.Employees) EmployeesDetailDTO {
return EmployeesDetailDTO{
EmployeesListDTO: ToEmployeesListDTO(e),
}
}
@@ -0,0 +1,25 @@
package employeess
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rEmployees "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/repositories"
sEmployees "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type EmployeesModule struct{}
func (EmployeesModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
employeesRepo := rEmployees.NewEmployeesRepository(db)
userRepo := rUser.NewUserRepository(db)
employeesService := sEmployees.NewEmployeesService(employeesRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
EmployeesRoutes(router, userService, employeesService)
}
@@ -0,0 +1,21 @@
package repository
import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
"gorm.io/gorm"
)
type EmployeesRepository interface {
repository.BaseRepository[entity.Employees]
}
type EmployeesRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Employees]
}
func NewEmployeesRepository(db *gorm.DB) EmployeesRepository {
return &EmployeesRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Employees](db),
}
}
@@ -0,0 +1,23 @@
package employeess
import (
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/controllers"
employees "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func EmployeesRoutes(v1 fiber.Router, u user.UserService, s employees.EmployeesService) {
ctrl := controller.NewEmployeesController(s)
route := v1.Group("/employees")
route.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -0,0 +1,265 @@
package service
import (
"errors"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/employees/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type EmployeesService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Employees, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Employees, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Employees, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Employees, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type employeesService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.EmployeesRepository
}
func NewEmployeesService(repo repository.EmployeesRepository, validate *validator.Validate) EmployeesService {
return &employeesService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s employeesService) withRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("EmployeeKandangs.Kandang").
Where("employees.deleted_at IS NULL")
}
func (s employeesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Employees, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
employeess, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
db = db.Where("employees.name LIKE ?", "%"+params.Search+"%")
}
if params.KandangId != nil {
db = db.Joins("JOIN employee_kandangs ek ON ek.employee_id = employees.id").
Where("ek.kandang_id = ?", *params.KandangId)
}
if params.IsActive != nil {
db = db.Where("employees.is_active = ?", *params.IsActive)
}
return db.Order("employees.created_at DESC").Order("employees.updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get employeess: %+v", err)
return nil, 0, err
}
return employeess, total, nil
}
func (s employeesService) GetOne(c *fiber.Ctx, id uint) (*entity.Employees, error) {
employees, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Employees not found")
}
if err != nil {
s.Log.Errorf("Failed get employees by id: %+v", err)
return nil, err
}
return employees, nil
}
func (s *employeesService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Employees, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
name := strings.TrimSpace(req.Name)
if name == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "name cannot be empty")
}
kandangIDs := normalizeKandangIDs(req.KandangIDs)
if len(kandangIDs) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids must contain at least one valid id")
}
if _, err := s.Repository.First(c.Context(), func(db *gorm.DB) *gorm.DB {
return db.Where("LOWER(name) = ?", strings.ToLower(name))
}); err == nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "employee already exists")
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed checking employee uniqueness: %+v", err)
return nil, err
}
createBody := &entity.Employees{
Name: name,
IsActive: req.IsActive,
}
if err := s.Repository.DB().Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
if err := repoTx.CreateOne(c.Context(), createBody, nil); err != nil {
return err
}
relations := make([]entity.EmployeeKandang, 0, len(kandangIDs))
for _, kandangID := range kandangIDs {
relations = append(relations, entity.EmployeeKandang{
EmployeeId: createBody.Id,
KandangId: kandangID,
})
}
if len(relations) > 0 {
if err := tx.WithContext(c.Context()).Create(&relations).Error; err != nil {
return err
}
}
return nil
}); err != nil {
s.Log.Errorf("Failed to create employees: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s employeesService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Employees, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
var (
kandangIDs []uint
needKandangUpdate bool
)
if req.Name != nil {
trimmed := strings.TrimSpace(*req.Name)
if trimmed == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "name cannot be empty")
}
if _, err := s.Repository.First(c.Context(), func(db *gorm.DB) *gorm.DB {
return db.Where("LOWER(name) = ? AND id <> ?", strings.ToLower(trimmed), id)
}); err == nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "employee already exists")
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed checking employee uniqueness: %+v", err)
return nil, err
}
updateBody["name"] = trimmed
}
if req.IsActive != nil {
updateBody["is_active"] = *req.IsActive
}
if req.KandangIDs != nil {
ids := normalizeKandangIDs(*req.KandangIDs)
if len(ids) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids must contain at least one valid id")
}
kandangIDs = ids
needKandangUpdate = true
}
if len(updateBody) == 0 && !needKandangUpdate {
return s.GetOne(c, id)
}
if err := s.Repository.DB().Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx)
if len(updateBody) > 0 {
if err := repoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil {
return err
}
}
if needKandangUpdate {
if err := tx.WithContext(c.Context()).
Where("employee_id = ?", id).
Delete(&entity.EmployeeKandang{}).Error; err != nil {
return err
}
relations := make([]entity.EmployeeKandang, 0, len(kandangIDs))
for _, kandangID := range kandangIDs {
relations = append(relations, entity.EmployeeKandang{
EmployeeId: id,
KandangId: kandangID,
})
}
if len(relations) > 0 {
if err := tx.WithContext(c.Context()).Create(&relations).Error; err != nil {
return err
}
}
}
return nil
}); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Employees not found")
}
s.Log.Errorf("Failed to update employees: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s employeesService) 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, "Employees not found")
}
s.Log.Errorf("Failed to delete employees: %+v", err)
return err
}
return nil
}
func normalizeKandangIDs(ids []uint) []uint {
result := make([]uint, 0, len(ids))
seen := make(map[uint]struct{})
for _, id := range ids {
if id == 0 {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
result = append(result, id)
}
return result
}
@@ -0,0 +1,21 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
KandangIDs []uint `json:"kandang_ids" validate:"required,min=1,dive,required"`
IsActive bool `json:"is_active"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"`
KandangIDs *[]uint `json:"kandang_ids,omitempty" validate:"omitempty,min=1,dive,required"`
IsActive *bool `json:"is_active,omitempty"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=500,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"`
KandangId *uint `query:"kandang_id" validate:"omitempty"`
IsActive *bool `query:"is_active" validate:"omitempty"`
}
@@ -23,10 +23,12 @@ func NewNonstockController(nonstockService service.NonstockService) *NonstockCon
} }
func (u *NonstockController) GetAll(c *fiber.Ctx) error { func (u *NonstockController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{ query := &validation.Query{
Page: c.QueryInt("page", 1), Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10), Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""), Search: c.Query("search", ""),
SupplierID: uint(c.QueryInt("supplier_id", 0)),
} }
if query.Page < 1 || query.Limit < 1 { if query.Page < 1 || query.Limit < 1 {
@@ -1,11 +1,12 @@
package dto package dto
import ( import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto" supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto" uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
"time"
) )
// === DTO Structs === // === DTO Structs ===
@@ -22,7 +23,7 @@ type NonstockListDTO struct {
Name string `json:"name"` Name string `json:"name"`
Flags []string `json:"flags"` Flags []string `json:"flags"`
Uom *uomDTO.UomRelationDTO `json:"uom"` Uom *uomDTO.UomRelationDTO `json:"uom"`
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers,omitempty"` Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"` CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
@@ -100,7 +101,7 @@ func ToNonstockDetailDTO(e entity.Nonstock) NonstockDetailDTO {
func toNonstockSupplierDTOs(relations []entity.NonstockSupplier) []supplierDTO.SupplierRelationDTO { func toNonstockSupplierDTOs(relations []entity.NonstockSupplier) []supplierDTO.SupplierRelationDTO {
if len(relations) == 0 { if len(relations) == 0 {
return nil return make([]supplierDTO.SupplierRelationDTO, 0)
} }
result := make([]supplierDTO.SupplierRelationDTO, 0, len(relations)) result := make([]supplierDTO.SupplierRelationDTO, 0, len(relations))
@@ -112,7 +113,7 @@ func toNonstockSupplierDTOs(relations []entity.NonstockSupplier) []supplierDTO.S
} }
if len(result) == 0 { if len(result) == 0 {
return nil return make([]supplierDTO.SupplierRelationDTO, 0)
} }
return result return result
@@ -58,7 +58,15 @@ func (s nonstockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
nonstocks, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { nonstocks, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
if params.SupplierID > 0 {
db = db.Joins("INNER JOIN nonstock_suppliers ON nonstock_suppliers.nonstock_id = nonstocks.id").
Where("nonstock_suppliers.supplier_id = ?", params.SupplierID).
Distinct()
}
db = s.withRelations(db) db = s.withRelations(db)
if params.Search != "" { if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%") return db.Where("name LIKE ?", "%"+params.Search+"%")
} }
@@ -3,8 +3,8 @@ package validation
type Create struct { type Create struct {
Name string `json:"name" validate:"required_strict,min=3,max=50"` Name string `json:"name" validate:"required_strict,min=3,max=50"`
UomID uint `json:"uom_id" validate:"required,gt=0"` UomID uint `json:"uom_id" validate:"required,gt=0"`
SupplierIDs []uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"` SupplierIDs []uint `json:"supplier_ids" validate:"dive,gt=0"`
Flags []string `json:"flags,omitempty" validate:"omitempty,dive,max=50"` Flags []string `json:"flags" validate:"dive,max=50"`
} }
type Update struct { type Update struct {
@@ -15,7 +15,8 @@ type Update struct {
} }
type Query struct { type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"` Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
Search string `query:"search" validate:"omitempty,max=50"` Search string `query:"search" validate:"omitempty,max=50"`
SupplierID uint `query:"supplier_id" validate:"omitempty,gt=0"`
} }
@@ -0,0 +1,145 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type PhaseActivityController struct {
PhaseActivityService service.PhaseActivityService
}
func NewPhaseActivityController(phaseActivityService service.PhaseActivityService) *PhaseActivityController {
return &PhaseActivityController{
PhaseActivityService: phaseActivityService,
}
}
func (u *PhaseActivityController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
query.PhaseIDs = c.Query("phase_ids", "")
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.PhaseActivityService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.PhaseActivityListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all phaseActivitys successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToPhaseActivityListDTOs(result),
})
}
func (u *PhaseActivityController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.PhaseActivityService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get phaseActivity successfully",
Data: dto.ToPhaseActivityListDTO(*result),
})
}
func (u *PhaseActivityController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.PhaseActivityService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create phaseActivity successfully",
Data: dto.ToPhaseActivityListDTO(*result),
})
}
func (u *PhaseActivityController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.PhaseActivityService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update phaseActivity successfully",
Data: dto.ToPhaseActivityListDTO(*result),
})
}
func (u *PhaseActivityController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.PhaseActivityService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete phaseActivity successfully",
})
}
@@ -0,0 +1,72 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type PhaseActivityRelationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type PhaseActivityListDTO struct {
Id uint `json:"id"`
PhaseId uint `json:"phase_id"`
Name string `json:"name"`
Description *string `json:"description"`
TimeType *string `json:"time_type"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type PhaseActivityDetailDTO struct {
PhaseActivityListDTO
}
// === Mapper Functions ===
func ToPhaseActivityRelationDTO(e entity.PhaseActivity) PhaseActivityRelationDTO {
return PhaseActivityRelationDTO{
Id: e.Id,
Name: e.Name,
}
}
func ToPhaseActivityListDTO(e entity.PhaseActivity) PhaseActivityListDTO {
var createdUser *userDTO.UserRelationDTO
// if e.CreatedUser.Id != 0 {
// mapped := userDTO.ToUserRelationDTO(e.CreatedUser)
// createdUser = &mapped
// }
return PhaseActivityListDTO{
Id: e.Id,
PhaseId: e.PhaseId,
Name: e.Name,
Description: e.Description,
TimeType: e.TimeType,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
func ToPhaseActivityListDTOs(e []entity.PhaseActivity) []PhaseActivityListDTO {
result := make([]PhaseActivityListDTO, len(e))
for i, r := range e {
result[i] = ToPhaseActivityListDTO(r)
}
return result
}
func ToPhaseActivityDetailDTO(e entity.PhaseActivity) PhaseActivityDetailDTO {
return PhaseActivityDetailDTO{
PhaseActivityListDTO: ToPhaseActivityListDTO(e),
}
}
@@ -0,0 +1,27 @@
package phaseActivity
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rPhaseActivity "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/repositories"
sPhaseActivity "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/services"
rPhases "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type PhaseActivityModule struct{}
func (PhaseActivityModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
phaseActivityRepo := rPhaseActivity.NewPhaseActivityRepository(db)
phasesRepo := rPhases.NewPhasesRepository(db)
userRepo := rUser.NewUserRepository(db)
phaseActivityService := sPhaseActivity.NewPhaseActivityService(phaseActivityRepo, phasesRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
PhaseActivityRoutes(router, userService, phaseActivityService)
}
@@ -0,0 +1,21 @@
package repository
import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
"gorm.io/gorm"
)
type PhaseActivityRepository interface {
repository.BaseRepository[entity.PhaseActivity]
}
type PhaseActivityRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.PhaseActivity]
}
func NewPhaseActivityRepository(db *gorm.DB) PhaseActivityRepository {
return &PhaseActivityRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.PhaseActivity](db),
}
}
@@ -0,0 +1,23 @@
package phaseActivity
import (
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/controllers"
phaseActivity "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func PhaseActivityRoutes(v1 fiber.Router, u user.UserService, s phaseActivity.PhaseActivityService) {
ctrl := controller.NewPhaseActivityController(s)
route := v1.Group("/phase-activities")
route.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -0,0 +1,187 @@
package service
import (
"errors"
"strconv"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phase-activities/validations"
phaseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type PhaseActivityService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.PhaseActivity, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.PhaseActivity, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.PhaseActivity, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.PhaseActivity, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type phaseActivityService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.PhaseActivityRepository
PhaseRepo phaseRepo.PhasesRepository
}
func NewPhaseActivityService(repo repository.PhaseActivityRepository, phaseRepo phaseRepo.PhasesRepository, validate *validator.Validate) PhaseActivityService {
return &phaseActivityService{
Log: utils.Log,
Validate: validate,
Repository: repo,
PhaseRepo: phaseRepo,
}
}
func (s phaseActivityService) withRelations(db *gorm.DB) *gorm.DB {
return db.Joins("JOIN phases ON phases.id = phase_activities.phase_id").
Where("phases.deleted_at IS NULL")
}
func (s phaseActivityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.PhaseActivity, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
phaseActivitys, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
db = db.Where("name LIKE ?", "%"+params.Search+"%")
}
if params.PhaseIDs != "" {
ids := parseIDs(params.PhaseIDs)
if len(ids) > 0 {
db = db.Where("phase_id IN ?", ids)
}
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get phaseActivitys: %+v", err)
return nil, 0, err
}
return phaseActivitys, total, nil
}
func (s phaseActivityService) GetOne(c *fiber.Ctx, id uint) (*entity.PhaseActivity, error) {
phaseActivity, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "PhaseActivity not found")
}
if err != nil {
s.Log.Errorf("Failed get phaseActivity by id: %+v", err)
return nil, err
}
return phaseActivity, nil
}
func (s *phaseActivityService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.PhaseActivity, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
phase, err := s.PhaseRepo.GetByID(c.Context(), req.PhaseId, nil)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusBadRequest, "phase not found")
}
if err != nil {
s.Log.Errorf("Failed to get phase: %+v", err)
return nil, err
}
name := strings.TrimSpace(req.Name)
if name == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "name cannot be empty")
}
timeType := strings.TrimSpace(req.TimeType)
if timeType == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "time_type cannot be empty")
}
createBody := &entity.PhaseActivity{
PhaseId: phase.Id,
Name: name,
Description: req.Description,
TimeType: &timeType,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
s.Log.Errorf("Failed to create phaseActivity: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s phaseActivityService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.PhaseActivity, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
trimmedName := strings.TrimSpace(req.Name)
if trimmedName == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "name cannot be empty")
}
trimmedTimeType := strings.TrimSpace(req.TimeType)
if trimmedTimeType == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "time_type cannot be empty")
}
updateBody := map[string]any{
"name": trimmedName,
"time_type": trimmedTimeType,
}
if req.Description != nil {
updateBody["description"] = *req.Description
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "PhaseActivity not found")
}
s.Log.Errorf("Failed to update phaseActivity: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s phaseActivityService) 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, "PhaseActivity not found")
}
s.Log.Errorf("Failed to delete phaseActivity: %+v", err)
return err
}
return nil
}
func parseIDs(raw string) []uint {
parts := strings.Split(raw, ",")
results := make([]uint, 0, len(parts))
for _, part := range parts {
value := strings.TrimSpace(part)
if value == "" {
continue
}
if n, err := strconv.ParseUint(value, 10, 64); err == nil {
results = append(results, uint(n))
}
}
return results
}
@@ -0,0 +1,21 @@
package validation
type Create struct {
PhaseId uint `json:"phase_id" validate:"required"`
Name string `json:"name" validate:"required_strict,min=3"`
Description *string `json:"description,omitempty"`
TimeType string `json:"time_type" validate:"required"`
}
type Update struct {
Name string `json:"name" validate:"required_strict,min=3"`
Description *string `json:"description,omitempty"`
TimeType string `json:"time_type" validate:"required"`
}
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"`
Search string `query:"search" validate:"omitempty,max=50"`
PhaseIDs string `query:"phase_ids" validate:"omitempty"`
}
@@ -0,0 +1,148 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type PhasesController struct {
PhasesService service.PhasesService
}
func NewPhasesController(phasesService service.PhasesService) *PhasesController {
return &PhasesController{
PhasesService: phasesService,
}
}
func (u *PhasesController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
if category := c.Query("category", ""); category != "" {
query.Category = &category
}
result, totalResults, err := u.PhasesService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.PhasesListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all phasess successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToPhasesListDTOs(result),
})
}
func (u *PhasesController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.PhasesService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get phases successfully",
Data: dto.ToPhasesListDTO(*result),
})
}
func (u *PhasesController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.PhasesService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create phases successfully",
Data: dto.ToPhasesListDTO(*result),
})
}
func (u *PhasesController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.PhasesService.UpdateOne(c, req, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Update phases successfully",
Data: dto.ToPhasesListDTO(*result),
})
}
func (u *PhasesController) DeleteOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
if err := u.PhasesService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete phases successfully",
})
}
@@ -0,0 +1,68 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type PhasesRelationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type PhasesListDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Category string `json:"category"`
IsActive bool `json:"is_active"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
}
type PhasesDetailDTO struct {
PhasesListDTO
}
// === Mapper Functions ===
func ToPhasesRelationDTO(e entity.Phases) PhasesRelationDTO {
return PhasesRelationDTO{
Id: e.Id,
Name: e.Name,
}
}
func ToPhasesListDTO(e entity.Phases) PhasesListDTO {
var createdUser *userDTO.UserRelationDTO
// if e.CreatedUser.Id != 0 {
// mapped := userDTO.ToUserRelationDTO(e.CreatedUser)
// createdUser = &mapped
// }
return PhasesListDTO{
Id: e.Id,
Name: e.Name,
Category: e.Category,
IsActive: e.IsActive,
CreatedAt: e.CreatedAt,
CreatedUser: createdUser,
}
}
func ToPhasesListDTOs(e []entity.Phases) []PhasesListDTO {
result := make([]PhasesListDTO, len(e))
for i, r := range e {
result[i] = ToPhasesListDTO(r)
}
return result
}
func ToPhasesDetailDTO(e entity.Phases) PhasesDetailDTO {
return PhasesDetailDTO{
PhasesListDTO: ToPhasesListDTO(e),
}
}
+25
View File
@@ -0,0 +1,25 @@
package phases
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rPhases "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories"
sPhases "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type PhasesModule struct{}
func (PhasesModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
phasesRepo := rPhases.NewPhasesRepository(db)
userRepo := rUser.NewUserRepository(db)
phasesService := sPhases.NewPhasesService(phasesRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
PhasesRoutes(router, userService, phasesService)
}
@@ -0,0 +1,21 @@
package repository
import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
"gorm.io/gorm"
)
type PhasesRepository interface {
repository.BaseRepository[entity.Phases]
}
type PhasesRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Phases]
}
func NewPhasesRepository(db *gorm.DB) PhasesRepository {
return &PhasesRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Phases](db),
}
}
+23
View File
@@ -0,0 +1,23 @@
package phases
import (
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/controllers"
phases "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func PhasesRoutes(v1 fiber.Router, u user.UserService, s phases.PhasesService) {
ctrl := controller.NewPhasesController(s)
route := v1.Group("/phases")
route.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -0,0 +1,158 @@
package service
import (
"errors"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/phasess/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type PhasesService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Phases, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.Phases, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Phases, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Phases, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type phasesService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.PhasesRepository
}
func NewPhasesService(repo repository.PhasesRepository, validate *validator.Validate) PhasesService {
return &phasesService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s phasesService) withRelations(db *gorm.DB) *gorm.DB {
return db
}
func (s phasesService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Phases, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
phasess, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%")
}
if params.Category != nil {
db = db.Where("category = ?", *params.Category)
}
return db.Order("created_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get phasess: %+v", err)
return nil, 0, err
}
return phasess, total, nil
}
func (s phasesService) GetOne(c *fiber.Ctx, id uint) (*entity.Phases, error) {
phases, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Phases not found")
}
if err != nil {
s.Log.Errorf("Failed get phases by id: %+v", err)
return nil, err
}
return phases, nil
}
func (s *phasesService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Phases, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
if _, err := s.Repository.First(c.Context(), func(db *gorm.DB) *gorm.DB {
return db.Where("LOWER(name) = ? AND category = ?", strings.ToLower(req.Name), req.Category)
}); err == nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "phase already exists")
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed checking phase uniqueness: %+v", err)
return nil, err
}
createBody := &entity.Phases{
Name: req.Name,
Category: req.Category,
IsActive: true,
}
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
s.Log.Errorf("Failed to create phases: %+v", err)
return nil, err
}
return s.GetOne(c, createBody.Id)
}
func (s phasesService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Phases, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
existing, err := s.Repository.GetByID(c.Context(), id, nil)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Phases not found")
}
if err != nil {
s.Log.Errorf("Failed get phases by id: %+v", err)
return nil, err
}
updateBody := make(map[string]any)
if req.Name != nil {
if _, err := s.Repository.First(c.Context(), func(db *gorm.DB) *gorm.DB {
return db.Where("LOWER(name) = ? AND category = ? AND id <> ?", strings.ToLower(*req.Name), existing.Category, id)
}); err == nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "phase already exists")
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed checking phase uniqueness: %+v", err)
return nil, err
}
updateBody["name"] = strings.TrimSpace(*req.Name)
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
s.Log.Errorf("Failed to update phases: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s phasesService) 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, "Phases not found")
}
s.Log.Errorf("Failed to delete phases: %+v", err)
return err
}
return nil
}
@@ -0,0 +1,17 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
Category string `json:"category" validate:"required"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"`
}
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"`
Search string `query:"search" validate:"omitempty,max=50"`
Category *string `query:"category" validate:"omitempty"`
}
@@ -33,6 +33,7 @@ type EggProductionStandardDetailDTO struct {
TargetHenHouseProduction *float64 `json:"target_hen_house_production"` TargetHenHouseProduction *float64 `json:"target_hen_house_production"`
TargetEggWeight *float64 `json:"target_egg_weight"` TargetEggWeight *float64 `json:"target_egg_weight"`
TargetEggMass *float64 `json:"target_egg_mass"` TargetEggMass *float64 `json:"target_egg_mass"`
StandardFCR *float64 `json:"standard_fcr"`
} }
type WeeklyProductionStandardDTO struct { type WeeklyProductionStandardDTO struct {
@@ -95,6 +96,7 @@ func ToWeeklyProductionStandardDTOWithDetails(growth entity.StandardGrowthDetail
TargetHenHouseProduction: detail.TargetHenHouseProduction, TargetHenHouseProduction: detail.TargetHenHouseProduction,
TargetEggWeight: detail.TargetEggWeight, TargetEggWeight: detail.TargetEggWeight,
TargetEggMass: detail.TargetEggMass, TargetEggMass: detail.TargetEggMass,
StandardFCR: detail.StandardFCR,
} }
return WeeklyProductionStandardDTO{ return WeeklyProductionStandardDTO{
@@ -148,6 +150,7 @@ func ToEggProductionStandardDetailDTO(e entity.ProductionStandardDetail) EggProd
TargetHenHouseProduction: e.TargetHenHouseProduction, TargetHenHouseProduction: e.TargetHenHouseProduction,
TargetEggWeight: e.TargetEggWeight, TargetEggWeight: e.TargetEggWeight,
TargetEggMass: e.TargetEggMass, TargetEggMass: e.TargetEggMass,
StandardFCR: e.StandardFCR,
} }
} }

Some files were not shown because too many files have changed in this diff Show More