Compare commits

...

201 Commits

Author SHA1 Message Date
kris 2a884a8d09 Edit .gitignore (Tom haye) 2026-01-14 07:35:57 +00:00
Adnan Zahir 9fc2d0556e Merge branch 'chore/test-staging' into 'development'
chore: test staging from development

See merge request mbugroup/lti-api!179
2026-01-14 13:46:38 +07:00
Adnan Zahir c2a89910fb chore: test staging from development 2026-01-14 13:46:15 +07:00
Hafizh A. Y. 1847e5590a Merge branch 'feat/informasi-umum' into 'development'
adjust api informasi umum filter per kandang

See merge request mbugroup/lti-api!177
2026-01-14 06:21:21 +00:00
Hafizh A. Y. 57094f664c Merge branch 'feat/BE/US-281-adjustment_recording' into 'development'
Feat/be/us 281 adjustment recording

See merge request mbugroup/lti-api!174
2026-01-14 06:20:57 +00:00
Hafizh A. Y. f8b6e12d16 Merge branch 'dev/teguh' into 'development'
FIX[BE]: fixing error on report marketing dto

See merge request mbugroup/lti-api!178
2026-01-14 06:19:52 +00:00
kris e12c34db13 Update .gitlab-ci.yml file 2026-01-14 06:18:47 +00:00
aguhh18 3012d260ec FIX[BE]: fixing error on report marketing dto 2026-01-14 11:57:56 +07:00
MacBook Air M1 8a639f127c adjust api informasi umum filter per kandang 2026-01-14 11:41:47 +07:00
Hafizh A. Y. 2acaa10b60 Merge branch 'fix/air.toml' into 'development'
fix(BE): add .air.toml

See merge request mbugroup/lti-api!176
2026-01-14 04:22:29 +00:00
MacBook Air M1 bd9d41e161 fix(BE): add .air.toml 2026-01-14 11:20:42 +07:00
Hafizh A. Y. 06d8d0b795 Merge branch 'feat/price-product-supplier' into 'development'
feat(BE): price-product-supplier

See merge request mbugroup/lti-api!160
2026-01-14 02:17:42 +00:00
ragilap 1d5e7b6e1a Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-281-adjustment_recording 2026-01-14 02:09:57 +07:00
Hafizh A. Y. da190f1b05 Merge branch 'feat/BE/Rekapitulasi-hutang-supplier' into 'development'
[Feat/be] Adjustment Counting debt supplier

See merge request mbugroup/lti-api!173
2026-01-13 17:40:18 +00:00
Hafizh A. Y. c8905eb715 Merge branch 'feat/BE/sapronak/data-produksi' into 'development'
Feat/be/sapronak/data produksi

See merge request mbugroup/lti-api!170
2026-01-13 17:39:33 +00:00
ragilap 03fbf7f4b7 production_standard 2026-01-13 20:06:37 +07:00
ragilap 7545e9b37d [FIX/BE-US] add ignore for dockerfile.local 2026-01-13 19:57:47 +07:00
ragilap 69f38bf16a Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into feat/BE/Rekapitulasi-hutang-supplier 2026-01-13 19:55:02 +07:00
ragilap 5730053e04 [FIX/BE-US-390] changes counting debt supplier 2026-01-13 19:54:47 +07:00
MacBook Air M1 b087a703ef resolve conflict to development 2026-01-13 16:47:16 +07:00
Adnan Zahir 00edcb6add Merge branch 'chore/test-container-versioning' into 'development'
chore: test image versioning 2

See merge request mbugroup/lti-api!171
2026-01-13 16:47:12 +07:00
Adnan Zahir de6580d11c chore: test image versioning 2 2026-01-13 16:46:49 +07:00
Hafizh A. Y. dcd6008946 Merge branch 'FEAT/BE/report-penjualan' into 'development'
[FEAT][BE]: add some filter on report penjualan API and fixing some error

See merge request mbugroup/lti-api!167
2026-01-13 09:26:01 +00:00
Hafizh A. Y. 711f58abae Merge branch 'feat/BE/US-390-Dashboard' into 'development'
Feat/be/us 390 dashboard changes calculate kilo units

See merge request mbugroup/lti-api!166
2026-01-13 09:25:49 +00:00
Hafizh A. Y. ffd3c905fe Merge branch 'FEAT/BE/Closing_overhead_perkandang' into 'development'
feat[BE]: enhance GetOverhead functionality with project flock kandang count...

See merge request mbugroup/lti-api!165
2026-01-13 09:25:38 +00:00
Hafizh A. Y. 6e4a8617da Merge branch 'Feat/BE/Closing_Penjualan_perkandang' into 'development'
[FEAT][BE] : create closing penjualan perkandang API

See merge request mbugroup/lti-api!164
2026-01-13 09:25:24 +00:00
Adnan Zahir 8a57d5d675 Merge branch 'chore/test-container-versioning' into 'development'
chore: test image versioning

See merge request mbugroup/lti-api!168
2026-01-13 16:18:45 +07:00
Adnan Zahir 5de81f6315 chore: test image versioning 2026-01-13 16:18:15 +07:00
MacBook Air M1 e50dd096a4 Merge branch 'development' into dev/gio 2026-01-13 16:15:15 +07:00
MacBook Air M1 7551d11888 add filter by kandang id sapronak 2026-01-13 16:14:11 +07:00
Adnan Zahir 7444cfac31 Merge branch 'staging' into 'development'
Staging 13 January 2026

See merge request mbugroup/lti-api!163
2026-01-13 16:10:56 +07:00
aguhh18 1b5b5bc847 feat[BE]: add MarketingType filter to marketing reports and update related validations 2026-01-13 16:10:27 +07:00
ragilap 5d7b613ffc [FIX/BE-US-281] changes calculate fcr egg 2026-01-13 15:39:20 +07:00
ragilap 33e89d65ab [FIX/BE-US-281] changes calculate fcr egg 2026-01-13 15:37:54 +07:00
MacBook Air M1 0f4cc6e379 adjust api closing data produksi 2026-01-13 15:32:43 +07:00
MacBook Air M1 590df26a1f adjust api closing production data 2026-01-13 14:43:37 +07:00
ragilap ce7ce778fd [FIX/BE-US-281] add response validation if weeks not have production standart 2026-01-13 14:39:59 +07:00
ragilap eaa208f733 [FIX/BE-US-281] response recording and add payload record_at only in createOne 2026-01-13 14:11:53 +07:00
aguhh18 b088eebac5 feat[BE]: enhance GetOverhead functionality with project flock kandang count mapping and update related DTOs 2026-01-13 13:36:08 +07:00
aguhh18 3c10866208 feat[BE]: add GetOverheadByProjectFlockKandang endpoint and update related services 2026-01-13 13:20:06 +07:00
aguhh18 f7a392be52 feat[BE]: add GetPenjualanByProjectFlockKandang endpoint and update related services 2026-01-13 11:37:23 +07:00
aguhh18 4bd8319e3b FIX[BE] : fixing typografical error on report marketing 2026-01-13 10:16:28 +07:00
Hafizh A. Y. f7b70d4b14 Merge branch 'feat/BE/Rekapitulasi-hutang-supplier' into 'development'
[FIX/BE] adjustment response

See merge request mbugroup/lti-api!162
2026-01-13 01:55:59 +00:00
Hafizh A. Y. 9f28294dc3 Merge branch 'FIX/BE/fix_chickin_can't_recording_because_replenish_isn't_correct_for_one_flag_product' into 'development'
FIX[BE] ; fixing chikin replenish. and other logic

See merge request mbugroup/lti-api!161
2026-01-13 01:55:42 +00:00
ragilap 6a166ceb86 [FIX/BE-US-390] dashboard statistic hpp global and avg seling price not refrence to filter 2026-01-13 00:15:48 +07:00
ragilap f37bf4d22d Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into feat/BE/Rekapitulasi-hutang-supplier 2026-01-12 16:37:31 +07:00
ragilap ac5edb36e7 [FIX/BE] adjustment response 2026-01-12 16:28:03 +07:00
Hafizh A. Y 8fab5d7d91 feat(BE): price-product-supplier 2026-01-12 14:54:14 +07:00
Hafizh A. Y. 5ddfb2c745 Merge branch 'Feat/BE/Expense_adjust_approval_flow' into 'development'
[Feat][BE}: expense adjust approval flow get rid approval manager change to head area and add business unit vice president approval

See merge request mbugroup/lti-api!159
2026-01-12 06:39:22 +00:00
Hafizh A. Y. 5cfa4d4a59 Merge branch 'fix/daily-checklist' into 'development'
fix route daily checklist

See merge request mbugroup/lti-api!158
2026-01-12 06:38:51 +00:00
aguhh18 80b2cafd2f FIX[BE] ; fixing chikin replenish. and other logic 2026-01-12 13:29:24 +07:00
MacBook Air M1 b47f26d448 adjust max limit location and kandang 2026-01-12 11:30:57 +07:00
M1 AIR 2f22182605 Merge branch 'staging' of https://gitlab.com/mbugroup/lti-api into staging 2026-01-12 11:16:43 +07:00
M1 AIR e2d352721c Merge from development 2026-01-12 11:15:59 +07:00
M1 AIR 068fe4329e Merge remote-tracking branch 'origin/development' into staging 2026-01-12 11:13:24 +07:00
MacBook Air M1 15be8dcbea fix route daily checklist 2026-01-12 11:09:32 +07:00
Hafizh A. Y. 041e8763ac Merge branch 'fix/not-showed-product-supplier' into 'development'
fix(BE): is visible to true in product service

See merge request mbugroup/lti-api!157
2026-01-12 03:39:59 +00:00
Hafizh A. Y. 644e9911e4 Merge branch 'FIX/BE/Case-sensitive' into 'development'
[FIX/BE] adjust case sensitive search To Incase sensitive

See merge request mbugroup/lti-api!156
2026-01-12 03:39:48 +00:00
Hafizh A. Y. bb04cb53d9 Merge branch 'feat/BE/Rekapitulasi-hutang-supplier' into 'development'
feat(BE):Rekapitulasi hutang supplier

See merge request mbugroup/lti-api!155
2026-01-12 03:39:26 +00:00
Hafizh A. Y. 048e607290 Merge branch 'feat/daily-checklist-upload-documents' into 'development'
[FEAT][BE]: add api upload documents daily checklist

See merge request mbugroup/lti-api!154
2026-01-12 03:39:06 +00:00
Hafizh A. Y. 18441eb19f Merge branch 'feat/BE/US-390-Dashboard' into 'development'
[FEAT/BE][US-390 dashboard calculation standart]

See merge request mbugroup/lti-api!152
2026-01-12 03:38:51 +00:00
Hafizh A. Y. 526e14f26e Merge branch 'FIX/BE/Transfer_to_laying' into 'development'
[FIX][BE]: fixing transfer to laying qty doesn't  listed on product warehouse and fixing wrong implementation of fifo stock on laying transfer

See merge request mbugroup/lti-api!151
2026-01-12 03:38:27 +00:00
Hafizh A. Y 539081ce99 fix(BE): is visible to true in product service 2026-01-12 10:37:55 +07:00
MacBook Air M1 d568b87e01 adjust response api summary daily checklist 2026-01-12 10:37:33 +07:00
aguhh18 9515848d8f feat(BE): update approval flow to use head area instead of manager 2026-01-12 10:11:31 +07:00
ragilap c15ff8a211 [FIX/BE] add percent rasio in statistic dashboard 2026-01-12 01:15:31 +07:00
ragilap d1d94357cf [FIX/BE] add clamp maximum value 2026-01-12 00:27:53 +07:00
ragilap 67f5165bfb [FIX/BE] adjust case sensitive search 2026-01-11 23:47:28 +07:00
ragilap 1217f34dcd feat(BE):change standart egg in fcr master data 2026-01-11 22:19:20 +07:00
MacBook Air M1 ae41422776 add api upload documents daily checklist 2026-01-11 22:02:21 +07:00
aguhh18 3978951d8f Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into Feat/BE/Expense_adjust_approval_flow 2026-01-11 21:19:35 +07:00
aguhh18 3422fceec7 feat(BE-ExpenseApproval): add unit vice president approval step and permissions 2026-01-11 20:10:19 +07:00
ragilap 09f1b29359 feat(BE):Rekapitulasi hutang supplier 2026-01-11 19:41:21 +07:00
ragilap 167d18fe87 feat(BE-309): add permission dashboard 2026-01-11 19:26:25 +07:00
ragilap 473f4504ea feat(BE-309): changes COMPARASION TO COMPARISON 2026-01-11 19:09:43 +07:00
ragilap dc7dc0ba47 adjustment meta 2026-01-11 18:54:05 +07:00
ragilap a54129866e feat(BE-390): adjustment calculate dashboard 2026-01-11 18:35:23 +07:00
ragilap d40243be4b Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-390-Dashboard 2026-01-11 18:26:40 +07:00
ragilap 525ff650f2 feat(BE-390): calculation dashboard 2026-01-11 15:40:47 +07:00
aguhh18 c1e9b5a975 FIX[BE]: add laying transfer source and target repositories to transfer laying service 2026-01-11 13:30:19 +07:00
aguhh18 272367d8ef FIX[BE]: fixing transfer to laying and implement correct fifo stock 2026-01-11 12:51:37 +07:00
Hafizh A. Y. d3c7d65bf5 Merge branch 'fix/not-showed-product-supplier' into 'development'
[FIX][BE] Permission in payment route

See merge request mbugroup/lti-api!150
2026-01-11 01:44:41 +00:00
Hafizh A. Y. 944fd860a3 Merge branch 'feat/BE/US-74-76-78-278/adjustment_recording_purchase_project_flock' into 'development'
[FIX][BE-74-76-78-278]:adjustment project flock,recording,purchase getall

See merge request mbugroup/lti-api!149
2026-01-11 01:44:25 +00:00
Hafizh A. Y af79db8726 fix(BE): permission in payment route 2026-01-11 08:41:45 +07:00
ragilap b42ca5e6fb feat(BE-74-76-78-278):delete unused code recording 2026-01-10 21:34:19 +07:00
ragilap 3b2c6f16c3 feat(BE-74-76-78-278):adjustment project flock,recording,purchase getall 2026-01-10 21:22:54 +07:00
Hafizh A. Y. 359e982e76 Merge branch 'fix/not-showed-product-supplier' into 'development'
[FIX][BE] Not showed supplier in master data product

See merge request mbugroup/lti-api!148
2026-01-10 11:41:54 +00:00
Hafizh A. Y bc0bf7fe16 fix(BE): not showed supplier in master data product 2026-01-10 18:25:04 +07:00
Hafizh A. Y. 70a7b1b888 Merge branch 'fix/master-data-internal-server-error' into 'development'
[FIX][BE]: code 500 when delete master data foreign to others table

See merge request mbugroup/lti-api!147
2026-01-09 09:44:21 +00:00
Hafizh A. Y 17d55bd2c0 fix(BE): fix code 500 when delete master data foreign to others table 2026-01-09 16:43:20 +07:00
Hafizh A. Y. 9cc86df1ed Merge branch 'fix/seeder-and-finance' into 'development'
[FIX][BE]: Add party account number in payments

See merge request mbugroup/lti-api!146
2026-01-09 09:06:40 +00:00
Hafizh A. Y b7914e8294 fix(BE): add party account number in payments 2026-01-09 16:03:41 +07:00
kris d33119661a Update .gitlab-ci.yml file 2026-01-09 08:37:55 +00:00
Hafizh A. Y 8a57d439dc unfinish: seeder and fix migration 2026-01-09 14:18:28 +07:00
kris 3d76854273 Update .gitlab-ci.yml file 2026-01-09 04:27:45 +00:00
kris b7a3882f20 Update .gitlab-ci.yml file 2026-01-09 04:19:24 +00:00
M1 AIR 29933a5df9 change cicd 2026-01-09 11:17:49 +07:00
M1 AIR f8aee4be7b penyesuaian flow cicid 2026-01-09 10:58:11 +07:00
Hafizh A. Y. 4ee5bf3628 Merge branch 'feat/BE/US-281-uniformity' into 'development'
Feat/be/us 281 uniformity

See merge request mbugroup/lti-api!145
2026-01-09 03:52:26 +00:00
Hafizh A. Y. 0f06dff761 Merge branch 'dev/teguh' into 'development'
FEAT[BE]: add expense to transfer stock, make  document optional on transfer stock,  fixing closing penjualan bad request

See merge request mbugroup/lti-api!144
2026-01-09 03:51:48 +00:00
Hafizh A. Y. 0629c5ccf6 Merge branch 'dev/daily-checklist' into 'development'
adjust validate patch daily checklist

See merge request mbugroup/lti-api!143
2026-01-09 03:44:27 +00:00
ragilap 43eb1df118 feat(BE-281): fixing duplicate 2026-01-09 10:06:22 +07:00
ragilap 338312edd1 feat(BE-281): unique uniformity weeks 2026-01-09 10:04:31 +07:00
ragilap f7522636e2 feat(BE-281): unique uniformity weeks 2026-01-09 09:27:49 +07:00
aguhh18 b11f03dfda feat(BE): fixing wrong perhitungan biaya 2026-01-09 09:15:53 +07:00
aguhh18 76e65704d7 Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2026-01-08 22:02:59 +07:00
aguhh18 857a3c284b feat(BE): implement movement number generation and refactor transfer creation logic 2026-01-08 22:00:32 +07:00
aguhh18 5606b9c4a3 FIX(BE): fix closing marketing 500 2026-01-08 20:44:56 +07:00
aguhh18 7af78d04dd feat(BE): add file size validation and improve document indexing for transfer creation 2026-01-08 18:52:12 +07:00
aguhh18 2650e919e7 feat(BE): implement expense tracking for stock transfers and enhance related services 2026-01-08 15:14:06 +07:00
MacBook Air M1 c729067ab5 adjust validate pactch daily checklist 2026-01-08 15:08:40 +07:00
Hafizh A. Y. 7b2d3ae025 Merge branch 'dev/daily-checklist' into 'development'
adjust get all phase activity

See merge request mbugroup/lti-api!142
2026-01-08 07:25:06 +00:00
MacBook Air M1 f079bee92a adjust get all phase activity 2026-01-08 13:37:53 +07:00
Hafizh A. Y. 64e8de2344 Merge branch 'feat/BE/US-281-uniformity' into 'development'
feat(BE-281): add types uniformity

See merge request mbugroup/lti-api!141
2026-01-08 06:12:41 +00:00
Hafizh A. Y. 2be9ae36c1 Merge branch 'dev/daily-checklist' into 'development'
Adjust limit for get all employee

See merge request mbugroup/lti-api!140
2026-01-08 06:12:06 +00:00
ragilap 6c08fe23ca feat(BE-281): add types uniformity 2026-01-08 12:40:36 +07:00
MacBook Air M1 f1f7edb9ab Adjust limit for get all employee 2026-01-08 11:50:34 +07:00
Hafizh A. Y. 8a64300ddd Merge branch 'dev/daily-checklist' into 'development'
add master data config checklist

See merge request mbugroup/lti-api!139
2026-01-08 02:18:22 +00:00
Hafizh A. Y. 9164550263 Merge branch 'feat/BE/US-281-uniformity' into 'development'
feat(BE-281): fixing recording error, fixing limit upload uniformity and...

See merge request mbugroup/lti-api!138
2026-01-08 02:17:49 +00:00
MacBook Air M1 a4840fc98a add master data config checklist 2026-01-07 21:37:51 +07:00
ragilap a2d2c4269a feat(BE-281): fixing recording error, fixing limit upload uniformity and purchase, add filter and statistic uniformity 2026-01-07 20:26:27 +07:00
Hafizh A. Y. 90f363bfdb Merge branch 'dev/daily-checklist' into 'development'
add module daily checklist

See merge request mbugroup/lti-api!137
2026-01-07 10:55:49 +00:00
MacBook Air M1 c3f8ae5887 add api daily checklist report 2026-01-07 17:39:17 +07:00
Hafizh A. Y. a7a784970d Merge branch 'dev/teguh' into 'development'
FIX[BE]: fix transfer to laying, fix delete biaya, fix chickin, fix nominal expense, and other

See merge request mbugroup/lti-api!136
2026-01-07 07:25:09 +00:00
aguhh18 18b0663dc6 Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2026-01-07 14:03:08 +07:00
aguhh18 375e057e7c feat(BE): enhance chickin and transfer laying services with product warehouse validation and stockable support 2026-01-07 14:02:39 +07:00
aguhh18 9336289573 feat(BE): add excluded stockables support in FIFO allocation and fetching methods 2026-01-07 13:54:55 +07:00
aguhh18 76d5b6b69a feat(BE): enhance ProjectFlockKandang structure and approval fetching methods 2026-01-07 13:39:54 +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
aguhh18 0a84e427c1 FIX[BE]: fixing bug transfer to laying, delet biaya, nominal expesen e, chickin 2026-01-07 09:27:39 +07:00
MacBook Air M1 dded9e807b add api check uncheck assignment 2026-01-06 23:59:16 +07:00
M1 AIR cad91957b3 Merge development into staging 2026-01-06 18:58:48 +07:00
Hafizh A. Y. fca2d63c6e Merge branch 'dev/gio' into 'development'
fix init population

See merge request mbugroup/lti-api!135
2026-01-06 11:27:13 +00:00
MacBook Air M1 f5a016b74b adjust init population 2026-01-06 17:23:06 +07:00
Hafizh A. Y. 82a7bada05 Merge branch 'dev/gio' into 'development'
feat[BE]: api production result

See merge request mbugroup/lti-api!131
2026-01-06 10:11:25 +00:00
Hafizh A. Y. c6626cb6f5 Merge branch 'development' into 'dev/gio'
# Conflicts:
#   internal/modules/repports/route.go
2026-01-06 10:09:45 +00:00
Hafizh A. Y. ebfa88e721 Merge branch 'dev/daily-checklist' into 'development'
add module daily checklist and master data employee, phase

See merge request mbugroup/lti-api!134
2026-01-06 10:09:12 +00:00
Hafizh A. Y. 705138795c Merge branch 'feat/BE/US-281-adjustment_recording' into 'development'
feat(BE-281): adjustment recording to cascade

See merge request mbugroup/lti-api!133
2026-01-06 10:08:54 +00:00
Hafizh A. Y. 538372a43a Merge branch 'feat/BE/US-281-uniformity' into 'development'
[FIX/BE-281] adjustment sso redirect,adjustment response closing,adjustment uniformity

See merge request mbugroup/lti-api!132
2026-01-06 10:08:24 +00:00
MacBook Air M1 3bd0602525 add daily checklist module;adjust master data;adjust migration 2026-01-06 17:03:55 +07:00
ragilap 7a26ca5fe5 feat(BE-281): adjustment recording to cascade 2026-01-06 17:01:09 +07:00
aguhh18 a08466a28e FIX(BE): update foreign key constraints to use ON DELETE NO ACTION for expense and marketing tables 2026-01-06 12:43:52 +07:00
ragilap 1bdaf63763 feat(BE-281): adjustment sso redirect,adjustment response closing,adjustment uniformity 2026-01-06 12:02:19 +07:00
aguhh18 d8fb427734 feat(BE): add grand total calculation to ExpenseListDTO and update CreateOne method in expense service 2026-01-06 11:12:41 +07:00
aguhh18 c9ebd88e9d Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2026-01-06 10:10:37 +07:00
aguhh18 0c6d42070a feat(BE): add items field to TransferDeliveryDTO in ToTransferDetailDTO function 2026-01-06 09:03:39 +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
MacBook Air M1 8725d79f8f Merge branch 'feat/BE/Sprint-8' into dev/gio 2026-01-02 12:25:50 +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
MacBook Air M1 39909d1c2e first commit api production-result 2026-01-02 11:24:26 +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
aguhh18 556540e97f Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-12-31 14:04:25 +07:00
aguhh18 e421307965 feat(BE): add validation for product category based on flock category in CreateOne method 2025-12-31 13:29:46 +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
aguhh18 1b5437bc01 FIX[BE]Lock chickins, accumulate pending qty, use qty key 2025-12-31 13:09:13 +07:00
aguhh18 7d6573fabd FIC[BE]Use qty column for warehouse updates 2025-12-31 13:05:54 +07:00
aguhh18 ce083bccdc feat(BE): add project flock ID to product warehouse creation for approval process 2025-12-31 12:59:17 +07:00
aguhh18 dc4729c3b9 Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-12-31 11:44:02 +07:00
aguhh18 bec6a93152 FIX[BE]Stop adjusting target product warehouse quantity
Avoid double counting: population entries and ConsumeChickinStocks already update inventory. Pullet/layer for the flock will
be added via other flows (purchase, transfer, etc.)
2025-12-31 11:43:03 +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
aguhh18 42853aaac0 fix[BE]Reset chickin pending usage and skip zero entries 2025-12-31 11:34:57 +07:00
aguhh18 610555c3cf Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into dev/teguh 2025-12-31 10:29:32 +07:00
aguhh18 c60c40af03 feat(be): add GetByProjectFlockKandangIDForUpdate method to lock chickin rows and prevent race conditions 2025-12-31 10:27:15 +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 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
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
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 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
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 411d6fe6a9 feat(BE-281): deleting bw in recording 2025-12-29 09:38:49 +07:00
kris 30231fabe9 Edit Dockerfile 2025-12-18 06:51:52 +00:00
kris e738a97e4c Delete docker-compose.yaml 2025-12-18 06:50:41 +00:00
kris 81f4a5e33e Delete docker-compose.local.yml 2025-12-18 06:50:22 +00:00
kris 1e9fdd2b0d Update .gitlab-ci.yml file 2025-12-18 06:41:04 +00:00
GitLab Deploy Bot b6a60d5009 remove 2025-12-15 09:25:50 +07:00
250 changed files with 15053 additions and 2310 deletions
Vendored
BIN
View File
Binary file not shown.
+4 -1
View File
@@ -9,11 +9,13 @@ main
bin/ bin/
*.exe *.exe
*.out *.out
.air.toml
Makefile Makefile
docker-compose.local.yml docker-compose.local.yml
docker-compose.yaml docker-compose.yaml
Dockerfile
Dockerfile.local Dockerfile.local
.gitlab-ci.yml
# Go build cache # Go build cache
.gocache/ .gocache/
vendor vendor
@@ -27,3 +29,4 @@ coverage/
.vscode/ .vscode/
.idea/ .idea/
*.swp *.swp
.DS_Store
+29 -11
View File
@@ -1,20 +1,38 @@
FROM golang:1.23-alpine # =========================
# Builder stage
# =========================
FROM golang:1.23-alpine AS builder
# Install dependensi dasar RUN apk add --no-cache git ca-certificates tzdata
RUN apk add --no-cache git curl bash build-base WORKDIR /app
# Install Air (pakai repo baru air-verse)
RUN go install github.com/air-verse/air@v1.52.3
WORKDIR /lti-api
# Cache dependencies
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
# Copy source code
COPY . . COPY . .
# Build API binary
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags="-s -w" -o lti-api ./cmd/api
# Build SEED binary (pastikan cmd/seed ada)
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags="-s -w" -o lti-seed ./cmd/seed
# =========================
# Runtime stage
# =========================
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata curl bash postgresql-client \
&& adduser -D -H -u 10001 appuser
WORKDIR /app
COPY --from=builder /app/lti-api /app/lti-api
COPY --from=builder /app/lti-seed /app/lti-seed
USER appuser
EXPOSE 8081 EXPOSE 8081
CMD ["air", "-c", ".air.toml"] CMD ["/app/lti-api"]
+1 -1
View File
@@ -110,4 +110,4 @@ IT Development PT Mitra Berlian Unggas Group
## 📃 License ## 📃 License
This project is private. All rights reserved. > This project is private. All rights reserved.
-77
View File
@@ -1,77 +0,0 @@
services:
postgresdb:
image: postgres:alpine
restart: always
ports:
- "${DB_PORT_HOST:-5542}:5432"
environment:
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
POSTGRES_DB: ${DB_NAME:-db_lti_erp}
volumes:
- dbdata:/var/lib/postgresql/data
- ./internal/database/init:/docker-entrypoint-initdb.d
networks: [go-network]
healthcheck:
test:
[
"CMD-SHELL",
"pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-db_lti_erp}",
]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
restart: unless-stopped
ports:
- "${REDIS_PORT_HOST:-6381}:6379"
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
interval: 5s
timeout: 3s
retries: 10
networks: [go-network]
app:
build:
context: .
dockerfile: Dockerfile.local
image: cosmtrek/air:v1.52.3
working_dir: /lti-api
volumes:
- .:/lti-api
- ./internal/config/jwtRS256.key:/run/keys/jwtRS256.key
- ./internal/config/jwtRS256.key.pub:/run/keys/jwtRS256.key.pub
command: air -c .air.toml
env_file:
- .env
environment:
DB_HOST: postgresdb
DB_PORT: 5432
DB_USER: ${DB_USER:-postgres}
DB_PASSWORD: ${DB_PASSWORD:-postgres}
DB_NAME: ${DB_NAME:-db_lti_erp}
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
ports:
- "${APP_PORT:-8081}:8081"
depends_on:
postgresdb:
condition: service_healthy
networks: [go-network]
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8081/healthz || exit 1"]
interval: 10s
timeout: 3s
retries: 10
start_period: 10s
volumes:
dbdata:
go-mod-cache:
go-build-cache:
networks:
go-network:
name: lti-api_go-network
driver: bridge
-98
View File
@@ -1,98 +0,0 @@
services:
dev-api-lti:
build:
context: .
dockerfile: Dockerfile
container_name: dev-api-lti
working_dir: /lti-api
command: ["/bin/sh", "scripts/entrypoint.sh"]
ports:
- "8081:8081"
env_file:
- .env
environment:
# override agar koneksi ke container internal
DB_HOST: dev-postgres-lti
DB_PORT: 5432
REDIS_URL: redis://dev-redis-lti:6379/0
volumes:
- .:/lti-api
- ./.air.toml:/lti-api/.air.toml:ro
- ./internal/config/jwtRS256.key:/run/keys/jwtRS256.key
- ./internal/config/jwtRS256.key.pub:/run/keys/jwtRS256.key.pub
depends_on:
- dev-postgres-lti
- dev-redis-lti
networks:
- lti-network
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8081/healthz || exit 1"]
interval: 10s
timeout: 3s
retries: 10
start_period: 10s
deploy:
resources:
limits:
cpus: "2.0"
memory: 2G
reservations:
cpus: "1.0"
memory: 512M
dev-postgres-lti:
image: postgres:15-alpine
container_name: dev-postgres-lti
restart: always
env_file:
- credential/.env.db
ports:
- "5433:5432"
volumes:
- dev-postgres-lti-data:/var/lib/postgresql/data
- ./credential:/docker-entrypoint-initdb.d:ro
networks:
- lti-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-db_lti_erp}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 5s
deploy:
resources:
limits:
cpus: "1.0"
memory: 2G
reservations:
cpus: "0.5"
memory: 512M
dev-redis-lti:
image: redis:7-alpine
container_name: dev-redis-lti
restart: always
ports:
- "6380:6379"
networks:
- lti-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 10
deploy:
resources:
limits:
cpus: "0.5"
memory: 512M
reservations:
cpus: "0.2"
memory: 256M
networks:
lti-network:
driver: bridge
volumes:
dev-postgres-lti-data:
+1 -1
View File
@@ -16,6 +16,7 @@ require (
github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/jackc/pgconn v1.14.1 github.com/jackc/pgconn v1.14.1
github.com/jackc/pgx/v5 v5.5.5
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
@@ -60,7 +61,6 @@ require (
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.2 // indirect github.com/jackc/pgproto3/v2 v2.3.2 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.5.5 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
-4
View File
@@ -262,14 +262,10 @@ github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVS
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 h1:llb0neMWDQe87IzJLS4Ci7psK/lVsjIS2otl+1WyRyY=
github.com/xuri/efp v0.0.0-20240408161823-9ad904a10d6d/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= 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 h1:1tgOaEq92IOEumR1/JfYS/eR0KHOCsRv/rYXXh6YJQE=
github.com/xuri/excelize/v2 v2.9.0/go.mod h1:uqey4QBZ9gdMeWApPLdhm9x+9o2lq4iVmjiLfBS5hdE= 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 h1:hPVCafDV85blFTabnqKgNhDCkJX25eik94Si9cTER4A=
github.com/xuri/nfp v0.0.0-20240318013403-ab9948c2c4a7/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= 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=
@@ -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
}
+28 -10
View File
@@ -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
@@ -230,7 +228,13 @@ func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*St
switch { switch {
case delta > 0: case delta > 0:
allocationRes, err := s.allocateFromStock(ctx, tx, productWarehouseID, req.UsableKey, req.UsableID, delta)
var excludedStockables []fifo.StockableKey
if cfg.ExcludedStockables != nil {
excludedStockables = cfg.ExcludedStockables
}
allocationRes, err := s.allocateFromStock(ctx, tx, productWarehouseID, req.UsableKey, req.UsableID, delta, excludedStockables)
if err != nil { if err != nil {
return err return err
} }
@@ -285,7 +289,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 +302,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 +312,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 {
@@ -415,8 +416,9 @@ func (s *fifoService) allocateFromStock(
usableKey fifo.UsableKey, usableKey fifo.UsableKey,
usableID uint, usableID uint,
requestQty float64, requestQty float64,
excludedStockables []fifo.StockableKey,
) (*allocationOutcome, error) { ) (*allocationOutcome, error) {
lots, err := s.fetchStockLots(ctx, tx, productWarehouseID) lots, err := s.fetchStockLots(ctx, tx, productWarehouseID, excludedStockables)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -497,14 +499,24 @@ func (s *fifoService) allocateFromStock(
}, nil }, nil
} }
func (s *fifoService) fetchStockLots(ctx context.Context, tx *gorm.DB, productWarehouseID uint) ([]stockLot, error) { func (s *fifoService) fetchStockLots(ctx context.Context, tx *gorm.DB, productWarehouseID uint, excludedStockables []fifo.StockableKey) ([]stockLot, error) {
configs := fifo.Stockables() configs := fifo.Stockables()
if len(configs) == 0 { if len(configs) == 0 {
return nil, nil return nil, nil
} }
// Create exclusion set for faster lookup
excludedSet := make(map[fifo.StockableKey]bool)
for _, key := range excludedStockables {
excludedSet[key] = true
}
var lots []stockLot var lots []stockLot
for key, cfg := range configs { for key, cfg := range configs {
// Skip excluded stockables
if excludedSet[key] {
continue
}
usesNumericTime := cfg.Columns.CreatedAt == cfg.Columns.ID usesNumericTime := cfg.Columns.CreatedAt == cfg.Columns.ID
@@ -621,7 +633,13 @@ func (s *fifoService) resolvePendingForWarehouse(ctx context.Context, tx *gorm.D
continue continue
} }
outcome, err := s.allocateFromStock(ctx, tx, productWarehouseID, candidate.UsableKey, candidate.UsableID, candidate.Pending) // Get excluded stockables from candidate usable config
var excludedStockables []fifo.StockableKey
if candidate.Config.ExcludedStockables != nil {
excludedStockables = candidate.Config.ExcludedStockables
}
outcome, err := s.allocateFromStock(ctx, tx, productWarehouseID, candidate.UsableKey, candidate.UsableID, candidate.Pending, excludedStockables)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -715,7 +733,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 {
+2
View File
@@ -54,6 +54,7 @@ var (
SSOAuthorizeURL string SSOAuthorizeURL string
SSOTokenURL string SSOTokenURL string
SSOGetMeURL string SSOGetMeURL string
SSOPortalURL string
SSOClients map[string]SSOClientConfig SSOClients map[string]SSOClientConfig
SSOAccessCookieName string SSOAccessCookieName string
SSORefreshCookieName string SSORefreshCookieName string
@@ -131,6 +132,7 @@ func init() {
SSOAuthorizeURL = viper.GetString("SSO_AUTHORIZE_URL") SSOAuthorizeURL = viper.GetString("SSO_AUTHORIZE_URL")
SSOTokenURL = viper.GetString("SSO_TOKEN_URL") SSOTokenURL = viper.GetString("SSO_TOKEN_URL")
SSOGetMeURL = viper.GetString("SSO_GETME_URL") SSOGetMeURL = viper.GetString("SSO_GETME_URL")
SSOPortalURL = strings.TrimSpace(viper.GetString("SSO_PORTAL_URL"))
SSOAccessCookieName = defaultString(viper.GetString("SSO_ACCESS_COOKIE_NAME"), "sso_access") SSOAccessCookieName = defaultString(viper.GetString("SSO_ACCESS_COOKIE_NAME"), "sso_access")
SSORefreshCookieName = defaultString(viper.GetString("SSO_REFRESH_COOKIE_NAME"), "sso_refresh") SSORefreshCookieName = defaultString(viper.GetString("SSO_REFRESH_COOKIE_NAME"), "sso_refresh")
SSOCookieDomain = viper.GetString("SSO_COOKIE_DOMAIN") SSOCookieDomain = viper.GetString("SSO_COOKIE_DOMAIN")
+1
View File
@@ -13,6 +13,7 @@ func FiberConfig() fiber.Config {
CaseSensitive: true, CaseSensitive: true,
ServerHeader: "Fiber", ServerHeader: "Fiber",
AppName: "Fiber API", AppName: "Fiber API",
BodyLimit: 8 * 1024 * 1024,
ErrorHandler: utils.ErrorHandler, ErrorHandler: utils.ErrorHandler,
JSONEncoder: sonic.Marshal, JSONEncoder: sonic.Marshal,
JSONDecoder: sonic.Unmarshal, JSONDecoder: sonic.Unmarshal,
@@ -1 +1,2 @@
DROP TABLE IF EXISTS expenses; DROP SEQUENCE IF EXISTS expenses_ref_seq;
DROP TABLE IF EXISTS expenses;
@@ -1,3 +1,3 @@
-- Drop function and sequence for sales order numbers -- Drop function and sequence for sales order numbers
DROP FUNCTION IF EXISTS generate_so_number();
DROP SEQUENCE IF EXISTS so_number_seq; DROP SEQUENCE IF EXISTS so_number_seq;
DROP FUNCTION IF EXISTS generate_so_number();
@@ -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,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,14 @@
-- Drop tables in correct order (child tables before parent tables)
DROP TABLE IF EXISTS daily_checklist_activity_task_assignments; -- Child table with FK to daily_checklist_activity_tasks
DROP TABLE IF EXISTS daily_checklist_activity_task_assignees;
DROP TABLE IF EXISTS daily_checklist_activity_tasks;
DROP TABLE IF EXISTS daily_checklist_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,21 @@
BEGIN;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_recordings_project_flock_kandang'
) THEN
ALTER TABLE recordings
DROP CONSTRAINT fk_recordings_project_flock_kandang;
END IF;
END $$;
ALTER TABLE recordings
ADD CONSTRAINT fk_recordings_project_flock_kandang
FOREIGN KEY (project_flock_kandangs_id)
REFERENCES project_flock_kandangs (id)
ON DELETE RESTRICT ON UPDATE CASCADE;
COMMIT;
@@ -0,0 +1,21 @@
BEGIN;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_recordings_project_flock_kandang'
) THEN
ALTER TABLE recordings
DROP CONSTRAINT fk_recordings_project_flock_kandang;
END IF;
END $$;
ALTER TABLE recordings
ADD CONSTRAINT fk_recordings_project_flock_kandang
FOREIGN KEY (project_flock_kandangs_id)
REFERENCES project_flock_kandangs (id)
ON DELETE CASCADE ON UPDATE CASCADE;
COMMIT;
@@ -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,15 @@
-- Revert back to NO ACTION (RESTRICT behavior)
ALTER TABLE expense_nonstocks DROP CONSTRAINT IF EXISTS fk_expense_nonstocks_expense_id;
ALTER TABLE expense_nonstocks
ADD CONSTRAINT fk_expense_nonstocks_expense_id
FOREIGN KEY (expense_id) REFERENCES expenses(id)
ON DELETE NO ACTION;
-- Revert expense_realizations FK
ALTER TABLE expense_realizations DROP CONSTRAINT IF EXISTS fk_expense_realizations_nonstock_id;
ALTER TABLE expense_realizations
ADD CONSTRAINT fk_expense_realizations_nonstock_id
FOREIGN KEY (expense_nonstock_id) REFERENCES expense_nonstocks(id)
ON DELETE NO ACTION;
@@ -0,0 +1,16 @@
-- Drop existing FK constraints
ALTER TABLE expense_nonstocks DROP CONSTRAINT IF EXISTS fk_expense_nonstocks_expense_id;
-- Recreate with ON DELETE CASCADE
ALTER TABLE expense_nonstocks
ADD CONSTRAINT fk_expense_nonstocks_expense_id
FOREIGN KEY (expense_id) REFERENCES expenses(id)
ON DELETE CASCADE;
-- Drop and recreate expense_realizations FK
ALTER TABLE expense_realizations DROP CONSTRAINT IF EXISTS fk_expense_realizations_nonstock_id;
ALTER TABLE expense_realizations
ADD CONSTRAINT fk_expense_realizations_nonstock_id
FOREIGN KEY (expense_nonstock_id) REFERENCES expense_nonstocks(id)
ON DELETE CASCADE;
@@ -0,0 +1,20 @@
-- Revert back to NO ACTION (for rollback safety)
DO $$
BEGIN
ALTER TABLE marketing_products DROP CONSTRAINT IF EXISTS fk_marketing_products_marketing_id;
ALTER TABLE marketing_products
ADD CONSTRAINT fk_marketing_products_marketing_id
FOREIGN KEY (marketing_id) REFERENCES marketings(id)
ON DELETE NO ACTION;
END $$;
DO $$
BEGIN
ALTER TABLE marketing_delivery_products DROP CONSTRAINT IF EXISTS fk_marketing_delivery_products_marketing_product_id;
ALTER TABLE marketing_delivery_products
ADD CONSTRAINT fk_marketing_delivery_products_marketing_product_id
FOREIGN KEY (marketing_product_id) REFERENCES marketing_products(id)
ON DELETE NO ACTION;
END $$;
@@ -0,0 +1,35 @@
-- Ensure marketing_products FK is CASCADE (it should already be, but let's make sure)
DO $$
BEGIN
-- Drop existing FK if exists
IF EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'fk_marketing_products_marketing_id'
) THEN
ALTER TABLE marketing_products DROP CONSTRAINT fk_marketing_products_marketing_id;
END IF;
-- Recreate with ON DELETE CASCADE
ALTER TABLE marketing_products
ADD CONSTRAINT fk_marketing_products_marketing_id
FOREIGN KEY (marketing_id) REFERENCES marketings(id)
ON DELETE CASCADE;
END $$;
-- Ensure marketing_delivery_products FK is CASCADE
DO $$
BEGIN
-- Drop existing FK if exists
IF EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'fk_marketing_delivery_products_marketing_product_id'
) THEN
ALTER TABLE marketing_delivery_products DROP CONSTRAINT fk_marketing_delivery_products_marketing_product_id;
END IF;
-- Recreate with ON DELETE CASCADE
ALTER TABLE marketing_delivery_products
ADD CONSTRAINT fk_marketing_delivery_products_marketing_product_id
FOREIGN KEY (marketing_product_id) REFERENCES marketing_products(id)
ON DELETE CASCADE;
END $$;
@@ -0,0 +1,9 @@
-- Drop foreign key and column
ALTER TABLE laying_transfers
DROP CONSTRAINT IF EXISTS fk_laying_transfers_product_warehouse_id;
ALTER TABLE laying_transfers
DROP COLUMN IF EXISTS product_warehouse_id;
-- Drop index
DROP INDEX IF EXISTS idx_laying_transfers_product_warehouse_id;
@@ -0,0 +1,19 @@
-- Add product_warehouse_id to laying_transfers for FIFO support
ALTER TABLE laying_transfers
ADD COLUMN product_warehouse_id BIGINT;
-- Add foreign key
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
ALTER TABLE laying_transfers
ADD CONSTRAINT fk_laying_transfers_product_warehouse_id
FOREIGN KEY (product_warehouse_id)
REFERENCES product_warehouses(id)
ON DELETE SET NULL;
END IF;
END $$;
-- Add index
CREATE INDEX idx_laying_transfers_product_warehouse_id
ON laying_transfers(product_warehouse_id);
@@ -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,14 @@
-- Rollback: Remove STOCKABLE fields from laying_transfers
-- Drop index
DROP INDEX IF EXISTS idx_laying_transfers_dest_product_warehouse_id;
-- Drop foreign key constraint
ALTER TABLE laying_transfers
DROP CONSTRAINT IF EXISTS fk_laying_transfers_dest_product_warehouse_id;
-- Drop columns
ALTER TABLE laying_transfers
DROP COLUMN IF EXISTS dest_product_warehouse_id,
DROP COLUMN IF EXISTS total_qty,
DROP COLUMN IF EXISTS total_used;
@@ -0,0 +1,30 @@
-- Add STOCKABLE fields to laying_transfers for destination warehouse
-- This enables Transfer to Laying to work as DUAL ROLE (Stockable + Usable)
-- Add columns for STOCKABLE role (destination warehouse)
ALTER TABLE laying_transfers
ADD COLUMN dest_product_warehouse_id BIGINT,
ADD COLUMN total_qty NUMERIC(15, 3) DEFAULT 0,
ADD COLUMN total_used NUMERIC(15, 3) DEFAULT 0;
-- Add foreign key constraint
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
ALTER TABLE laying_transfers
ADD CONSTRAINT fk_laying_transfers_dest_product_warehouse_id
FOREIGN KEY (dest_product_warehouse_id)
REFERENCES product_warehouses(id)
ON DELETE SET NULL;
END IF;
END $$;
-- Add index for performance
CREATE INDEX idx_laying_transfers_dest_product_warehouse_id
ON laying_transfers(dest_product_warehouse_id);
-- Add comment for documentation
COMMENT ON COLUMN laying_transfers.product_warehouse_id IS 'Product warehouse at source (Growing flock) - for USABLE role';
COMMENT ON COLUMN laying_transfers.dest_product_warehouse_id IS 'Product warehouse at destination (Laying flock) - for STOCKABLE role';
COMMENT ON COLUMN laying_transfers.total_qty IS 'Total lot quantity introduced to destination warehouse - for STOCKABLE role';
COMMENT ON COLUMN laying_transfers.total_used IS 'Quantity already consumed from this lot at destination - for STOCKABLE role';
@@ -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,7 @@
-- Remove chart_data, uniform_date, and related indexes
DROP INDEX IF EXISTS idx_project_flock_kandang_uniformity_uniform_date;
DROP INDEX IF EXISTS idx_project_flock_kandang_uniformity_unique;
ALTER TABLE project_flock_kandang_uniformity
DROP COLUMN IF EXISTS chart_data,
DROP COLUMN IF EXISTS uniform_date;
@@ -0,0 +1,25 @@
-- Add uniform_date (if missing), chart_data, and unique constraint for uniformity records
ALTER TABLE project_flock_kandang_uniformity
ADD COLUMN IF NOT EXISTS uniform_date TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS chart_data JSONB;
DO $$
BEGIN
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
CREATE UNIQUE INDEX IF NOT EXISTS idx_project_flock_kandang_uniformity_unique
ON project_flock_kandang_uniformity (project_flock_kandang_id, week, uniform_date)
WHERE deleted_at IS NULL;
ELSE
CREATE UNIQUE INDEX IF NOT EXISTS idx_project_flock_kandang_uniformity_unique
ON project_flock_kandang_uniformity (project_flock_kandang_id, week, uniform_date);
END IF;
END $$;
CREATE INDEX IF NOT EXISTS idx_project_flock_kandang_uniformity_uniform_date
ON project_flock_kandang_uniformity (uniform_date);
@@ -0,0 +1,4 @@
-- Remove expense_nonstock_id from stock_transfer_details
ALTER TABLE stock_transfer_details DROP CONSTRAINT IF EXISTS fk_stock_transfer_details_expense_nonstock;
ALTER TABLE stock_transfer_details DROP COLUMN IF EXISTS expense_nonstock_id;
DROP INDEX IF EXISTS idx_stock_transfer_details_expense_nonstock_id;
@@ -0,0 +1,10 @@
-- Add expense_nonstock_id to stock_transfer_details
-- This allows tracking expedition/transport costs for stock transfers (same as purchase)
ALTER TABLE stock_transfer_details
ADD COLUMN expense_nonstock_id BIGINT,
ADD CONSTRAINT fk_stock_transfer_details_expense_nonstock
FOREIGN KEY (expense_nonstock_id) REFERENCES expense_nonstocks(id) ON DELETE SET NULL;
-- Create index for better query performance
CREATE INDEX idx_stock_transfer_details_expense_nonstock_id ON stock_transfer_details(expense_nonstock_id);
@@ -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
);
@@ -0,0 +1,6 @@
BEGIN;
ALTER TABLE payments
DROP COLUMN IF EXISTS party_account_number;
COMMIT;
@@ -0,0 +1,6 @@
BEGIN;
ALTER TABLE payments
ADD COLUMN IF NOT EXISTS party_account_number VARCHAR(50);
COMMIT;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS projects;
@@ -0,0 +1 @@
DROP TABLE IF EXISTS projects;
@@ -0,0 +1,20 @@
-- Revert master data foreign keys to CASCADE delete (except FCR)
ALTER TABLE nonstock_suppliers
DROP CONSTRAINT IF EXISTS nonstock_suppliers_nonstock_id_fkey,
DROP CONSTRAINT IF EXISTS nonstock_suppliers_supplier_id_fkey;
ALTER TABLE nonstock_suppliers
ADD CONSTRAINT nonstock_suppliers_nonstock_id_fkey FOREIGN KEY (nonstock_id)
REFERENCES nonstocks (id) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT nonstock_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id)
REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE product_suppliers
DROP CONSTRAINT IF EXISTS product_suppliers_product_id_fkey,
DROP CONSTRAINT IF EXISTS product_suppliers_supplier_id_fkey;
ALTER TABLE product_suppliers
ADD CONSTRAINT product_suppliers_product_id_fkey FOREIGN KEY (product_id)
REFERENCES products (id) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT product_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id)
REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,20 @@
-- Update master data foreign keys to RESTRICT delete (except FCR)
ALTER TABLE nonstock_suppliers
DROP CONSTRAINT IF EXISTS nonstock_suppliers_nonstock_id_fkey,
DROP CONSTRAINT IF EXISTS nonstock_suppliers_supplier_id_fkey;
ALTER TABLE nonstock_suppliers
ADD CONSTRAINT nonstock_suppliers_nonstock_id_fkey FOREIGN KEY (nonstock_id)
REFERENCES nonstocks (id) ON DELETE RESTRICT ON UPDATE CASCADE,
ADD CONSTRAINT nonstock_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id)
REFERENCES suppliers (id) ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE product_suppliers
DROP CONSTRAINT IF EXISTS product_suppliers_product_id_fkey,
DROP CONSTRAINT IF EXISTS product_suppliers_supplier_id_fkey;
ALTER TABLE product_suppliers
ADD CONSTRAINT product_suppliers_product_id_fkey FOREIGN KEY (product_id)
REFERENCES products (id) ON DELETE RESTRICT ON UPDATE CASCADE,
ADD CONSTRAINT product_suppliers_supplier_id_fkey FOREIGN KEY (supplier_id)
REFERENCES suppliers (id) ON DELETE RESTRICT ON UPDATE CASCADE;
@@ -0,0 +1,79 @@
-- Rollback: Revert FIFO fields back to laying_transfers from detail tables
-- ============================================================================
-- PART 1: Remove FIFO columns from detail tables
-- ============================================================================
-- Add back old qty column first
ALTER TABLE laying_transfer_sources
ADD COLUMN IF NOT EXISTS qty NUMERIC(15, 3) NOT NULL DEFAULT 0;
ALTER TABLE laying_transfer_targets
ADD COLUMN IF NOT EXISTS qty NUMERIC(15, 3) NOT NULL DEFAULT 0;
-- Now drop FIFO columns
ALTER TABLE laying_transfer_sources
DROP COLUMN IF EXISTS usage_qty,
DROP COLUMN IF EXISTS pending_usage_qty;
ALTER TABLE laying_transfer_targets
DROP COLUMN IF EXISTS total_qty,
DROP COLUMN IF EXISTS total_used;
-- ============================================================================
-- PART 2: Add back FIFO columns to laying_transfers table
-- ============================================================================
-- Add columns back for USABLE role (source warehouse)
ALTER TABLE laying_transfers
ADD COLUMN product_warehouse_id BIGINT,
ADD COLUMN pending_usage_qty NUMERIC(15, 3),
ADD COLUMN usage_qty NUMERIC(15, 3);
-- Add columns back for STOCKABLE role (destination warehouse)
ALTER TABLE laying_transfers
ADD COLUMN dest_product_warehouse_id BIGINT,
ADD COLUMN total_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL,
ADD COLUMN total_used NUMERIC(15, 3) DEFAULT 0 NOT NULL;
-- ============================================================================
-- PART 3: Recreate foreign key constraints
-- ============================================================================
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
-- Add source product warehouse FK
ALTER TABLE laying_transfers
ADD CONSTRAINT fk_laying_transfers_product_warehouse_id
FOREIGN KEY (product_warehouse_id)
REFERENCES product_warehouses(id)
ON DELETE SET NULL;
-- Add destination product warehouse FK
ALTER TABLE laying_transfers
ADD CONSTRAINT fk_laying_transfers_dest_product_warehouse_id
FOREIGN KEY (dest_product_warehouse_id)
REFERENCES product_warehouses(id)
ON DELETE SET NULL;
END IF;
END $$;
-- ============================================================================
-- PART 4: Recreate indexes for performance
-- ============================================================================
CREATE INDEX idx_laying_transfers_product_warehouse_id
ON laying_transfers(product_warehouse_id);
CREATE INDEX idx_laying_transfers_dest_product_warehouse_id
ON laying_transfers(dest_product_warehouse_id);
-- ============================================================================
-- PART 5: Recreate comments for documentation
-- ============================================================================
COMMENT ON COLUMN laying_transfers.product_warehouse_id IS 'Product warehouse at source (Growing flock) - for USABLE role';
COMMENT ON COLUMN laying_transfers.dest_product_warehouse_id IS 'Product warehouse at destination (Laying flock) - for STOCKABLE role';
COMMENT ON COLUMN laying_transfers.total_qty IS 'Total lot quantity introduced to destination warehouse - for STOCKABLE role';
COMMENT ON COLUMN laying_transfers.total_used IS 'Quantity already consumed from this lot at destination - for FIFO STOCKABLE role';
@@ -0,0 +1,73 @@
-- Move FIFO fields from laying_transfers to detail tables (sources & targets)
-- This enables proper FIFO integration for transfer laying with multiple sources and targets
-- ============================================================================
-- PART 1: Remove FIFO-related columns from laying_transfers table
-- ============================================================================
-- Drop foreign key constraints first
DO $$
BEGIN
-- Drop source product warehouse FK
IF EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'fk_laying_transfers_product_warehouse_id'
) THEN
ALTER TABLE laying_transfers
DROP CONSTRAINT fk_laying_transfers_product_warehouse_id;
END IF;
-- Drop destination product warehouse FK
IF EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'fk_laying_transfers_dest_product_warehouse_id'
) THEN
ALTER TABLE laying_transfers
DROP CONSTRAINT fk_laying_transfers_dest_product_warehouse_id;
END IF;
END $$;
-- Drop indexes
DROP INDEX IF EXISTS idx_laying_transfers_product_warehouse_id;
DROP INDEX IF EXISTS idx_laying_transfers_dest_product_warehouse_id;
-- Remove columns from laying_transfers
ALTER TABLE laying_transfers
DROP COLUMN IF EXISTS product_warehouse_id,
DROP COLUMN IF EXISTS dest_product_warehouse_id,
DROP COLUMN IF EXISTS pending_usage_qty,
DROP COLUMN IF EXISTS usage_qty,
DROP COLUMN IF EXISTS total_qty,
DROP COLUMN IF EXISTS total_used;
-- ============================================================================
-- PART 2: Add FIFO columns to laying_transfer_sources (USABLE role)
-- ============================================================================
ALTER TABLE laying_transfer_sources
ADD COLUMN usage_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL,
ADD COLUMN pending_usage_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL;
-- Add comments for documentation
COMMENT ON COLUMN laying_transfer_sources.usage_qty IS 'Quantity consumed from this source - for FIFO USABLE role';
COMMENT ON COLUMN laying_transfer_sources.pending_usage_qty IS 'Quantity pending to consume from this source - for FIFO USABLE role';
-- Drop old qty column as it's replaced by usage_qty
ALTER TABLE laying_transfer_sources
DROP COLUMN IF EXISTS qty;
-- ============================================================================
-- PART 3: Add FIFO columns to laying_transfer_targets (STOCKABLE role)
-- ============================================================================
ALTER TABLE laying_transfer_targets
ADD COLUMN total_qty NUMERIC(15, 3) DEFAULT 0 NOT NULL,
ADD COLUMN total_used NUMERIC(15, 3) DEFAULT 0 NOT NULL;
-- Add comments for documentation
COMMENT ON COLUMN laying_transfer_targets.total_qty IS 'Total lot quantity introduced to this target warehouse - for FIFO STOCKABLE role';
COMMENT ON COLUMN laying_transfer_targets.total_used IS 'Quantity already consumed from this lot at target warehouse - for FIFO STOCKABLE role';
-- Drop old qty column as it's replaced by total_qty
ALTER TABLE laying_transfer_targets
DROP COLUMN IF EXISTS qty;
@@ -0,0 +1,59 @@
BEGIN;
ALTER TABLE recordings
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v4;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'recordings' AND column_name = 'hen_day'
) THEN
ALTER TABLE recordings RENAME COLUMN hen_day TO hand_day;
END IF;
END $$;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'recordings' AND column_name = 'hen_house'
) THEN
ALTER TABLE recordings RENAME COLUMN hen_house TO hand_house;
END IF;
END $$;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'recordings' AND column_name = 'egg_mass'
) THEN
ALTER TABLE recordings RENAME COLUMN egg_mass TO egg_mesh;
END IF;
END $$;
ALTER TABLE recordings
ADD COLUMN IF NOT EXISTS daily_gain NUMERIC(7,3),
ADD COLUMN IF NOT EXISTS avg_daily_gain NUMERIC(7,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)
);
COMMIT;
@@ -0,0 +1,57 @@
BEGIN;
ALTER TABLE recordings
DROP CONSTRAINT IF EXISTS chk_recordings_nonnegatives_v3;
ALTER TABLE recordings
DROP COLUMN IF EXISTS daily_gain,
DROP COLUMN IF EXISTS avg_daily_gain;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'recordings' AND column_name = 'hand_day'
) THEN
ALTER TABLE recordings RENAME COLUMN hand_day TO hen_day;
END IF;
END $$;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'recordings' AND column_name = 'hand_house'
) THEN
ALTER TABLE recordings RENAME COLUMN hand_house TO hen_house;
END IF;
END $$;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'recordings' AND column_name = 'egg_mesh'
) THEN
ALTER TABLE recordings RENAME COLUMN egg_mesh TO egg_mass;
END IF;
END $$;
ALTER TABLE recordings
ADD CONSTRAINT chk_recordings_nonnegatives_v4 CHECK (
(total_depletion_qty IS NULL OR total_depletion_qty >= 0) AND
(cum_depletion_rate IS NULL OR cum_depletion_rate >= 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
(hen_day IS NULL OR hen_day >= 0) AND
(hen_house IS NULL OR hen_house >= 0) AND
(feed_intake IS NULL OR feed_intake >= 0) AND
(egg_mass IS NULL OR egg_mass >= 0) AND
(egg_weight IS NULL OR egg_weight >= 0)
);
COMMIT;
@@ -0,0 +1,6 @@
-- Rollback: remove price from supplier relations
ALTER TABLE product_suppliers
DROP COLUMN IF EXISTS price;
ALTER TABLE nonstock_suppliers
DROP COLUMN IF EXISTS price;
@@ -0,0 +1,6 @@
-- Migration: add price to supplier relations
ALTER TABLE product_suppliers
ADD COLUMN IF NOT EXISTS price NUMERIC(15, 3) NOT NULL DEFAULT 0;
ALTER TABLE nonstock_suppliers
ADD COLUMN IF NOT EXISTS price NUMERIC(15, 3) NOT NULL DEFAULT 0;
+1
View File
@@ -299,6 +299,7 @@ func seedProducts(tx *gorm.DB, createdBy uint, uoms map[string]uint, categories
Tax: tax, Tax: tax,
ExpiryPeriod: seed.Expiry, ExpiryPeriod: seed.Expiry,
CreatedBy: createdBy, CreatedBy: createdBy,
IsVisible: seed.IsVisible,
} }
if err := tx.Create(&product).Error; err != nil { if err := tx.Create(&product).Error; err != nil {
return err return err
+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"`
}
+18
View File
@@ -0,0 +1,18 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type Dashboard 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"`
}
+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"`
}
+9 -9
View File
@@ -5,15 +5,15 @@ import (
) )
type ExpenseNonstock struct { type ExpenseNonstock struct {
Id uint64 `gorm:"primaryKey;autoIncrement"` Id uint64 `gorm:"primaryKey;autoIncrement"`
ExpenseId *uint64 `gorm:""` ExpenseId *uint64 `gorm:""`
ProjectFlockKandangId *uint64 `gorm:""` ProjectFlockKandangId *uint64 `gorm:""`
KandangId *uint64 `gorm:""` KandangId *uint64 `gorm:""`
NonstockId *uint64 `gorm:""` NonstockId *uint64 `gorm:""`
Qty float64 `gorm:"type:numeric(15,3);not null"` Qty float64 `gorm:"type:numeric(15,3);not null"`
Price float64 `gorm:"type:numeric(15,3);not null;column:price"` Price float64 `gorm:"type:numeric(15,3);not null;column:price"`
Notes string `gorm:"type:text;column:notes"` Notes string `gorm:"type:text;column:notes"`
CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"` CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"`
Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"` Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"`
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
+6 -8
View File
@@ -12,18 +12,16 @@ type LayingTransfer struct {
FromProjectFlockId uint `gorm:"not null"` FromProjectFlockId uint `gorm:"not null"`
ToProjectFlockId uint `gorm:"not null"` ToProjectFlockId uint `gorm:"not null"`
TransferDate time.Time `gorm:"type:date;not null"` TransferDate time.Time `gorm:"type:date;not null"`
PendingUsageQty *float64 `gorm:"type:numeric(15,3)"`
UsageQty *float64 `gorm:"type:numeric(15,3)"`
Notes string `gorm:"type:text"` Notes string `gorm:"type:text"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"` DeletedAt gorm.DeletedAt `gorm:"index"`
FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"` FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"`
ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"` ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"` Sources []LayingTransferSource `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"` Targets []LayingTransferTarget `gorm:"foreignKey:LayingTransferId;constraint:OnDelete:CASCADE"`
LatestApproval *Approval `gorm:"-" json:"-"` LatestApproval *Approval `gorm:"-" json:"-"`
} }
+2 -1
View File
@@ -11,7 +11,8 @@ type LayingTransferSource struct {
LayingTransferId uint `gorm:"index;not null"` LayingTransferId uint `gorm:"index;not null"`
SourceProjectFlockKandangId uint `gorm:"not null"` SourceProjectFlockKandangId uint `gorm:"not null"`
ProductWarehouseId *uint `gorm:""` ProductWarehouseId *uint `gorm:""`
Qty float64 `gorm:"type:numeric(15,3);not null"` UsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO USABLE field
Note string `gorm:"type:text"` Note string `gorm:"type:text"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
+2 -1
View File
@@ -10,7 +10,8 @@ type LayingTransferTarget struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
LayingTransferId uint `gorm:"index;not null"` LayingTransferId uint `gorm:"index;not null"`
TargetProjectFlockKandangId uint `gorm:"not null"` TargetProjectFlockKandangId uint `gorm:"not null"`
Qty float64 `gorm:"type:numeric(15,3);not null"` TotalQty float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO STOCKABLE field
TotalUsed float64 `gorm:"type:numeric(15,3);default:0;not null"` // FIFO STOCKABLE field
ProductWarehouseId *uint `gorm:""` ProductWarehouseId *uint `gorm:""`
Note string `gorm:"type:text"` Note string `gorm:"type:text"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
+1
View File
@@ -5,6 +5,7 @@ import "time"
type NonstockSupplier struct { type NonstockSupplier struct {
NonstockId uint `gorm:"not null"` NonstockId uint `gorm:"not null"`
SupplierId uint `gorm:"not null"` SupplierId uint `gorm:"not null"`
Price float64 `gorm:"type:numeric(15,3);not null;default:0"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
Nonstock Nonstock `gorm:"foreignKey:NonstockId;references:Id"` Nonstock Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
+17 -16
View File
@@ -7,22 +7,23 @@ import (
) )
type Payment struct { type Payment struct {
Id uint `gorm:"primaryKey;autoIncrement"` Id uint `gorm:"primaryKey;autoIncrement"`
PaymentCode string `gorm:"type:varchar(50);not null"` PaymentCode string `gorm:"type:varchar(50);not null"`
ReferenceNumber *string `gorm:"type:varchar(100)"` ReferenceNumber *string `gorm:"type:varchar(100)"`
TransactionType string `gorm:"type:varchar(50)"` TransactionType string `gorm:"type:varchar(50)"`
PartyType string `gorm:"type:varchar(50);not null;index:payments_party_polymorphic,priority:1"` PartyType string `gorm:"type:varchar(50);not null;index:payments_party_polymorphic,priority:1"`
PartyId uint `gorm:"not null;index:payments_party_polymorphic,priority:2"` PartyId uint `gorm:"not null;index:payments_party_polymorphic,priority:2"`
PaymentDate time.Time `gorm:"not null"` PartyAccountNumber *string `gorm:"type:varchar(50)"`
PaymentMethod string `gorm:"type:varchar(20);not null"` PaymentDate time.Time `gorm:"not null"`
BankId *uint `gorm:"not null;index:idx_payments_bank_id"` PaymentMethod string `gorm:"type:varchar(20);not null"`
Direction string `gorm:"type:varchar(5);not null"` BankId *uint `gorm:"not null;index:idx_payments_bank_id"`
Nominal float64 `gorm:"type:numeric(15,3);not null"` Direction string `gorm:"type:varchar(5);not null"`
Notes string `gorm:"type:text;not null"` Nominal float64 `gorm:"type:numeric(15,3);not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` Notes string `gorm:"type:text;not null"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
CreatedBy uint `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedBy uint `gorm:"index" json:"-"`
BankWarehouse Bank `gorm:"foreignKey:BankId;references:Id"` BankWarehouse Bank `gorm:"foreignKey:BankId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
+44
View File
@@ -0,0 +1,44 @@
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:"-"`
ActivityCount int `gorm:"-" 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"`
}
+1 -1
View File
@@ -21,7 +21,7 @@ type Product struct {
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
IsVisible bool `gorm:"column:is_visible;default:true"` IsVisible bool ``
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
Uom Uom `gorm:"foreignKey:UomId;references:Id"` Uom Uom `gorm:"foreignKey:UomId;references:Id"`
+1
View File
@@ -5,6 +5,7 @@ import "time"
type ProductSupplier struct { type ProductSupplier struct {
ProductId uint `gorm:"not null"` ProductId uint `gorm:"not null"`
SupplierId uint `gorm:"not null"` SupplierId uint `gorm:"not null"`
Price float64 `gorm:"type:numeric(15,3);not null;default:0"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
Product Product `gorm:"foreignKey:ProductId;references:Id"` Product Product `gorm:"foreignKey:ProductId;references:Id"`
@@ -1,27 +1,24 @@
package entities package entities
import ( import (
"encoding/json"
"time" "time"
"gorm.io/gorm"
) )
type ProjectFlockKandangUniformity struct { type ProjectFlockKandangUniformity struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Uniformity float64 `gorm:"type:numeric(15,3)"` Uniformity float64 `gorm:"type:numeric(15,3)"`
Week int `gorm:"not null"` Week int `gorm:"not null"`
Cv float64 `gorm:"type:numeric(15,3)"` Cv float64 `gorm:"type:numeric(15,3)"`
ChickQtyOfWeight float64 `gorm:"type:numeric(15,3)"` ChickQtyOfWeight float64 `gorm:"type:numeric(15,3)"`
MeanUp float64 `gorm:"type:numeric(15,3)"` MeanUp float64 `gorm:"type:numeric(15,3)"`
MeanDown float64 `gorm:"type:numeric(15,3)"` MeanDown float64 `gorm:"type:numeric(15,3)"`
ProjectFlockKandangId uint `gorm:"not null"` ProjectFlockKandangId uint `gorm:"not null"`
UniformQty float64 `gorm:"type:numeric(15,3)"` UniformQty float64 `gorm:"type:numeric(15,3)"`
NotUniformQty float64 `gorm:"type:numeric(15,3)"` NotUniformQty float64 `gorm:"type:numeric(15,3)"`
UniformDate *time.Time `gorm:"type:timestamptz"` ChartData json.RawMessage `gorm:"type:jsonb"`
CreatedAt time.Time `gorm:"autoCreateTime"` UniformDate *time.Time `gorm:"type:timestamptz"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` CreatedBy uint `gorm:"not null"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedBy uint `gorm:"not null"`
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"`
+5 -4
View File
@@ -10,8 +10,9 @@ type ProjectFlockKandang struct {
ClosedAt *time.Time `gorm:"index"` ClosedAt *time.Time `gorm:"index"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"` ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"` Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"-"` LatestProjectFlockApproval *Approval `gorm:"-" json:"-"`
LatestChickinApproval *Approval `gorm:"-" json:"-"`
} }
+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"`
HenDay *float64 `gorm:"column:hen_day"`
HenHouse *float64 `gorm:"column:hen_house"`
FeedIntake *float64 `gorm:"column:feed_intake"`
EggMass *float64 `gorm:"column:egg_mass"`
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:"-"`
StandardHenDay *float64 `gorm:"-"`
StandardHenHouse *float64 `gorm:"-"`
StandardFeedIntake *float64 `gorm:"-"`
StandardMaxDepletion *float64 `gorm:"-"`
StandardEggMass *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"`
}
+1
View File
@@ -12,6 +12,7 @@ type RecordingEgg struct {
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
ProductFlagName *string `gorm:"->;column:product_flag_name" json:"-"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
Recording Recording `gorm:"foreignKey:RecordingId;references:Id"` Recording Recording `gorm:"foreignKey:RecordingId;references:Id"`
} }
+8 -13
View File
@@ -8,27 +8,22 @@ type StockTransferDetail struct {
StockTransferId uint64 StockTransferId uint64
ProductId uint64 ProductId uint64
// === FIFO FIELDS - SOURCE WAREHOUSE (Usable) ===
// Tracking stock yang DIAMBIL dari source warehouse
SourceProductWarehouseID *uint64 `gorm:"column:source_product_warehouse_id"` SourceProductWarehouseID *uint64 `gorm:"column:source_product_warehouse_id"`
UsageQty float64 `gorm:"column:usage_qty;default:0"` // Actual yang berhasil diambil UsageQty float64 `gorm:"column:usage_qty;default:0"` // Actual yang berhasil diambil
PendingQty float64 `gorm:"column:pending_qty;default:0"` // Yang pending (nunggu stock) PendingQty float64 `gorm:"column:pending_qty;default:0"` // Yang pending (nunggu stock)
DestProductWarehouseID *uint64 `gorm:"column:dest_product_warehouse_id"`
// === FIFO FIELDS - DESTINATION WAREHOUSE (Stockable) === TotalQty float64 `gorm:"column:total_qty;default:0"` // Total lot yang tersedia
// Tracking stock yang DITAMBAHKAN ke destination warehouse TotalUsed float64 `gorm:"column:total_used;default:0"` // Yang sudah dipakai dari lot ini
DestProductWarehouseID *uint64 `gorm:"column:dest_product_warehouse_id"` ExpenseNonstockId *uint64 `gorm:"column:expense_nonstock_id"`
TotalQty float64 `gorm:"column:total_qty;default:0"` // Total lot yang tersedia CreatedAt time.Time
TotalUsed float64 `gorm:"column:total_used;default:0"` // Yang sudah dipakai dari lot ini UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
// === METADATA ===
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
// === RELATIONS === // === RELATIONS ===
StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"` StockTransfer *StockTransfer `gorm:"foreignKey:StockTransferId"`
Product *Product `gorm:"foreignKey:ProductId"` Product *Product `gorm:"foreignKey:ProductId"`
SourceProductWarehouse *ProductWarehouse `gorm:"foreignKey:SourceProductWarehouseID"` SourceProductWarehouse *ProductWarehouse `gorm:"foreignKey:SourceProductWarehouseID"`
DestProductWarehouse *ProductWarehouse `gorm:"foreignKey:DestProductWarehouseID"` DestProductWarehouse *ProductWarehouse `gorm:"foreignKey:DestProductWarehouseID"`
ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
DeliveryItems []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDetailId"` DeliveryItems []StockTransferDeliveryItem `gorm:"foreignKey:StockTransferDetailId"`
} }
+39 -32
View File
@@ -1,5 +1,8 @@
package middleware package middleware
const(
P_DashboardGetAll = "lti.dashboard.list"
)
// project-flock // project-flock
const ( const (
P_ProjectFlockKandangsClosing = "lti.production.project_flock_kandangs.closing" P_ProjectFlockKandangsClosing = "lti.production.project_flock_kandangs.closing"
@@ -19,18 +22,19 @@ const (
) )
const ( const (
P_ExpenseGetAll = "lti.expense.list" P_ExpenseGetAll = "lti.expense.list"
P_ExpenseCreateOne = "lti.expense.create" P_ExpenseCreateOne = "lti.expense.create"
P_ExpenseUpdateOne = "lti.expense.update" P_ExpenseUpdateOne = "lti.expense.update"
P_ExpenseGetOne = "lti.expense.detail" P_ExpenseGetOne = "lti.expense.detail"
P_ExpenseDeleteOne = "lti.expense.delete" P_ExpenseDeleteOne = "lti.expense.delete"
P_ExpenseApprovalManager = "lti.expense.approve.manager" P_ExpenseApprovalHeadArea = "lti.expense.approve.head_area"
P_ExpenseApprovalFinance = "lti.expense.approve.finance" P_ExpenseApprovalFinance = "lti.expense.approve.finance"
P_ExpenseCreateRealizations = "lti.expense.create.realization" P_ExpenseApprovalUnitVicePresident = "lti.expense.approve.unit_vice_president"
P_ExpenseUpdateRealizations = "lti.expense.update.realization" P_ExpenseCreateRealizations = "lti.expense.create.realization"
P_ExpenseCompleteExpense = "lti.expense.complete.expense" P_ExpenseUpdateRealizations = "lti.expense.update.realization"
P_ExpenseDocument = "lti.expense.document" P_ExpenseCompleteExpense = "lti.expense.complete.expense"
P_ExpenseDocumentRealizations = "lti.expense.document.realization" P_ExpenseDocument = "lti.expense.document"
P_ExpenseDocumentRealizations = "lti.expense.document.realization"
) )
const ( const (
P_AdjustmentGetAll = "lti.inventory.list" P_AdjustmentGetAll = "lti.inventory.list"
@@ -44,6 +48,9 @@ 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_ReportDebtSupplierGetAll = "lti.repport.debtsupplier.list"
P_ReportHppPerKandangGetAll = "lti.repport.gethppperkandang.list"
P_ReportProductionResultGetAll = "lti.repport.production_result.list"
) )
const ( const (
@@ -133,18 +140,18 @@ const (
P_NonstocksUpdateOne = "lti.master.nonstocks.update" P_NonstocksUpdateOne = "lti.master.nonstocks.update"
P_NonstocksDeleteOne = "lti.master.nonstocks.delete" P_NonstocksDeleteOne = "lti.master.nonstocks.delete"
P_ProductCategoriesGetAll = "lti.master.Product_categories.list" P_ProductCategoriesGetAll = "lti.master.product_categories.list"
P_ProductCategoriesGetOne = "lti.master.Product_categories.detail" P_ProductCategoriesGetOne = "lti.master.product_categories.detail"
P_ProductCategoriesCreateOne = "lti.master.Product_categories.create" P_ProductCategoriesCreateOne = "lti.master.product_categories.create"
P_ProductCategoriesUpdateOne = "lti.master.Product_categories.update" P_ProductCategoriesUpdateOne = "lti.master.product_categories.update"
P_ProductCategoriesDeleteOne = "lti.master.Product_categories.delete" P_ProductCategoriesDeleteOne = "lti.master.product_categories.delete"
P_ProductsGetAll = "lti.master.Products.list"
P_ProductsGetOne = "lti.master.Products.detail"
P_ProductsCreateOne = "lti.master.Products.create"
P_ProductsUpdateOne = "lti.master.Products.update"
P_ProductsDeleteOne = "lti.master.Products.delete"
P_ProductsGetAll = "lti.master.products.list"
P_ProductsGetOne = "lti.master.products.detail"
P_ProductsCreateOne = "lti.master.products.create"
P_ProductsUpdateOne = "lti.master.products.update"
P_ProductsDeleteOne = "lti.master.products.delete"
P_SuppliersGetAll = "lti.master.suppliers.list" P_SuppliersGetAll = "lti.master.suppliers.list"
P_SuppliersGetOne = "lti.master.suppliers.detail" P_SuppliersGetOne = "lti.master.suppliers.detail"
P_SuppliersCreateOne = "lti.master.suppliers.create" P_SuppliersCreateOne = "lti.master.suppliers.create"
@@ -206,15 +213,15 @@ const (
) )
const ( const (
P_PurchaseGetAll = "lti.Purchase.list" P_PurchaseGetAll = "lti.purchase.list"
P_PurchaseGetOne = "lti.Purchase.detail" P_PurchaseGetOne = "lti.purchase.detail"
P_PurchaseCreateOne = "lti.Purchase.create" P_PurchaseCreateOne = "lti.purchase.create"
P_PurchaseUpdateOne = "lti.Purchase.update" P_PurchaseUpdateOne = "lti.purchase.update"
P_PurchaseDeleteOne = "lti.Purchase.delete" P_PurchaseDeleteOne = "lti.purchase.delete"
P_PurchaseItemDeleteOne = "lti.Purchase.delete.item" P_PurchaseItemDeleteOne = "lti.purchase.delete.item"
P_PurchaseReceive = "lti.Purchase.receive" P_PurchaseReceive = "lti.purchase.receive"
P_PurchaseApprovalStaff = "lti.Purchase.approve.staff" P_PurchaseApprovalStaff = "lti.purchase.approve.staff"
P_PurchaseApprovalManager = "lti.Purchase.approve.manager" P_PurchaseApprovalManager = "lti.purchase.approve.manager"
) )
const ( const (
@@ -78,6 +78,36 @@ func (u *ClosingController) GetOne(c *fiber.Ctx) error {
}) })
} }
func (u *ClosingController) GetOverheadByProjectFlockKandang(c *fiber.Ctx) error {
projectParam := c.Params("project_flock_id")
kandangParam := c.Params("project_flock_kandang_id")
projectFlockID, err := strconv.Atoi(projectParam)
if err != nil || projectFlockID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
}
pfkID, err := strconv.Atoi(kandangParam)
if err != nil || pfkID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
}
kandangID := uint(pfkID)
result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID), &kandangID)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get overhead by project flock kandang successfully",
Data: result,
})
}
func (u *ClosingController) GetClosingSummary(c *fiber.Ctx) error { func (u *ClosingController) GetClosingSummary(c *fiber.Ctx) error {
param := c.Params("projectFlockId") param := c.Params("projectFlockId")
@@ -86,7 +116,17 @@ func (u *ClosingController) GetClosingSummary(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId") return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
} }
result, err := u.ClosingService.GetClosingSummary(c, uint(id)) var kandangID *uint
if raw := c.Query("kandang_id"); raw != "" {
kandangInt, convErr := strconv.Atoi(raw)
if convErr != nil || kandangInt <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
}
kandangUint := uint(kandangInt)
kandangID = &kandangUint
}
result, err := u.ClosingService.GetClosingSummary(c, uint(id), kandangID)
if err != nil { if err != nil {
return err return err
} }
@@ -108,12 +148,7 @@ func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id") return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
} }
projectFlock, err := u.ClosingService.GetProjectFlockByID(c, uint(projectFlockID)) result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID), nil)
if err != nil {
return err
}
result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID))
if err != nil { if err != nil {
return err return err
} }
@@ -123,19 +158,60 @@ func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
Code: fiber.StatusOK, Code: fiber.StatusOK,
Status: "success", Status: "success",
Message: "Get closing penjualan successfully", Message: "Get closing penjualan successfully",
Data: dto.ToPenjualanRealisasiResponseDTO(projectFlock.Category, uint(projectFlockID), result), Data: dto.ToPenjualanRealisasiResponseDTO(uint(projectFlockID), result),
})
}
func (u *ClosingController) GetPenjualanByProjectFlockKandang(c *fiber.Ctx) error {
projectParam := c.Params("project_flock_id")
kandangParam := c.Params("project_flock_kandang_id")
projectFlockID, err := strconv.Atoi(projectParam)
if err != nil || projectFlockID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
}
pfkID, err := strconv.Atoi(kandangParam)
if err != nil || pfkID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
}
kandangID := uint(pfkID)
result, err := u.ClosingService.GetPenjualan(c, uint(projectFlockID), &kandangID)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get closing penjualan by project flock kandang successfully",
Data: dto.ToPenjualanRealisasiResponseDTO(uint(projectFlockID), result),
}) })
} }
func (u *ClosingController) GetOverhead(c *fiber.Ctx) error { func (u *ClosingController) GetOverhead(c *fiber.Ctx) error {
param := c.Params("project_flock_id") projectParam := c.Params("project_flock_id")
kandangParam := c.Params("project_flock_kandang_id")
projectFlockID, err := strconv.Atoi(param) projectFlockID, err := strconv.Atoi(projectParam)
if err != nil { if err != nil || projectFlockID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id") return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
} }
result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID)) var projectFlockKandangID *uint
if kandangParam != "" {
pfkID, err := strconv.Atoi(kandangParam)
if err != nil || pfkID <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_kandang_id")
}
kandangID := uint(pfkID)
projectFlockKandangID = &kandangID
}
result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID), projectFlockKandangID)
if err != nil { if err != nil {
return err return err
} }
@@ -162,6 +238,14 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error {
Page: c.QueryInt("page", 1), Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10), Limit: c.QueryInt("limit", 10),
} }
if raw := c.Query("kandang_id"); raw != "" {
kandangInt, convErr := strconv.Atoi(raw)
if convErr != nil || kandangInt <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
}
kandangUint := uint(kandangInt)
query.KandangID = &kandangUint
}
if query.Page < 1 || query.Limit < 1 { if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
@@ -247,7 +331,7 @@ func (u *ClosingController) GetSapronakByKandang(c *fiber.Ctx) error {
} }
func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error { func (u *ClosingController) GetClosingKeuangan(c *fiber.Ctx) error {
param := c.Params("project_flock_id") param := c.Params("projectFlockId")
projectFlockID, err := strconv.Atoi(param) projectFlockID, err := strconv.Atoi(param)
if err != nil { if err != nil {
@@ -338,7 +422,18 @@ func (u *ClosingController) GetClosingDataProduksi(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId") return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId")
} }
result, err := u.ClosingService.GetClosingDataProduksi(c, uint(id)) var kandangID *uint
if raw := c.Query("kandang_id"); raw != "" {
kandangInt, convErr := strconv.Atoi(raw)
if convErr != nil || kandangInt <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
}
kandangUint := uint(kandangInt)
kandangID = &kandangUint
}
result, err := u.ClosingService.GetClosingDataProduksi(c, uint(id), kandangID)
if err != nil { if err != nil {
return err return err
} }
+42 -16
View File
@@ -59,39 +59,65 @@ type ClosingSummaryDTO struct {
StatusClosing string `json:"closing_status"` StatusClosing string `json:"closing_status"`
} }
type ClosingSummaryKandangDTO struct {
FlockID uint `json:"flock_id"`
Period int `json:"period"`
LocationName string `json:"location_name"`
Population int `json:"population"`
PopulationFormatted string `json:"population_formatted"`
ProjectType string `json:"project_type"`
ClosingDate string `json:"closing_date"`
KandangName string `json:"kandang_name"`
ChickInDate string `json:"chick_in_date"`
PicName string `json:"pic_name"`
ApprovalDate string `json:"approval_date"`
ProjectStatus string `json:"project_status"`
}
type ClosingPurchaseDTO struct { type ClosingPurchaseDTO struct {
InitialPopulation int `json:"initial_population"` InitialPopulation int `json:"initial_population"`
ClaimCulling int `json:"claim_culling"` ClaimCulling int `json:"claim_culling"`
FinalPopulation int `json:"final_population"` FinalPopulation int `json:"final_population"`
FeedIn float64 `json:"feed_in"` FeedIn float64 `json:"feed_in"`
FeedUsed float64 `json:"feed_used"` FeedUsed float64 `json:"feed_used"`
FeedUsedPerHead float64 `json:"feed_used_per_head"` // FeedUsedPerHead float64 `json:"feed_used_per_head"`
} }
type ClosingSalesDTO struct { type ClosingSalesDTO struct {
SalesPopulation int `json:"sales_population"` SalesPopulation int `json:"sales_population"`
SalesWeight float64 `json:"sales_weight"` SalesWeight float64 `json:"sales_weight"`
AverageWeight float64 `json:"average_weight"` AverageWeight float64 `json:"avg_weight"`
AverageSellingPrice float64 `json:"chicken_average_selling_price"` AverageSellingPrice float64 `json:"avg_selling_price"`
} }
type ClosingEggSalesDTO struct { type ClosingEggSalesDTO struct {
EggPieces int `json:"egg_pieces"` EggPieces int `json:"egg_pieces"`
EggMassKg float64 `json:"egg_mass_kg"` EggMassKg float64 `json:"egg_mass"`
AverageEggWeightKg float64 `json:"average_egg_weight_kg"` AverageEggWeightKg float64 `json:"avg_egg_weight"`
AverageSellingPrice float64 `json:"egg_average_selling_price"` AverageSellingPrice float64 `json:"avg_selling_price"`
} }
type ClosingPerformanceDTO struct { type ClosingPerformanceDTO struct {
Depletion float64 `json:"depletion"` Depletion float64 `json:"depletion"`
Age float64 `json:"age_day"` Age float64 `json:"age_day"`
MortalityStd float64 `json:"mortality_std"` MortalityStd float64 `json:"mor_std"`
MortalityAct float64 `json:"mortality_act"` MortalityAct float64 `json:"mor_act"`
DeffMortality float64 `json:"deff_mortality"` DeffMortality float64 `json:"mor_diff"`
FcrStd float64 `json:"fcr_std"` FcrStd float64 `json:"fcr_std"`
FcrAct float64 `json:"fcr_act"` FcrAct float64 `json:"fcr_act"`
DeffFcr float64 `json:"deff_fcr"` DeffFcr float64 `json:"fcr_diff"`
Awg float64 `json:"awg"` AwgAct float64 `json:"awg_act"`
AwgStd float64 `json:"awg_std"`
FeedIntake float64 `json:"feed_intake"`
FeedIntakeStd float64 `json:"feed_intake_std"`
HenDayAct *float64 `json:"hen_day_act,omitempty"`
HendayStd *float64 `json:"hen_day_std,omitempty"`
EggMass *float64 `json:"egg_mass,omitempty"`
EggMassStd *float64 `json:"egg_mass_std,omitempty"`
EggWeight *float64 `json:"egg_weight,omitempty"`
EggWeightStd *float64 `json:"egg_weight_std,omitempty"`
HenHouseAct *float64 `json:"hen_housed_act,omitempty"`
HenHouseStd *float64 `json:"hen_housed_std,omitempty"`
} }
type ClosingSalesGroupDTO struct { type ClosingSalesGroupDTO struct {
@@ -164,7 +190,7 @@ func sumPopulation(history []entity.ProjectFlockKandang) float64 {
var total float64 var total float64
for _, h := range history { for _, h := range history {
for _, chickin := range h.Chickins { for _, chickin := range h.Chickins {
total += chickin.UsageQty + chickin.PendingUsageQty total += chickin.UsageQty
} }
} }
return total return total
@@ -79,10 +79,11 @@ type HppGroup struct {
} }
type SummaryHpp struct { type SummaryHpp struct {
Label string `json:"label"` Label string `json:"label"`
Comparison `json:"-"` Budgeting FinancialMetrics `json:"budgeting"`
EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"` Realization FinancialMetrics `json:"realization"`
EggRealization *FinancialMetrics `json:"egg_realization,omitempty"` EggBudgeting *FinancialMetrics `json:"egg_budgeting,omitempty"`
EggRealization *FinancialMetrics `json:"egg_realization,omitempty"`
} }
type HppPurchasesSection struct { type HppPurchasesSection struct {
@@ -246,11 +247,9 @@ func ToSummaryHpp(label string, purchaseItems []entities.PurchaseItem, budgets [
realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, ctx.TotalPopulation, ctx.TotalWeightProduced) realizationRpPerBird, realizationRpPerKg := calculatePerUnitMetrics(totalRealization, ctx.TotalPopulation, ctx.TotalWeightProduced)
summary := SummaryHpp{ summary := SummaryHpp{
Label: label, Label: label,
Comparison: ToComparison( Budgeting: ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget),
ToFinancialMetrics(budgetRpPerBird, budgetRpPerKg, totalBudget), Realization: ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization),
ToFinancialMetrics(realizationRpPerBird, realizationRpPerKg, totalRealization),
),
} }
if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) && ctx.TotalEggWeightKg > 0 { if projectFlockCategory == string(utils.ProjectFlockCategoryLaying) && ctx.TotalEggWeightKg > 0 {
@@ -55,16 +55,21 @@ func ToSalesDTO(e entity.MarketingDeliveryProduct) SalesDTO {
kandang = &mapped kandang = &mapped
} }
var realizationDate time.Time
if e.DeliveryDate != nil {
realizationDate = *e.DeliveryDate
}
doNumber := deliveryOrdersDTO.GenerateDeliveryOrderNumber(e.MarketingProduct.Marketing.SoNumber, e.DeliveryDate, e.MarketingProduct.ProductWarehouse.Warehouse.Id) doNumber := deliveryOrdersDTO.GenerateDeliveryOrderNumber(e.MarketingProduct.Marketing.SoNumber, e.DeliveryDate, e.MarketingProduct.ProductWarehouse.Warehouse.Id)
return SalesDTO{ return SalesDTO{
Id: e.Id, Id: e.Id,
RealizationDate: *e.DeliveryDate, RealizationDate: realizationDate,
Age: age, Age: age,
DoNumber: doNumber, DoNumber: doNumber,
Product: product, Product: product,
Customer: customer, Customer: customer,
Qty: e.UsageQty, // Show allocated quantity from FIFO Qty: e.UsageQty,
Weight: e.TotalWeight, Weight: e.TotalWeight,
AvgWeight: e.AvgWeight, AvgWeight: e.AvgWeight,
Price: e.UnitPrice, Price: e.UnitPrice,
@@ -82,7 +87,7 @@ func ToSalesDTOs(e []entity.MarketingDeliveryProduct) []SalesDTO {
return result return result
} }
func ToPenjualanRealisasiResponseDTO(projectType string, projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO { func ToPenjualanRealisasiResponseDTO(projectFlockID uint, e []entity.MarketingDeliveryProduct) PenjualanRealisasiResponseDTO {
return PenjualanRealisasiResponseDTO{ return PenjualanRealisasiResponseDTO{
@@ -1,6 +1,8 @@
package dto package dto
import ( import (
"encoding/json"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
) )
@@ -69,7 +71,7 @@ func ToOverheadDTO(budget *entity.ProjectBudget, realization *entity.ExpenseReal
return dto return dto
} }
func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty, totalActualPopulation float64) OverheadListDTO { func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty, totalActualPopulation float64, isPerKandang bool, totalKandangCount int, projectFlockKandangCountMap map[uint]int) OverheadListDTO {
overheadsByNonstockID := make(map[uint]*OverheadDTO) overheadsByNonstockID := make(map[uint]*OverheadDTO)
latestDateByNonstockID := make(map[uint]string) latestDateByNonstockID := make(map[uint]string)
@@ -82,9 +84,20 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
itemName, itemUOM := getItemInfo(budgets[i].Nonstock) itemName, itemUOM := getItemInfo(budgets[i].Nonstock)
overheadsByNonstockID[nonstockID].ItemName = itemName overheadsByNonstockID[nonstockID].ItemName = itemName
overheadsByNonstockID[nonstockID].UOMName = itemUOM overheadsByNonstockID[nonstockID].UOMName = itemUOM
overheadsByNonstockID[nonstockID].BudgetQuantity = budgets[i].Qty
overheadsByNonstockID[nonstockID].BudgetUnitPrice = budgets[i].Price budgetQty := budgets[i].Qty
overheadsByNonstockID[nonstockID].BudgetTotalAmount = calculateTotal(budgets[i].Qty, budgets[i].Price) budgetPrice := budgets[i].Price
budgetTotal := calculateTotal(budgets[i].Qty, budgets[i].Price)
// Budget division: per kandang view only
if isPerKandang && totalKandangCount > 0 {
budgetQty = budgetQty / float64(totalKandangCount)
budgetTotal = budgetTotal / float64(totalKandangCount)
}
overheadsByNonstockID[nonstockID].BudgetQuantity = budgetQty
overheadsByNonstockID[nonstockID].BudgetUnitPrice = budgetPrice
overheadsByNonstockID[nonstockID].BudgetTotalAmount = budgetTotal
} }
for i := range realizations { for i := range realizations {
@@ -97,8 +110,40 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
overheadsByNonstockID[nonstockID] = &OverheadDTO{} overheadsByNonstockID[nonstockID] = &OverheadDTO{}
} }
overheadsByNonstockID[nonstockID].ActualQuantity += realizations[i].Qty qty := realizations[i].Qty
overheadsByNonstockID[nonstockID].ActualTotalAmount += calculateTotal(realizations[i].Qty, realizations[i].Price) totalAmount := calculateTotal(realizations[i].Qty, realizations[i].Price)
// Farm-level expense division
if realizations[i].ExpenseNonstock.Expense != nil &&
realizations[i].ExpenseNonstock.Expense.ProjectFlockId != nil {
projectFlockIDs := parseProjectFlockIDsFromJSON(*realizations[i].ExpenseNonstock.Expense.ProjectFlockId)
if len(projectFlockIDs) > 0 {
totalKandangInAllProjects := 0
for _, pfID := range projectFlockIDs {
if count, exists := projectFlockKandangCountMap[pfID]; exists {
totalKandangInAllProjects += count
}
}
if totalKandangInAllProjects > 0 {
if isPerKandang {
qty = qty / float64(totalKandangInAllProjects)
totalAmount = totalAmount / float64(totalKandangInAllProjects)
} else {
// Overhead ALL: divide by total kandang then multiply by this project's kandang count
perKandangAmount := totalAmount / float64(totalKandangInAllProjects)
perKandangQty := qty / float64(totalKandangInAllProjects)
qty = perKandangQty * float64(totalKandangCount)
totalAmount = perKandangAmount * float64(totalKandangCount)
}
}
}
}
overheadsByNonstockID[nonstockID].ActualQuantity += qty
overheadsByNonstockID[nonstockID].ActualTotalAmount += totalAmount
if overheadsByNonstockID[nonstockID].ItemName == "" { if overheadsByNonstockID[nonstockID].ItemName == "" {
itemName, itemUOM := getItemInfo(realizations[i].ExpenseNonstock.Nonstock) itemName, itemUOM := getItemInfo(realizations[i].ExpenseNonstock.Nonstock)
@@ -146,7 +191,26 @@ func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.Ex
} }
} }
// === Helper Functions === func parseProjectFlockIDsFromJSON(projectFlockJSON string) []uint {
if projectFlockJSON == "" {
return []uint{}
}
var projectFlocks []uint
if err := json.Unmarshal([]byte(projectFlockJSON), &projectFlocks); err != nil {
return []uint{}
}
return projectFlocks
}
func countProjectFlocksInJSON(projectFlockJSON string) int {
projectFlocks := parseProjectFlockIDsFromJSON(projectFlockJSON)
if len(projectFlocks) == 0 {
return 1
}
return len(projectFlocks)
}
func getItemInfo(nonstock *entity.Nonstock) (string, string) { func getItemInfo(nonstock *entity.Nonstock) (string, string) {
if nonstock != nil && nonstock.Id != 0 { if nonstock != nil && nonstock.Id != 0 {
@@ -134,7 +134,14 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
report = &SapronakReportDTO{} report = &SapronakReportDTO{}
} }
filter := strings.ToUpper(strings.TrimSpace(flag)) normalizeFlag := func(raw string) string {
normalized := strings.ToUpper(strings.TrimSpace(raw))
if normalized == "PULLET" {
return "DOC"
}
return normalized
}
filter := normalizeFlag(flag)
byFlag := map[string]**SapronakCategoryDTO{} byFlag := map[string]**SapronakCategoryDTO{}
if filter == "" || filter == "DOC" { if filter == "" || filter == "DOC" {
@@ -149,10 +156,6 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
result.Pakan = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)} result.Pakan = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
byFlag["PAKAN"] = &result.Pakan byFlag["PAKAN"] = &result.Pakan
} }
if filter == "" || filter == "PULLET" {
result.Pullet = &SapronakCategoryDTO{Rows: make([]SapronakCategoryRowDTO, 0)}
byFlag["PULLET"] = &result.Pullet
}
formatDate := func(t *time.Time) string { formatDate := func(t *time.Time) string {
if t == nil { if t == nil {
@@ -162,7 +165,7 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
} }
for _, group := range report.Groups { for _, group := range report.Groups {
flagKey := strings.ToUpper(group.Flag) flagKey := normalizeFlag(group.Flag)
ptr := byFlag[flagKey] ptr := byFlag[flagKey]
if ptr == nil || *ptr == nil { if ptr == nil || *ptr == nil {
continue continue
@@ -182,7 +185,7 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
} }
for idx, item := range group.Items { for idx, item := range group.Items {
productKey := strings.ToUpper(group.Flag + "|" + item.ProductName) productKey := strings.ToUpper(flagKey + "|" + item.ProductName)
baseRow := SapronakCategoryRowDTO{ baseRow := SapronakCategoryRowDTO{
ID: idx + 1, ID: idx + 1,
Date: formatDate(item.Tanggal), Date: formatDate(item.Tanggal),
@@ -246,7 +249,5 @@ func ToSapronakProjectAggregatedFromReport(report *SapronakReportDTO, flag strin
buildTotals(result.Doc, "TOTAL DOC") buildTotals(result.Doc, "TOTAL DOC")
buildTotals(result.Ovk, "TOTAL OVK") buildTotals(result.Ovk, "TOTAL OVK")
buildTotals(result.Pakan, "TOTAL PAKAN") buildTotals(result.Pakan, "TOTAL PAKAN")
buildTotals(result.Pullet, "TOTAL PULLET")
return result return result
} }
+4 -1
View File
@@ -11,6 +11,7 @@ import (
sClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services" sClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
rExpenseRealization "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" rExpenseRealization "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
@@ -33,11 +34,13 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db) expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db)
chickinRepo := rChickin.NewChickinRepository(db) chickinRepo := rChickin.NewChickinRepository(db)
recordingRepo := rRecording.NewRecordingRepository(db) recordingRepo := rRecording.NewRecordingRepository(db)
standardGrowthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db)
productionStandardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db)
purchaseRepo := rPurchase.NewPurchaseRepository(db) purchaseRepo := rPurchase.NewPurchaseRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo) approvalService := commonSvc.NewApprovalService(approvalRepo)
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, validate) closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, projectFlockKandangRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, purchaseRepo, recordingRepo, standardGrowthDetailRepo, productionStandardDetailRepo, validate)
sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate) sapronakService := sClosing.NewSapronakService(closingRepo, projectFlockKandangRepo, validate)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
@@ -10,6 +10,7 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -17,6 +18,7 @@ type ClosingRepository interface {
repository.BaseRepository[entity.ProjectFlock] repository.BaseRepository[entity.ProjectFlock]
GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error)
SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, error)
SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error)
SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error) SumMarketingWeightAndQtyByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, float64, float64, error)
SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error) SumMarketingWeightAndQtyByProjectFlockKandangIDsAndFlagNames(ctx context.Context, projectFlockKandangIDs []uint, flagNames []string) (float64, float64, float64, error)
@@ -165,6 +167,23 @@ func (r *ClosingRepositoryImpl) SumFeedPurchaseAndUsedByProjectFlockKandangIDs(c
return purchaseAgg.TotalIn, usageAgg.TotalUsed, nil return purchaseAgg.TotalIn, usageAgg.TotalUsed, nil
} }
func (r *ClosingRepositoryImpl) SumProjectChickinUsageByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, nil
}
var total float64
if err := r.DB().WithContext(ctx).
Model(&entity.ProjectChickin{}).
Where("project_flock_kandang_id IN ?", projectFlockKandangIDs).
Select("COALESCE(SUM(usage_qty), 0)").
Scan(&total).Error; err != nil {
return 0, err
}
return total, nil
}
func (r *ClosingRepositoryImpl) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) { func (r *ClosingRepositoryImpl) SumClaimCullingByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) (float64, error) {
if len(projectFlockKandangIDs) == 0 { if len(projectFlockKandangIDs) == 0 {
return 0, nil return 0, nil
@@ -407,7 +426,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 +475,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
@@ -914,9 +933,8 @@ func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.C
var rows []ActualUsageCostRow var rows []ActualUsageCostRow
// Part 1: Get usage from recording_stocks (PAKAN, OVK, Vitamin, Obat, Kimia, dll) purchaseStockableKey := fifo.StockableKeyPurchaseItems.String()
purchaseStockableKey := "PURCHASE_ITEMS" transferStockableKey := fifo.StockableKeyStockTransferIn.String()
transferStockableKey := "STOCK_TRANSFER_DETAILS"
recordingQuery := db. recordingQuery := db.
Table("recordings AS r"). Table("recordings AS r").
@@ -927,34 +945,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`,
@@ -982,7 +1000,6 @@ func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.C
return nil, err return nil, err
} }
// Part 2: Get usage from project_chickins (DOC, Pullet)
chickinQuery := db. chickinQuery := db.
Table("project_chickins AS pc"). Table("project_chickins AS pc").
Select(` Select(`
@@ -1006,7 +1023,6 @@ func (r *ClosingRepositoryImpl) GetActualUsageCostByProjectFlockID(ctx context.C
return nil, err return nil, err
} }
// Merge results
rows = append(rows, chickinRows...) rows = append(rows, chickinRows...)
return rows, nil return rows, nil
+4 -1
View File
@@ -23,13 +23,16 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
route.Get("/", m.RequirePermissions(m.P_ClosingGetAll), ctrl.GetAll) route.Get("/", m.RequirePermissions(m.P_ClosingGetAll), ctrl.GetAll)
route.Get("/:project_flock_id/penjualan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetPenjualan) route.Get("/:project_flock_id/penjualan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetPenjualan)
route.Get("/:project_flock_id/:project_flock_kandang_id/penjualan", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetPenjualanByProjectFlockKandang)
route.Get("/:projectFlockId", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSummary) route.Get("/:projectFlockId", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetClosingSummary)
route.Get("/:project_flock_id/overhead", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetOverhead) route.Get("/:project_flock_id/overhead", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetOverhead)
route.Get("/:project_flock_id/:project_flock_kandang_id/overhead", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetOverhead)
route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByKandang) route.Get("/:project_flock_id/:project_flock_kandang_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByKandang)
route.Get("/:project_flock_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByProject) route.Get("/:project_flock_id/perhitungan_sapronak", m.RequirePermissions(m.P_ClosingDetail), ctrl.GetSapronakByProject)
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)
} }
@@ -2,7 +2,9 @@ package service
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt"
"math" "math"
"strconv" "strconv"
"strings" "strings"
@@ -16,6 +18,7 @@ import (
expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories" marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
productionStandardRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
recordingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" recordingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
@@ -32,10 +35,10 @@ import (
type ClosingService interface { type ClosingService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.ClosingListItemDTO, int64, error) GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.ClosingListItemDTO, int64, error)
GetProjectFlockByID(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error) GetProjectFlockByID(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
GetPenjualan(ctx *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error) GetPenjualan(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error)
GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (any, error)
GetOverhead(ctx *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error) GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, error)
GetClosingDataProduksi(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error) GetOverhead(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error)
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) GetClosingKeuangan(ctx *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error)
GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error) GetExpeditionHPP(ctx *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.ExpeditionHPPDTO, error)
@@ -46,6 +49,7 @@ type closingService struct {
Validate *validator.Validate Validate *validator.Validate
Repository repository.ClosingRepository Repository repository.ClosingRepository
ProjectFlockRepo projectflockRepository.ProjectflockRepository ProjectFlockRepo projectflockRepository.ProjectflockRepository
ProjectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository
MarketingRepo marketingRepository.MarketingRepository MarketingRepo marketingRepository.MarketingRepository
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
ApprovalSvc commonSvc.ApprovalService ApprovalSvc commonSvc.ApprovalService
@@ -54,14 +58,17 @@ type closingService struct {
ChickinRepo chickinRepository.ProjectChickinRepository ChickinRepo chickinRepository.ProjectChickinRepository
PurchaseRepo purchaseRepository.PurchaseRepository PurchaseRepo purchaseRepository.PurchaseRepository
RecordingRepo recordingRepository.RecordingRepository RecordingRepo recordingRepository.RecordingRepository
StandardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository
ProductionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository
} }
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, validate *validator.Validate) ClosingService { func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, projectFlockKandangRepo projectflockRepository.ProjectFlockKandangRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, purchaseRepo purchaseRepository.PurchaseRepository, recordingRepo recordingRepository.RecordingRepository, standardGrowthDetailRepo productionStandardRepository.StandardGrowthDetailRepository, productionStandardDetailRepo productionStandardRepository.ProductionStandardDetailRepository, validate *validator.Validate) ClosingService {
return &closingService{ return &closingService{
Log: utils.Log, Log: utils.Log,
Validate: validate, Validate: validate,
Repository: repo, Repository: repo,
ProjectFlockRepo: projectFlockRepo, ProjectFlockRepo: projectFlockRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo,
MarketingRepo: marketingRepo, MarketingRepo: marketingRepo,
MarketingDeliveryProductRepo: marketingDeliveryProductRepo, MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
ApprovalSvc: approvalSvc, ApprovalSvc: approvalSvc,
@@ -70,6 +77,8 @@ func NewClosingService(repo repository.ClosingRepository, projectFlockRepo proje
ChickinRepo: chickinRepo, ChickinRepo: chickinRepo,
PurchaseRepo: purchaseRepo, PurchaseRepo: purchaseRepo,
RecordingRepo: recordingRepo, RecordingRepo: recordingRepo,
StandardGrowthDetailRepo: standardGrowthDetailRepo,
ProductionStandardDetailRepo: productionStandardDetailRepo,
} }
} }
@@ -94,7 +103,7 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.Cl
closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withClosingRelations(db) db = s.withClosingRelations(db)
if params.Search != "" { if params.Search != "" {
return db.Where("flock_name LIKE ?", "%"+params.Search+"%") return db.Where("flock_name ILIKE ?", "%"+params.Search+"%")
} }
return db.Order("created_at DESC").Order("updated_at DESC") return db.Order("created_at DESC").Order("updated_at DESC")
}) })
@@ -129,38 +138,28 @@ func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.Proj
return projectFlock, nil return projectFlock, nil
} }
func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error) { func (s closingService) GetPenjualan(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) ([]entity.MarketingDeliveryProduct, error) {
realisasi, err := s.MarketingDeliveryProductRepo.GetDeliveryProductsByProjectFlockID(c.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB { realisasi, err := s.MarketingDeliveryProductRepo.GetClosingPenjualan(c.Context(), projectFlockID, projectFlockKandangID)
return db.
Preload("MarketingProduct").
Preload("MarketingProduct.ProductWarehouse").
Preload("MarketingProduct.ProductWarehouse.Product").
Preload("MarketingProduct.ProductWarehouse.Product.ProductCategory").
Preload("MarketingProduct.ProductWarehouse.Product.Uom").
Preload("MarketingProduct.ProductWarehouse.Product.Flags").
Preload("MarketingProduct.ProductWarehouse.Warehouse").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Kandang").
Preload("MarketingProduct.ProductWarehouse.ProjectFlockKandang.Chickins").
Preload("MarketingProduct.Marketing").
Preload("MarketingProduct.Marketing.Customer").
Order("marketing_delivery_products.delivery_date DESC")
})
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(realisasi) == 0 { if len(realisasi) == 0 {
return nil, fiber.NewError(fiber.StatusNotFound, "Penjualan realisasi not found") return []entity.MarketingDeliveryProduct{}, nil
} }
return realisasi, nil return realisasi, nil
} }
func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) { func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint, kandangID *uint) (any, error) {
if projectFlockID == 0 { if projectFlockID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
} }
if kandangID != nil {
return s.getClosingSummaryByKandang(c.Context(), projectFlockID, *kandangID)
}
project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withClosingRelations) project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withClosingRelations)
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found") return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found")
@@ -181,6 +180,124 @@ func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*d
return &summary, nil return &summary, nil
} }
func (s closingService) getClosingSummaryByKandang(ctx context.Context, projectFlockID uint, kandangID uint) (*dto.ClosingSummaryKandangDTO, error) {
if projectFlockID == 0 || kandangID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id or kandang id")
}
db := s.Repository.DB().WithContext(ctx)
var kandang entity.ProjectFlockKandang
if err := db.
Preload("Kandang").
Preload("Kandang.Location").
Preload("Kandang.Pic").
Where("project_flock_id = ?", projectFlockID).
Where("kandang_id = ?", kandangID).
First(&kandang).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock kandang not found")
}
s.Log.Errorf("Failed get project flock kandang %d/%d: %+v", projectFlockID, kandangID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
}
var project entity.ProjectFlock
if err := db.
Select("id", "category").
First(&project, projectFlockID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found")
}
s.Log.Errorf("Failed get project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
}
var population float64
if err := db.
Table("project_flock_populations pfp").
Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
Where("pc.project_flock_kandang_id = ?", kandang.Id).
Select("COALESCE(SUM(pfp.total_qty), 0)").
Scan(&population).Error; err != nil {
s.Log.Errorf("Failed to sum population for project flock kandang %d: %+v", kandang.Id, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch population data")
}
var chickInDate time.Time
if err := db.
Table("project_chickins").
Where("project_flock_kandang_id = ?", kandang.Id).
Select("MIN(chick_in_date)").
Scan(&chickInDate).Error; err != nil {
s.Log.Errorf("Failed to fetch chick in date for project flock kandang %d: %+v", kandang.Id, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch chick in date")
}
statusProject := "Belum Selesai"
var approvalDate string
if s.ApprovalSvc != nil {
records, _, err := s.ApprovalSvc.List(ctx, utils.ApprovalWorkflowProjectFlockKandang.String(), &kandang.Id, 1, 1000, "")
if err != nil {
s.Log.Errorf("Failed to fetch approvals for project flock kandang %d: %+v", kandang.Id, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch approval data")
}
var (
minStep uint16
latestActionAt time.Time
)
for _, rec := range records {
if minStep == 0 || rec.StepNumber < minStep {
minStep = rec.StepNumber
}
if latestActionAt.IsZero() || rec.ActionAt.After(latestActionAt) {
latestActionAt = rec.ActionAt
statusProject = rec.StepName
}
}
if statusProject == "" && minStep > 0 {
if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowProjectFlockKandang, approvalutils.ApprovalStep(minStep)); ok {
statusProject = label
}
}
if !latestActionAt.IsZero() {
approvalDate = latestActionAt.Format("2006-01-02")
}
}
closingDate := ""
if kandang.ClosedAt != nil {
closingDate = kandang.ClosedAt.Format("2006-01-02")
}
chickInDateStr := ""
if !chickInDate.IsZero() {
chickInDateStr = chickInDate.Format("2006-01-02")
}
populationInt := int(population)
return &dto.ClosingSummaryKandangDTO{
FlockID: projectFlockID,
Period: kandang.Period,
LocationName: kandang.Kandang.Location.Name,
Population: populationInt,
PopulationFormatted: fmt.Sprintf("%d Ekor", populationInt),
ProjectType: project.Category,
ClosingDate: closingDate,
KandangName: kandang.Kandang.Name,
ChickInDate: chickInDateStr,
PicName: kandang.Kandang.Pic.Name,
ApprovalDate: approvalDate,
ProjectStatus: statusProject,
}, nil
}
func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) { func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, params *validation.ClosingSapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) {
if projectFlockID == 0 { if projectFlockID == 0 {
return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
@@ -221,7 +338,7 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa
var projectFlockKandangIDs []uint var projectFlockKandangIDs []uint
if params.Type == validation.SapronakTypeOutgoing { if params.Type == validation.SapronakTypeOutgoing {
projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID) projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID, params.KandangID)
if err != nil { if err != nil {
s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err) s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err)
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang") return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang")
@@ -301,12 +418,15 @@ func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, proje
return ids, nil return ids, nil
} }
func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint) ([]uint, error) { func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint, kandangID *uint) ([]uint, error) {
var ids []uint var ids []uint
err := s.Repository.DB().WithContext(ctx). query := s.Repository.DB().WithContext(ctx).
Model(&entity.ProjectFlockKandang{}). Model(&entity.ProjectFlockKandang{}).
Where("project_flock_id = ?", projectFlockID). Where("project_flock_id = ?", projectFlockID)
Pluck("id", &ids).Error if kandangID != nil {
query = query.Where("kandang_id = ?", *kandangID)
}
err := query.Order("id ASC").Pluck("id", &ids).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -369,52 +489,98 @@ func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID
return statusProject, statusClosing, nil return statusProject, statusClosing, nil
} }
func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error) { func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint, projectFlockKandangID *uint) (*dto.OverheadListDTO, error) {
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID) budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID) realizations, err := s.ExpenseRealizationRepo.GetClosingOverhead(c.Context(), projectFlockID, projectFlockKandangID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
projectFlockKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
return nil, err
}
totalKandangCount := len(projectFlockKandangs)
// Build kandang count map for farm expense division
projectFlockKandangCountMap := make(map[uint]int)
projectFlockKandangCountMap[projectFlockID] = totalKandangCount
involvedProjectFlocks := make(map[uint]bool)
for _, realization := range realizations {
if realization.ExpenseNonstock != nil &&
realization.ExpenseNonstock.Expense != nil &&
realization.ExpenseNonstock.Expense.ProjectFlockId != nil {
var projectFlockIDs []uint
if err := json.Unmarshal([]byte(*realization.ExpenseNonstock.Expense.ProjectFlockId), &projectFlockIDs); err == nil {
for _, pfID := range projectFlockIDs {
if pfID != projectFlockID {
involvedProjectFlocks[pfID] = true
}
}
}
}
}
for pfID := range involvedProjectFlocks {
if pfKandangs, err := s.ProjectFlockKandangRepo.GetByProjectFlockID(c.Context(), pfID); err == nil {
projectFlockKandangCountMap[pfID] = len(pfKandangs)
}
}
chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID) chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var totalChickinQty float64 var totalChickinQty float64
for _, chickin := range chickins { var totalDepletion float64
totalChickinQty += chickin.UsageQty
}
totalDepletion, err := s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID) if projectFlockKandangID != nil {
if err != nil { for _, chickin := range chickins {
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err) if chickin.ProjectFlockKandangId == *projectFlockKandangID {
totalChickinQty += chickin.UsageQty
}
}
var depletionResult float64
err = s.RecordingRepo.DB().WithContext(c.Context()).
Table("recording_depletions").
Select("COALESCE(SUM(recording_depletions.qty), 0)").
Joins("JOIN recordings ON recordings.id = recording_depletions.recording_id").
Where("recordings.project_flock_kandangs_id = ?", *projectFlockKandangID).
Scan(&depletionResult).Error
if err != nil {
s.Log.Warnf("GetTotalDepletionByProjectFlockKandangID error: %v", err)
} else {
totalDepletion = depletionResult
}
} else {
for _, chickin := range chickins {
totalChickinQty += chickin.UsageQty
}
totalDepletion, err = s.RecordingRepo.GetTotalDepletionByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
s.Log.Warnf("GetTotalDepletionByProjectFlockID error: %v", err)
}
} }
totalActualPopulation := totalChickinQty - totalDepletion totalActualPopulation := totalChickinQty - totalDepletion
result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation) result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty, totalActualPopulation, projectFlockKandangID != nil, totalKandangCount, projectFlockKandangCountMap)
return &result, nil return &result, nil
} }
func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) { func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*dto.ReportResponse, error) {
if projectFlockID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
}
if err := commonSvc.EnsureRelations(c.Context(), if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: func(ctx context.Context, id uint) (bool, error) { commonSvc.RelationCheck{Name: "Project Flock", ID: &projectFlockID, Exists: s.ProjectFlockRepo.IdExists},
_, err := s.ProjectFlockRepo.GetByID(ctx, id, nil)
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
return false, nil
}
return err == nil, err
}},
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -429,13 +595,11 @@ func (s closingService) GetClosingKeuangan(c *fiber.Ctx, projectFlockID uint) (*
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch budgets")
} }
// Get actual usage cost instead of purchase items
actualUsageRows, err := s.Repository.GetActualUsageCostByProjectFlockID(c.Context(), projectFlockID) actualUsageRows, err := s.Repository.GetActualUsageCostByProjectFlockID(c.Context(), projectFlockID)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch actual usage cost") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch actual usage cost")
} }
// Convert actual usage rows to pseudo purchase items
purchaseItems := s.convertActualUsageToPurchaseItems(c.Context(), actualUsageRows) purchaseItems := s.convertActualUsageToPurchaseItems(c.Context(), actualUsageRows)
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID) realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
@@ -521,12 +685,22 @@ func (s closingService) GetExpeditionHPP(c *fiber.Ctx, projectFlockID uint, proj
return result, nil return result, nil
} }
func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint) (*dto.ClosingProductionReportDTO, error) { func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint, kandangID *uint) (*dto.ClosingProductionReportDTO, error) {
if projectFlockID == 0 { if projectFlockID == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id")
} }
project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withClosingRelations) projectFlockKandangIDs, err := s.getProjectFlockKandangIDs(c.Context(), projectFlockID, kandangID)
if err != nil {
s.Log.Errorf("Failed to fetch project flock kandangs for %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandangs")
}
if len(projectFlockKandangIDs) == 0 {
return nil, fiber.NewError(fiber.StatusNotFound, "No project flock kandang found")
}
project, err := s.Repository.GetByID(c.Context(), projectFlockID, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found") return nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found")
} }
@@ -535,19 +709,29 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
} }
var population float64 population, err := s.Repository.SumProjectChickinUsageByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs)
for _, history := range project.KandangHistory { if err != nil {
for _, chickin := range history.Chickins { s.Log.Errorf("Failed to sum population for project flock %d: %+v", projectFlockID, err)
population += chickin.UsageQty + chickin.PendingUsageQty return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch population data")
}
} }
isGrowing := strings.EqualFold(project.Category, string(utils.ProjectFlockCategoryGrowing)) isGrowing := strings.EqualFold(project.Category, string(utils.ProjectFlockCategoryGrowing))
projectFlockKandangIDs, err := s.getProjectFlockKandangIDs(c.Context(), projectFlockID) currentWeek, err := s.determineProductionWeek(c.Context(), projectFlockKandangIDs)
if err != nil { if err != nil {
s.Log.Errorf("Failed to fetch project flock kandangs for %d: %+v", projectFlockID, err) s.Log.Errorf("Failed to determine production week for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandangs") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to determine production week")
}
targetAverages, err := s.RecordingRepo.GetAverageTargetMetricsByProjectFlockKandangID(c.Context(), projectFlockKandangIDs[0], !isGrowing)
if err != nil {
s.Log.Errorf("Failed to calculate target metrics for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch target metrics data")
}
var fcrActFromRecording *float64
if targetAverages.FcrCount > 0 {
fcrAvg := targetAverages.FcrAvg
fcrActFromRecording = &fcrAvg
} }
feedIn, feedUsed, err := s.Repository.SumFeedPurchaseAndUsedByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs) feedIn, feedUsed, err := s.Repository.SumFeedPurchaseAndUsedByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs)
@@ -556,6 +740,40 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch feed purchase data") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch feed purchase data")
} }
averageFeedIntake := targetAverages.FeedIntakeAvg
feedIntakeStd := 0.0
var mortalityStdFromGrowth *float64
if project.ProductionStandardId > 0 && currentWeek > 0 && s.StandardGrowthDetailRepo != nil {
growthDetail, growthErr := s.StandardGrowthDetailRepo.GetByStandardIDAndWeek(c.Context(), project.ProductionStandardId, currentWeek)
if growthErr != nil {
if !errors.Is(growthErr, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to fetch growth detail for project flock %d: %+v", projectFlockID, growthErr)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch growth standard data")
}
} else if growthDetail != nil {
if growthDetail.FeedIntake != nil {
feedIntakeStd = *growthDetail.FeedIntake
}
if growthDetail.MaxDepletion != nil {
mortalityStdFromGrowth = growthDetail.MaxDepletion
}
}
}
var productionStandardDetail *entity.ProductionStandardDetail
if project.ProductionStandardId > 0 && currentWeek > 0 && s.ProductionStandardDetailRepo != nil {
productionStandardDetail, err = s.ProductionStandardDetailRepo.GetByStandardIDAndWeek(c.Context(), project.ProductionStandardId, currentWeek)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
productionStandardDetail = nil
} else {
s.Log.Errorf("Failed to fetch production standard detail for project flock %d: %+v", projectFlockID, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch production standard detail data")
}
}
}
claimCulling, err := s.Repository.SumClaimCullingByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs) claimCulling, err := s.Repository.SumClaimCullingByProjectFlockKandangIDs(c.Context(), projectFlockKandangIDs)
if err != nil { if err != nil {
s.Log.Errorf("Failed to sum claim culling for project flock %d: %+v", projectFlockID, err) s.Log.Errorf("Failed to sum claim culling for project flock %d: %+v", projectFlockID, err)
@@ -578,10 +796,10 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales age data") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales age data")
} }
feedUsedPerHead := 0.0 // feedUsedPerHead := 0.0
if population > 0 { // if population > 0 {
feedUsedPerHead = feedUsed / population // feedUsedPerHead = feedUsed / population
} // }
purchase := dto.ClosingPurchaseDTO{ purchase := dto.ClosingPurchaseDTO{
InitialPopulation: int(population), InitialPopulation: int(population),
@@ -589,7 +807,7 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
FinalPopulation: int(finalPopulation), FinalPopulation: int(finalPopulation),
FeedIn: feedIn, FeedIn: feedIn,
FeedUsed: feedUsed, FeedUsed: feedUsed,
FeedUsedPerHead: feedUsedPerHead, // FeedUsedPerHead: feedUsedPerHead,
} }
chickenFlagNames := []string{string(utils.FlagPullet)} chickenFlagNames := []string{string(utils.FlagPullet)}
@@ -622,6 +840,9 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
} }
chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age, standards) chickenPerformance := calculatePerformanceMetrics(chickenAverageWeight, chickenSalesWeight, feedUsed, population, chickenDepletion, age, standards)
if fcrActFromRecording != nil {
chickenPerformance.FcrAct = *fcrActFromRecording
}
var eggSales *dto.ClosingEggSalesDTO var eggSales *dto.ClosingEggSalesDTO
var eggPerformance *dto.ClosingPerformanceDTO var eggPerformance *dto.ClosingPerformanceDTO
@@ -669,6 +890,9 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
} }
eggPerf := calculatePerformanceMetrics(averageEggWeight, eggSalesWeight, feedUsed, harvestEggQty, eggDepletion, age, standards) eggPerf := calculatePerformanceMetrics(averageEggWeight, eggSalesWeight, feedUsed, harvestEggQty, eggDepletion, age, standards)
if fcrActFromRecording != nil {
eggPerf.FcrAct = *fcrActFromRecording
}
eggPerformance = &eggPerf eggPerformance = &eggPerf
} }
@@ -685,15 +909,63 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
DeffMortality: chickenPerformance.DeffMortality, DeffMortality: chickenPerformance.DeffMortality,
} }
if eggPerformance != nil { if eggPerformance != nil {
performance.FcrStd = eggPerformance.FcrStd // performance.FcrStd = eggPerformance.FcrStd
performance.FcrAct = eggPerformance.FcrAct performance.FcrAct = eggPerformance.FcrAct
performance.DeffFcr = eggPerformance.DeffFcr // performance.DeffFcr = eggPerformance.DeffFcr
performance.Awg = eggPerformance.Awg performance.AwgAct = eggPerformance.AwgAct
} else { } else {
performance.FcrStd = chickenPerformance.FcrStd // performance.FcrStd = chickenPerformance.FcrStd
performance.FcrAct = chickenPerformance.FcrAct performance.FcrAct = chickenPerformance.FcrAct
performance.DeffFcr = chickenPerformance.DeffFcr // performance.DeffFcr = chickenPerformance.DeffFcr
performance.Awg = chickenPerformance.Awg performance.AwgAct = chickenPerformance.AwgAct
}
performance.FeedIntake = averageFeedIntake
performance.FeedIntakeStd = feedIntakeStd
if targetAverages.CumDepletionRateCount > 0 {
performance.MortalityAct = targetAverages.CumDepletionRateAvg
performance.DeffMortality = performance.MortalityAct - performance.MortalityStd
}
if mortalityStdFromGrowth != nil {
performance.MortalityStd = *mortalityStdFromGrowth
performance.DeffMortality = performance.MortalityAct - performance.MortalityStd
}
if !isGrowing {
if targetAverages.HenDayCount > 0 {
henDayAct := targetAverages.HenDayAvg
performance.HenDayAct = &henDayAct
}
if targetAverages.HenHouseCount > 0 {
henHouseAct := targetAverages.HenHouseAvg
performance.HenHouseAct = &henHouseAct
}
if targetAverages.EggWeightCount > 0 {
eggWeight := targetAverages.EggWeightAvg
performance.EggWeight = &eggWeight
}
if targetAverages.EggMassCount > 0 {
eggMass := targetAverages.EggMassAvg
performance.EggMass = &eggMass
}
}
performance.DeffFcr = performance.FcrStd - performance.FcrAct
if productionStandardDetail != nil {
if productionStandardDetail.StandardFCR != nil {
performance.FcrStd = *productionStandardDetail.StandardFCR
}
if !isGrowing {
if productionStandardDetail.TargetHenDayProduction != nil {
performance.HendayStd = productionStandardDetail.TargetHenDayProduction
}
if productionStandardDetail.TargetHenHouseProduction != nil {
performance.HenHouseStd = productionStandardDetail.TargetHenHouseProduction
}
if productionStandardDetail.TargetEggWeight != nil {
performance.EggWeightStd = productionStandardDetail.TargetEggWeight
}
if productionStandardDetail.TargetEggMass != nil {
performance.EggMassStd = productionStandardDetail.TargetEggMass
}
}
} }
result := dto.ClosingProductionReportDTO{ result := dto.ClosingProductionReportDTO{
@@ -739,6 +1011,46 @@ func (s closingService) calculateAverageSalesAge(ctx context.Context, projectFlo
return totalAgeWeeks / totalQty, nil return totalAgeWeeks / totalQty, nil
} }
func (s closingService) determineProductionWeek(ctx context.Context, projectFlockKandangIDs []uint) (int, error) {
if len(projectFlockKandangIDs) == 0 {
return 0, nil
}
firstKandangID := projectFlockKandangIDs[0]
var chickin entity.ProjectChickin
if err := s.Repository.DB().WithContext(ctx).
Where("project_flock_kandang_id = ?", firstKandangID).
Order("chick_in_date ASC").
First(&chickin).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, nil
}
return 0, err
}
recording, err := s.RecordingRepo.GetLatestByProjectFlockKandangID(ctx, firstKandangID)
if err != nil {
return 0, err
}
if recording == nil {
return 0, nil
}
if recording.RecordDatetime.Before(chickin.ChickInDate) {
return 0, nil
}
elapsed := recording.RecordDatetime.Sub(chickin.ChickInDate)
weekFloat := elapsed.Hours() / (24 * 7)
week := int(math.Ceil(weekFloat))
if week <= 0 {
week = 1
}
return week, nil
}
func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopulation, depletion, age float64, standards []entity.FcrStandard) dto.ClosingPerformanceDTO { func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopulation, depletion, age float64, standards []entity.FcrStandard) dto.ClosingPerformanceDTO {
mortalityStd, fcrStd := closestFcrValues(standards, averageWeight) mortalityStd, fcrStd := closestFcrValues(standards, averageWeight)
@@ -769,7 +1081,7 @@ func calculatePerformanceMetrics(averageWeight, totalWeight, feedUsed, basePopul
FcrStd: fcrStd, FcrStd: fcrStd,
FcrAct: fcrAct, FcrAct: fcrAct,
DeffFcr: deffFcr, DeffFcr: deffFcr,
Awg: awg, AwgAct: awg,
} }
} }
@@ -359,7 +359,11 @@ func (s sapronakService) buildSapronakItems(ctx context.Context, pfk entity.Proj
if filterFlag == "" { if filterFlag == "" {
return true return true
} }
return strings.ToUpper(f) == filterFlag candidate := strings.ToUpper(f)
if filterFlag == "DOC" || filterFlag == "PULLET" {
return candidate == "DOC" || candidate == "PULLET"
}
return candidate == filterFlag
} }
// For project flocks with category GROWING, pullet usage from chickin // For project flocks with category GROWING, pullet usage from chickin
@@ -20,7 +20,8 @@ const (
) )
type ClosingSapronakQuery struct { type ClosingSapronakQuery struct {
Type string `query:"type" validate:"required,oneof=incoming outgoing"` Type string `query:"type" validate:"required,oneof=incoming outgoing"`
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
KandangID *uint `query:"kandang_id" validate:"omitempty,gt=0"`
} }
@@ -0,0 +1,545 @@
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,
RejectReason: item.RejectReason,
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,
Kandang: dto.DailyChecklistReportEntityDTO{
Id: summary.KandangID,
Name: summary.KandangName,
},
}
}
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
}
documentDTOs := make([]dto.DailyChecklistDocumentDTO, len(detail.DocumentURLs))
for i, doc := range detail.DocumentURLs {
documentDTOs[i] = dto.DailyChecklistDocumentDTO{
Id: doc.ID,
Name: doc.Name,
Size: doc.Size,
URL: doc.URL,
}
}
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, documentDTOs),
})
}
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")
}
form, err := c.MultipartForm()
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid multipart form")
}
req.Documents = form.File["documents"]
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",
})
}

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