Compare commits

..

98 Commits

Author SHA1 Message Date
rstubryan 6b13794ee5 Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into feat/FE/US-281-439/TASK-440-441-recording-page-and-form-adjustment 2025-12-31 09:44:10 +07:00
rstubryan 8e88355894 refactor(FE-441): Refactor RecordingForm layout and labels 2025-12-31 09:43:05 +07:00
Rivaldi A N S 1b98e5d4d8 Merge branch 'feat/FE/US-81/production-result-report' into 'development'
[FEAT/FE][US#81] Production Result Report

See merge request mbugroup/lti-web-client!126
2025-12-31 02:42:23 +00:00
rstubryan d8daf09844 refactorF(FE-441): Display formatted fcr_std in RecordingForm 2025-12-31 09:20:49 +07:00
ValdiANS 0bb9aee139 chore: set dummy permission 2025-12-31 09:09:55 +07:00
Rivaldi A N S 306b8d3bf3 Merge branch 'hotfix/dashboard-permission' into 'development'
[HOTFIX/FE][US#390] Hotfix temporary permission for dashboard and finance

See merge request mbugroup/lti-web-client!124
2025-12-31 02:06:42 +00:00
rstubryan 2bf764a05c refactor(FE-441): Display '-' for empty/zero numeric fields 2025-12-31 09:02:15 +07:00
rstubryan 6c3285f624 refactor(FE-441): Make recording metrics grid responsive 2025-12-31 08:54:15 +07:00
rstubryan afb79b0589 feat(FE-441): Format and display intake, hand house, and totals 2025-12-31 08:50:51 +07:00
rstubryan 4f571f1c16 feat(FE-441): Show laying metrics and extend ProductionMetrics 2025-12-31 08:40:56 +07:00
rstubryan 81ca60a09b Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into feat/FE/US-281-439/TASK-440-441-recording-page-and-form-adjustment 2025-12-31 08:22:40 +07:00
randy-ar 23453eb8f5 hotfix(FE): change temporary permission for dashboard, finance and master data standard 2025-12-30 23:52:13 +07:00
Rivaldi A N S 3dc32da834 Merge branch 'feat/FE/US-81/production-result-report' into 'development'
[FEAT/FE][US#81/TASK-442] Production Result Report

See merge request mbugroup/lti-web-client!123
2025-12-30 16:02:58 +00:00
ValdiANS f089492830 chore: adjust closing data type 2025-12-30 22:49:39 +07:00
ValdiANS 6eaa92dfd4 Merge branch 'development' into feat/FE/US-81/production-result-report 2025-12-30 22:30:56 +07:00
rstubryan 8d668429e1 Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into feat/FE/US-281-439/TASK-440-441-recording-page-and-form-adjustment 2025-12-30 22:29:28 +07:00
rstubryan 8d1a3b665e refactor(FE-440): Use styled HTML tables for FCR and mortality 2025-12-30 22:29:15 +07:00
ValdiANS 7b99bae197 feat(FE-443): create ProductionResult type 2025-12-30 22:27:59 +07:00
ValdiANS dad169d854 feat(FE-443): create ProductionResultReportApiService 2025-12-30 22:27:33 +07:00
ValdiANS f985c041e4 chore: create getAll method in BaseApiService 2025-12-30 22:26:43 +07:00
ValdiANS 5326eff293 feat(FE-442): add laporan hasil produksi route permission 2025-12-30 22:26:29 +07:00
ValdiANS d66bd8c606 feat(FE-442): add Laporan Hasil Produksi menu 2025-12-30 22:25:53 +07:00
ValdiANS ea5ad20684 feat(FE-442): create ProductionResultProjectFlockKandangTable component 2025-12-30 22:25:27 +07:00
ValdiANS 23ee8828f0 feat(FE-442): create ProductionResultContent component 2025-12-30 22:24:22 +07:00
ValdiANS 0dd2edfe01 feat(FE-442): create Production Result Report page 2025-12-30 22:22:23 +07:00
Rivaldi A N S 6edc278bdf Merge branch 'feat/FE/US-391/TASK-435-436-expense-adjustment' into 'development'
[FEAT/FE][US#391/TASK-435-436] Expense Adjustment

See merge request mbugroup/lti-web-client!122
2025-12-30 15:21:09 +00:00
Rivaldi A N S f81c49becb Merge branch 'dev/randy' into 'development'
[FIX/FE][US#337-390] Fix issue in finance and adding dummy dashboard

See merge request mbugroup/lti-web-client!121
2025-12-30 15:20:45 +00:00
rstubryan 03a9451fc8 feat(FE-441): Add FCR and mortality tables to detail view 2025-12-30 22:18:51 +07:00
rstubryan cc0b051a0a refactor(FE-440): Remove body_weights handling from recording forms 2025-12-30 21:55:37 +07:00
rstubryan 865438e3fb refactor(FE-436): Check kandang_id before showing kandang name 2025-12-30 21:26:40 +07:00
rstubryan d39b71e759 refactor(FE-436): Use fallback when kandang name missing 2025-12-30 21:21:35 +07:00
rstubryan 5e6b03ef08 refactor(FE-435,436): Use location name as fallback for expense titles 2025-12-30 21:17:37 +07:00
rstubryan c291ba3246 refactor(FE-435,436): Use S3 public base URL for document links 2025-12-30 20:52:12 +07:00
randy-ar ab2e7db9d0 fix(FE): error null in marketing 2025-12-30 20:32:23 +07:00
rstubryan 13c1a82142 refactor(FE-435,436): Select Nominal Biaya by approval step 2025-12-30 20:19:05 +07:00
rstubryan 6185fafb57 refactor(FE-435): Multiply qty by price when summing expenses 2025-12-30 19:56:31 +07:00
randy-ar 2ab7c10d5d feat(FE): adding column standard fcr in master data standar production 2025-12-30 19:53:50 +07:00
randy-ar bc6ebcfeda fix(FE): add optional chaining for sapronak calculation in closing odule 2025-12-30 19:34:32 +07:00
randy-ar 10fb9fc990 feat(FE-390): slicing UI and API integration for production dashboard 2025-12-30 19:30:52 +07:00
randy-ar 28639516d5 feat(FE-390): slicing UI and API integration for production dashboard 2025-12-30 19:29:42 +07:00
rstubryan 2bf0f2874e refactor(FE-435): Allow realizations without kandang 2025-12-30 19:28:38 +07:00
rstubryan a81a61135f refactor(FE-435,436): Exclude null kandang_id from expense payload 2025-12-30 19:03:26 +07:00
rstubryan d2e88c2061 refactor(FE-435,436): Allow optional kandang and location expenses 2025-12-30 18:53:46 +07:00
rstubryan 8f4f3d93b8 refactor(FE-435,436): Add location_id to expense form and payload 2025-12-30 18:38:59 +07:00
rstubryan 7daca04cc1 refactor(FE-435): Add required asterisks to table headers 2025-12-30 17:54:03 +07:00
rstubryan 2c5168badf Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into dev/restu 2025-12-30 17:31:28 +07:00
randy-ar 2712821f4e fix(FE-337): fix finance input and transaction type 2025-12-30 09:25:54 +07:00
randy-ar 7e73c99074 Merge branch 'development' of https://gitlab.com/mbugroup/lti-web-client into dev/randy 2025-12-29 16:35:11 +07:00
randy-ar 0f86ba0f2d fix(FE): adjust options in master data product 2025-12-29 15:52:43 +07:00
randy-ar 10a37fde75 fix(FE): fix delete and update button row in master data production standards 2025-12-29 15:01:42 +07:00
randy-ar cd3a5ad441 feat(FE): adding production standard select options for project flock 2025-12-29 13:50:23 +07:00
randy-ar cd42bd6bc0 fix(FE): change hatchery to optional in master data supplier 2025-12-29 13:26:39 +07:00
randy-ar ea7f8a68f4 fix(FE): change select option warehouse marketing to correct data warehouses 2025-12-29 13:19:16 +07:00
Rivaldi A N S e0371b0884 Merge branch 'feat/FE/US-388/master-data-uniformity-standard' into 'development'
[FEAT/FE][US#388] Master Data Standar Produksi

See merge request mbugroup/lti-web-client!118
2025-12-28 07:15:27 +00:00
Rivaldi A N S 432d837aaf Merge branch 'dev/randy' into 'feat/FE/US-388/master-data-uniformity-standard'
[FEAT/FE][US#388] Master Data Standar Produksi

See merge request mbugroup/lti-web-client!116
2025-12-28 05:23:38 +00:00
randy-ar 77b05c6440 chore(FE): delete dummy data 2025-12-28 04:39:40 +07:00
randy-ar 731bec5a94 feat(FE-337): slicing ui form finance and API integration 2025-12-28 04:28:02 +07:00
randy-ar 6ea25aa3b1 feat(FE-331): implement permission guard in project flock, chickin and closing kandang 2025-12-28 01:00:20 +07:00
randy-ar d4f4505405 feat(FE-331): implement permission guard in project flock, chickin and closing kandang 2025-12-28 00:56:39 +07:00
randy-ar bd653851e2 feat(FE-331): implement permission guard in master data productions standards 2025-12-28 00:23:50 +07:00
randy-ar b9b349aa7a fix(FE): resolve git pull merge development 2025-12-27 16:49:16 +07:00
Rivaldi A N S 6ec6323bbc Merge branch 'feat/FE/US-304/permission-guard' into 'development'
[FEAT/FE][US#304] Permission Guard

See merge request mbugroup/lti-web-client!115
2025-12-27 09:41:13 +00:00
randy-ar c44e63bd2b fix(FE): fix page responsive in project flock dan marketing modules 2025-12-27 16:36:07 +07:00
ValdiANS 500c30c2bc feat(FE-331): implement permission guard in transfer to laying 2025-12-27 16:05:50 +07:00
ValdiANS 507c4005af feat(FE-331): implement permission guard in recording 2025-12-27 14:34:59 +07:00
randy-ar d49bca1d40 feat(FE): api integration production standards 2025-12-27 13:46:19 +07:00
randy-ar 663c1dea14 feat(FE): add master data production standard, slicing form and index table 2025-12-27 03:23:03 +07:00
ValdiANS 9e0d3e2bbf feat(FE-331): implement permission guard in project flock 2025-12-26 21:08:00 +07:00
ValdiANS 4be719b9d8 feat(FE-331): implement permission guard in purchase 2025-12-26 16:21:22 +07:00
ValdiANS 1152cd2bef chore(FE-331): update report permission 2025-12-26 15:46:06 +07:00
randy-ar 4ddd1dc8e3 feat(FE-337): slicing ui form add finance 2025-12-24 18:45:33 +07:00
randy-ar 8c95dc8327 feat(FE-350): add filtering table 2025-12-24 16:44:53 +07:00
ValdiANS df6c1ae49d feat(FE-331): implement permission guard in marketing 2025-12-24 15:43:45 +07:00
ValdiANS 42a56a08d7 chore(FE-331): update /marketing/detail/ permission name 2025-12-24 15:31:50 +07:00
ValdiANS 6ed7dcfa6d feat(FE-331): implement permission guard in expense 2025-12-24 11:08:37 +07:00
ValdiANS dda29e10d1 chore: change "Biaya Operasional" menu text to "Biaya" 2025-12-24 11:07:40 +07:00
ValdiANS 9d9b9d93db feat(FE-331): implement permission guard in closing 2025-12-24 09:41:48 +07:00
ValdiANS f41899dbc9 feat(FE-331): add /report route to ROUTE_PERMISSIONS 2025-12-24 09:24:16 +07:00
randy-ar 36ff6d04ee feat(FE-337): init slicing UI and define data types 2025-12-23 17:38:16 +07:00
ValdiANS de63b6721a feat(FE-331): implement permission guard in inventory 2025-12-23 16:40:32 +07:00
ValdiANS a200dac23c chore(FE-331): fix inventory route path 2025-12-23 16:39:08 +07:00
ValdiANS fcfd2fb576 Merge branch 'feat/FE/US-304/permission-guard' into feat/FE/US-304/permission-guard-master-data 2025-12-23 15:55:50 +07:00
ValdiANS 2c28d0a831 Merge branch 'feat/FE/US-304/permission-guard' into feat/FE/US-304/permission-guard-master-data 2025-12-23 15:53:05 +07:00
ValdiANS addfaff692 feat(FE-331): implement permission guard in master data 2025-12-23 12:10:07 +07:00
ValdiANS ecdbb764d5 feat(FE-331): only show menu if user has the permission 2025-12-23 12:09:49 +07:00
ValdiANS a3be3de338 feat(FE-331): return PermissionNotFound component if user is not permitted 2025-12-23 12:09:06 +07:00
ValdiANS 9e895af62a chore: refresh user data every 13 minutes 2025-12-23 12:08:39 +07:00
ValdiANS 1f9992c1c8 feat(FE-331): add permissions to MAIN_DRAWER_LINKS 2025-12-23 12:08:16 +07:00
ValdiANS 574fb3b371 feat(FE-331): create ROUTE_PERMISSION constant 2025-12-23 12:07:55 +07:00
ValdiANS 4643a39c3e feat(FE-331): create RequirePermission helper component 2025-12-23 12:07:38 +07:00
ValdiANS 88b8767ca4 chore: lint 2025-12-23 12:07:24 +07:00
ValdiANS de19cc5de2 feat(FE-331): create PermissionNotFound component 2025-12-23 12:07:14 +07:00
rstubryan b9dad3094c Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into dev/restu 2025-12-23 11:08:36 +07:00
rstubryan ff427d13cc Merge branch 'feat/FE/US-334/expedition-hpp-report' of gitlab.com:mbugroup/lti-web-client into dev/restu 2025-12-23 10:55:03 +07:00
rstubryan 8295943b82 Merge branch 'feat/FE/US-339/purchase-report' of gitlab.com:mbugroup/lti-web-client into dev/restu 2025-12-23 10:53:44 +07:00
rstubryan a3169d582d Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into dev/restu 2025-12-23 10:52:24 +07:00
rstubryan dd6c6263e7 Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into dev/restu 2025-12-23 09:32:08 +07:00
rstubryan 5d03b68576 Merge branch 'development' of gitlab.com:mbugroup/lti-web-client into dev/restu 2025-12-22 15:48:43 +07:00
139 changed files with 13046 additions and 5212 deletions
+1 -1
View File
@@ -1,3 +1,3 @@
npm run format
npm run lint
npm run build
npx tsc --noEmit
+383 -12
View File
@@ -25,6 +25,7 @@
"react-hot-toast": "^2.6.0",
"react-number-format": "^5.4.4",
"react-select": "^5.10.2",
"recharts": "^3.6.0",
"swr": "^2.3.6",
"tailwind-merge": "^3.3.1",
"use-debounce": "^10.0.6",
@@ -1450,6 +1451,42 @@
"@react-pdf/stylesheet": "^6.1.1"
}
},
"node_modules/@reduxjs/toolkit": {
"version": "2.11.2",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
"integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@standard-schema/utils": "^0.3.0",
"immer": "^11.0.0",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"reselect": "^5.1.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-redux": {
"optional": true
}
}
},
"node_modules/@reduxjs/toolkit/node_modules/immer": {
"version": "11.1.3",
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz",
"integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -1464,6 +1501,18 @@
"dev": true,
"license": "MIT"
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"license": "MIT"
},
"node_modules/@standard-schema/utils": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT"
},
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -1804,6 +1853,69 @@
"tslib": "^2.4.0"
}
},
"node_modules/@types/d3-array": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
"license": "MIT"
},
"node_modules/@types/d3-color": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
"license": "MIT"
},
"node_modules/@types/d3-ease": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
"license": "MIT"
},
"node_modules/@types/d3-interpolate": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
"license": "MIT",
"dependencies": {
"@types/d3-color": "*"
}
},
"node_modules/@types/d3-path": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
"license": "MIT"
},
"node_modules/@types/d3-scale": {
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
"license": "MIT",
"dependencies": {
"@types/d3-time": "*"
}
},
"node_modules/@types/d3-shape": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
"license": "MIT",
"dependencies": {
"@types/d3-path": "*"
}
},
"node_modules/@types/d3-time": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
"license": "MIT"
},
"node_modules/@types/d3-timer": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1871,7 +1983,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -1902,6 +2013,12 @@
"license": "MIT",
"optional": true
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.46.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz",
@@ -1948,7 +2065,6 @@
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.46.2",
"@typescript-eslint/types": "8.46.2",
@@ -2472,7 +2588,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -3138,8 +3253,128 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"license": "ISC",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-path": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"license": "ISC",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-shape": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
"license": "ISC",
"dependencies": {
"d3-path": "^3.1.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"license": "ISC",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time-format": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"license": "ISC",
"dependencies": {
"d3-time": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/daisyui": {
"version": "5.5.8",
@@ -3245,6 +3480,12 @@
}
}
},
"node_modules/decimal.js-light": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
"license": "MIT"
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -3587,6 +3828,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/es-toolkit": {
"version": "1.43.0",
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.43.0.tgz",
"integrity": "sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==",
"license": "MIT",
"workspaces": [
"docs",
"benchmarks"
]
},
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -3605,7 +3856,6 @@
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -3779,7 +4029,6 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -4026,6 +4275,12 @@
"node": ">=0.10.0"
}
},
"node_modules/eventemitter3": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
"license": "MIT"
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@@ -4651,6 +4906,16 @@
"node": ">= 4"
}
},
"node_modules/immer": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -4698,6 +4963,15 @@
"node": ">= 0.4"
}
},
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/iobuffer": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
@@ -5244,7 +5518,6 @@
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.4.tgz",
"integrity": "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4",
"fast-png": "^6.2.0",
@@ -6345,7 +6618,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -6376,7 +6648,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
@@ -6440,6 +6711,29 @@
"react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT",
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
},
"peerDependencies": {
"@types/react": "^18.2.25 || ^19",
"react": "^18.0 || ^19",
"redux": "^5.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"redux": {
"optional": true
}
}
},
"node_modules/react-select": {
"version": "5.10.2",
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.10.2.tgz",
@@ -6477,6 +6771,51 @@
"react-dom": ">=16.6.0"
}
},
"node_modules/recharts": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.6.0.tgz",
"integrity": "sha512-L5bjxvQRAe26RlToBAziKUB7whaGKEwD3znoM6fz3DrTowCIC/FnJYnuq1GEzB8Zv2kdTfaxQfi5GoH0tBinyg==",
"license": "MIT",
"workspaces": [
"www"
],
"dependencies": {
"@reduxjs/toolkit": "1.x.x || 2.x.x",
"clsx": "^2.1.1",
"decimal.js-light": "^2.5.1",
"es-toolkit": "^1.39.3",
"eventemitter3": "^5.0.1",
"immer": "^10.1.1",
"react-redux": "8.x.x || 9.x.x",
"reselect": "5.1.1",
"tiny-invariant": "^1.3.3",
"use-sync-external-store": "^1.2.2",
"victory-vendor": "^37.0.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT"
},
"node_modules/redux-thunk": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
"license": "MIT",
"peerDependencies": {
"redux": "^5.0.0"
}
},
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -6543,6 +6882,12 @@
"node": ">=0.10.0"
}
},
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@@ -7263,6 +7608,12 @@
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
"license": "MIT"
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
"node_modules/tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
@@ -7310,7 +7661,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -7478,7 +7828,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -7635,6 +7984,28 @@
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/victory-vendor": {
"version": "37.3.6",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
"integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
"license": "MIT AND ISC",
"dependencies": {
"@types/d3-array": "^3.0.3",
"@types/d3-ease": "^3.0.0",
"@types/d3-interpolate": "^3.0.1",
"@types/d3-scale": "^4.0.2",
"@types/d3-shape": "^3.1.0",
"@types/d3-time": "^3.0.0",
"@types/d3-timer": "^3.0.0",
"d3-array": "^3.1.6",
"d3-ease": "^3.0.1",
"d3-interpolate": "^3.0.1",
"d3-scale": "^4.0.2",
"d3-shape": "^3.1.0",
"d3-time": "^3.0.0",
"d3-timer": "^3.0.1"
}
},
"node_modules/vite-compatible-readable-stream": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/vite-compatible-readable-stream/-/vite-compatible-readable-stream-3.6.1.tgz",
+1
View File
@@ -28,6 +28,7 @@
"react-hot-toast": "^2.6.0",
"react-number-format": "^5.4.4",
"react-select": "^5.10.2",
"recharts": "^3.6.0",
"swr": "^2.3.6",
"tailwind-merge": "^3.3.1",
"use-debounce": "^10.0.6",
+3 -5
View File
@@ -1,9 +1,7 @@
import DashboardProduction from '@/components/pages/dashboard/DashboardProduction';
const Dashboard = () => {
return (
<section className='w-full p-4'>
<h1 className='text-3xl font-bold text-primary'>Dashboard</h1>
</section>
);
return <DashboardProduction />;
};
export default Dashboard;
+5
View File
@@ -0,0 +1,5 @@
const FinanceAdjust = () => {
return <div>Finance Adjust</div>;
};
export default FinanceAdjust;
@@ -0,0 +1,7 @@
import FormFinanceAddInitialBalance from '@/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance';
const FinanceAddInitialBalancePage = () => {
return <FormFinanceAddInitialBalance type='add' />;
};
export default FinanceAddInitialBalancePage;
+7
View File
@@ -0,0 +1,7 @@
import FormFinanceInjection from '@/components/pages/finance/add/injection/FormFinanceInjection';
const FinanceAddInjectionPage = () => {
return <FormFinanceInjection type='add' />;
};
export default FinanceAddInjectionPage;
+7
View File
@@ -0,0 +1,7 @@
import FormFinanceAdd from '@/components/pages/finance/add/FormFinanceAdd';
const FinanceAddPage = () => {
return <FormFinanceAdd />;
};
export default FinanceAddPage;
@@ -0,0 +1,51 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import { FinanceApi } from '@/services/api/finance';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import FormFinanceAddInitialBalance from '@/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance';
const EditFinanceInitialBalancePage = () => {
const router = useRouter();
const searchParams = useSearchParams();
const financeId = searchParams.get('financeId');
const { data: finance, isLoading: isLoadingFinance } = useSWR(
financeId,
(id: number) => FinanceApi.getSingle(id)
);
if (!financeId) {
router.back();
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if (!isLoadingFinance && (!finance || isResponseError(finance))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingFinance && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingFinance && (
<FormFinanceAddInitialBalance
type='edit'
initialValues={isResponseSuccess(finance) ? finance.data : undefined}
/>
)}
</div>
);
};
export default EditFinanceInitialBalancePage;
@@ -0,0 +1,51 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import { FinanceApi } from '@/services/api/finance';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import FormFinanceInjection from '@/components/pages/finance/add/injection/FormFinanceInjection';
const EditFinanceInjectionPage = () => {
const router = useRouter();
const searchParams = useSearchParams();
const financeId = searchParams.get('financeId');
const { data: finance, isLoading: isLoadingFinance } = useSWR(
financeId,
(id: number) => FinanceApi.getSingle(id)
);
if (!financeId) {
router.back();
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if (!isLoadingFinance && (!finance || isResponseError(finance))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingFinance && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingFinance && (
<FormFinanceInjection
type='edit'
initialValues={isResponseSuccess(finance) ? finance.data : undefined}
/>
)}
</div>
);
};
export default EditFinanceInjectionPage;
+52
View File
@@ -0,0 +1,52 @@
'use client';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import { FinanceApi } from '@/services/api/finance';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import FormFinanceAdd from '@/components/pages/finance/add/FormFinanceAdd';
import FormFinanceAddInitialBalance from '@/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance';
const EditFinanceTransactionPage = () => {
const router = useRouter();
const searchParams = useSearchParams();
const financeId = searchParams.get('financeId');
const { data: finance, isLoading: isLoadingFinance } = useSWR(
financeId,
(id: number) => FinanceApi.getSingle(id)
);
if (!financeId) {
router.back();
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if (!isLoadingFinance && (!finance || isResponseError(finance))) {
router.replace('/404');
return;
}
return (
<div className='w-full p-4 flex flex-row justify-center'>
{isLoadingFinance && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingFinance && (
<FormFinanceAdd
type='edit'
initialValues={isResponseSuccess(finance) ? finance.data : undefined}
/>
)}
</div>
);
};
export default EditFinanceTransactionPage;
+41
View File
@@ -0,0 +1,41 @@
'use client';
import FinanceDetail from '@/components/pages/finance/FinanceDetail';
import useSWR from 'swr';
import { useRouter, useSearchParams } from 'next/navigation';
import { FinanceApi } from '@/services/api/finance';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
const FinanceDetailPage = () => {
const router = useRouter();
const financeId = useSearchParams().get('financeId');
const { data: finance } = useSWR(financeId, () =>
FinanceApi.getSingle(Number(financeId))
);
if (!financeId) {
router.back();
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
console.log(finance);
// if (!finance || isResponseError(finance)) {
// router.replace('/404');
// return;
// }
return (
<>
{isResponseSuccess(finance) && <FinanceDetail finance={finance.data} />}
</>
);
};
export default FinanceDetailPage;
+14
View File
@@ -0,0 +1,14 @@
'use client';
import FinanceTable from '@/components/pages/finance/FinanceTable';
const Finance = () => {
return (
<section className='size-full p-6'>
<div className='flex flex-row gap-4'></div>
<FinanceTable />
</section>
);
};
export default Finance;
@@ -0,0 +1,13 @@
'use client';
import ProductionStandardForm from '@/components/pages/master-data/production-standard/form/ProductionStandardForm';
const AddProductionStandardPage = () => {
return (
<>
<ProductionStandardForm formType='add' />
</>
);
};
export default AddProductionStandardPage;
@@ -0,0 +1,56 @@
'use client';
import ProductionStandardForm from '@/components/pages/master-data/production-standard/form/ProductionStandardForm';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { ProductionStandardApi } from '@/services/api/master-data';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
const EditProductionStandardPage = () => {
const router = useRouter();
const searchParams = useSearchParams();
// Get Query Params
const productionStandardId = searchParams.get('productionStandardId');
// Fetch Data
const { data: productionStandard, isLoading: isLoadingProductionStandard } =
useSWR(productionStandardId, (id: number) =>
ProductionStandardApi.getSingle(id)
);
if (!productionStandardId) {
router.back();
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if (
!isLoadingProductionStandard &&
(!productionStandard || isResponseError(productionStandard))
) {
router.replace('/404');
return;
}
return (
<>
{isLoadingProductionStandard && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingProductionStandard &&
isResponseSuccess(productionStandard) && (
<ProductionStandardForm
formType='edit'
initialValue={productionStandard.data}
/>
)}
</>
);
};
export default EditProductionStandardPage;
@@ -0,0 +1,11 @@
import SuspenseHelper from '@/components/helper/SuspenseHelper';
const Layout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return <SuspenseHelper>{children}</SuspenseHelper>;
};
export default Layout;
@@ -0,0 +1,56 @@
'use client';
import ProductionStandardForm from '@/components/pages/master-data/production-standard/form/ProductionStandardForm';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { ProductionStandardApi } from '@/services/api/master-data';
import { useRouter, useSearchParams } from 'next/navigation';
import useSWR from 'swr';
const DetailProductionStandardPage = () => {
const router = useRouter();
const searchParams = useSearchParams();
// Get Query Params
const productionStandardId = searchParams.get('productionStandardId');
// Fetch Data
const { data: productionStandard, isLoading: isLoadingProductionStandard } =
useSWR(productionStandardId, (id: number) =>
ProductionStandardApi.getSingle(id)
);
if (!productionStandardId) {
router.back();
return (
<div className='w-full flex flex-row justify-center items-center p-4'>
<span className='loading loading-spinner loading-xl' />
</div>
);
}
if (
!isLoadingProductionStandard &&
(!productionStandard || isResponseError(productionStandard))
) {
router.replace('/404');
return;
}
return (
<>
{isLoadingProductionStandard && (
<span className='loading loading-spinner loading-xl' />
)}
{!isLoadingProductionStandard &&
isResponseSuccess(productionStandard) && (
<ProductionStandardForm
formType='detail'
initialValue={productionStandard.data}
/>
)}
</>
);
};
export default DetailProductionStandardPage;
@@ -0,0 +1,11 @@
import ProductionStandardTable from '@/components/pages/master-data/production-standard/ProductionStandardTable';
const ProductionStandardPage = () => {
return (
<div className='w-full'>
<ProductionStandardTable />
</div>
);
};
export default ProductionStandardPage;
@@ -1,20 +0,0 @@
'use client';
import { FormHeader } from '@/components/helper/form/FormHeader';
import ProjectFlockChickinDetail from '@/components/pages/production/project-flock/chickin/ProjectFlockChickinDetail';
import { useSearchParams } from 'next/navigation';
const AddChickin = () => {
const searchParams = useSearchParams();
const projectFlockId = searchParams.get('projectFlockId');
return (
<>
<section className='w-full'>
<ProjectFlockChickinDetail projectFlockId={Number(projectFlockId)} />
</section>
</>
);
};
export default AddChickin;
@@ -1,10 +0,0 @@
import ChickinTable from '@/components/pages/production/chickin/ChickinTable';
const Chickin = () => {
return (
<section className='w-full'>
<ChickinTable />
</section>
);
};
export default Chickin;
+11
View File
@@ -0,0 +1,11 @@
import ProductionResultContent from '@/components/pages/report/production-result/ProductionResultContent';
const ProductionResultReportPage = () => {
return (
<section className='w-full max-w-7xl pb-16'>
<ProductionResultContent />
</section>
);
};
export default ProductionResultReportPage;
+2 -2
View File
@@ -64,7 +64,7 @@ const Drawer = ({
),
drawerSidebarContent: cn(
baseClassNames.drawerSidebarContent,
'w-full min-w-120 sm:w-fit'
'w-full sm:min-w-120 sm:w-fit'
),
};
} else if (variant === 'left') {
@@ -76,7 +76,7 @@ const Drawer = ({
),
drawerSidebarContent: cn(
baseClassNames.drawerSidebarContent,
'w-full min-w-120 sm:w-fit'
'w-full sm:min-w-120 sm:w-fit'
),
};
}
+52 -24
View File
@@ -5,6 +5,8 @@ import Tooltip from '@/components/Tooltip';
import { cn } from '@/lib/helper';
import { Icon } from '@iconify/react';
import { useAuth } from '@/services/hooks/useAuth';
type FloatingActionsButtonProps = {
actions: {
action: 'DETAIL' | 'EDIT' | 'DELETE';
@@ -13,6 +15,7 @@ type FloatingActionsButtonProps = {
onClick?: () => void;
hidden?: boolean;
disabled?: boolean;
permissions?: string | string[];
}[];
approvals: {
action: 'APPROVED' | 'REJECTED';
@@ -20,6 +23,7 @@ type FloatingActionsButtonProps = {
label?: string;
onClick?: () => void;
disabled?: boolean;
permissions?: string | string[];
}[];
selectedRowIds: number[];
onClose: () => void;
@@ -31,6 +35,7 @@ const FloatingActionsButton = ({
selectedRowIds,
onClose,
}: FloatingActionsButtonProps) => {
const { permissionCheck } = useAuth();
// Jika tidak ada baris yang dipilih, jangan tampilkan FAB
const positionStyles =
selectedRowIds.length > 0
@@ -71,7 +76,18 @@ const FloatingActionsButton = ({
<div className='flex gap-4 items-center'>
{/* Render Aksi dari props.actions */}
{actions
.filter((action) => !action.hidden)
.filter((action) => {
if (action.hidden) return false;
if (action.permissions) {
if (typeof action.permissions === 'string') {
return permissionCheck(action.permissions);
}
return action.permissions.some((permission) =>
permissionCheck(permission)
);
}
return true;
})
.map((action, index) => {
return (
<Button
@@ -111,29 +127,41 @@ const FloatingActionsButton = ({
{/* === BARIS BAWAH: Approval Buttons (Approve/Reject) === */}
<div className={`grid grid-cols-${approvals.length} gap-3`}>
{approvals.map((approval, index) => (
<Button
key={index}
onClick={approval.onClick}
className={cn(
'btn btn-lg w-full',
'bg-white/20 border-white/30',
'text-white/50 font-semibold flex items-center gap-2 rounded-lg transition-all duration-200',
approval.disabled
? 'cursor-not-allowed'
: 'hover:text-white/100 hover:bg-white/40 hover:border-white/50'
)}
disabled={approval.disabled}
>
<Icon
icon={approval.icon}
width={20}
height={20}
className={`text-${getApprovalColor(approval.action)}`}
/>
{approval.label || approval.action}
</Button>
))}
{approvals
.filter((approval) => {
if (approval.permissions) {
if (typeof approval.permissions === 'string') {
return permissionCheck(approval.permissions);
}
return approval.permissions.some((permission) =>
permissionCheck(permission)
);
}
return true;
})
.map((approval, index) => (
<Button
key={index}
onClick={approval.onClick}
className={cn(
'btn btn-lg w-full',
'bg-white/20 border-white/30',
'text-white/50 font-semibold flex items-center gap-2 rounded-lg transition-all duration-200',
approval.disabled
? 'cursor-not-allowed'
: 'hover:text-white/100 hover:bg-white/40 hover:border-white/50'
)}
disabled={approval.disabled}
>
<Icon
icon={approval.icon}
width={20}
height={20}
className={`text-${getApprovalColor(approval.action)}`}
/>
{approval.label || approval.action}
</Button>
))}
</div>
</div>
</div>
+12
View File
@@ -9,10 +9,13 @@ import Drawer from '@/components/Drawer';
import Navbar from '@/components/Navbar';
import Button from '@/components/Button';
import SidebarMenu from '@/components/molecules/SidebarMenu';
import PermissionNotFound from '@/components/helper/PermissionNotFound';
import { useUiStore } from '@/stores/ui/ui.store';
import { MAIN_DRAWER_LINKS } from '@/config/constant';
import { isPathActive } from '@/lib/helper';
import { ROUTE_PERMISSIONS } from '@/config/route-permission';
import { useAuth } from '@/services/hooks/useAuth';
const MainDrawerContent = () => {
const pathname = usePathname();
@@ -62,6 +65,11 @@ const MainDrawer = ({
}>) => {
const { mainDrawerOpen, setMainDrawerOpen } = useUiStore();
const pathname = usePathname();
const { permissionCheck } = useAuth();
const isPermitted = ROUTE_PERMISSIONS[pathname]?.some((permission) =>
permissionCheck(permission)
);
const getPageTitle = useCallback(() => {
let title = '';
@@ -101,6 +109,10 @@ const MainDrawer = ({
setMainDrawerOpen(!mainDrawerOpen);
};
if (!isPermitted) {
return <PermissionNotFound />;
}
return (
<Drawer
open={mainDrawerOpen}
@@ -0,0 +1,12 @@
const PermissionNotFound = () => {
return (
<div className='w-full h-screen flex flex-col justify-center items-center gap-4'>
<h2 className='text-2xl font-bold text-error'>Permission Not Found</h2>
<p className='text-gray-600 text-center'>
You do not have permission to access this page.
</p>
</div>
);
};
export default PermissionNotFound;
@@ -0,0 +1,28 @@
'use client';
import { useAuth } from '@/services/hooks/useAuth';
interface RequirePermissionProps {
children: React.ReactNode;
permissions: string | string[];
}
const RequirePermission = ({
children,
permissions,
}: RequirePermissionProps) => {
const { permissionCheck } = useAuth();
const isPermitted =
typeof permissions === 'string'
? permissionCheck(permissions)
: permissions.some((permission) => permissionCheck(permission));
if (!isPermitted) {
return null;
}
return <>{children}</>;
};
export default RequirePermission;
+20 -7
View File
@@ -2,6 +2,7 @@ import Link from 'next/link';
import Menu from '@/components/menu/Menu';
import { Icon } from '@iconify/react';
import { cn, isPathActive } from '@/lib/helper';
import { useAuth } from '@/services/hooks/useAuth';
export interface SidebarMenuItem {
type?: 'item' | 'title';
@@ -9,6 +10,7 @@ export interface SidebarMenuItem {
link: string;
icon?: string;
submenu?: SidebarMenuItem[];
permission?: string[];
}
interface SidebarMenuItemProps {
@@ -22,8 +24,17 @@ interface SidebarMenuProps {
}
const SidebarMenuItem = ({ item, activeLink }: SidebarMenuItemProps) => {
const { permissionCheck } = useAuth();
const isItemActive = isPathActive(activeLink, item.link);
const isUserPermitted = item.permission
? item.permission?.some((permissionName) => permissionCheck(permissionName))
: true;
if (!isUserPermitted) {
return null;
}
const menuItemWithoutSubmenu = (
<li>
<Link
@@ -78,13 +89,15 @@ const SidebarMenuItem = ({ item, activeLink }: SidebarMenuItemProps) => {
const SidebarMenu = ({ menu, activeLink }: SidebarMenuProps) => {
return (
<Menu>
{menu.map((menuItem, menuIdx) => (
<SidebarMenuItem
key={menuIdx}
item={menuItem}
activeLink={activeLink}
/>
))}
{menu.map((menuItem, menuIdx) => {
return (
<SidebarMenuItem
key={menuIdx}
item={menuItem}
activeLink={activeLink}
/>
);
})}
</Menu>
);
};
@@ -25,9 +25,9 @@ const ClosingGeneralInformationTable = ({
<td>{initialValue?.period}</td>
</tr>
<tr>
<td>Kategori</td>
<td>Project Flock</td>
<td>:</td>
<td>{initialValue?.project_category}</td>
<td>{initialValue?.project_flock?.name}</td>
</tr>
<tr>
<td>Populasi</td>
@@ -3,7 +3,7 @@
import Card from '@/components/Card';
import Table from '@/components/Table';
import { cn, formatCurrency, formatNumber } from '@/lib/helper';
import { formatCurrency, formatNumber } from '@/lib/helper';
import {
RowSapronakCalculation,
TotalSapronakCalculation,
@@ -54,7 +54,7 @@ const ClosingSapronakCalculationTable = ({
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{formatNumber(total.qty_masuk)}
{formatNumber(total?.qty_masuk)}
</div>
)
: '',
@@ -66,7 +66,7 @@ const ClosingSapronakCalculationTable = ({
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{formatNumber(total.qty_keluar)}
{formatNumber(total?.qty_keluar)}
</div>
)
: '',
@@ -78,7 +78,7 @@ const ClosingSapronakCalculationTable = ({
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{formatNumber(total.qty_pakai)}
{formatNumber(total?.qty_pakai)}
</div>
)
: '',
@@ -102,7 +102,7 @@ const ClosingSapronakCalculationTable = ({
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{formatCurrency(total.harga_beli_per_qty)}
{formatCurrency(total?.harga_beli_per_qty)}
</div>
)
: '',
@@ -114,7 +114,7 @@ const ClosingSapronakCalculationTable = ({
footer: total
? () => (
<div className='font-semibold text-gray-900'>
{formatCurrency(total.total_harga)}
{formatCurrency(total?.total_harga)}
</div>
)
: '',
@@ -131,7 +131,7 @@ const ClosingSapronakCalculationTable = ({
const docBroilerColumns = useMemo(
() =>
isResponseSuccess(sapronakCalculation)
? createColumns(sapronakCalculation.data?.doc_broiler.total)
? createColumns(sapronakCalculation.data?.doc_broiler?.total)
: createColumns(),
[sapronakCalculation]
);
@@ -139,7 +139,7 @@ const ClosingSapronakCalculationTable = ({
const ovkColumns = useMemo(
() =>
isResponseSuccess(sapronakCalculation)
? createColumns(sapronakCalculation.data?.ovk.total)
? createColumns(sapronakCalculation.data?.ovk?.total)
: createColumns(),
[sapronakCalculation]
);
@@ -147,7 +147,7 @@ const ClosingSapronakCalculationTable = ({
const pakanColumns = useMemo(
() =>
isResponseSuccess(sapronakCalculation)
? createColumns(sapronakCalculation.data?.pakan.total)
? createColumns(sapronakCalculation.data?.pakan?.total)
: createColumns(),
[sapronakCalculation]
);
@@ -166,7 +166,7 @@ const ClosingSapronakCalculationTable = ({
<Table<RowSapronakCalculation>
data={
isResponseSuccess(sapronakCalculation)
? (sapronakCalculation.data?.doc_broiler.rows ?? [])
? (sapronakCalculation.data?.doc_broiler?.rows ?? [])
: []
}
columns={docBroilerColumns}
@@ -189,7 +189,7 @@ const ClosingSapronakCalculationTable = ({
<Table<RowSapronakCalculation>
data={
isResponseSuccess(sapronakCalculation)
? (sapronakCalculation.data?.ovk.rows ?? [])
? (sapronakCalculation.data?.ovk?.rows ?? [])
: []
}
columns={ovkColumns}
@@ -212,7 +212,7 @@ const ClosingSapronakCalculationTable = ({
<Table<RowSapronakCalculation>
data={
isResponseSuccess(sapronakCalculation)
? (sapronakCalculation.data?.pakan.rows ?? [])
? (sapronakCalculation.data?.pakan?.rows ?? [])
: []
}
columns={pakanColumns}
+13 -32
View File
@@ -15,6 +15,8 @@ import SelectInput, {
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission';
import { cn, formatCurrency, formatDate } from '@/lib/helper';
import { isResponseSuccess } from '@/lib/api-helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
@@ -43,17 +45,18 @@ const RowOptionsMenu = ({
}) => {
return (
<RowOptionsMenuWrapper type={type}>
{/* TODO: apply RBAC */}
<div className='w-full max-h-40 overflow-auto flex flex-col gap-1'>
<Button
href={`/closing/detail/?closingId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
<RequirePermission permissions='lti.closing.detail'>
<Button
href={`/closing/detail/?closingId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
</RequirePermission>
</div>
</RowOptionsMenuWrapper>
);
@@ -123,28 +126,6 @@ const ClosingsTable = () => {
accessorKey: 'shed_label',
header: 'Jumlah Kandang',
},
{
accessorKey: 'sales_paid_amount',
header: 'Jumlah Sudah Bayar',
cell: (props) => (
<span className='text-success'>
{formatCurrency(props.row.original.sales_paid_amount)}
</span>
),
},
{
accessorKey: 'sales_remaining_amount',
header: 'Jumlah Sisa Bayar',
cell: (props) => (
<span className='text-error'>
{formatCurrency(props.row.original.sales_remaining_amount)}
</span>
),
},
{
accessorKey: 'sales_payment_status',
header: 'Status Pembayaran',
},
{
accessorKey: 'project_status',
header: 'Status',
@@ -0,0 +1,399 @@
'use client';
import Button from '@/components/Button';
import Card from '@/components/Card';
import { Icon } from '@iconify/react';
import ProductionLineChart from '@/components/pages/dashboard/chart/ProductionLineChart';
import StandardLineChart from '@/components/pages/dashboard/chart/StandardLineChart';
import EggWeightBarChart from '@/components/pages/dashboard/chart/EggWeightBarChart';
import FCRBarChart from '@/components/pages/dashboard/chart/FCRBarChart';
import ProductionStat from '@/components/pages/dashboard/chart/ProductionStat';
import Modal, { useModal } from '@/components/Modal';
import DateInput from '@/components/input/DateInput';
import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import { RadioGroup } from '@/components/input/RadioInput';
import { useState } from 'react';
import useSWR from 'swr';
import { DashboardApi } from '@/services/api/dashboard';
import { useFormik } from 'formik';
import dashboardProductionFilterSchema from '@/components/pages/dashboard/filter/DashboardProductionFilter.schema';
import { ProjectFlockApi } from '@/services/api/production';
import { ProductionStandardApi } from '@/services/api/master-data';
const DashboardProduction = () => {
const filterModal = useModal();
const [selectedPeriod, setSelectedPeriod] = useState('daily');
const [selectedStandards, setSelectedStandards] = useState<string[]>([
'hen_day',
'hen_house',
]);
const [endpointUrl, setEndpointUrl] = useState('/dashboard');
// ===== FETCH DATA =====
const {
data: dashboardProductionResponse,
isLoading: isLoadingDashboardProductionData,
error: dashboardProductionError,
} = useSWR(endpointUrl, () =>
DashboardApi.getDashboardProductionFetcher(endpointUrl)
);
const dashboardProductionData =
dashboardProductionResponse?.status === 'success'
? dashboardProductionResponse.data
: undefined;
// ===== SELECT =====
const { options: flockOptions, isLoadingOptions: isLoadingFlockOptions } =
useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', '', {
limit: 'limit',
category: 'LAYING',
});
const {
options: standardProductionOptions,
isLoadingOptions: isLoadingStandardProductionOptions,
} = useSelect(ProductionStandardApi.basePath, 'id', 'name', '', {
limit: 'limit',
});
// ===== FORMIK =====
const formik = useFormik({
initialValues: {
startDate: '',
endDate: '',
flock: [] as OptionType[],
standard_production_id: [] as OptionType[],
standard_productions: [] as OptionType[],
period: selectedPeriod,
},
validationSchema: dashboardProductionFilterSchema,
onSubmit: (values) => {
console.log(values);
// Build URL with query parameters
const params = new URLSearchParams();
if (values.startDate) params.set('startDate', values.startDate);
if (values.endDate) params.set('endDate', values.endDate);
if (values.flock && values.flock.length > 0) {
const flockIds = values.flock
.map((f: OptionType) => f.value || f)
.join(',');
params.set('flock', flockIds);
}
if (
values.standard_production_id &&
values.standard_production_id.length > 0
) {
const standardIds = values.standard_production_id
.map((s: OptionType) => s.value || s)
.join(',');
params.set('standard_production_id', standardIds);
}
if (selectedStandards.length > 0) {
params.set('standards', selectedStandards.join(','));
}
params.set('period', selectedPeriod);
const newUrl = `/dashboard?${params.toString()}`;
setEndpointUrl(newUrl);
// Close modal after applying filter
filterModal.closeModal();
},
});
const handleResetFilter = () => {
formik.resetForm();
setSelectedPeriod('daily');
setSelectedStandards(['hen_day', 'hen_house']);
setEndpointUrl('/dashboard');
};
if (isLoadingDashboardProductionData) {
return (
<div className='w-full min-h-screen flex items-center justify-center'>
<span className='loading loading-spinner loading-xl'></span>
</div>
);
}
return (
<>
<section className='w-full p-4 space-y-6'>
<div className='flex flex-col sm:flex-row items-center justify-between gap-4'>
<h1 className='text-3xl font-bold text-primary'>Dashboard</h1>
<div className='flex flex-row justify-end gap-2'>
<Button
variant='outline'
className='min-w-28 rounded-lg'
onClick={() => filterModal.openModal()}
>
<Icon icon='heroicons:funnel' width={20} height={20} />
Filter
</Button>
<Button
variant='outline'
color='neutral'
className='min-w-28 rounded-lg'
>
<Icon icon='heroicons:arrow-down-tray' width={20} height={20} />
Export
<Icon icon='heroicons:chevron-down' width={20} height={20} />
</Button>
</div>
</div>
{/* Dashboard Statistics */}
<ProductionStat data={dashboardProductionData?.statistics_data} />
{/* Charts Grid */}
<div className='grid grid-cols-1 gap-4'>
{/* Production Line Chart */}
<Card
variant='bordered'
className={{ wrapper: 'w-full', body: 'p-6' }}
>
<ProductionLineChart
period={
selectedPeriod as 'daily' | 'weekly' | 'monthly' | 'yearly'
}
data={dashboardProductionData?.production_charts}
/>
</Card>
{/* Standard Line Chart */}
<Card
variant='bordered'
className={{ wrapper: 'w-full', body: 'p-6' }}
>
<StandardLineChart
selectedStandards={selectedStandards}
data={dashboardProductionData?.standard_productions}
/>
</Card>
{/* Bar Charts Grid - 2 columns */}
<div className='grid grid-cols-1 lg:grid-cols-2 gap-4'>
{/* FCR Bar Chart */}
<Card
variant='bordered'
className={{ wrapper: 'w-full', body: 'p-6' }}
>
<FCRBarChart data={dashboardProductionData?.fcr_data} />
</Card>
{/* Egg Weight Bar Chart */}
<Card
variant='bordered'
className={{ wrapper: 'w-full', body: 'p-6' }}
>
<EggWeightBarChart data={dashboardProductionData?.egg_weights} />
</Card>
</div>
</div>
</section>
<Modal
ref={filterModal.ref}
className={{
modal: 'p-0',
modalBox: 'p-0 rounded-xl',
}}
>
<div className='space-y-6'>
{/* Modal Header */}
<div className='flex items-center justify-between gap-2 py-3 border-b border-gray-300'>
<div className='flex items-center gap-2 ms-4'>
<Icon icon='heroicons:funnel' width={20} height={20} />
<h3 className='font-semibold'>Filter Data</h3>
</div>
<Button
variant='link'
onClick={() => filterModal.closeModal()}
className='text-gray-500 hover:text-gray-700 me-4 '
>
<Icon icon='heroicons:x-mark' width={20} height={20} />
</Button>
</div>
<form className='space-y-4' onSubmit={formik.handleSubmit}>
{/* Rentang Waktu */}
<div className='px-4'>
<label className='flex items-center gap-2 mb-3'>
<Icon icon='heroicons:calendar' width={20} height={20} />
Rentang Waktu
</label>
<div className='flex items-center gap-2'>
<DateInput
name='startDate'
placeholder='Tanggal Mulai'
value={formik.values.startDate}
errorMessage={formik.errors.startDate}
onChange={formik.handleChange}
className={{
inputWrapper: 'rounded-lg',
}}
isError={
Boolean(formik.errors.startDate) &&
Boolean(formik.touched.startDate)
}
/>
<span className='hidden md:block text-center'></span>
<DateInput
name='endDate'
placeholder='Tanggal Akhir'
value={formik.values.endDate}
errorMessage={formik.errors.endDate}
onChange={formik.handleChange}
className={{
inputWrapper: 'rounded-lg',
}}
isError={
Boolean(formik.errors.endDate) &&
Boolean(formik.touched.endDate)
}
/>
</div>
</div>
{/* Flock */}
<div className='px-4'>
<SelectInput
label='Flock'
value={formik.values.flock}
onChange={(selected) => formik.setFieldValue('flock', selected)}
errorMessage={formik.errors.flock as string}
options={flockOptions}
isLoading={isLoadingFlockOptions}
isMulti
isError={
Boolean(formik.errors.flock) && Boolean(formik.touched.flock)
}
/>
</div>
{/* Production */}
<div className='px-4'>
<SelectInput
label='Standard Produksi'
value={formik.values.standard_production_id}
onChange={(selected) =>
formik.setFieldValue('standard_production_id', selected)
}
errorMessage={formik.errors.standard_production_id as string}
options={standardProductionOptions}
isLoading={isLoadingStandardProductionOptions}
isMulti
isError={
Boolean(formik.errors.standard_production_id) &&
Boolean(formik.touched.standard_production_id)
}
/>
</div>
{/* Standard */}
<div className='px-4'>
<SelectInput
label='Standard'
value={selectedStandards.map((s) => ({
value: s,
label:
s === 'hen_day'
? 'Hen Day'
: s === 'hen_house'
? 'Hen House'
: s === 'uniformity'
? 'Uniformity'
: s === 'egg_weight'
? 'Egg Weight'
: 'Egg Mass',
}))}
options={[
{ value: 'hen_day', label: 'Hen Day' },
{ value: 'hen_house', label: 'Hen House' },
{ value: 'uniformity', label: 'Uniformity' },
{ value: 'egg_weight', label: 'Egg Weight' },
{ value: 'egg_mass', label: 'Egg Mass' },
]}
isMulti
onChange={(selected: OptionType | OptionType[] | null) => {
const values = Array.isArray(selected)
? selected.map((item) => String(item.value))
: [];
setSelectedStandards(
values.length > 0 ? values : ['hen_day']
);
}}
isError={
Boolean(formik.errors.standard_productions) &&
Boolean(formik.touched.standard_productions)
}
/>
</div>
{/* Periode Perbandingan */}
<div className='px-4'>
<label className='block mb-3'>Periode Perbandingan</label>
<div className='grid grid-cols-4 gap-2'>
<Button
variant={selectedPeriod === 'daily' ? 'active' : 'soft'}
type='button'
className='rounded-lg'
onClick={() => setSelectedPeriod('daily')}
>
Harian
</Button>
<Button
variant={selectedPeriod === 'weekly' ? 'active' : 'soft'}
type='button'
className='rounded-lg'
onClick={() => setSelectedPeriod('weekly')}
>
Mingguan
</Button>
<Button
variant={selectedPeriod === 'monthly' ? 'active' : 'soft'}
type='button'
className='rounded-lg'
onClick={() => setSelectedPeriod('monthly')}
>
Bulanan
</Button>
<Button
variant={selectedPeriod === 'yearly' ? 'active' : 'soft'}
type='button'
className='rounded-lg'
onClick={() => setSelectedPeriod('yearly')}
>
Tahunan
</Button>
</div>
</div>
{/* Action Buttons */}
<div className='flex justify-between gap-4 py-4 mt-8 border-t border-gray-300 bg-gray-100'>
<Button
type='reset'
variant='soft'
className='ms-4 min-w-36 rounded-lg'
onClick={handleResetFilter}
>
Reset Filter
</Button>
<Button type='submit' className='me-4 min-w-36 rounded-lg'>
Terapkan Filter
</Button>
</div>
</form>
</div>
</Modal>
</>
);
};
export default DashboardProduction;
@@ -0,0 +1,89 @@
'use client';
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Cell,
} from 'recharts';
import { DashboardProductionEggWeights } from '@/types/api/dashboard/dashboard-production';
interface EggWeightBarChartProps {
data?: DashboardProductionEggWeights[];
}
const EggWeightBarChart = ({ data }: EggWeightBarChartProps) => {
// Show loading state if no data
if (!data || data.length === 0) {
return (
<div className='w-full h-full'>
<h3 className='text-lg font-semibold mb-4'>
Rata-rata Berat Telur (EW)
</h3>
<div className='flex items-center justify-center h-[350px]'>
<p className='text-gray-500'>Memuat data...</p>
</div>
</div>
);
}
return (
<div className='w-full h-full'>
<h3 className='text-lg font-semibold mb-4'>Rata-rata Berat Telur (EW)</h3>
<ResponsiveContainer width='100%' height={350}>
<BarChart
data={data}
margin={{
top: 5,
right: 30,
left: 0,
bottom: 5,
}}
>
<CartesianGrid strokeDasharray='3 3' stroke='#e5e7eb' />
<XAxis
dataKey='flock.name'
tick={{ fontSize: 12 }}
tickLine={false}
axisLine={{ stroke: '#e5e7eb' }}
/>
<YAxis
tick={{ fontSize: 12 }}
tickLine={false}
axisLine={{ stroke: '#e5e7eb' }}
domain={[0, 'auto']}
label={{
value: 'Berat (gram)',
angle: -90,
position: 'insideLeft',
style: { fontSize: 12 },
}}
/>
<Tooltip
contentStyle={{
backgroundColor: 'white',
border: '1px solid #e5e7eb',
borderRadius: '8px',
padding: '8px 12px',
}}
formatter={(value: number | undefined) =>
value !== undefined ? [`${value} gram`, ''] : ['', '']
}
cursor={{ fill: 'rgba(59, 130, 246, 0.1)' }}
/>
<Bar dataKey='weight' radius={[8, 8, 0, 0]}>
{data.map((entry, index) => (
<Cell key={`cell-${index}`} fill='#3b82f6' />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
);
};
export default EggWeightBarChart;
@@ -0,0 +1,97 @@
'use client';
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Cell,
} from 'recharts';
import { DashboardProductionFcrData } from '@/types/api/dashboard/dashboard-production';
interface FCRBarChartProps {
data?: DashboardProductionFcrData[];
}
// Alternating colors: green and red
const colors = ['#10b981', '#ef4444'];
const FCRBarChart = ({ data }: FCRBarChartProps) => {
// Show loading state if no data
if (!data || data.length === 0) {
return (
<div className='w-full h-full'>
<h3 className='text-lg font-semibold mb-4'>
Feed Conversion Ratio (FCR)
</h3>
<div className='flex items-center justify-center h-[350px]'>
<p className='text-gray-500'>Memuat data...</p>
</div>
</div>
);
}
return (
<div className='w-full h-full'>
<h3 className='text-lg font-semibold mb-4'>
Feed Conversion Ratio (FCR)
</h3>
<ResponsiveContainer width='100%' height={350}>
<BarChart
data={data}
margin={{
top: 5,
right: 30,
left: 0,
bottom: 5,
}}
>
<CartesianGrid strokeDasharray='3 3' stroke='#e5e7eb' />
<XAxis
dataKey='flock.name'
tick={{ fontSize: 12 }}
tickLine={false}
axisLine={{ stroke: '#e5e7eb' }}
/>
<YAxis
tick={{ fontSize: 12 }}
tickLine={false}
axisLine={{ stroke: '#e5e7eb' }}
domain={[0, 'auto']}
label={{
value: 'FCR',
angle: -90,
position: 'insideLeft',
style: { fontSize: 12 },
}}
/>
<Tooltip
contentStyle={{
backgroundColor: 'white',
border: '1px solid #e5e7eb',
borderRadius: '8px',
padding: '8px 12px',
}}
formatter={(value: number | undefined) =>
value !== undefined ? [value.toFixed(2), 'FCR'] : ['', '']
}
cursor={{ fill: 'rgba(16, 185, 129, 0.1)' }}
/>
<Bar dataKey='fcr' radius={[8, 8, 0, 0]}>
{data.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={colors[index % colors.length]}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
);
};
export default FCRBarChart;
@@ -0,0 +1,357 @@
'use client';
import { useState } from 'react';
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from 'recharts';
// Sample data in API format
const sampleApiData: ProductionChartItem[] = [
{
date: '2025-12-01T00:00:00Z',
flocks: [
{ id: 1, name: 'Flock A-002', data: 88 },
{ id: 2, name: 'Flock A-001', data: 92 },
{ id: 3, name: 'Flock B-001', data: 90 },
{ id: 4, name: 'Flock B-002', data: 85 },
],
},
{
date: '2025-12-03T00:00:00Z',
flocks: [
{ id: 1, name: 'Flock A-002', data: 85 },
{ id: 2, name: 'Flock A-001', data: 95 },
{ id: 3, name: 'Flock B-001', data: 93 },
{ id: 4, name: 'Flock B-002', data: 87 },
],
},
{
date: '2025-12-05T00:00:00Z',
flocks: [
{ id: 1, name: 'Flock A-002', data: 82 },
{ id: 2, name: 'Flock A-001', data: 98 },
{ id: 3, name: 'Flock B-001', data: 91 },
{ id: 4, name: 'Flock B-002', data: 84 },
],
},
{
date: '2025-12-07T00:00:00Z',
flocks: [
{ id: 1, name: 'Flock A-002', data: 80 },
{ id: 2, name: 'Flock A-001', data: 89 },
{ id: 3, name: 'Flock B-001', data: 88 },
{ id: 4, name: 'Flock B-002', data: 82 },
],
},
{
date: '2025-12-08T00:00:00Z',
flocks: [
{ id: 1, name: 'Flock A-002', data: 83 },
{ id: 2, name: 'Flock A-001', data: 92 },
{ id: 3, name: 'Flock B-001', data: 95 },
{ id: 4, name: 'Flock B-002', data: 85 },
],
},
{
date: '2025-12-11T00:00:00Z',
flocks: [
{ id: 1, name: 'Flock A-002', data: 81 },
{ id: 2, name: 'Flock A-001', data: 88 },
{ id: 3, name: 'Flock B-001', data: 92 },
{ id: 4, name: 'Flock B-002', data: 83 },
],
},
{
date: '2025-12-13T00:00:00Z',
flocks: [
{ id: 1, name: 'Flock A-002', data: 84 },
{ id: 2, name: 'Flock A-001', data: 90 },
{ id: 3, name: 'Flock B-001', data: 89 },
{ id: 4, name: 'Flock B-002', data: 86 },
],
},
{
date: '2025-12-15T00:00:00Z',
flocks: [
{ id: 1, name: 'Flock A-002', data: 82 },
{ id: 2, name: 'Flock A-001', data: 94 },
{ id: 3, name: 'Flock B-001', data: 96 },
{ id: 4, name: 'Flock B-002', data: 84 },
],
},
{
date: '2025-12-17T00:00:00Z',
flocks: [
{ id: 1, name: 'Flock A-002', data: 80 },
{ id: 2, name: 'Flock A-001', data: 91 },
{ id: 3, name: 'Flock B-001', data: 93 },
{ id: 4, name: 'Flock B-002', data: 82 },
],
},
{
date: '2025-12-19T00:00:00Z',
flocks: [
{ id: 1, name: 'Flock A-002', data: 79 },
{ id: 2, name: 'Flock A-001', data: 88 },
{ id: 3, name: 'Flock B-001', data: 90 },
{ id: 4, name: 'Flock B-002', data: 81 },
],
},
{
date: '2025-12-21T00:00:00Z',
flocks: [
{ id: 1, name: 'Flock A-002', data: 81 },
{ id: 2, name: 'Flock A-001', data: 97 },
{ id: 3, name: 'Flock B-001', data: 92 },
{ id: 4, name: 'Flock B-002', data: 83 },
],
},
{
date: '2025-12-23T00:00:00Z',
flocks: [
{ id: 1, name: 'Flock A-002', data: 83 },
{ id: 2, name: 'Flock A-001', data: 95 },
{ id: 3, name: 'Flock B-001', data: 98 },
{ id: 4, name: 'Flock B-002', data: 85 },
],
},
{
date: '2025-12-25T00:00:00Z',
flocks: [
{ id: 1, name: 'Flock A-002', data: 80 },
{ id: 2, name: 'Flock A-001', data: 89 },
{ id: 3, name: 'Flock B-001', data: 94 },
{ id: 4, name: 'Flock B-002', data: 82 },
],
},
{
date: '2025-12-27T00:00:00Z',
flocks: [
{ id: 1, name: 'Flock A-002', data: 82 },
{ id: 2, name: 'Flock A-001', data: 93 },
{ id: 3, name: 'Flock B-001', data: 96 },
{ id: 4, name: 'Flock B-002', data: 84 },
],
},
{
date: '2025-12-28T00:00:00Z',
flocks: [
{ id: 1, name: 'Flock A-002', data: 85 },
{ id: 2, name: 'Flock A-001', data: 96 },
{ id: 3, name: 'Flock B-001', data: 95 },
{ id: 4, name: 'Flock B-002', data: 87 },
],
},
];
// Helper function to format date based on period
const formatDateByPeriod = (
dateString: string,
period: 'daily' | 'weekly' | 'monthly' | 'yearly'
): string => {
const date = new Date(dateString);
const monthNames = [
'Jan',
'Feb',
'Mar',
'Apr',
'Mei',
'Jun',
'Jul',
'Agu',
'Sep',
'Okt',
'Nov',
'Des',
];
switch (period) {
case 'daily':
// Format: "1 Des"
return `${date.getDate()} ${monthNames[date.getMonth()]}`;
case 'weekly':
// Format: "Week 1 Des"
const weekNumber = Math.ceil(date.getDate() / 7);
return `Week ${weekNumber} ${monthNames[date.getMonth()]}`;
case 'monthly':
// Format: "Des"
return monthNames[date.getMonth()];
case 'yearly':
// Format: "2025"
return date.getFullYear().toString();
default:
return dateString;
}
};
// Type definitions for API data
interface FlockData {
id: number;
name: string;
data: number;
}
interface ProductionChartItem {
date: string;
flocks: FlockData[];
}
interface ProductionChartsData {
production_charts: ProductionChartItem[];
}
// Transform API data to Recharts format
const transformProductionData = (apiData: ProductionChartItem[]) => {
return apiData.map((item) => {
const transformed: Record<string, string | number> = {
date: item.date.split('T')[0], // Extract YYYY-MM-DD from ISO string
};
// Add each flock's data as a property
item.flocks.forEach((flock) => {
transformed[flock.name] = flock.data;
});
return transformed;
});
};
interface ProductionLineChartProps {
period?: 'daily' | 'weekly' | 'monthly' | 'yearly';
data?: ProductionChartItem[]; // Optional API data
}
const ProductionLineChart = ({
period = 'daily',
data: apiData,
}: ProductionLineChartProps) => {
// State to track which lines are hidden
const [hiddenLines, setHiddenLines] = useState<string[]>([]);
// Use API data if provided, otherwise use sample data
const chartData = apiData
? transformProductionData(apiData)
: transformProductionData(sampleApiData);
// Handle legend click to show/hide lines
const handleLegendClick = (dataKey: string) => {
setHiddenLines((prev) =>
prev.includes(dataKey)
? prev.filter((key) => key !== dataKey)
: [...prev, dataKey]
);
};
return (
<div className='w-full h-full'>
<h3 className='text-lg font-semibold mb-4'>
Performa Produksi per Flock
</h3>
<ResponsiveContainer width='100%' height={400}>
<LineChart
data={chartData}
margin={{
top: 5,
right: 30,
left: 0,
bottom: 5,
}}
>
<CartesianGrid strokeDasharray='3 3' stroke='#e5e7eb' />
<XAxis
dataKey='date'
tick={{ fontSize: 12 }}
tickLine={false}
axisLine={{ stroke: '#e5e7eb' }}
tickFormatter={(value) => formatDateByPeriod(value, period)}
/>
<YAxis
tick={{ fontSize: 12 }}
tickLine={false}
axisLine={{ stroke: '#e5e7eb' }}
domain={[0, 100]}
label={{
value: 'Persentase (%)',
angle: -90,
position: 'insideLeft',
style: { fontSize: 12 },
}}
/>
<Tooltip
contentStyle={{
backgroundColor: 'white',
border: '1px solid #e5e7eb',
borderRadius: '8px',
padding: '8px 12px',
}}
labelFormatter={(value) =>
formatDateByPeriod(value as string, period)
}
/>
<Legend
wrapperStyle={{
paddingTop: '20px',
}}
iconType='circle'
onClick={(e) => {
if (e.dataKey) handleLegendClick(e.dataKey as string);
}}
style={{ cursor: 'pointer' }}
/>
<Line
type='monotone'
dataKey='Flock A-002'
stroke='#3b82f6'
strokeWidth={2}
dot={{ r: 4, fill: '#3b82f6' }}
activeDot={{ r: 6 }}
hide={hiddenLines.includes('Flock A-002')}
/>
<Line
type='monotone'
dataKey='Flock A-001'
stroke='#10b981'
strokeWidth={2}
dot={{ r: 4, fill: '#10b981' }}
activeDot={{ r: 6 }}
hide={hiddenLines.includes('Flock A-001')}
/>
<Line
type='monotone'
dataKey='Flock B-001'
stroke='#f59e0b'
strokeWidth={2}
dot={{ r: 4, fill: '#f59e0b' }}
activeDot={{ r: 6 }}
hide={hiddenLines.includes('Flock B-001')}
/>
<Line
type='monotone'
dataKey='Flock B-002'
stroke='#ef4444'
strokeWidth={2}
dot={{ r: 4, fill: '#ef4444' }}
activeDot={{ r: 6 }}
hide={hiddenLines.includes('Flock B-002')}
/>
</LineChart>
</ResponsiveContainer>
</div>
);
};
export default ProductionLineChart;
// Export types for external use
export type { FlockData, ProductionChartItem, ProductionChartsData };
@@ -0,0 +1,107 @@
import Card from '@/components/Card';
import { Icon } from '@iconify/react';
import { DashboardProductionStatisticsData } from '@/types/api/dashboard/dashboard-production';
import { formatCurrency } from '@/lib/helper';
interface ProductionStatProps {
data?: DashboardProductionStatisticsData[];
}
const ProductionStat = ({ data }: ProductionStatProps) => {
// Helper function to get icon based on title
const getIcon = (title: string) => {
if (title.toLowerCase().includes('keuangan'))
return 'heroicons:currency-dollar';
if (title.toLowerCase().includes('penjualan'))
return 'heroicons:arrow-trending-up';
if (title.toLowerCase().includes('pembelian'))
return 'heroicons:shopping-cart';
if (title.toLowerCase().includes('overhead')) return 'heroicons:calculator';
return 'heroicons:chart-bar';
};
// Helper function to get icon background color
const getIconBgColor = (title: string) => {
if (title.toLowerCase().includes('keuangan')) return 'bg-blue-500';
if (title.toLowerCase().includes('penjualan')) return 'bg-green-500';
if (title.toLowerCase().includes('pembelian')) return 'bg-orange-500';
if (title.toLowerCase().includes('overhead')) return 'bg-purple-500';
return 'bg-gray-500';
};
// Show loading state if no data
if (!data || data.length === 0) {
return (
<section className='grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4'>
{[1, 2, 3, 4].map((i) => (
<Card
key={i}
variant='bordered'
className={{ wrapper: 'w-full', body: 'p-4' }}
>
<div className='animate-pulse'>
<div className='h-4 bg-gray-200 rounded w-1/2 mb-2'></div>
<div className='h-6 bg-gray-200 rounded w-3/4 mb-1'></div>
<div className='h-4 bg-gray-200 rounded w-1/3'></div>
</div>
</Card>
))}
</section>
);
}
return (
<section className='grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4'>
{data.map((stat, index) => (
<Card
key={index}
variant='bordered'
className={{ wrapper: 'w-full', body: 'p-4' }}
>
<div className='flex items-start justify-between'>
<div className='flex-1'>
<p className='text-sm text-gray-600 mb-2'>{stat.title}</p>
<p className='text-xl font-bold text-gray-900 mb-1'>
{formatCurrency(stat.value)}
</p>
<p
className={`text-sm flex items-center gap-1 ${
stat.changeType === 'increase'
? 'text-green-600'
: 'text-red-600'
}`}
>
<Icon
icon={
stat.changeType === 'increase'
? 'heroicons:arrow-trending-up'
: 'heroicons:arrow-trending-down'
}
width={16}
height={16}
/>
{stat.change > 0 ? '+' : ''}
{stat.change}% vs{' '}
{stat.period === 'monthly' ? 'bulan lalu' : 'periode lalu'}
</p>
</div>
<div className='flex-shrink-0'>
<div
className={`w-12 h-12 rounded-lg ${getIconBgColor(stat.title)} flex items-center justify-center`}
>
<Icon
icon={getIcon(stat.title)}
width={24}
height={24}
className='text-white'
/>
</div>
</div>
</div>
</Card>
))}
</section>
);
};
export default ProductionStat;
@@ -0,0 +1,691 @@
'use client';
import { useState } from 'react';
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from 'recharts';
// Type definitions for API data
interface FlockData {
id: number;
name: string;
data: number;
}
interface StandardData {
name: string;
value: number;
}
interface StandardChartItem {
week: number;
standards: StandardData[];
flocks: FlockData[];
}
// Sample data in API format
const sampleApiData: StandardChartItem[] = [
{
week: 18,
standards: [
{ name: 'hen_day', value: 40 },
{ name: 'hen_house', value: 38 },
{ name: 'uniformity', value: 85 },
{ name: 'egg_weight', value: 52 },
{ name: 'egg_mass', value: 20 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 38 },
{ id: 2, name: 'Flock A-002', data: 37 },
{ id: 3, name: 'Flock B-001', data: 39 },
{ id: 4, name: 'Flock B-002', data: 36 },
],
},
{
week: 20,
standards: [
{ name: 'hen_day', value: 45 },
{ name: 'hen_house', value: 43 },
{ name: 'uniformity', value: 86 },
{ name: 'egg_weight', value: 54 },
{ name: 'egg_mass', value: 24 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 43 },
{ id: 2, name: 'Flock A-002', data: 42 },
{ id: 3, name: 'Flock B-001', data: 44 },
{ id: 4, name: 'Flock B-002', data: 41 },
],
},
{
week: 22,
standards: [
{ name: 'hen_day', value: 48 },
{ name: 'hen_house', value: 46 },
{ name: 'uniformity', value: 87 },
{ name: 'egg_weight', value: 55 },
{ name: 'egg_mass', value: 26 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 47 },
{ id: 2, name: 'Flock A-002', data: 46 },
{ id: 3, name: 'Flock B-001', data: 48 },
{ id: 4, name: 'Flock B-002', data: 45 },
],
},
{
week: 24,
standards: [
{ name: 'hen_day', value: 50 },
{ name: 'hen_house', value: 48 },
{ name: 'uniformity', value: 88 },
{ name: 'egg_weight', value: 56 },
{ name: 'egg_mass', value: 28 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 49 },
{ id: 2, name: 'Flock A-002', data: 48 },
{ id: 3, name: 'Flock B-001', data: 50 },
{ id: 4, name: 'Flock B-002', data: 47 },
],
},
{
week: 26,
standards: [
{ name: 'hen_day', value: 52 },
{ name: 'hen_house', value: 50 },
{ name: 'uniformity', value: 89 },
{ name: 'egg_weight', value: 57 },
{ name: 'egg_mass', value: 30 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 50 },
{ id: 2, name: 'Flock A-002', data: 49 },
{ id: 3, name: 'Flock B-001', data: 51 },
{ id: 4, name: 'Flock B-002', data: 48 },
],
},
{
week: 28,
standards: [
{ name: 'hen_day', value: 55 },
{ name: 'hen_house', value: 53 },
{ name: 'uniformity', value: 90 },
{ name: 'egg_weight', value: 58 },
{ name: 'egg_mass', value: 32 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 53 },
{ id: 2, name: 'Flock A-002', data: 52 },
{ id: 3, name: 'Flock B-001', data: 54 },
{ id: 4, name: 'Flock B-002', data: 51 },
],
},
{
week: 30,
standards: [
{ name: 'hen_day', value: 58 },
{ name: 'hen_house', value: 56 },
{ name: 'uniformity', value: 91 },
{ name: 'egg_weight', value: 59 },
{ name: 'egg_mass', value: 34 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 55 },
{ id: 2, name: 'Flock A-002', data: 54 },
{ id: 3, name: 'Flock B-001', data: 56 },
{ id: 4, name: 'Flock B-002', data: 53 },
],
},
{
week: 32,
standards: [
{ name: 'hen_day', value: 60 },
{ name: 'hen_house', value: 58 },
{ name: 'uniformity', value: 92 },
{ name: 'egg_weight', value: 60 },
{ name: 'egg_mass', value: 36 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 58 },
{ id: 2, name: 'Flock A-002', data: 57 },
{ id: 3, name: 'Flock B-001', data: 59 },
{ id: 4, name: 'Flock B-002', data: 56 },
],
},
{
week: 34,
standards: [
{ name: 'hen_day', value: 62 },
{ name: 'hen_house', value: 60 },
{ name: 'uniformity', value: 92 },
{ name: 'egg_weight', value: 61 },
{ name: 'egg_mass', value: 38 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 60 },
{ id: 2, name: 'Flock A-002', data: 59 },
{ id: 3, name: 'Flock B-001', data: 61 },
{ id: 4, name: 'Flock B-002', data: 58 },
],
},
{
week: 36,
standards: [
{ name: 'hen_day', value: 64 },
{ name: 'hen_house', value: 62 },
{ name: 'uniformity', value: 93 },
{ name: 'egg_weight', value: 62 },
{ name: 'egg_mass', value: 40 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 62 },
{ id: 2, name: 'Flock A-002', data: 61 },
{ id: 3, name: 'Flock B-001', data: 63 },
{ id: 4, name: 'Flock B-002', data: 60 },
],
},
{
week: 38,
standards: [
{ name: 'hen_day', value: 66 },
{ name: 'hen_house', value: 64 },
{ name: 'uniformity', value: 93 },
{ name: 'egg_weight', value: 63 },
{ name: 'egg_mass', value: 42 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 64 },
{ id: 2, name: 'Flock A-002', data: 63 },
{ id: 3, name: 'Flock B-001', data: 65 },
{ id: 4, name: 'Flock B-002', data: 62 },
],
},
{
week: 40,
standards: [
{ name: 'hen_day', value: 68 },
{ name: 'hen_house', value: 66 },
{ name: 'uniformity', value: 94 },
{ name: 'egg_weight', value: 64 },
{ name: 'egg_mass', value: 44 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 66 },
{ id: 2, name: 'Flock A-002', data: 65 },
{ id: 3, name: 'Flock B-001', data: 67 },
{ id: 4, name: 'Flock B-002', data: 64 },
],
},
{
week: 42,
standards: [
{ name: 'hen_day', value: 70 },
{ name: 'hen_house', value: 68 },
{ name: 'uniformity', value: 94 },
{ name: 'egg_weight', value: 65 },
{ name: 'egg_mass', value: 46 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 68 },
{ id: 2, name: 'Flock A-002', data: 67 },
{ id: 3, name: 'Flock B-001', data: 69 },
{ id: 4, name: 'Flock B-002', data: 66 },
],
},
{
week: 44,
standards: [
{ name: 'hen_day', value: 72 },
{ name: 'hen_house', value: 70 },
{ name: 'uniformity', value: 95 },
{ name: 'egg_weight', value: 66 },
{ name: 'egg_mass', value: 48 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 70 },
{ id: 2, name: 'Flock A-002', data: 69 },
{ id: 3, name: 'Flock B-001', data: 71 },
{ id: 4, name: 'Flock B-002', data: 68 },
],
},
{
week: 46,
standards: [
{ name: 'hen_day', value: 74 },
{ name: 'hen_house', value: 72 },
{ name: 'uniformity', value: 95 },
{ name: 'egg_weight', value: 67 },
{ name: 'egg_mass', value: 50 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 72 },
{ id: 2, name: 'Flock A-002', data: 71 },
{ id: 3, name: 'Flock B-001', data: 73 },
{ id: 4, name: 'Flock B-002', data: 70 },
],
},
{
week: 48,
standards: [
{ name: 'hen_day', value: 76 },
{ name: 'hen_house', value: 74 },
{ name: 'uniformity', value: 95 },
{ name: 'egg_weight', value: 68 },
{ name: 'egg_mass', value: 52 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 74 },
{ id: 2, name: 'Flock A-002', data: 73 },
{ id: 3, name: 'Flock B-001', data: 75 },
{ id: 4, name: 'Flock B-002', data: 72 },
],
},
{
week: 50,
standards: [
{ name: 'hen_day', value: 78 },
{ name: 'hen_house', value: 76 },
{ name: 'uniformity', value: 96 },
{ name: 'egg_weight', value: 69 },
{ name: 'egg_mass', value: 54 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 76 },
{ id: 2, name: 'Flock A-002', data: 75 },
{ id: 3, name: 'Flock B-001', data: 77 },
{ id: 4, name: 'Flock B-002', data: 74 },
],
},
{
week: 52,
standards: [
{ name: 'hen_day', value: 80 },
{ name: 'hen_house', value: 78 },
{ name: 'uniformity', value: 96 },
{ name: 'egg_weight', value: 70 },
{ name: 'egg_mass', value: 56 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 78 },
{ id: 2, name: 'Flock A-002', data: 77 },
{ id: 3, name: 'Flock B-001', data: 79 },
{ id: 4, name: 'Flock B-002', data: 76 },
],
},
{
week: 54,
standards: [
{ name: 'hen_day', value: 82 },
{ name: 'hen_house', value: 80 },
{ name: 'uniformity', value: 96 },
{ name: 'egg_weight', value: 71 },
{ name: 'egg_mass', value: 58 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 80 },
{ id: 2, name: 'Flock A-002', data: 79 },
{ id: 3, name: 'Flock B-001', data: 81 },
{ id: 4, name: 'Flock B-002', data: 78 },
],
},
{
week: 56,
standards: [
{ name: 'hen_day', value: 84 },
{ name: 'hen_house', value: 82 },
{ name: 'uniformity', value: 97 },
{ name: 'egg_weight', value: 72 },
{ name: 'egg_mass', value: 60 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 82 },
{ id: 2, name: 'Flock A-002', data: 81 },
{ id: 3, name: 'Flock B-001', data: 83 },
{ id: 4, name: 'Flock B-002', data: 80 },
],
},
{
week: 58,
standards: [
{ name: 'hen_day', value: 86 },
{ name: 'hen_house', value: 84 },
{ name: 'uniformity', value: 97 },
{ name: 'egg_weight', value: 73 },
{ name: 'egg_mass', value: 62 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 84 },
{ id: 2, name: 'Flock A-002', data: 83 },
{ id: 3, name: 'Flock B-001', data: 85 },
{ id: 4, name: 'Flock B-002', data: 82 },
],
},
{
week: 60,
standards: [
{ name: 'hen_day', value: 88 },
{ name: 'hen_house', value: 86 },
{ name: 'uniformity', value: 97 },
{ name: 'egg_weight', value: 74 },
{ name: 'egg_mass', value: 64 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 86 },
{ id: 2, name: 'Flock A-002', data: 85 },
{ id: 3, name: 'Flock B-001', data: 87 },
{ id: 4, name: 'Flock B-002', data: 84 },
],
},
{
week: 62,
standards: [
{ name: 'hen_day', value: 90 },
{ name: 'hen_house', value: 88 },
{ name: 'uniformity', value: 98 },
{ name: 'egg_weight', value: 75 },
{ name: 'egg_mass', value: 66 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 88 },
{ id: 2, name: 'Flock A-002', data: 87 },
{ id: 3, name: 'Flock B-001', data: 89 },
{ id: 4, name: 'Flock B-002', data: 86 },
],
},
{
week: 64,
standards: [
{ name: 'hen_day', value: 92 },
{ name: 'hen_house', value: 90 },
{ name: 'uniformity', value: 98 },
{ name: 'egg_weight', value: 76 },
{ name: 'egg_mass', value: 68 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 90 },
{ id: 2, name: 'Flock A-002', data: 89 },
{ id: 3, name: 'Flock B-001', data: 91 },
{ id: 4, name: 'Flock B-002', data: 88 },
],
},
{
week: 66,
standards: [
{ name: 'hen_day', value: 94 },
{ name: 'hen_house', value: 92 },
{ name: 'uniformity', value: 98 },
{ name: 'egg_weight', value: 77 },
{ name: 'egg_mass', value: 70 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 92 },
{ id: 2, name: 'Flock A-002', data: 91 },
{ id: 3, name: 'Flock B-001', data: 93 },
{ id: 4, name: 'Flock B-002', data: 90 },
],
},
{
week: 68,
standards: [
{ name: 'hen_day', value: 95 },
{ name: 'hen_house', value: 93 },
{ name: 'uniformity', value: 98 },
{ name: 'egg_weight', value: 78 },
{ name: 'egg_mass', value: 72 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 93 },
{ id: 2, name: 'Flock A-002', data: 92 },
{ id: 3, name: 'Flock B-001', data: 94 },
{ id: 4, name: 'Flock B-002', data: 91 },
],
},
{
week: 70,
standards: [
{ name: 'hen_day', value: 96 },
{ name: 'hen_house', value: 94 },
{ name: 'uniformity', value: 99 },
{ name: 'egg_weight', value: 79 },
{ name: 'egg_mass', value: 74 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 94 },
{ id: 2, name: 'Flock A-002', data: 93 },
{ id: 3, name: 'Flock B-001', data: 95 },
{ id: 4, name: 'Flock B-002', data: 92 },
],
},
{
week: 72,
standards: [
{ name: 'hen_day', value: 97 },
{ name: 'hen_house', value: 95 },
{ name: 'uniformity', value: 99 },
{ name: 'egg_weight', value: 80 },
{ name: 'egg_mass', value: 76 },
],
flocks: [
{ id: 1, name: 'Flock A-001', data: 95 },
{ id: 2, name: 'Flock A-002', data: 94 },
{ id: 3, name: 'Flock B-001', data: 96 },
{ id: 4, name: 'Flock B-002', data: 93 },
],
},
];
// Transform API data to Recharts format
const transformStandardData = (
apiData: StandardChartItem[],
selectedStandards: string[] = [
'hen_day',
'hen_house',
'uniformity',
'egg_weight',
'egg_mass',
]
) => {
return apiData.map((item) => {
const transformed: Record<string, number> = {
week: item.week,
};
// Add selected standards as properties
selectedStandards.forEach((standardName) => {
const standardData = item.standards.find((s) => s.name === standardName);
if (standardData) {
transformed[standardName] = standardData.value;
}
});
// Add each flock's data as a property
item.flocks.forEach((flock) => {
transformed[flock.name] = flock.data;
});
return transformed;
});
};
interface StandardLineChartProps {
data?: StandardChartItem[];
selectedStandards?: string[];
}
const StandardLineChart = ({
data: apiData,
selectedStandards = [
'hen_day',
'hen_house',
'uniformity',
'egg_weight',
'egg_mass',
],
}: StandardLineChartProps) => {
// State to track which lines are hidden
const [hiddenLines, setHiddenLines] = useState<string[]>([]);
// Use API data if provided, otherwise use sample data
const chartData = apiData
? transformStandardData(apiData, selectedStandards)
: transformStandardData(sampleApiData, selectedStandards);
// Handle legend click to show/hide lines
const handleLegendClick = (dataKey: string) => {
setHiddenLines((prev) =>
prev.includes(dataKey)
? prev.filter((key) => key !== dataKey)
: [...prev, dataKey]
);
};
// Standard line colors mapping
const standardColors: Record<string, string> = {
hen_day: '#94a3b8',
hen_house: '#64748b',
uniformity: '#475569',
egg_weight: '#334155',
egg_mass: '#1e293b',
};
// Standard names mapping for display
const standardLabels: Record<string, string> = {
hen_day: 'Hen Day',
hen_house: 'Hen House',
uniformity: 'Uniformity',
egg_weight: 'Egg Weight',
egg_mass: 'Egg Mass',
};
return (
<div className='w-full h-full'>
<h3 className='text-lg font-semibold mb-4'>
Perbandingan Henday per Umur
</h3>
<ResponsiveContainer width='100%' height={400}>
<LineChart
data={chartData}
margin={{
top: 5,
right: 30,
left: 0,
bottom: 5,
}}
>
<CartesianGrid strokeDasharray='3 3' stroke='#e5e7eb' />
<XAxis
dataKey='week'
tick={{ fontSize: 12 }}
tickLine={false}
axisLine={{ stroke: '#e5e7eb' }}
label={{
value: 'Umur (minggu)',
position: 'insideBottom',
offset: -5,
style: { fontSize: 12 },
}}
/>
<YAxis
tick={{ fontSize: 12 }}
tickLine={false}
axisLine={{ stroke: '#e5e7eb' }}
domain={[0, 100]}
label={{
value: 'Henday (%)',
angle: -90,
position: 'insideLeft',
style: { fontSize: 12 },
}}
/>
<Tooltip
contentStyle={{
backgroundColor: 'white',
border: '1px solid #e5e7eb',
borderRadius: '8px',
padding: '8px 12px',
}}
formatter={(value: number | undefined) =>
value !== undefined ? [`${value}%`, ''] : ['', '']
}
labelFormatter={(label) => `Minggu ${label}`}
/>
<Legend
wrapperStyle={{
paddingTop: '20px',
}}
iconType='circle'
onClick={(e) => {
if (e.dataKey) handleLegendClick(e.dataKey as string);
}}
style={{ cursor: 'pointer' }}
/>
{/* Dynamic Standard Lines */}
{selectedStandards.map((standardName) => (
<Line
key={standardName}
type='monotone'
dataKey={standardName}
name={standardLabels[standardName] || standardName}
stroke={standardColors[standardName] || '#94a3b8'}
strokeWidth={2}
strokeDasharray='5 5'
dot={{ r: 3, fill: standardColors[standardName] || '#94a3b8' }}
activeDot={{ r: 5 }}
hide={hiddenLines.includes(standardName)}
/>
))}
{/* Flock Lines */}
<Line
type='monotone'
dataKey='Flock A-002'
stroke='#3b82f6'
strokeWidth={2}
dot={{ r: 4, fill: '#3b82f6' }}
activeDot={{ r: 6 }}
hide={hiddenLines.includes('Flock A-002')}
/>
<Line
type='monotone'
dataKey='Flock A-001'
stroke='#10b981'
strokeWidth={2}
dot={{ r: 4, fill: '#10b981' }}
activeDot={{ r: 6 }}
hide={hiddenLines.includes('Flock A-001')}
/>
<Line
type='monotone'
dataKey='Flock B-001'
stroke='#f59e0b'
strokeWidth={2}
dot={{ r: 4, fill: '#f59e0b' }}
activeDot={{ r: 6 }}
hide={hiddenLines.includes('Flock B-001')}
/>
<Line
type='monotone'
dataKey='Flock B-002'
stroke='#ef4444'
strokeWidth={2}
dot={{ r: 4, fill: '#ef4444' }}
activeDot={{ r: 6 }}
hide={hiddenLines.includes('Flock B-002')}
/>
</LineChart>
</ResponsiveContainer>
</div>
);
};
export default StandardLineChart;
// Export types for external use
export type { FlockData, StandardData, StandardChartItem };
@@ -0,0 +1,16 @@
import * as yup from 'yup';
const dashboardProductionFilterSchema = yup.object({
startDate: yup.string().optional(),
endDate: yup.string().optional(),
flock: yup.array().optional(),
standard_production_id: yup.array().optional(),
standard_productions: yup.array().optional(),
period: yup.string().optional(),
});
export type DashboardProductionFilterValues = yup.InferType<
typeof dashboardProductionFilterSchema
>;
export default dashboardProductionFilterSchema;
@@ -4,6 +4,7 @@ import toast from 'react-hot-toast';
import Link from 'next/link';
import { Icon } from '@iconify/react';
import Button from '@/components/Button';
import RequirePermission from '@/components/helper/RequirePermission';
import Card from '@/components/Card';
import DropFileInput from '@/components/input/DropFileInput';
@@ -15,7 +16,7 @@ import {
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
import { ExpenseApi } from '@/services/api/expense';
import { isResponseSuccess } from '@/lib/api-helper';
import { ACCEPTED_FILE_TYPE } from '@/config/constant';
import { ACCEPTED_FILE_TYPE, S3_PUBLIC_BASE_URL } from '@/config/constant';
interface ExpenseRealizationContentProps {
initialValues?: Expense;
@@ -62,16 +63,17 @@ const ExpenseRealizationContent = ({
<div>
<div className='w-full max-w-5xl mx-auto flex flex-col sm:flex-row justify-end gap-2'>
<div className='w-full sm:w-fit sm:ml-2 flex flex-row gap-2 items-center'>
{/* TODO: apply RBAC */}
<Button
type='button'
color='warning'
href={`/expense/realization/edit/?expenseId=${initialValues?.id}`}
className='px-4 grow sm:grow-0'
>
<Icon icon='mdi:pencil-outline' width={24} height={24} />
Edit Realisasi
</Button>
<RequirePermission permissions='lti.expense.update.realization'>
<Button
type='button'
color='warning'
href={`/expense/realization/edit/?expenseId=${initialValues?.id}`}
className='px-4 grow sm:grow-0'
>
<Icon icon='mdi:pencil-outline' width={24} height={24} />
Edit Realisasi
</Button>
</RequirePermission>
</div>
</div>
@@ -101,59 +103,69 @@ const ExpenseRealizationContent = ({
initialValues?.realization_docs.length > 0 && (
<ul className='list-disc'>
{initialValues?.realization_docs.map(
(realizationDocument, realizationDocumentIdx) => (
<li key={realizationDocumentIdx}>
<Link
href={realizationDocument.path}
target='_blank'
rel='noopener noreferrer'
className='text-blue-500 underline'
>
{realizationDocument.path}{' '}
<Icon
icon='cuida:open-in-new-tab-outline'
width={12}
height={12}
className='inline'
/>
</Link>
</li>
)
(realizationDocument, realizationDocumentIdx) => {
const path = realizationDocument.path.startsWith(
'/'
)
? realizationDocument.path.slice(1)
: realizationDocument.path;
const documentUrl = `${S3_PUBLIC_BASE_URL}/${path}`;
return (
<li key={realizationDocumentIdx}>
<Link
href={documentUrl}
target='_blank'
rel='noopener noreferrer'
className='text-blue-500 underline'
>
{realizationDocument.path}{' '}
<Icon
icon='cuida:open-in-new-tab-outline'
width={12}
height={12}
className='inline'
/>
</Link>
</li>
);
}
)}
</ul>
)}
</div>
<div className='flex flex-col gap-2'>
<DropFileInput
name='documents'
values={formik.values.documents}
onChange={realizationDocumentsChangeHandler}
onDelete={realizationDocumentsDeleteHandler}
accept={{
...ACCEPTED_FILE_TYPE.PDF,
...ACCEPTED_FILE_TYPE.IMAGE,
}}
maxFiles={10}
className={{
wrapper: 'mt-2',
inputWrapper: 'flex items-center',
}}
/>
<RequirePermission permissions='lti.expense.document.realization'>
<div className='flex flex-col gap-2'>
<DropFileInput
name='documents'
values={formik.values.documents}
onChange={realizationDocumentsChangeHandler}
onDelete={realizationDocumentsDeleteHandler}
accept={{
...ACCEPTED_FILE_TYPE.PDF,
...ACCEPTED_FILE_TYPE.IMAGE,
}}
maxFiles={10}
className={{
wrapper: 'mt-2',
inputWrapper: 'flex items-center',
}}
/>
{formik.values.documents &&
formik.values.documents.length > 0 && (
<Button
onClick={formik.submitForm}
disabled={formik.isSubmitting}
isLoading={formik.isSubmitting}
className='w-fit self-end'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
)}
</div>
{formik.values.documents &&
formik.values.documents.length > 0 && (
<Button
onClick={formik.submitForm}
disabled={formik.isSubmitting}
isLoading={formik.isSubmitting}
className='w-fit self-end'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
)}
</div>
</RequirePermission>
</td>
</tr>
</tbody>
@@ -207,7 +219,7 @@ const ExpenseRealizationContent = ({
let expenseGrandTotal = 0;
kandangExpense.pengajuans?.forEach(
(item) => (expenseGrandTotal += item.price)
(item) => (expenseGrandTotal += item.qty * item.price)
);
return (
@@ -269,7 +281,7 @@ const ExpenseRealizationContent = ({
let expenseGrandTotal = 0;
kandangExpense.realisasi?.forEach(
(item) => (expenseGrandTotal += item.price)
(item) => (expenseGrandTotal += item.qty * item.price)
);
return (
@@ -19,6 +19,7 @@ import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import ExpensePDFPreviewButton from '@/components/pages/expense//pdf/ExpensePDFButton';
import RequirePermission from '@/components/helper/RequirePermission';
import { Expense } from '@/types/api/expense';
import { formatCurrency, formatDate } from '@/lib/helper';
@@ -26,7 +27,7 @@ import {
UploadRequestDocumentsFormSchema,
UploadRequestDocumentsFormValues,
} from '@/components/pages/expense/form/ExpenseRequestForm.schema';
import { ACCEPTED_FILE_TYPE } from '@/config/constant';
import { ACCEPTED_FILE_TYPE, S3_PUBLIC_BASE_URL } from '@/config/constant';
import { ExpenseApi } from '@/services/api/expense';
import { isResponseSuccess } from '@/lib/api-helper';
import { EXPENSE_REQUEST_APPROVAL_LINE } from '@/config/approval-line';
@@ -255,100 +256,119 @@ const ExpenseRequestContent = ({
<div className='w-full max-w-5xl mx-auto flex flex-col sm:flex-row justify-end gap-2'>
{isCurrentApprovalOnManager && (
<Button
variant='outline'
color='info'
onClick={approveClickHandler}
className='w-full sm:w-fit'
>
<Icon icon='lucide-lab:farm' width={24} height={24} />
Approve Manager
</Button>
<RequirePermission permissions='lti.expense.approve.manager'>
<Button
variant='outline'
color='info'
onClick={approveClickHandler}
className='w-full sm:w-fit'
>
<Icon icon='lucide-lab:farm' width={24} height={24} />
Approve Manager
</Button>
</RequirePermission>
)}
{isCurrentApprovalOnFinance && (
<Button
variant='outline'
color='success'
onClick={approveClickHandler}
className='w-full sm:w-fit'
>
<Icon icon='tdesign:money' width={24} height={24} />
Approve Finance
</Button>
<RequirePermission permissions='lti.expense.approve.finance'>
<Button
variant='outline'
color='success'
onClick={approveClickHandler}
className='w-full sm:w-fit'
>
<Icon icon='tdesign:money' width={24} height={24} />
Approve Finance
</Button>
</RequirePermission>
)}
{isCurrentApprovalOnRealization && (
<Button
variant='outline'
color='success'
onClick={completeExpenseClickHandler}
className='w-full sm:w-fit'
>
<Icon
icon='material-symbols:done-all-rounded'
width={24}
height={24}
/>
Selesai
</Button>
<RequirePermission permissions='lti.expense.complete.expense'>
<Button
variant='outline'
color='success'
onClick={completeExpenseClickHandler}
className='w-full sm:w-fit'
>
<Icon
icon='material-symbols:done-all-rounded'
width={24}
height={24}
/>
Selesai
</Button>
</RequirePermission>
)}
{showRejectButton && (
<Button
variant='outline'
color='error'
onClick={rejectClickHandler}
className='w-full:w-fit'
<RequirePermission
permissions={[
'lti.expense.approve.manager',
'lti.expense.approve.finance',
]}
>
<Icon icon='material-symbols:close' width={24} height={24} />
Reject
</Button>
<Button
variant='outline'
color='error'
onClick={rejectClickHandler}
className='w-full:w-fit'
>
<Icon icon='material-symbols:close' width={24} height={24} />
Reject
</Button>
</RequirePermission>
)}
{isExpenseCanBeRealized && (
<Button
variant='outline'
color='info'
href={`/expense/realization/?expenseId=${initialValues?.id}`}
className='w-full sm:w-fit'
>
<Icon
icon='material-symbols:money-bag-rounded'
width={24}
height={24}
/>
Realisasi
</Button>
<RequirePermission permissions='lti.expense.create.realization'>
<Button
variant='outline'
color='info'
href={`/expense/realization/?expenseId=${initialValues?.id}`}
className='w-full sm:w-fit'
>
<Icon
icon='material-symbols:money-bag-rounded'
width={24}
height={24}
/>
Realisasi
</Button>
</RequirePermission>
)}
<div className='w-full sm:w-fit sm:ml-2 flex flex-row gap-2 items-center'>
{showEditButton && (
<Button
type='button'
color='warning'
href={`/expense/detail/edit/?expenseId=${initialValues?.id}`}
className='px-4 grow sm:grow-0'
>
<Icon icon='mdi:pencil-outline' width={24} height={24} />
Edit
</Button>
<RequirePermission permissions='lti.expense.update'>
<Button
type='button'
color='warning'
href={`/expense/detail/edit/?expenseId=${initialValues?.id}`}
className='px-4 grow sm:grow-0'
>
<Icon icon='mdi:pencil-outline' width={24} height={24} />
Edit
</Button>
</RequirePermission>
)}
<Button
type='button'
color='error'
onClick={deleteExpenseClickHandler}
className='px-4 grow sm:grow-0'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Delete
</Button>
<RequirePermission permissions='lti.expense.delete'>
<Button
type='button'
color='error'
onClick={deleteExpenseClickHandler}
className='px-4 grow sm:grow-0'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Delete
</Button>
</RequirePermission>
</div>
</div>
@@ -388,9 +408,13 @@ const ExpenseRequestContent = ({
<th>Kandang</th>
<th>:</th>
<td>
{initialValues?.kandangs
.map((item) => item.name)
.join(', ')}
{initialValues?.kandangs &&
initialValues?.kandangs.some((k) => k.name)
? initialValues?.kandangs
.filter((item) => item.name)
.map((item) => item.name)
.join(', ')
: '-'}
</td>
</tr>
<tr>
@@ -428,7 +452,14 @@ const ExpenseRequestContent = ({
<tr>
<th>Nominal Biaya</th>
<th>:</th>
<td>{formatCurrency(initialValues?.grand_total ?? 0)}</td>
<td>
{formatCurrency(
initialValues?.latest_approval.step_number === 4 ||
initialValues?.latest_approval.step_number === 5
? (initialValues?.total_realisasi ?? 0)
: (initialValues?.total_pengajuan ?? 0)
)}
</td>
</tr>
<tr>
<th>Status Pencairan</th>
@@ -462,59 +493,73 @@ const ExpenseRequestContent = ({
initialValues?.documents.length > 0 && (
<ul className='list-disc'>
{initialValues?.documents.map(
(requestDocument, requestDocumentIdx) => (
<li key={requestDocumentIdx}>
<Link
href={requestDocument.path}
target='_blank'
rel='noopener noreferrer'
className='text-blue-500 underline'
>
{requestDocument.path}{' '}
<Icon
icon='cuida:open-in-new-tab-outline'
width={12}
height={12}
className='inline'
/>
</Link>
</li>
)
(requestDocument, requestDocumentIdx) => {
const path = requestDocument.path.startsWith(
'/'
)
? requestDocument.path.slice(1)
: requestDocument.path;
const documentUrl = `${S3_PUBLIC_BASE_URL}/${path}`;
return (
<li key={requestDocumentIdx}>
<Link
href={documentUrl}
target='_blank'
rel='noopener noreferrer'
className='text-blue-500 underline'
>
{requestDocument.path}{' '}
<Icon
icon='cuida:open-in-new-tab-outline'
width={12}
height={12}
className='inline'
/>
</Link>
</li>
);
}
)}
</ul>
)}
</div>
<div className='flex flex-col gap-2'>
<DropFileInput
name='documents'
values={formik.values.documents}
onChange={requestDocumentsChangeHandler}
onDelete={requestDocumentsDeleteHandler}
accept={{
...ACCEPTED_FILE_TYPE.PDF,
...ACCEPTED_FILE_TYPE.IMAGE,
}}
maxFiles={10}
className={{
wrapper: 'mt-2',
inputWrapper: 'flex items-center',
}}
/>
<RequirePermission permissions='lti.expense.document'>
<div className='flex flex-col gap-2'>
<DropFileInput
name='documents'
values={formik.values.documents}
onChange={requestDocumentsChangeHandler}
onDelete={requestDocumentsDeleteHandler}
accept={{
...ACCEPTED_FILE_TYPE.PDF,
...ACCEPTED_FILE_TYPE.IMAGE,
}}
maxFiles={10}
className={{
wrapper: 'mt-2',
inputWrapper: 'flex items-center',
}}
/>
{formik.values.documents &&
formik.values.documents.length > 0 && (
<Button
onClick={formik.submitForm}
disabled={formik.isSubmitting}
isLoading={formik.isSubmitting}
className='w-fit self-end'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
)}
</div>
{formik.values.documents &&
formik.values.documents.length > 0 && (
<Button
onClick={formik.submitForm}
disabled={formik.isSubmitting}
isLoading={formik.isSubmitting}
className='w-fit self-end'
>
<Icon
icon='ic:round-plus'
width={24}
height={24}
/>
Tambah
</Button>
)}
</div>
</RequirePermission>
</td>
</tr>
</tbody>
@@ -532,7 +577,7 @@ const ExpenseRequestContent = ({
let expenseGrandTotal = 0;
kandangExpense.pengajuans?.forEach(
(item) => (expenseGrandTotal += item.price)
(item) => (expenseGrandTotal += item.qty * item.price)
);
return (
@@ -547,7 +592,9 @@ const ExpenseRequestContent = ({
colSpan={5}
className='font-bold text-center text-base-content text-lg'
>
Biaya {kandangExpense.name}
{kandangExpense.kandang_id && kandangExpense.name
? `Biaya ${kandangExpense.name}`
: `Biaya ${initialValues?.location.name || 'Umum'}`}
</th>
</tr>
<tr>
+113 -87
View File
@@ -28,6 +28,7 @@ import ExpenseStatusBadge from '@/components/pages/expense/ExpenseStatusBadge';
import CheckboxInput from '@/components/input/CheckboxInput';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import DateInput from '@/components/input/DateInput';
import RequirePermission from '@/components/helper/RequirePermission';
import { Expense } from '@/types/api/expense';
import { ExpenseApi } from '@/services/api/expense';
@@ -67,58 +68,70 @@ const RowOptionsMenu = ({
return (
<RowOptionsMenuWrapper type={type}>
<div className='w-full max-h-40 overflow-auto flex flex-col gap-1'>
<Button
href={`/expense/detail/?expenseId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
{showEditButton && (
<RequirePermission permissions='lti.expense.detail'>
<Button
href={`/expense/detail/edit/?expenseId=${props.row.original.id}`}
href={`/expense/detail/?expenseId=${props.row.original.id}`}
variant='ghost'
color='warning'
color='primary'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
</RequirePermission>
{showEditButton && (
<RequirePermission permissions='lti.expense.update'>
<Button
href={`/expense/detail/edit/?expenseId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon
icon='material-symbols:edit-outline'
width={16}
height={16}
/>
Edit
</Button>
</RequirePermission>
)}
{showRealizationButton && (
<Button
href={`/expense/realization/?expenseId=${props.row.original.id}`}
variant='ghost'
color='info'
className='justify-start text-sm text-info focus-visible:text-info-content hover:text-info-content'
>
<Icon
icon='material-symbols:money-bag-rounded'
width={16}
height={16}
/>
Realisasi
</Button>
<RequirePermission permissions='lti.expense.create.realization'>
<Button
href={`/expense/realization/?expenseId=${props.row.original.id}`}
variant='ghost'
color='info'
className='justify-start text-sm text-info focus-visible:text-info-content hover:text-info-content'
>
<Icon
icon='material-symbols:money-bag-rounded'
width={16}
height={16}
/>
Realisasi
</Button>
</RequirePermission>
)}
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button>
<RequirePermission permissions='lti.expense.delete'>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button>
</RequirePermission>
</div>
</RowOptionsMenuWrapper>
);
@@ -559,57 +572,70 @@ const ExpensesTable = () => {
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-4'>
<div className='w-full sm:w-fit flex flex-col sm:flex-row self-start gap-2'>
<Button
href='/expense/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
<RequirePermission permissions='lti.expense.create'>
<Button
href='/expense/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
</RequirePermission>
{selectedRowIds.length > 0 && (
<>
<Button
variant='outline'
color='info'
onClick={bulkApproveClickHandler}
disabled={!isAllSelectedRowLatestApprovalOnManager}
className='w-full sm:w-fit'
>
<Icon icon='lucide-lab:farm' width={24} height={24} />
Approve Manager
</Button>
<RequirePermission permissions='lti.expense.approve.manager'>
<Button
variant='outline'
color='info'
onClick={bulkApproveClickHandler}
disabled={!isAllSelectedRowLatestApprovalOnManager}
className='w-full sm:w-fit'
>
<Icon icon='lucide-lab:farm' width={24} height={24} />
Approve Manager
</Button>
</RequirePermission>
<Button
variant='outline'
color='success'
onClick={bulkApproveClickHandler}
disabled={!isAllSelectedRowLatestApprovalOnFinance}
className='w-full sm:w-fit'
>
<Icon icon='tdesign:money' width={24} height={24} />
Approve Finance
</Button>
<RequirePermission permissions='lti.expense.approve.finance'>
<Button
variant='outline'
color='success'
onClick={bulkApproveClickHandler}
disabled={!isAllSelectedRowLatestApprovalOnFinance}
className='w-full sm:w-fit'
>
<Icon icon='tdesign:money' width={24} height={24} />
Approve Finance
</Button>
</RequirePermission>
<Button
variant='outline'
color='error'
onClick={bulkRejectClickHandler}
disabled={
!isAllSelectedRowLatestApprovalOnManager &&
!isAllSelectedRowLatestApprovalOnFinance
}
className='w-full sm:w-fit'
<RequirePermission
permissions={[
'lti.expense.approve.manager',
'lti.expense.approve.finance',
]}
>
<Icon
icon='material-symbols:close'
width={24}
height={24}
/>
Reject
</Button>
<Button
variant='outline'
color='error'
onClick={bulkRejectClickHandler}
disabled={
!isAllSelectedRowLatestApprovalOnManager &&
!isAllSelectedRowLatestApprovalOnFinance
}
className='w-full sm:w-fit'
>
<Icon
icon='material-symbols:close'
width={24}
height={24}
/>
Reject
</Button>
</RequirePermission>
</>
)}
</div>
@@ -20,10 +20,10 @@ interface ExpenseKandangsTableProps {
locationId?: number;
type: 'add' | 'edit' | 'detail';
selectedKandangs: {
id: number;
name: string;
id?: number;
name?: string;
}[];
onChange: (kandangs: { id: number; name: string }[]) => void;
onChange: (kandangs: { id?: number; name?: string }[]) => void;
className?: {
wrapper?: string;
};
@@ -67,7 +67,11 @@ const ExpenseKandangsTable = ({
);
const [sorting, setSorting] = useState<SortingState>([]);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>(
convertRowSelectionArrToObj(selectedKandangs.map((item) => item.id))
convertRowSelectionArrToObj(
selectedKandangs
.map((item) => item.id)
.filter((id): id is number => id !== undefined)
)
);
const kandangsColumns: ColumnDef<Kandang>[] = [
@@ -1,6 +1,7 @@
import * as Yup from 'yup';
import { Expense } from '@/types/api/expense';
import { formatDate } from '@/lib/helper';
import { S3_PUBLIC_BASE_URL } from '@/config/constant';
type ExpenseRealizationFormSchemaType = {
category?: {
@@ -12,7 +13,7 @@ type ExpenseRealizationFormSchemaType = {
label: string;
};
realization_date?: string;
kandangs?: { id: number; name: string }[];
kandangs?: { id?: number; name?: string }[];
supplier?: {
value: number;
label: string;
@@ -20,7 +21,7 @@ type ExpenseRealizationFormSchemaType = {
existing_documents?: { name: string; url: string }[];
documents?: File[];
realizations: {
kandang_id: number;
kandang_id?: number;
cost_items: {
nonstock?: {
value: number;
@@ -49,12 +50,11 @@ export const ExpenseRealizationFormSchema: Yup.ObjectSchema<ExpenseRealizationFo
kandangs: Yup.array()
.of(
Yup.object({
id: Yup.number().required('Kandang wajib dipilih!'),
name: Yup.string().required('Kandang wajib dipilih!'),
id: Yup.number().optional(),
name: Yup.string().optional(),
})
)
.min(1, 'Kandang wajib dipilih!')
.required('Kandang wajib dipilih!'),
.optional(),
supplier: Yup.object({
value: Yup.number().min(1).required(),
@@ -73,7 +73,7 @@ export const ExpenseRealizationFormSchema: Yup.ObjectSchema<ExpenseRealizationFo
realizations: Yup.array()
.of(
Yup.object({
kandang_id: Yup.number().min(1, 'Wajib memilih kandang!').required(),
kandang_id: Yup.number().min(1, 'Wajib memilih kandang!').optional(),
cost_items: Yup.array()
.of(
Yup.object({
@@ -86,12 +86,12 @@ export const ExpenseRealizationFormSchema: Yup.ObjectSchema<ExpenseRealizationFo
notes: Yup.string(),
})
)
.min(1, 'Kandang harus memiliki setidaknya 1 biaya!')
.required('Biaya kandang wajib diisi!'),
.min(1, 'Harus memiliki setidaknya 1 biaya!')
.required('Biaya wajib diisi!'),
})
)
.min(1, 'Biaya kandang wajib diisi!')
.required('Biaya kandang wajib diisi!'),
.min(1, 'Biaya wajib diisi!')
.required('Biaya wajib diisi!'),
});
export const UpdateExpenseRealizationFormSchema = ExpenseRealizationFormSchema;
@@ -139,10 +139,13 @@ export const getExpenseRealizationFormInitialValues = (
label: initialValues.supplier.name,
}
: undefined,
existing_documents: initialValues?.realization_docs?.map((doc) => ({
name: doc.path,
url: doc.path,
})),
existing_documents: initialValues?.realization_docs?.map((doc) => {
const path = doc.path.startsWith('/') ? doc.path.slice(1) : doc.path;
return {
name: doc.path,
url: `${S3_PUBLIC_BASE_URL}/${path}`,
};
}),
documents: [],
realizations: initialValues?.kandangs
? initialValues.kandangs.map((kandangExpense) => {
@@ -16,6 +16,7 @@ import DateInput from '@/components/input/DateInput';
import DropFileInput from '@/components/input/DropFileInput';
import ExpenseKandangsTable from '@/components/pages/expense/form/ExpenseKandangsTable';
import ExpenseRealizationKandangDetailExpense from '@/components/pages/expense/form/ExpenseRealizationKandangDetailExpense';
import RequirePermission from '@/components/helper/RequirePermission';
import {
CreateExpenseRealizationPayload,
@@ -149,25 +150,10 @@ const ExpenseRealizationForm = ({
formik.setFieldValue('location', val);
formik.setFieldValue('kandangs', []);
formik.setFieldValue('realizations', []);
};
const kandangsChangeHandler = (kandangs: { id: number; name: string }[]) => {
formik.setFieldTouched('kandangs', true);
formik.setFieldValue('kandangs', kandangs);
const newRealizations = [...(formik.values.realizations ?? [])];
// add new realizations
kandangs.forEach((kandangItem) => {
const isKandangExistInRealization = newRealizations.find(
(realizationItem) => realizationItem.kandang_id === kandangItem.id
);
if (isKandangExistInRealization) return;
newRealizations.push({
kandang_id: kandangItem.id,
// Auto-create realization item for location (without kandang)
formik.setFieldValue('realizations', [
{
cost_items: [
{
nonstock: undefined,
@@ -176,25 +162,57 @@ const ExpenseRealizationForm = ({
notes: '',
},
],
},
]);
};
const kandangsChangeHandler = (
kandangs: { id?: number; name?: string }[]
) => {
formik.setFieldTouched('kandangs', true);
formik.setFieldValue('kandangs', kandangs);
// If no kandangs selected, create realization item for location
if (kandangs.length === 0) {
formik.setFieldValue('realizations', [
{
cost_items: [
{
nonstock: undefined,
quantity: undefined,
price: undefined,
notes: '',
},
],
},
]);
return;
}
// Start with empty array when kandangs are selected
const newRealizations: typeof formik.values.realizations = [];
// add new realizations for each kandang
kandangs.forEach((kandangItem) => {
if (!kandangItem.id) return;
const existingRealization = formik.values.realizations?.find(
(realizationItem) => realizationItem.kandang_id === kandangItem.id
);
newRealizations.push({
kandang_id: kandangItem.id,
cost_items: existingRealization?.cost_items || [
{
nonstock: undefined,
quantity: undefined,
price: undefined,
notes: '',
},
],
});
});
// prune realizations
const kandangIds = new Set(kandangs.map((kandang) => kandang.id));
const deletedRealizationsIdx: number[] = [];
newRealizations.forEach((realization, idx) => {
const isRealizationValid = kandangIds.has(realization.kandang_id);
if (!isRealizationValid) {
deletedRealizationsIdx.push(idx);
}
});
deletedRealizationsIdx.forEach((deletedRealizationIdx) => {
newRealizations.splice(deletedRealizationIdx, 1);
});
formik.setFieldValue('realizations', newRealizations);
};
@@ -290,21 +308,23 @@ const ExpenseRealizationForm = ({
className={{ wrapper: 'col-span-12' }}
/>
<DropFileInput
label='Dokumen Realisasi'
name='documents'
values={formik.values.documents}
onChange={realizationDocumentsChangeHandler}
onDelete={realizationDocumentsDeleteHandler}
accept={{
...ACCEPTED_FILE_TYPE.PDF,
...ACCEPTED_FILE_TYPE.IMAGE,
}}
className={{
wrapper: 'col-span-12',
inputWrapper: 'h-12 flex items-center',
}}
/>
<RequirePermission permissions='lti.expense.document.realization'>
<DropFileInput
label='Dokumen Realisasi'
name='documents'
values={formik.values.documents}
onChange={realizationDocumentsChangeHandler}
onDelete={realizationDocumentsDeleteHandler}
accept={{
...ACCEPTED_FILE_TYPE.PDF,
...ACCEPTED_FILE_TYPE.IMAGE,
}}
className={{
wrapper: 'col-span-12',
inputWrapper: 'h-12 flex items-center',
}}
/>
</RequirePermission>
{formik.values.existing_documents &&
formik.values.existing_documents.length > 0 && (
@@ -335,7 +355,10 @@ const ExpenseRealizationForm = ({
)}
<ExpenseRealizationKandangDetailExpense
type={type}
formik={formik}
supplierId={formik.values.supplier?.value as number}
location={formik.values.location}
className={{
wrapper: 'col-span-12',
}}
@@ -357,20 +380,22 @@ const ExpenseRealizationForm = ({
{type !== 'add' && (
<div className='flex flex-row justify-start gap-2'>
{type !== 'edit' && (
<Button
type='button'
color='warning'
href={`/expense/detail/edit/?expenseId=${initialValues?.id}`}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
</Button>
<RequirePermission permissions='lti.expense.update'>
<Button
type='button'
color='warning'
href={`/expense/detail/edit/?expenseId=${initialValues?.id}`}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
</Button>
</RequirePermission>
)}
</div>
)}
@@ -18,6 +18,11 @@ import { Nonstock } from '@/types/api/master-data/nonstock';
interface ExpenseRealizationKandangDetailExpenseProps {
type?: 'add' | 'edit' | 'detail';
formik: FormikContextType<ExpenseRealizationFormValues>;
supplierId?: number;
location?: {
value: number;
label: string;
};
className?: {
wrapper?: string;
};
@@ -25,12 +30,18 @@ interface ExpenseRealizationKandangDetailExpenseProps {
const ExpenseRealizationKandangDetailExpense: React.FC<
ExpenseRealizationKandangDetailExpenseProps
> = ({ type, formik, className }) => {
> = ({ type, formik, supplierId, location, className }) => {
const {
setInputValue: setNonstockInputValue,
options: nonstockOptions,
isLoadingOptions: isLoadingNonstockOptions,
} = useSelect<Nonstock>(NonstockApi.basePath, 'id', 'name');
} = useSelect<Nonstock>(
NonstockApi.basePath,
'id',
'name',
'search',
supplierId ? { supplier_id: String(supplierId) } : undefined
);
const nonstockChangeHandler = (
kandangExpenseIdx: number,
@@ -82,140 +93,159 @@ const ExpenseRealizationKandangDetailExpense: React.FC<
</div>
<div className='w-full flex flex-col gap-6'>
{formik.values.realizations.length === 0 && (
{!formik.values.supplier?.value && (
<div>
<p className='text-sm text-gray-400 text-center'>
Pilih kandang terlebih dahulu!
Pilih supplier terlebih dahulu!
</p>
</div>
)}
{formik.values.realizations.map((kandangExpense, kandangExpenseIdx) => {
const kandangName = formik.values.kandangs?.find(
(kandang) => kandang.id === kandangExpense.kandang_id
);
{formik.values.realizations.length === 0 &&
formik.values.supplier?.value && (
<div>
<p className='text-sm text-gray-400 text-center'>
Belum ada item biaya. Silakan pilih lokasi terlebih dahulu.
</p>
</div>
)}
return (
kandangName?.name && (
<div
key={`kandangExpense-${kandangExpenseIdx}`}
className='w-full flex flex-col gap-4'
>
<div>
<h5 className='mb-2 text-lg font-bold text-center'>
Biaya {kandangName?.name}
</h5>
{formik.values.realizations.length > 0 &&
formik.values.supplier?.value &&
formik.values.realizations.map(
(kandangExpense, kandangExpenseIdx) => {
const kandangName = kandangExpense.kandang_id
? formik.values.kandangs?.find(
(kandang) => kandang.id === kandangExpense.kandang_id
)
: null;
<div className='overflow-x-auto'>
<table className='table'>
<thead>
<tr>
<th>Nonstock</th>
<th>Total Kuantitas</th>
<th>Harga Satuan</th>
<th>Catatan</th>
</tr>
</thead>
return (
(kandangName?.name || !kandangExpense.kandang_id) && (
<div
key={`kandangExpense-${kandangExpenseIdx}`}
className='w-full flex flex-col gap-4'
>
<div>
<h5 className='mb-2 text-lg font-bold text-center'>
{kandangName?.name
? `Biaya ${kandangName.name}`
: location?.label
? `Biaya ${location.label}`
: 'Biaya Umum'}
</h5>
<tbody>
{kandangExpense.cost_items.map(
(expenseItem, expenseIdx) => (
<tr key={`expense-${expenseIdx}`}>
<td className='p-2'>
<SelectInput
placeholder='Pilih Nonstock'
value={expenseItem.nonstock}
onChange={(val) => {
nonstockChangeHandler(
kandangExpenseIdx,
expenseIdx,
val
);
}}
options={nonstockOptions}
isLoading={isLoadingNonstockOptions}
onInputChange={setNonstockInputValue}
className={{ wrapper: 'min-w-48' }}
isDisabled
/>
</td>
<td className='p-2'>
<NumberInput
required
name={`realizations[${kandangExpenseIdx}].cost_items[${expenseIdx}].quantity`}
placeholder='Masukkan Total Kuantitas'
value={
formik.values.realizations[
kandangExpenseIdx
].cost_items[expenseIdx].quantity ?? ''
}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={isExpenseRepeaterInputError(
'quantity',
kandangExpenseIdx,
expenseIdx
)}
className={{ wrapper: 'min-w-24' }}
/>
</td>
<td className='p-2'>
<NumberInput
name={`realizations[${kandangExpenseIdx}].cost_items[${expenseIdx}].price`}
placeholder='Masukkan Harga Satuan'
value={
formik.values.realizations[
kandangExpenseIdx
].cost_items[expenseIdx].price ?? ''
}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={isExpenseRepeaterInputError(
'price',
kandangExpenseIdx,
expenseIdx
)}
inputPrefix={
<span className='text-gray-600 font-medium'>
Rp
</span>
}
className={{ wrapper: 'min-w-24' }}
/>
</td>
<td className='p-2'>
<TextInput
name={`realizations[${kandangExpenseIdx}].cost_items[${expenseIdx}].notes`}
placeholder='Tuliskan catatan'
value={
formik.values.realizations[
kandangExpenseIdx
].cost_items[expenseIdx].notes ?? ''
}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={isExpenseRepeaterInputError(
'notes',
kandangExpenseIdx,
expenseIdx
)}
className={{ wrapper: 'min-w-24' }}
/>
</td>
<div className='overflow-x-auto'>
<table className='table'>
<thead>
<tr>
<th>Nonstock</th>
<th>Total Kuantitas</th>
<th>Harga Satuan</th>
<th>Catatan</th>
</tr>
)
)}
</tbody>
</table>
</thead>
<tbody>
{kandangExpense.cost_items.map(
(expenseItem, expenseIdx) => (
<tr key={`expense-${expenseIdx}`}>
<td className='p-2'>
<SelectInput
placeholder='Pilih Nonstock'
value={expenseItem.nonstock}
onChange={(val) => {
nonstockChangeHandler(
kandangExpenseIdx,
expenseIdx,
val
);
}}
options={nonstockOptions}
isLoading={isLoadingNonstockOptions}
onInputChange={setNonstockInputValue}
className={{ wrapper: 'min-w-48' }}
isDisabled
/>
</td>
<td className='p-2'>
<NumberInput
required
name={`realizations[${kandangExpenseIdx}].cost_items[${expenseIdx}].quantity`}
placeholder='Masukkan Total Kuantitas'
value={
formik.values.realizations[
kandangExpenseIdx
].cost_items[expenseIdx].quantity ?? ''
}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={isExpenseRepeaterInputError(
'quantity',
kandangExpenseIdx,
expenseIdx
)}
className={{ wrapper: 'min-w-24' }}
/>
</td>
<td className='p-2'>
<NumberInput
name={`realizations[${kandangExpenseIdx}].cost_items[${expenseIdx}].price`}
placeholder='Masukkan Harga Satuan'
value={
formik.values.realizations[
kandangExpenseIdx
].cost_items[expenseIdx].price ?? ''
}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={isExpenseRepeaterInputError(
'price',
kandangExpenseIdx,
expenseIdx
)}
inputPrefix={
<span className='text-gray-600 font-medium'>
Rp
</span>
}
className={{ wrapper: 'min-w-24' }}
/>
</td>
<td className='p-2'>
<TextInput
name={`realizations[${kandangExpenseIdx}].cost_items[${expenseIdx}].notes`}
placeholder='Tuliskan catatan'
value={
formik.values.realizations[
kandangExpenseIdx
].cost_items[expenseIdx].notes ?? ''
}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={isExpenseRepeaterInputError(
'notes',
kandangExpenseIdx,
expenseIdx
)}
className={{ wrapper: 'min-w-24' }}
/>
</td>
</tr>
)
)}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
)
);
})}
)
);
}
)}
</div>
</Card>
);
@@ -1,6 +1,7 @@
import * as Yup from 'yup';
import { Expense } from '@/types/api/expense';
import { formatDate } from '@/lib/helper';
import { S3_PUBLIC_BASE_URL } from '@/config/constant';
type ExpenseFormSchemaType = {
category?: {
@@ -11,8 +12,9 @@ type ExpenseFormSchemaType = {
value: number;
label: string;
};
location_id: number;
transaction_date?: string;
kandangs?: { id: number; name: string }[];
kandangs?: { id?: number; name?: string }[];
supplier?: {
value: number;
label: string;
@@ -21,7 +23,7 @@ type ExpenseFormSchemaType = {
deleted_documents?: number[];
documents?: File[];
expense_nonstocks: {
kandang_id: number;
kandang_id?: number;
cost_items: {
nonstock?: {
value: number;
@@ -46,16 +48,17 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
label: Yup.string().required(),
}).required('Lokasi wajib diisi!'),
location_id: Yup.number().min(1).required('Lokasi wajib diisi!'),
transaction_date: Yup.string().required('Tanggal transaksi wajib diisi!'),
kandangs: Yup.array()
.of(
Yup.object({
id: Yup.number().required('Kandang wajib dipilih!'),
name: Yup.string().required('Kandang wajib dipilih!'),
id: Yup.number().optional(),
name: Yup.string().optional(),
})
)
.min(1, 'Kandang wajib dipilih!')
.required('Kandang wajib dipilih!'),
.optional(),
supplier: Yup.object({
value: Yup.number().min(1).required(),
@@ -77,7 +80,7 @@ export const ExpenseRequestFormSchema: Yup.ObjectSchema<ExpenseFormSchemaType> =
expense_nonstocks: Yup.array()
.of(
Yup.object({
kandang_id: Yup.number().min(1, 'Wajib memilih kandang!').required(),
kandang_id: Yup.number().min(1, 'Wajib memilih kandang!').optional(),
cost_items: Yup.array()
.of(
Yup.object({
@@ -128,6 +131,7 @@ export const getExpenseFormInitialValues = (
label: initialValues.location.name,
}
: undefined,
location_id: Number(initialValues?.location.id || 0),
transaction_date: initialValues?.transaction_date
? formatDate(initialValues.transaction_date, 'YYYY-MM-DD')
: undefined,
@@ -141,11 +145,14 @@ export const getExpenseFormInitialValues = (
label: initialValues.supplier.name,
}
: undefined,
existing_documents: initialValues?.documents?.map((doc) => ({
id: doc.id,
name: doc.path,
url: doc.path,
})),
existing_documents: initialValues?.documents?.map((doc) => {
const path = doc.path.startsWith('/') ? doc.path.slice(1) : doc.path;
return {
id: doc.id,
name: doc.path,
url: `${S3_PUBLIC_BASE_URL}/${path}`,
};
}),
deleted_documents: [],
documents: [],
expense_nonstocks: initialValues?.kandangs
@@ -18,6 +18,7 @@ import DateInput from '@/components/input/DateInput';
import ExpenseKandangsTable from '@/components/pages/expense/form/ExpenseKandangsTable';
import DropFileInput from '@/components/input/DropFileInput';
import ExpenseRequestKandangDetailExpense from '@/components/pages/expense/form/ExpenseRequestKandangDetailExpense';
import RequirePermission from '@/components/helper/RequirePermission';
import {
ExpenseRequestFormSchema,
@@ -107,18 +108,24 @@ const ExpenseRequestForm = ({
const expensePayload: CreateExpensePayload = {
category: formik.values.category?.value as 'BOP' | 'NON-BOP',
location_id: values.location_id as number,
transaction_date: values?.transaction_date as string,
supplier_id: values.supplier?.value as number,
documents: values.documents as File[],
expense_nonstocks: values.expense_nonstocks.map((expenseNonstock) => ({
kandang_id: expenseNonstock.kandang_id,
cost_items: expenseNonstock.cost_items.map((costItem) => ({
nonstock_id: costItem.nonstock?.value as number,
quantity: parseFloat(String(costItem.quantity)) as number,
price: parseFloat(String(costItem.price)) as number,
notes: costItem.notes ?? '',
})),
})),
expense_nonstocks: values.expense_nonstocks.map((expenseNonstock) => {
const basePayload = {
cost_items: expenseNonstock.cost_items.map((costItem) => ({
nonstock_id: costItem.nonstock?.value as number,
quantity: parseFloat(String(costItem.quantity)) as number,
price: parseFloat(String(costItem.price)) as number,
notes: costItem.notes ?? '',
})),
};
return expenseNonstock.kandang_id
? { ...basePayload, kandang_id: expenseNonstock.kandang_id }
: basePayload;
}),
};
switch (type) {
@@ -129,19 +136,25 @@ const ExpenseRequestForm = ({
case 'edit':
const expenseUpdatePayload: UpdateExpensePayload = {
category: formik.values.category?.value as 'BOP' | 'NON-BOP',
location_id: values.location_id as number,
transaction_date: values?.transaction_date as string,
supplier_id: values.supplier?.value as number,
documents: values.documents as File[],
expense_nonstocks: values.expense_nonstocks.map(
(expenseNonstock) => ({
kandang_id: expenseNonstock.kandang_id,
cost_items: expenseNonstock.cost_items.map((costItem) => ({
nonstock_id: costItem.nonstock?.value as number,
quantity: parseFloat(String(costItem.quantity)) as number,
price: parseFloat(String(costItem.price)) as number,
notes: costItem.notes ?? '',
})),
})
(expenseNonstock) => {
const basePayload = {
cost_items: expenseNonstock.cost_items.map((costItem) => ({
nonstock_id: costItem.nonstock?.value as number,
quantity: parseFloat(String(costItem.quantity)) as number,
price: parseFloat(String(costItem.price)) as number,
notes: costItem.notes ?? '',
})),
};
return expenseNonstock.kandang_id
? { ...basePayload, kandang_id: expenseNonstock.kandang_id }
: basePayload;
}
),
};
@@ -178,27 +191,14 @@ const ExpenseRequestForm = ({
formik.setFieldTouched('location', true);
formik.setFieldValue('location', val);
const locationId = Array.isArray(val) ? val[0]?.value : val?.value;
formik.setFieldValue('location_id', locationId);
formik.setFieldValue('kandangs', []);
formik.setFieldValue('expense_nonstocks', []);
};
const kandangsChangeHandler = (kandangs: { id: number; name: string }[]) => {
formik.setFieldTouched('kandangs', true);
formik.setFieldValue('kandangs', kandangs);
const newExpenseNonstocks = [...(formik.values.expense_nonstocks ?? [])];
// add new expense_nonstocks
kandangs.forEach((kandangItem) => {
const isKandangExistInExpenseNonstocks = newExpenseNonstocks.find(
(expenseNonstockItem) =>
expenseNonstockItem.kandang_id === kandangItem.id
);
if (isKandangExistInExpenseNonstocks) return;
newExpenseNonstocks.push({
kandang_id: kandangItem.id,
// Auto-create expense item for location (without kandang)
formik.setFieldValue('expense_nonstocks', [
{
cost_items: [
{
nonstock: undefined,
@@ -207,25 +207,56 @@ const ExpenseRequestForm = ({
notes: '',
},
],
},
]);
};
const kandangsChangeHandler = (
kandangs: { id?: number; name?: string }[]
) => {
formik.setFieldTouched('kandangs', true);
formik.setFieldValue('kandangs', kandangs);
// If no kandangs selected, create expense item for location
if (kandangs.length === 0) {
formik.setFieldValue('expense_nonstocks', [
{
cost_items: [
{
nonstock: undefined,
quantity: undefined,
price: undefined,
notes: '',
},
],
},
]);
return;
}
const newExpenseNonstocks: typeof formik.values.expense_nonstocks = [];
kandangs.forEach((kandangItem) => {
if (!kandangItem.id) return;
const existingExpenseNonstock = formik.values.expense_nonstocks?.find(
(expenseNonstockItem) =>
expenseNonstockItem.kandang_id === kandangItem.id
);
newExpenseNonstocks.push({
kandang_id: kandangItem.id,
cost_items: existingExpenseNonstock?.cost_items || [
{
nonstock: undefined,
quantity: undefined,
price: undefined,
notes: '',
},
],
});
});
// prune expense_nonstocks
const kandangIds = new Set(kandangs.map((kandang) => kandang.id));
const deletedExpenseNonstocksIdx: number[] = [];
newExpenseNonstocks.forEach((expenseNonstock, idx) => {
const isExpenseNonstockValid = kandangIds.has(expenseNonstock.kandang_id);
if (!isExpenseNonstockValid) {
deletedExpenseNonstocksIdx.push(idx);
}
});
deletedExpenseNonstocksIdx.forEach((deletedExpenseNonstockIdx) => {
newExpenseNonstocks.splice(deletedExpenseNonstockIdx, 1);
});
formik.setFieldValue('expense_nonstocks', newExpenseNonstocks);
};
@@ -385,21 +416,23 @@ const ExpenseRequestForm = ({
className={{ wrapper: 'col-span-12' }}
/>
<DropFileInput
label='Dokumen Pengajuan'
name='documents'
values={formik.values.documents}
onChange={requestDocumentsChangeHandler}
onDelete={requestDocumentsDeleteHandler}
accept={{
...ACCEPTED_FILE_TYPE.PDF,
...ACCEPTED_FILE_TYPE.IMAGE,
}}
className={{
wrapper: 'col-span-12',
inputWrapper: 'h-12 flex items-center',
}}
/>
<RequirePermission permissions='lti.expense.document'>
<DropFileInput
label='Dokumen Pengajuan'
name='documents'
values={formik.values.documents}
onChange={requestDocumentsChangeHandler}
onDelete={requestDocumentsDeleteHandler}
accept={{
...ACCEPTED_FILE_TYPE.PDF,
...ACCEPTED_FILE_TYPE.IMAGE,
}}
className={{
wrapper: 'col-span-12',
inputWrapper: 'h-12 flex items-center',
}}
/>
</RequirePermission>
{formik.values.existing_documents &&
formik.values.existing_documents.length > 0 && (
@@ -451,7 +484,10 @@ const ExpenseRequestForm = ({
)}
<ExpenseRequestKandangDetailExpense
type={type}
formik={formik}
supplierId={formik.values.supplier?.value as number}
location={formik.values.location}
className={{
wrapper: 'col-span-12',
}}
@@ -461,36 +497,40 @@ const ExpenseRequestForm = ({
<div className='flex flex-row justify-between gap-2 flex-wrap'>
{type !== 'add' && (
<div className='flex flex-row justify-start gap-2'>
<Button
type='button'
color='error'
onClick={deleteExpenseClickHandler}
className='px-4'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Delete
</Button>
{type !== 'edit' && (
<RequirePermission permissions='lti.expense.delete'>
<Button
type='button'
color='warning'
href={`/expense/detail/edit/?expenseId=${initialValues?.id}`}
color='error'
onClick={deleteExpenseClickHandler}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
Delete
</Button>
</RequirePermission>
{type !== 'edit' && (
<RequirePermission permissions='lti.expense.update'>
<Button
type='button'
color='warning'
href={`/expense/detail/edit/?expenseId=${initialValues?.id}`}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
</Button>
</RequirePermission>
)}
</div>
)}
@@ -21,6 +21,11 @@ import { removeArrayItemAndSync } from '@/lib/utils/formik';
interface ExpenseRequestKandangDetailExpenseProps {
type?: 'add' | 'edit' | 'detail';
formik: FormikContextType<ExpenseRequestFormValues>;
supplierId?: number;
location?: {
value: number;
label: string;
};
className?: {
wrapper?: string;
};
@@ -28,12 +33,18 @@ interface ExpenseRequestKandangDetailExpenseProps {
const ExpenseRequestKandangDetailExpense: React.FC<
ExpenseRequestKandangDetailExpenseProps
> = ({ type, formik, className }) => {
> = ({ type, formik, supplierId, location, className }) => {
const {
setInputValue: setNonstockInputValue,
options: nonstockOptions,
isLoadingOptions: isLoadingNonstockOptions,
} = useSelect<Nonstock>(NonstockApi.basePath, 'id', 'name');
} = useSelect<Nonstock>(
NonstockApi.basePath,
'id',
'name',
'search',
supplierId ? { supplier_id: String(supplierId) } : undefined
);
const nonstockChangeHandler = (
kandangExpenseIdx: number,
@@ -113,41 +124,57 @@ const ExpenseRequestKandangDetailExpense: React.FC<
</div>
<div className='w-full flex flex-col gap-6'>
{(formik.values.expense_nonstocks.length === 0 ||
!formik.values.supplier?.value) && (
{!formik.values.supplier?.value && (
<div>
<p className='text-sm text-gray-400 text-center'>
Pilih kandang terlebih dahulu!
Pilih supplier terlebih dahulu!
</p>
</div>
)}
{formik.values.expense_nonstocks.length === 0 &&
formik.values.supplier?.value && (
<div>
<p className='text-sm text-gray-400 text-center'>
Belum ada item biaya. Silakan pilih lokasi terlebih dahulu.
</p>
</div>
)}
{formik.values.expense_nonstocks.length > 0 &&
formik.values.supplier?.value &&
formik.values.expense_nonstocks.map(
(kandangExpense, kandangExpenseIdx) => {
const kandangName = formik.values.kandangs?.find(
(kandang) => kandang.id === kandangExpense.kandang_id
);
const kandangName = kandangExpense.kandang_id
? formik.values.kandangs?.find(
(kandang) => kandang.id === kandangExpense.kandang_id
)
: null;
return (
kandangName?.name && (
(kandangName?.name || !kandangExpense.kandang_id) && (
<div
key={`kandangExpense-${kandangExpenseIdx}`}
className='w-full flex flex-col gap-4'
>
<div>
<h5 className='mb-2 text-lg font-bold text-center'>
Biaya {kandangName?.name}
Biaya {kandangName?.name || location?.label || 'Umum'}
</h5>
<div className='overflow-x-auto'>
<table className='table'>
<thead>
<tr>
<th>Nonstock</th>
<th>Total Kuantitas</th>
<th>Harga Satuan</th>
<th className='after:content-["*"] after:text-red-500 after:ml-0.5'>
Nonstock
</th>
<th className='after:content-["*"] after:text-red-500 after:ml-0.5'>
Total Kuantitas
</th>
<th className='after:content-["*"] after:text-red-500 after:ml-0.5'>
Harga Satuan
</th>
<th>Catatan</th>
{type !== 'detail' && <th>Aksi</th>}
</tr>
@@ -219,7 +219,13 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
{ label: 'Lokasi', value: expense?.location.name },
{
label: 'Kandang',
value: expense?.kandangs.map((item) => item.name).join(', '),
value:
expense?.kandangs && expense?.kandangs.some((k) => k.name)
? expense?.kandangs
.filter((item) => item.name)
.map((item) => item.name)
.join(', ')
: '-',
},
{ label: 'Vendor', value: expense?.supplier.name },
{
@@ -235,7 +241,12 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
{ label: 'Nama Pengaju', value: expense?.created_user.name },
{
label: 'Nominal Biaya',
value: formatCurrency(expense?.grand_total ?? 0),
value: formatCurrency(
expense?.latest_approval.step_number === 4 ||
expense?.latest_approval.step_number === 5
? (expense?.total_realisasi ?? 0)
: (expense?.total_pengajuan ?? 0)
),
},
{
label: 'Nominal Pengajuan',
@@ -326,7 +337,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
let expenseRequestTotal = 0;
kandangExpense.pengajuans?.forEach(
(item) => (expenseRequestTotal += item.price)
(item) => (expenseRequestTotal += item.qty * item.price)
);
return (
@@ -335,7 +346,9 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
style={ExpensePDFStyle.kandangExpenseContainer}
>
<Text style={ExpensePDFStyle.kandangExpenseTitle}>
{kandangExpense.name}
{kandangExpense.kandang_id && kandangExpense.name
? `Biaya ${kandangExpense.name}`
: `Biaya ${expense?.location.name || 'Umum'}`}
</Text>
<View style={ExpensePDFStyle.kandangExpenseTable}>
@@ -484,7 +497,7 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
let expenseRealizationTotal = 0;
kandangExpense.realisasi?.forEach(
(item) => (expenseRealizationTotal += item.price)
(item) => (expenseRealizationTotal += item.qty * item.price)
);
return (
@@ -493,7 +506,9 @@ const ExpensePDF = ({ expense }: ExpensePDFProps) => {
style={ExpensePDFStyle.kandangExpenseContainer}
>
<Text style={ExpensePDFStyle.kandangExpenseTitle}>
{kandangExpense.name}
{kandangExpense.kandang_id && kandangExpense.name
? `Biaya ${kandangExpense.name}`
: `Biaya ${expense?.location.name || 'Umum'}`}
</Text>
<View style={ExpensePDFStyle.kandangExpenseTable}>
@@ -0,0 +1,194 @@
import Button from '@/components/Button';
import Card from '@/components/Card';
import { FormHeader } from '@/components/helper/form/FormHeader';
import RequirePermission from '@/components/helper/RequirePermission';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import Table from '@/components/Table';
import {
FINANCE_INITIAL_BALANCE_STATUS,
FINANCE_TRANSACTION_STATUS,
} from '@/config/constant';
import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
import { FinanceApi } from '@/services/api/finance';
import { Finance } from '@/types/api/finance/finance';
import { Icon } from '@iconify/react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import toast from 'react-hot-toast';
const FinanceDetail = ({ finance }: { finance: Finance }) => {
const router = useRouter();
const deleteModal = useModal();
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const informasiUmum = [
{
label: 'ID',
value: finance.payment_code,
},
{
label: 'Jenis Transaksi',
value: finance.transaction_type,
},
{
label: 'Pihak',
value: finance.party.name,
},
{
label: 'Tanggal',
value: formatDate(finance.payment_date, 'DD MMM yyyy'),
},
{
label: 'Metode Pembayaran',
value: finance.payment_method,
},
{
label: 'Catatan',
value: finance.notes || '-',
},
];
const informasiTransfer = [
{
label: 'No. Referensi',
value: finance.reference_number,
},
{
label: 'Nomor Rekening',
value: `${finance.bank.alias} - ${finance.bank.account_number} - ${finance.bank.owner}`,
},
{
label: `Rekening ${formatTitleCase(finance.party.type)}`,
value: finance.party.account_number,
},
{
label: 'Nominal',
value: formatCurrency(finance.expense_amount),
},
{
label: 'Sisa',
value: formatCurrency(finance.income_amount),
},
];
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
await FinanceApi.delete(finance.id as number);
router.back();
deleteModal.closeModal();
toast.success('Successfully delete Finance!');
setIsDeleteLoading(false);
};
return (
<div className='flex flex-col gap-6 p-6'>
<FormHeader title='' backUrl='/finance' />
<Card
title='Detail Keuangan'
className={{
wrapper: 'w-full',
}}
variant='bordered'
>
<div className='grid grid-cols-2 gap-4 mb-6'>
<Table
data={informasiUmum}
columns={[
{
header: '',
id: 'label',
accessorKey: 'label',
},
{
header: '',
id: 'value',
accessorKey: 'value',
},
]}
className={{
headerRowClassName: 'hidden',
paginationClassName: 'hidden',
containerClassName: 'mb-0',
}}
/>
<Table
data={informasiTransfer}
columns={[
{
header: '',
id: 'label',
accessorKey: 'label',
},
{
header: '',
id: 'value',
accessorKey: 'value',
},
]}
className={{
headerRowClassName: 'hidden',
paginationClassName: 'hidden',
containerClassName: 'mb-0',
}}
/>
</div>
</Card>
<div className='flex flex-row gap-2 justify-end'>
{FINANCE_TRANSACTION_STATUS.includes(finance.transaction_type) && (
<RequirePermission permissions='lti.finance.payments.update'>
<Button
color='warning'
className='min-w-24'
href={`/finance/detail/edit?financeId=${finance.id}`}
>
<Icon icon='mdi:pencil-outline' />
Edit
</Button>
</RequirePermission>
)}
{FINANCE_INITIAL_BALANCE_STATUS.includes(finance.transaction_type) && (
<RequirePermission permissions='lti.finance.initial_balances.update'>
<Button
color='warning'
className='min-w-24'
href={`/finance/detail/edit/initial-balance?financeId=${finance.id}`}
>
<Icon icon='mdi:pencil-outline' />
Edit
</Button>
</RequirePermission>
)}
<RequirePermission permissions='lti.finance.transaction.delete'>
<Button
color='error'
className='min-w-24'
onClick={() => deleteModal.openModal()}
>
<Icon icon='mdi:delete-outline' />
Delete
</Button>
</RequirePermission>
</div>
<ConfirmationModal
ref={deleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data Finance ini (${finance?.payment_code})?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isDeleteLoading,
onClick: confirmationModalDeleteClickHandler,
}}
/>
</div>
);
};
export default FinanceDetail;
@@ -0,0 +1,564 @@
import { ChangeEventHandler, useMemo, useState } from 'react';
import { CellContext, Row } from '@tanstack/react-table';
import { useSearchParams } from 'next/navigation';
import useSWR from 'swr';
import Button from '@/components/Button';
import Card from '@/components/Card';
import Dropdown from '@/components/dropdown/Dropdown';
import DateInput from '@/components/input/DateInput';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import Menu from '@/components/menu/Menu';
import MenuItem from '@/components/menu/MenuItem';
import Table from '@/components/Table';
import Tooltip from '@/components/Tooltip';
import { formatCurrency, formatDate, formatTitleCase } from '@/lib/helper';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { Finance } from '@/types/api/finance/finance';
import {
FINANCE_INITIAL_BALANCE_STATUS,
FINANCE_INJECTION_STATUS,
FINANCE_TRANSACTION_STATUS,
ROWS_OPTIONS,
} from '@/config/constant';
import { FinanceApi } from '@/services/api/finance';
import { isResponseSuccess } from '@/lib/api-helper';
import { BankApi, CustomerApi, SupplierApi } from '@/services/api/master-data';
import { Bank } from '@/types/api/master-data/bank';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import toast from 'react-hot-toast';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission';
import { Icon } from '@iconify/react';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
const RowOptionsMenu = ({
type = 'dropdown',
props,
deleteClickHandler,
}: {
type: 'dropdown' | 'collapse';
props: CellContext<Finance, unknown>;
deleteClickHandler: () => void;
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<RequirePermission permissions='lti.finance.transaction.detail'>
<Button
href={`/finance/detail?financeId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
</RequirePermission>
{FINANCE_TRANSACTION_STATUS.includes(
props.row.original.transaction_type
) && (
<RequirePermission permissions='lti.finance.payments.update'>
<Button
href={`/finance/detail/edit?financeId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
</RequirePermission>
)}
{FINANCE_INITIAL_BALANCE_STATUS.includes(
props.row.original.transaction_type
) && (
<RequirePermission permissions='lti.finance.initial_balances.update'>
<Button
href={`/finance/detail/edit/initial-balance?financeId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
</RequirePermission>
)}
{FINANCE_INJECTION_STATUS.includes(
props.row.original.transaction_type
) && (
<RequirePermission permissions='lti.finance.injections.update'>
<Button
href={`/finance/detail/edit/injection?financeId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
</RequirePermission>
)}
<RequirePermission permissions='lti.finance.transaction.delete'>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button>
</RequirePermission>
</RowOptionsMenuWrapper>
);
};
const FinanceTable = () => {
const {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
search: '',
transactionType: '',
bankId: '',
partyType: '',
sortBy: '',
startDate: '',
endDate: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
transactionType: 'transaction_type',
bankId: 'bank_id',
partyType: 'party_type',
sortBy: 'sort_date',
startDate: 'start_date',
endDate: 'end_date',
},
});
// ===== State =====
const [searchParams, setSearchParams] = useSearchParams();
const deleteModal = useModal();
const [pendingFilters, setPendingFilters] = useState({
search: '',
transactionType: '',
bankId: '',
partyType: '',
sortBy: '',
startDate: '',
endDate: '',
});
const [selectedTransactionType, setSelectedTransactionType] =
useState<OptionType | null>(null);
const [selectedBank, setSelectedBank] = useState<OptionType | null>(null);
const [selectedPartyType, setSelectedPartyType] = useState<OptionType | null>(
null
);
const [selectedSortBy, setSelectedSortBy] = useState<OptionType | null>(null);
const [selectedFinance, setSelectedFinance] = useState<Finance | null>(null);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
// ===== Fetch Data =====
const {
data: finances,
isLoading,
mutate: refreshFinances,
} = useSWR(
`${FinanceApi.basePath}/transactions${getTableFilterQueryString()}`,
FinanceApi.getAllFetcher
);
// ===== Options =====
const transactionTypeOptions = useMemo(() => {
return [
{ label: 'Transfer', value: 'TRANSFER' },
{ label: 'Cash', value: 'CASH' },
{ label: 'Card', value: 'CARD' },
{ label: 'Cheque', value: 'CHEQUE' },
{ label: 'Saldo', value: 'SALDO' },
];
}, []);
const partyTypeOptions = useMemo(() => {
return [
{ label: 'Customer', value: 'CUSTOMER' },
{ label: 'Supplier', value: 'SUPPLIER' },
];
}, []);
const sortByOptions = useMemo(() => {
return [
{ label: 'Tanggal Pembayaran', value: 'payment_date' },
{ label: 'Tanggal Dibuat', value: 'created_at' },
];
}, []);
const { options: bankOptions, rawData: bankRawData } = useSelect<Bank>(
BankApi.basePath,
'id',
'alias',
'',
{
limit: 'limit',
}
);
// ===== Handler =====
const searchChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
setPendingFilters((prev) => ({ ...prev, search: e.target.value }));
};
const transactionTypeChangeHandler = (
val: OptionType | OptionType[] | null
) => {
setSelectedTransactionType(val as OptionType);
setPendingFilters((prev) => ({
...prev,
transactionType: val ? ((val as OptionType).value as string) : '',
}));
};
const bankChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedBank(val as OptionType);
setPendingFilters((prev) => ({
...prev,
bankId: val ? ((val as OptionType).value as string) : '',
}));
};
const partyTypeChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedPartyType(val as OptionType);
setPendingFilters((prev) => ({
...prev,
partyType: val ? ((val as OptionType).value as string) : '',
}));
};
const sortByChangeHandler = (val: OptionType | OptionType[] | null) => {
setSelectedSortBy(val as OptionType);
setPendingFilters((prev) => ({
...prev,
sortBy: val ? ((val as OptionType).value as string) : '',
}));
};
const startDateChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
setPendingFilters((prev) => ({ ...prev, startDate: e.target.value }));
};
const endDateChangeHandler: ChangeEventHandler<HTMLInputElement> = (e) => {
setPendingFilters((prev) => ({ ...prev, endDate: e.target.value }));
};
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
const newVal = val as OptionType;
setPageSize(newVal.value as number);
};
const submitFilterHandler = () => {
updateFilter('search', pendingFilters.search);
updateFilter('transactionType', pendingFilters.transactionType);
updateFilter('bankId', pendingFilters.bankId);
updateFilter('partyType', pendingFilters.partyType);
updateFilter('sortBy', pendingFilters.sortBy);
updateFilter('startDate', pendingFilters.startDate);
updateFilter('endDate', pendingFilters.endDate);
};
const resetFilterHandler = () => {
setSelectedTransactionType(null);
setSelectedBank(null);
setSelectedPartyType(null);
setSelectedSortBy(null);
const emptyFilters = {
search: '',
transactionType: '',
bankId: '',
partyType: '',
sortBy: '',
startDate: '',
endDate: '',
};
setPendingFilters(emptyFilters);
updateFilter('search', '');
updateFilter('transactionType', '');
updateFilter('bankId', '');
updateFilter('partyType', '');
updateFilter('sortBy', '');
updateFilter('startDate', '');
updateFilter('endDate', '');
};
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
await FinanceApi.delete(selectedFinance?.id as number);
refreshFinances();
deleteModal.closeModal();
toast.success('Successfully delete Finance!');
setIsDeleteLoading(false);
};
const columns = useMemo(() => {
return [
{
header: 'ID',
accessorKey: 'payment_code',
},
{
header: 'References Number',
accessorKey: 'reference_number',
cell: (props: CellContext<Finance, unknown>) => {
const value = props.row.original.reference_number;
return <span>{value ?? '-'}</span>;
},
},
{
header: 'Jenis Transaksi',
accessorKey: 'transaction_type',
cell: (props: CellContext<Finance, unknown>) => {
const value = props.row.original.transaction_type
.split('_')
.join(' ');
return <span>{formatTitleCase(value)}</span>;
},
},
{
header: 'Pihak',
accessorFn: (finance: Finance) => finance.party.name,
cell: (props: CellContext<Finance, unknown>) => {
if (props.row.original.party.id) {
return <span>{props.row.original.party.name}</span>;
}
return <span>{'-'}</span>;
},
},
{
header: 'Tanggal',
accessorFn: (finance: Finance) =>
formatDate(finance.payment_date, 'DD MMM YYYY'),
},
{
header: 'Metode Pembayaran',
accessorKey: 'payment_method',
cell: (props: CellContext<Finance, unknown>) => {
const value = props.row.original.payment_method.split('_').join(' ');
return <span>{formatTitleCase(value)}</span>;
},
},
{
header: 'Bank',
accessorFn: (finance: Finance) =>
`${finance.bank.alias} - ${finance.bank.account_number} - ${finance.bank.owner}`,
},
{
header: 'Pengeluaran (Rp)',
accessorFn: (finance: Finance) =>
formatCurrency(finance.expense_amount),
},
{
header: 'Pemasukan (Rp)',
accessorFn: (finance: Finance) => formatCurrency(finance.income_amount),
},
{
header: 'Aksi',
cell: (props: CellContext<Finance, unknown>) => {
const currentPageSize =
props.table.getPaginationRowModel().rows.length;
const currentPageRows = props.table.getPaginationRowModel().flatRows;
const currentRowRelativeIndex =
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
const deleteClickHandler = () => {
setSelectedFinance(props.row.original);
deleteModal.openModal();
};
return (
<>
{currentPageSize > 2 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowCollapseOptions>
)}
</>
);
},
},
];
}, []);
return (
<section className='size-full p-6 flex flex-col gap-6'>
<div className='flex justify-end gap-2'>
<RequirePermission permissions='lti.finance.injections.create'>
<Button
color='warning'
className='min-w-24'
href='/finance/add/injection'
>
Injection Saldo Bank
</Button>
</RequirePermission>
<RequirePermission permissions='lti.finance.initial_balances.create'>
<Button
color='info'
className='text-white min-w-24'
href='/finance/add/initial-balance'
>
Saldo Awal
</Button>
</RequirePermission>
<RequirePermission permissions='lti.finance.payments.create'>
<Button color='primary' className='min-w-24' href='/finance/add'>
Tambah
</Button>
</RequirePermission>
</div>
<Card
variant='bordered'
className={{
wrapper: 'w-full',
}}
footer={
<div className='flex justify-end gap-2'>
<Button
color='warning'
className='min-w-24'
onClick={resetFilterHandler}
>
Reset
</Button>
<Button
color='primary'
className='min-w-24'
onClick={submitFilterHandler}
>
Cari
</Button>
</div>
}
>
<div className='grid grid-cols-4 gap-6'>
<SelectInput
options={transactionTypeOptions}
label='Jenis Transaksi'
value={selectedTransactionType}
onChange={transactionTypeChangeHandler}
isClearable
/>
<SelectInput
options={
isResponseSuccess(bankRawData)
? bankOptions.map((bank) => ({
label:
bankRawData.data.find((data) => data.id === bank.value)
?.alias +
' - ' +
bankRawData.data.find((data) => data.id === bank.value)
?.account_number +
' - ' +
bankRawData.data.find((data) => data.id === bank.value)
?.owner,
value: bank.value,
}))
: []
}
label='Bank'
value={selectedBank}
onChange={bankChangeHandler}
isClearable
/>
<SelectInput
options={partyTypeOptions}
label='Pihak'
value={selectedPartyType}
onChange={partyTypeChangeHandler}
isClearable
/>
<DebouncedTextInput
name='search'
label='Cari'
placeholder='Cari'
value={pendingFilters.search}
onChange={searchChangeHandler}
/>
<SelectInput
options={sortByOptions}
label='Urutkan Berdasarkan'
value={selectedSortBy}
onChange={sortByChangeHandler}
isClearable
/>
<DateInput
name='startDate'
label='Periode Tanggal (Mulai)'
value={pendingFilters.startDate}
onChange={startDateChangeHandler}
/>
<DateInput
name='endDate'
label='Periode Tanggal (Akhir)'
value={pendingFilters.endDate}
onChange={endDateChangeHandler}
/>
</div>
</Card>
<Table<Finance>
data={isResponseSuccess(finances) ? finances.data : []}
columns={columns}
pageSize={tableFilterState.pageSize}
page={tableFilterState.page}
onPageChange={setPage}
onPageSizeChange={setPageSize}
totalItems={
isResponseSuccess(finances) ? finances.meta?.total_results : 0
}
isLoading={isLoading}
/>
<ConfirmationModal
ref={deleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data Finance ini (${selectedFinance?.payment_code})?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isDeleteLoading,
onClick: confirmationModalDeleteClickHandler,
}}
/>
</section>
);
};
export default FinanceTable;
@@ -0,0 +1,67 @@
import * as Yup from 'yup';
import { OptionType } from '@/components/input/SelectInput';
/**
* API Payload format:
* {
"party_id": 1,
"party_type": "CUSTOMER",
"payment_date": "2025-11-21",
"payment_method": "Transfer",
"bank_id": 1,
"reference_number": "DO.MBU.123",
"nominal": 25000000,
"notes": "Pembayaran piutang penjualan telur"
}
*/
// Type for form values (includes option objects for SelectInput)
export type FinanceFormValues = {
party_type_option: OptionType | null;
party_id_option: OptionType | null;
party_account_number: string;
payment_date: string;
payment_method_option: OptionType | null;
bank_id_option: OptionType | null;
reference_number: string;
nominal: string;
notes: string;
};
export const FinanceFormSchema = Yup.object().shape({
party_type_option: Yup.mixed()
.nullable()
.test(
'is-valid-option',
'Jenis transaksi wajib diisi',
(value) => value !== null && value !== undefined
),
party_id_option: Yup.mixed()
.nullable()
.test(
'is-valid-option',
'Pihak wajib diisi',
(value) => value !== null && value !== undefined
),
party_account_number: Yup.string().required('Nomor rekening wajib diisi'),
payment_date: Yup.string().required('Tanggal pembayaran wajib diisi'),
payment_method_option: Yup.mixed()
.nullable()
.test(
'is-valid-option',
'Metode pembayaran wajib diisi',
(value) => value !== null && value !== undefined
),
bank_id_option: Yup.mixed()
.nullable()
.test(
'is-valid-option',
'Bank wajib diisi',
(value) => value !== null && value !== undefined
),
reference_number: Yup.string().required('Nomor referensi wajib diisi'),
nominal: Yup.string().required('Nominal wajib diisi'),
notes: Yup.string().required('Catatan wajib diisi'),
});
export const UpdateFinanceFormSchema = FinanceFormSchema;
@@ -0,0 +1,410 @@
'use client';
import Button from '@/components/Button';
import Card from '@/components/Card';
import { FormHeader } from '@/components/helper/form/FormHeader';
import DateInput from '@/components/input/DateInput';
import NumberInput from '@/components/input/NumberInput';
import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import TextArea from '@/components/input/TextArea';
import TextInput from '@/components/input/TextInput';
import {
FinanceFormSchema,
FinanceFormValues,
} from '@/components/pages/finance/add/FormFinanceAdd.schema';
import {
FINANCE_PARTY_TYPE_OPTIONS,
FINANCE_PAYMENT_METHOD_OPTIONS,
} from '@/config/constant';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { formatDate, formatTitleCase } from '@/lib/helper';
import { FinanceApi } from '@/services/api/finance';
import { BankApi, CustomerApi, SupplierApi } from '@/services/api/master-data';
import {
CreateFinancePayment,
Finance,
UpdateFinancePayment,
} from '@/types/api/finance/finance';
import { Bank } from '@/types/api/master-data/bank';
import { useFormik } from 'formik';
import { useRouter } from 'next/navigation';
import { useCallback, useMemo } from 'react';
import toast from 'react-hot-toast';
interface FormFinanceAddProps {
type?: 'add' | 'edit';
initialValues?: Finance;
}
interface PartyCommonProps {
id: number;
name: string;
account_number: string;
}
const FormFinanceAdd = ({
type = 'add',
initialValues,
}: FormFinanceAddProps) => {
const router = useRouter();
// ===== Formik =====
const formikInitialValues = useMemo((): FinanceFormValues => {
return {
party_type_option:
FINANCE_PARTY_TYPE_OPTIONS.find(
(option) => option.value === initialValues?.party.type
) || null,
party_id_option: initialValues?.party
? {
label: initialValues?.party.name || '',
value: initialValues?.party.id || 0,
}
: null,
payment_date: initialValues?.payment_date || '',
payment_method_option:
FINANCE_PAYMENT_METHOD_OPTIONS.find(
(option) => option.value === initialValues?.payment_method
) || null,
bank_id_option: initialValues?.bank
? {
label: initialValues.bank.name,
value: initialValues.bank.id,
}
: null,
party_account_number: initialValues?.party.account_number || '',
reference_number: initialValues?.reference_number || '',
nominal: initialValues?.nominal.toString() || '',
notes: initialValues?.notes || '',
};
}, [initialValues]);
const formik = useFormik<FinanceFormValues>({
initialValues: formikInitialValues,
validationSchema: FinanceFormSchema,
validateOnChange: true,
validateOnBlur: true,
onSubmit: async (values) => {
const payload = transformFormValuesToPayload(values);
switch (type) {
case 'add':
await createFinance(payload);
break;
case 'edit':
if (initialValues?.id) {
await updateFinance(initialValues.id, payload);
}
break;
}
},
});
// ===== Options =====
const {
options: partyOptions,
isLoadingOptions: isLoadingPartyOptions,
rawData: partyRawData,
} = useSelect<PartyCommonProps>(
formik.values.party_type_option?.value === 'CUSTOMER'
? CustomerApi.basePath
: SupplierApi.basePath,
'id',
'name',
'',
{ limit: 'limit' }
);
const {
options: bankOptions,
rawData: bankRawData,
isLoadingOptions: isLoadingBankOptions,
} = useSelect<Bank>(BankApi.basePath, 'id', 'name', '', { limit: 'limit' });
// ===== Helper Functions =====
const transformFormValuesToPayload = (
values: FinanceFormValues
): CreateFinancePayment => {
return {
party_id: Number(values.party_id_option?.value) || 0,
party_type: (values.party_type_option?.value as string) || '',
payment_date: formatDate(values.payment_date, 'YYYY-MM-DD'),
payment_method: (values.payment_method_option?.value as string) || '',
bank_id: Number(values.bank_id_option?.value) || 0,
reference_number: values.reference_number,
nominal: Number(values.nominal.replace(/\D/g, '')) || 0,
notes: values.notes,
};
};
// ===== Handler =====
const createFinance = useCallback(
async (payload: CreateFinancePayment) => {
const response = await FinanceApi.create(payload);
if (isResponseError(response)) {
toast.error(response.message);
return;
}
toast.success('Data berhasil ditambahkan');
router.refresh();
router.push('/finance');
},
[router]
);
const updateFinance = useCallback(
async (financeId: number, payload: UpdateFinancePayment) => {
const response = await FinanceApi.update(financeId, payload);
if (isResponseError(response)) {
toast.error(response.message);
return;
}
toast.success('Data berhasil diperbarui');
router.refresh();
router.push('/finance');
},
[router]
);
return (
<>
<section className='w-full max-w-xl mx-auto'>
<div className='flex flex-col gap-6 p-6'>
<FormHeader
title={`${type === 'add' ? 'Tambah' : 'Ubah'} Data Keuangan`}
backUrl='/finance'
/>
<form className='flex flex-col gap-4' onSubmit={formik.handleSubmit}>
<SelectInput
label='Jenis Transaksi'
placeholder='Pilih jenis transaksi'
options={FINANCE_PARTY_TYPE_OPTIONS}
value={formik.values.party_type_option}
onChange={(value) => {
formik.setFieldValue('party_type_option', value);
}}
isError={Boolean(
formik.touched.party_type_option &&
formik.errors.party_type_option
)}
errorMessage={
formik.touched.party_type_option &&
formik.errors.party_type_option
? formik.errors.party_type_option
: ''
}
required
isClearable
/>
<SelectInput
label={
formik.values.party_type_option?.value
? formatTitleCase(
formik.values.party_type_option.value as string
)
: 'Pilih Jenis Transaksi Dahulu'
}
placeholder={`Pilih ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'jenis transaksi dahulu'}`}
options={partyOptions}
value={formik.values.party_id_option}
onChange={(value) => {
formik.setFieldValue('party_id_option', value);
if (isResponseSuccess(partyRawData) && value) {
formik.setFieldValue(
'party_account_number',
partyRawData.data?.find(
(item) => item.id === (value as OptionType)?.value
)?.account_number || ''
);
}
}}
isLoading={isLoadingPartyOptions}
isError={Boolean(
formik.touched.party_id_option && formik.errors.party_id_option
)}
errorMessage={
formik.touched.party_id_option && formik.errors.party_id_option
? formik.errors.party_id_option
: ''
}
required
isClearable
isDisabled={!formik.values.party_type_option?.value}
/>
<DateInput
label='Tanggal'
placeholder='Pilih tanggal'
name='payment_date'
value={formik.values.payment_date}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={Boolean(
formik.touched.payment_date && formik.errors.payment_date
)}
errorMessage={
formik.touched.payment_date && formik.errors.payment_date
? formik.errors.payment_date
: ''
}
required
/>
<SelectInput
label='Metode Pembayaran'
placeholder='Pilih metode pembayaran'
options={FINANCE_PAYMENT_METHOD_OPTIONS}
value={formik.values.payment_method_option}
onChange={(value) => {
formik.setFieldValue('payment_method_option', value);
}}
isError={Boolean(
formik.touched.payment_method_option &&
formik.errors.payment_method_option
)}
errorMessage={
formik.touched.payment_method_option &&
formik.errors.payment_method_option
? formik.errors.payment_method_option
: ''
}
required
isClearable
/>
<SelectInput
label='Bank'
placeholder='Pilih bank'
options={
isResponseSuccess(bankRawData)
? bankOptions.map((option) => ({
label:
bankRawData.data?.find(
(item) => item.id === option.value
)?.alias +
' - ' +
bankRawData.data?.find(
(item) => item.id === option.value
)?.account_number +
' - ' +
bankRawData.data?.find(
(item) => item.id === option.value
)?.owner,
value: option.value,
}))
: []
}
value={formik.values.bank_id_option}
onChange={(value) => {
formik.setFieldValue('bank_id_option', value);
}}
isLoading={isLoadingBankOptions}
isError={Boolean(
formik.touched.bank_id_option && formik.errors.bank_id_option
)}
errorMessage={
formik.touched.bank_id_option && formik.errors.bank_id_option
? formik.errors.bank_id_option
: ''
}
required
isClearable
/>
<TextInput
label={`Nomor Rekening ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'Pihak'}`}
placeholder={`Masukkan nomor rekening ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'pihak'}`}
name='party_account_number'
value={formik.values.party_account_number}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={Boolean(
formik.touched.party_account_number &&
formik.errors.party_account_number
)}
errorMessage={
formik.touched.party_account_number &&
formik.errors.party_account_number
? formik.errors.party_account_number
: ''
}
required
readOnly
/>
<TextInput
label='Nomor Referensi'
placeholder='Masukkan nomor referensi'
name='reference_number'
value={formik.values.reference_number}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={Boolean(
formik.touched.reference_number &&
formik.errors.reference_number
)}
errorMessage={
formik.touched.reference_number &&
formik.errors.reference_number
? formik.errors.reference_number
: ''
}
required
/>
<NumberInput
label='Nominal'
placeholder='Masukkan nominal'
name='nominal'
value={formik.values.nominal}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={Boolean(formik.touched.nominal && formik.errors.nominal)}
errorMessage={
formik.touched.nominal && formik.errors.nominal
? formik.errors.nominal
: ''
}
required
/>
<TextArea
label='Catatan'
placeholder='Masukkan catatan'
name='notes'
value={formik.values.notes}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={Boolean(formik.touched.notes && formik.errors.notes)}
errorMessage={
formik.touched.notes && formik.errors.notes
? formik.errors.notes
: ''
}
required
/>
<div className='flex justify-center gap-4'>
<Button
type='reset'
color='warning'
className='w-min-24'
onClick={() => formik.resetForm()}
disabled={formik.isSubmitting}
>
Reset
</Button>
<Button
type='submit'
className='w-min-24'
disabled={formik.isSubmitting || !formik.isValid}
>
Submit
</Button>
</div>
</form>
</div>
</section>
</>
);
};
export default FormFinanceAdd;
@@ -0,0 +1,49 @@
import * as Yup from 'yup';
import { OptionType } from '@/components/input/SelectInput';
// Type for form values (includes option objects for SelectInput)
export type InitialBalanceFormValues = {
party_type_option: OptionType | null;
party_id_option: OptionType | null;
bank_id_option: OptionType | null;
reference_number: string;
initial_balance_type_option: OptionType | null;
nominal: string;
note: string;
};
export const InitialBalanceFormSchema = Yup.object().shape({
party_type_option: Yup.mixed()
.nullable()
.test(
'is-valid-option',
'Jenis pihak wajib diisi',
(value) => value !== null && value !== undefined
),
party_id_option: Yup.mixed()
.nullable()
.test(
'is-valid-option',
'Pihak wajib diisi',
(value) => value !== null && value !== undefined
),
bank_id_option: Yup.mixed()
.nullable()
.test(
'is-valid-option',
'Bank wajib diisi',
(value) => value !== null && value !== undefined
),
reference_number: Yup.string().required('Nomor referensi wajib diisi'),
initial_balance_type_option: Yup.mixed()
.nullable()
.test(
'is-valid-option',
'Tipe saldo awal wajib diisi',
(value) => value !== null && value !== undefined
),
nominal: Yup.string().required('Nominal wajib diisi'),
note: Yup.string().required('Catatan wajib diisi'),
});
export const UpdateInitialBalanceFormSchema = InitialBalanceFormSchema;
@@ -0,0 +1,378 @@
'use client';
import Button from '@/components/Button';
import { FormHeader } from '@/components/helper/form/FormHeader';
import NumberInput from '@/components/input/NumberInput';
import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import TextArea from '@/components/input/TextArea';
import TextInput from '@/components/input/TextInput';
import {
InitialBalanceFormSchema,
InitialBalanceFormValues,
} from '@/components/pages/finance/add/initial-balance/FormFinanceAddInitialBalance.schema';
import {
FINANCE_INITIAL_BALANCE_TYPE_OPTIONS,
FINANCE_PARTY_TYPE_OPTIONS,
} from '@/config/constant';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { formatTitleCase } from '@/lib/helper';
import { FinanceApi } from '@/services/api/finance';
import { BankApi, CustomerApi, SupplierApi } from '@/services/api/master-data';
import {
CreateInitialBalance,
Finance,
UpdateInitialBalance,
} from '@/types/api/finance/finance';
import { Bank } from '@/types/api/master-data/bank';
import { Icon } from '@iconify/react';
import { useFormik } from 'formik';
import { useRouter } from 'next/navigation';
import { useCallback, useMemo } from 'react';
import toast from 'react-hot-toast';
interface FormFinanceAddInitialBalanceProps {
type?: 'add' | 'edit';
initialValues?: Finance;
}
const FormFinanceAddInitialBalance = ({
type = 'add',
initialValues,
}: FormFinanceAddInitialBalanceProps) => {
const router = useRouter();
// ===== Formik =====
const formikInitialValues = useMemo((): InitialBalanceFormValues => {
// Type assertion to handle potential initial_balance_type field
const extendedInitialValues = initialValues as Finance & {
initial_balance_type?: string;
};
return {
party_type_option:
FINANCE_PARTY_TYPE_OPTIONS.find(
(option) => option.value === initialValues?.party.type
) || null,
party_id_option: initialValues?.party
? {
label: initialValues.party.name,
value: initialValues.party.id,
}
: null,
bank_id_option: initialValues?.bank
? {
label: initialValues.bank.name,
value: initialValues.bank.id,
}
: null,
reference_number: initialValues?.reference_number || '',
initial_balance_type_option:
(initialValues?.nominal ?? 0) < 0
? FINANCE_INITIAL_BALANCE_TYPE_OPTIONS.find(
(option) => option.value === 'NEGATIVE'
) || null
: FINANCE_INITIAL_BALANCE_TYPE_OPTIONS.find(
(option) => option.value === 'POSITIVE'
) || null,
nominal: initialValues?.nominal?.toString() || '',
note: initialValues?.notes || '',
};
}, [initialValues]);
const formik = useFormik<InitialBalanceFormValues>({
initialValues: formikInitialValues,
validationSchema: InitialBalanceFormSchema,
validateOnChange: true,
validateOnBlur: true,
onSubmit: async (values) => {
const payload = transformFormValuesToPayload(values);
switch (type) {
case 'add':
await createInitialBalance(payload);
break;
case 'edit':
if (initialValues?.id) {
await updateInitialBalance(initialValues.id, payload);
}
break;
}
},
});
// ===== Options =====
const { options: partyOptions, isLoadingOptions: isLoadingPartyOptions } =
useSelect(
formik.values.party_type_option?.value === 'CUSTOMER'
? CustomerApi.basePath
: SupplierApi.basePath,
'id',
'name',
'',
{ limit: 'limit' }
);
const {
options: bankOptions,
rawData: bankRawData,
isLoadingOptions: isLoadingBankOptions,
} = useSelect<Bank>(BankApi.basePath, 'id', 'name', '', { limit: 'limit' });
// ===== Helper Functions =====
const transformFormValuesToPayload = (
values: InitialBalanceFormValues
): CreateInitialBalance => {
return {
party_type: (values.party_type_option?.value as string) || '',
party_id: Number(values.party_id_option?.value) || 0,
bank_id: Number(values.bank_id_option?.value) || 0,
reference_number: values.reference_number,
initial_balance_type:
(values.initial_balance_type_option?.value as string) || '',
nominal: Number(values.nominal.replace(/\D/g, '')) || 0,
note: values.note,
};
};
// ===== Handler =====
const createInitialBalance = useCallback(
async (payload: CreateInitialBalance) => {
const response = await FinanceApi.createInitialBalances(payload);
if (isResponseError(response)) {
toast.error(response.message);
return;
}
toast.success('Saldo awal berhasil ditambahkan');
router.refresh();
router.push('/finance');
},
[router]
);
const updateInitialBalance = useCallback(
async (financeId: number, payload: UpdateInitialBalance) => {
const response = await FinanceApi.updateInitialBalances(
financeId,
payload
);
if (isResponseError(response)) {
toast.error(response.message);
return;
}
toast.success('Saldo awal berhasil diperbarui');
router.refresh();
router.push('/finance');
},
[router]
);
return (
<>
<section className='w-full max-w-xl mx-auto'>
<div className='flex flex-col gap-6 p-6'>
<FormHeader
title={`${type === 'add' ? 'Tambah' : 'Ubah'} Saldo Awal`}
backUrl='/finance'
/>
<form className='flex flex-col gap-4' onSubmit={formik.handleSubmit}>
<SelectInput
label='Jenis Pihak'
placeholder='Pilih jenis pihak'
options={FINANCE_PARTY_TYPE_OPTIONS}
value={formik.values.party_type_option}
onChange={(value) => {
formik.setFieldValue('party_type_option', value);
}}
isError={Boolean(
formik.touched.party_type_option &&
formik.errors.party_type_option
)}
errorMessage={
formik.touched.party_type_option &&
formik.errors.party_type_option
? formik.errors.party_type_option
: ''
}
required
isClearable
/>
<SelectInput
label={
formik.values.party_type_option?.value
? formatTitleCase(
formik.values.party_type_option.value as string
)
: 'Pilih Jenis Pihak Dahulu'
}
placeholder={`Pilih ${formik.values.party_type_option?.value ? formatTitleCase(formik.values.party_type_option.value as string) : 'jenis pihak dahulu'}`}
options={partyOptions}
value={formik.values.party_id_option}
onChange={(value) => {
formik.setFieldValue('party_id_option', value);
}}
isLoading={isLoadingPartyOptions}
isError={Boolean(
formik.touched.party_id_option && formik.errors.party_id_option
)}
errorMessage={
formik.touched.party_id_option && formik.errors.party_id_option
? formik.errors.party_id_option
: ''
}
required
isClearable
isDisabled={!formik.values.party_type_option?.value}
/>
<SelectInput
label='Bank'
placeholder='Pilih bank'
options={
isResponseSuccess(bankRawData)
? bankOptions.map((option) => ({
label:
bankRawData.data?.find(
(item) => item.id === option.value
)?.alias +
' - ' +
bankRawData.data?.find(
(item) => item.id === option.value
)?.account_number +
' - ' +
bankRawData.data?.find(
(item) => item.id === option.value
)?.owner,
value: option.value,
}))
: []
}
value={formik.values.bank_id_option}
onChange={(value) => {
formik.setFieldValue('bank_id_option', value);
}}
isLoading={isLoadingBankOptions}
isError={Boolean(
formik.touched.bank_id_option && formik.errors.bank_id_option
)}
errorMessage={
formik.touched.bank_id_option && formik.errors.bank_id_option
? formik.errors.bank_id_option
: ''
}
required
isClearable
/>
<TextInput
label='Nomor Referensi'
placeholder='Masukkan nomor referensi'
name='reference_number'
value={formik.values.reference_number}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={Boolean(
formik.touched.reference_number &&
formik.errors.reference_number
)}
errorMessage={
formik.touched.reference_number &&
formik.errors.reference_number
? formik.errors.reference_number
: ''
}
required
/>
<SelectInput
label='Tipe Saldo Awal'
placeholder='Pilih tipe saldo awal'
options={FINANCE_INITIAL_BALANCE_TYPE_OPTIONS}
value={formik.values.initial_balance_type_option}
onChange={(value) => {
formik.setFieldValue('initial_balance_type_option', value);
}}
isError={Boolean(
formik.touched.initial_balance_type_option &&
formik.errors.initial_balance_type_option
)}
errorMessage={
formik.touched.initial_balance_type_option &&
formik.errors.initial_balance_type_option
? formik.errors.initial_balance_type_option
: ''
}
required
isClearable
/>
<NumberInput
label='Nominal'
placeholder='Masukkan nominal'
name='nominal'
value={formik.values.nominal}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={Boolean(formik.touched.nominal && formik.errors.nominal)}
errorMessage={
formik.touched.nominal && formik.errors.nominal
? formik.errors.nominal
: ''
}
allowNegative={false}
startAdornment={
formik.values.initial_balance_type_option?.value ===
'POSITIVE' ? (
<Icon icon='mdi:plus' />
) : formik.values.initial_balance_type_option?.value ===
'NEGATIVE' ? (
<Icon icon='mdi:minus' />
) : (
''
)
}
required
/>
<TextArea
label='Catatan'
placeholder='Masukkan catatan'
name='note'
value={formik.values.note}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={Boolean(formik.touched.note && formik.errors.note)}
errorMessage={
formik.touched.note && formik.errors.note
? formik.errors.note
: ''
}
required
/>
<div className='flex justify-center gap-4'>
<Button
type='reset'
color='warning'
className='w-min-24'
onClick={() => formik.resetForm()}
disabled={formik.isSubmitting}
>
Reset
</Button>
<Button
type='submit'
className='w-min-24'
disabled={formik.isSubmitting || !formik.isValid}
>
Submit
</Button>
</div>
</form>
</div>
</section>
</>
);
};
export default FormFinanceAddInitialBalance;
@@ -0,0 +1,25 @@
import { OptionType } from '@/components/input/SelectInput';
import * as Yup from 'yup';
// Type for form values (includes option objects for SelectInput)
export type InjectionFormValues = {
bank_id_option: OptionType | null;
adjustment_date: string;
nominal: string;
note: string;
};
export const InjectionFormSchema = Yup.object<InjectionFormValues>({
bank_id_option: Yup.mixed()
.nullable()
.test(
'is-valid-option',
'Bank wajib diisi',
(value) => value !== null && value !== undefined
),
adjustment_date: Yup.string().required('Tanggal penyesuaian wajib diisi'),
nominal: Yup.string().required('Nominal wajib diisi'),
note: Yup.string().required('Catatan wajib diisi'),
});
export const UpdateInjectionFormSchema = InjectionFormSchema;
@@ -0,0 +1,251 @@
'use client';
import Button from '@/components/Button';
import { FormHeader } from '@/components/helper/form/FormHeader';
import DateInput from '@/components/input/DateInput';
import NumberInput from '@/components/input/NumberInput';
import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import TextArea from '@/components/input/TextArea';
import {
InjectionFormSchema,
InjectionFormValues,
} from '@/components/pages/finance/add/injection/FormFinanceInjection.schema';
import { isResponseError, isResponseSuccess } from '@/lib/api-helper';
import { formatDate } from '@/lib/helper';
import { FinanceApi } from '@/services/api/finance';
import { BankApi } from '@/services/api/master-data';
import {
CreateInjection,
Finance,
UpdateInjection,
} from '@/types/api/finance/finance';
import { Bank } from '@/types/api/master-data/bank';
import { useFormik } from 'formik';
import { useRouter } from 'next/navigation';
import { useCallback, useMemo } from 'react';
import toast from 'react-hot-toast';
interface FormFinanceInjectionProps {
type?: 'add' | 'edit';
initialValues?: Finance;
}
const FormFinanceInjection = ({
type = 'add',
initialValues,
}: FormFinanceInjectionProps) => {
const router = useRouter();
// ===== Formik =====
const formikInitialValues = useMemo((): InjectionFormValues => {
return {
bank_id_option: initialValues?.bank
? {
label: initialValues.bank.name,
value: initialValues.bank.id,
}
: null,
adjustment_date: initialValues?.payment_date || '',
nominal: initialValues?.nominal?.toString() || '',
note: initialValues?.notes || '',
};
}, [initialValues]);
const formik = useFormik<InjectionFormValues>({
initialValues: formikInitialValues,
validationSchema: InjectionFormSchema,
validateOnChange: true,
validateOnBlur: true,
onSubmit: async (values) => {
const payload = transformFormValuesToPayload(values);
switch (type) {
case 'add':
await createInjection(payload);
break;
case 'edit':
if (initialValues?.id) {
await updateInjection(initialValues.id, payload);
}
break;
}
},
});
// ===== Options =====
const {
options: bankOptions,
rawData: bankRawData,
isLoadingOptions: isLoadingBankOptions,
} = useSelect<Bank>(BankApi.basePath, 'id', 'name', '', { limit: 'limit' });
// ===== Helper Functions =====
const transformFormValuesToPayload = (
values: InjectionFormValues
): CreateInjection => {
return {
bank_id: Number(values.bank_id_option?.value) || 0,
adjustment_date: formatDate(values.adjustment_date, 'YYYY-MM-DD'),
nominal: Number(values.nominal.replace(/\D/g, '')) || 0,
notes: values.note,
};
};
// ===== Handler =====
const createInjection = useCallback(
async (payload: CreateInjection) => {
const response = await FinanceApi.createInjections(payload);
if (isResponseError(response)) {
toast.error(response.message);
return;
}
toast.success('Injeksi dana berhasil ditambahkan');
router.refresh();
router.push('/finance');
},
[router]
);
const updateInjection = useCallback(
async (financeId: number, payload: UpdateInjection) => {
const response = await FinanceApi.updateInjections(financeId, payload);
if (isResponseError(response)) {
toast.error(response.message);
return;
}
toast.success('Injeksi dana berhasil diperbarui');
router.refresh();
router.push('/finance');
},
[router]
);
return (
<>
<section className='w-full max-w-xl mx-auto'>
<div className='flex flex-col gap-6 p-6'>
<FormHeader
title={`${type === 'add' ? 'Tambah' : 'Ubah'} Injeksi Dana`}
backUrl='/finance'
/>
<form className='flex flex-col gap-4' onSubmit={formik.handleSubmit}>
<SelectInput
label='Bank'
placeholder='Pilih bank'
options={
isResponseSuccess(bankRawData)
? bankOptions.map((option) => ({
label:
bankRawData.data?.find(
(item) => item.id === option.value
)?.alias +
' - ' +
bankRawData.data?.find(
(item) => item.id === option.value
)?.account_number +
' - ' +
bankRawData.data?.find(
(item) => item.id === option.value
)?.owner,
value: option.value,
}))
: []
}
value={formik.values.bank_id_option}
onChange={(value) => {
formik.setFieldValue('bank_id_option', value);
}}
isLoading={isLoadingBankOptions}
isError={Boolean(
formik.touched.bank_id_option && formik.errors.bank_id_option
)}
errorMessage={
formik.touched.bank_id_option && formik.errors.bank_id_option
? formik.errors.bank_id_option
: ''
}
required
isClearable
/>
<DateInput
label='Tanggal Penyesuaian'
placeholder='Pilih tanggal penyesuaian'
name='adjustment_date'
value={formik.values.adjustment_date}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={Boolean(
formik.touched.adjustment_date && formik.errors.adjustment_date
)}
errorMessage={
formik.touched.adjustment_date && formik.errors.adjustment_date
? formik.errors.adjustment_date
: ''
}
required
/>
<NumberInput
label='Nominal'
placeholder='Masukkan nominal'
name='nominal'
value={formik.values.nominal}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={Boolean(formik.touched.nominal && formik.errors.nominal)}
errorMessage={
formik.touched.nominal && formik.errors.nominal
? formik.errors.nominal
: ''
}
allowNegative={true}
required
/>
<TextArea
label='Catatan'
placeholder='Masukkan catatan'
name='note'
value={formik.values.note}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isError={Boolean(formik.touched.note && formik.errors.note)}
errorMessage={
formik.touched.note && formik.errors.note
? formik.errors.note
: ''
}
required
/>
<div className='flex justify-center gap-4'>
<Button
type='reset'
color='warning'
className='w-min-24'
onClick={() => formik.resetForm()}
disabled={formik.isSubmitting}
>
Reset
</Button>
<Button
type='submit'
className='w-min-24'
disabled={formik.isSubmitting || !formik.isValid}
>
Submit
</Button>
</div>
</form>
</div>
</section>
</>
);
};
export default FormFinanceInjection;
@@ -4,6 +4,7 @@ import Badge from '@/components/Badge';
import Button from '@/components/Button';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import Table from '@/components/Table';
import RequirePermission from '@/components/helper/RequirePermission';
import { ROWS_OPTIONS } from '@/config/constant';
import { isResponseSuccess } from '@/lib/api-helper';
import { cn } from '@/lib/helper';
@@ -78,18 +79,6 @@ const InventoryAdjustmentTable = () => {
year: 'numeric',
}),
},
// {
// id: 'before_quantity',
// header: 'Stok Sebelum',
// accessorFn: (row) =>
// formatNumber(String(row.product_warehouse?.quantity)),
// },
// {
// id: 'after_quantity',
// header: 'Stok Sesudah',
// accessorFn: (row) =>
// formatNumber(String(row.product_warehouse?.quantity)),
// },
{
id: 'quantity',
header: 'Kuantitas',
@@ -175,23 +164,17 @@ const InventoryAdjustmentTable = () => {
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
<div className='w-full flex flex-row'>
<Button
href='/inventory/adjustment/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
{/* <DebouncedTextInput
name='search'
placeholder='Cari Stock Adjustment'
value={tableFilterState.search}
onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }}
/> */}
<RequirePermission permissions='lti.inventory.create'>
<Button
href='/inventory/adjustment/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
</RequirePermission>
</div>
<div className='flex flex-row justify-end'>
@@ -19,6 +19,7 @@ import SelectInput from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission';
const RowOptionsMenu = ({
type = 'dropdown',
@@ -28,15 +29,17 @@ const RowOptionsMenu = ({
props: CellContext<Movement, unknown>;
}) => (
<RowOptionsMenuWrapper type={type}>
<Button
href={`/inventory/movement/detail/?movementId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
<RequirePermission permissions='lti.inventory.transfer.detail'>
<Button
href={`/inventory/movement/detail/?movementId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
</RequirePermission>
</RowOptionsMenuWrapper>
);
@@ -145,15 +148,17 @@ const MovementTable = () => {
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
<div className='w-full flex flex-row gap-2'>
<Button
href='/inventory/movement/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
<RequirePermission permissions='lti.inventory.transfer.create'>
<Button
href='/inventory/movement/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
</RequirePermission>
</div>
<DebouncedTextInput
@@ -7,6 +7,7 @@ import Table from '@/components/Table';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission';
import { ROWS_OPTIONS } from '@/config/constant';
import { isResponseSuccess } from '@/lib/api-helper';
import { cn, formatCurrency, formatNumber } from '@/lib/helper';
@@ -31,15 +32,17 @@ const RowOptionsMenu = ({
props: CellContext<InventoryProduct, unknown>;
}) => (
<RowOptionsMenuWrapper type={type}>
<Button
href={`/inventory/product/detail?inventoryProductId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
<RequirePermission permissions='lti.inventory.product_stock.detail'>
<Button
href={`/inventory/product/detail?inventoryProductId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
</RequirePermission>
</RowOptionsMenuWrapper>
);
@@ -26,6 +26,8 @@ import { useRouter } from 'next/navigation';
import { useCallback, useState } from 'react';
import toast from 'react-hot-toast';
import useSWR from 'swr';
import RequirePermission from '@/components/helper/RequirePermission';
import { useAuth } from '@/services/hooks/useAuth';
const RowsOptionsMenu = ({
type = 'dropdown',
@@ -50,57 +52,71 @@ const RowsOptionsMenu = ({
)}
>
<div className='flex flex-col gap-1'>
<Button
href={`/marketing/detail?marketingId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
{props.row.original.latest_approval.step_number != 1 && (
<RequirePermission permissions='lti.marketing.delivery_order.detail'>
<Button
href={
props.row.original.latest_approval.step_number == 3
? `/marketing/detail/delivery-orders/edit?marketingId=${props.row.original.id}`
: props.row.original.latest_approval.step_number == 2
? `/marketing/add/delivery-orders?marketingId=${props.row.original.id}`
: undefined
}
onClick={() => {
if (props.row.original.latest_approval.step_number == 2) {
deliveryClickHandler?.();
}
}}
href={`/marketing/detail?marketingId=${props.row.original.id}`}
variant='ghost'
color='success'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:truck' width={16} height={16} />
Deliver
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
</RequirePermission>
{props.row.original.latest_approval.step_number != 1 && (
<RequirePermission
permissions={
props.row.original.latest_approval.step_number == 3
? 'lti.marketing.delivery_order.update'
: 'lti.marketing.delivery_order.create'
}
>
<Button
href={
props.row.original.latest_approval.step_number == 3
? `/marketing/detail/delivery-orders/edit?marketingId=${props.row.original.id}`
: props.row.original.latest_approval.step_number == 2
? `/marketing/add/delivery-orders?marketingId=${props.row.original.id}`
: undefined
}
onClick={() => {
if (props.row.original.latest_approval.step_number == 2) {
deliveryClickHandler?.();
}
}}
variant='ghost'
color='success'
className='justify-start text-sm'
>
<Icon icon='mdi:truck' width={16} height={16} />
Deliver
</Button>
</RequirePermission>
)}
{props.row.original.latest_approval.step_number != 3 && (
<Button
href={`/marketing/detail/sales-orders/edit?marketingId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='mdi:pencil-outline' width={16} height={16} />
Edit
</Button>
<RequirePermission permissions='lti.marketing.sales_order.update'>
<Button
href={`/marketing/detail/sales-orders/edit?marketingId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='mdi:pencil-outline' width={16} height={16} />
Edit
</Button>
</RequirePermission>
)}
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit justify-start text-sm'
>
<Icon icon='mdi:delete-outline' width={16} height={16} />
Delete
</Button>
<RequirePermission permissions='lti.marketing.sales_order.delete'>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit justify-start text-sm'
>
<Icon icon='mdi:delete-outline' width={16} height={16} />
Delete
</Button>
</RequirePermission>
</div>
</div>
);
@@ -116,6 +132,7 @@ const MarketingTable = () => {
);
const [selectedItem, setSelectedItem] = useState<Marketing | null>(null);
const [rowSelection, setRowSelection] = useState<Record<string, boolean>>({});
const { permissionCheck } = useAuth();
const router = useRouter();
@@ -270,10 +287,14 @@ const MarketingTable = () => {
<div className='flex flex-col gap-4'>
<div className='flex flex-col gap-2 mb-4'>
<TableToolbar
addButton={{
href: '/marketing/add/sales-orders',
label: 'Tambah Sales Order',
}}
addButton={
permissionCheck('lti.marketing.sales_order.create')
? {
href: '/marketing/add/sales-orders',
label: 'Tambah Sales Order',
}
: undefined
}
search={{
value: search,
onChange: searchChangeHandler,
@@ -281,30 +302,35 @@ const MarketingTable = () => {
}}
/>
<div className='flex flex-row gap-2'>
<Button
color='success'
onClick={approveClickHandler}
className='justify-start text-sm'
disabled={disableApprove}
>
<Icon icon='material-symbols:check' width={24} height={24} />
Approve
</Button>
<RequirePermission permissions='lti.marketing.sales_order.approve'>
<Button
color='success'
onClick={approveClickHandler}
className='justify-start text-sm'
disabled={disableApprove}
>
<Icon icon='material-symbols:check' width={24} height={24} />
Approve
</Button>
</RequirePermission>
<Button
color='error'
onClick={rejectClickHandler}
className='justify-start text-sm'
disabled={disableReject}
>
<Icon icon='material-symbols:close' width={24} height={24} />
Reject
</Button>
<RequirePermission permissions='lti.marketing.sales_order.approve'>
<Button
color='error'
onClick={rejectClickHandler}
className='justify-start text-sm'
disabled={disableReject}
>
<Icon icon='material-symbols:close' width={24} height={24} />
Reject
</Button>
</RequirePermission>
</div>
<TableRowSizeSelector
value={pageSize}
onChange={pageSizeChangeHandler}
options={ROWS_OPTIONS}
className='flex sm:flex-row flex-col gap-3 items-end justify-end'
>
{/* select multiple product */}
<SelectInput
@@ -33,6 +33,7 @@ import { useState } from 'react';
import toast from 'react-hot-toast';
import SalesOrderExport from '@/components/pages/marketing/pdf/SalesOrderExport';
import DeliveryOrderExport from '@/components/pages/marketing/pdf/DeliveryOrderExport';
import RequirePermission from '@/components/helper/RequirePermission';
const MarketingDetail = ({
initialValues,
@@ -134,45 +135,58 @@ const MarketingDetail = ({
<div className='flex-row flex gap-3'>
{initialValues?.latest_approval?.step_number == 1 && (
<>
<Button
color='success'
onClick={approveClickHandler}
disabled={
initialValues?.latest_approval?.step_number == 1 &&
initialValues?.latest_approval?.action == 'REJECTED'
}
>
<Icon icon='mdi:check' width={24} height={24} />
Approve
</Button>
<Button
color='error'
onClick={rejectClickHandler}
disabled={
initialValues?.latest_approval?.step_number == 1 &&
initialValues?.latest_approval?.action == 'REJECTED'
}
>
<Icon icon='mdi:close' width={24} height={24} />
Reject
</Button>
<RequirePermission permissions='lti.marketing.sales_order.approve'>
<Button
color='success'
onClick={approveClickHandler}
disabled={
initialValues?.latest_approval?.step_number == 1 &&
initialValues?.latest_approval?.action == 'REJECTED'
}
>
<Icon icon='mdi:check' width={24} height={24} />
Approve
</Button>
</RequirePermission>
<RequirePermission permissions='lti.marketing.sales_order.approve'>
<Button
color='error'
onClick={rejectClickHandler}
disabled={
initialValues?.latest_approval?.step_number == 1 &&
initialValues?.latest_approval?.action == 'REJECTED'
}
>
<Icon icon='mdi:close' width={24} height={24} />
Reject
</Button>
</RequirePermission>
</>
)}
{initialValues?.latest_approval?.step_number != 1 && (
<Button
color='success'
href={
<RequirePermission
permissions={
initialValues?.latest_approval?.step_number == 3
? `/marketing/detail/delivery-orders/edit?marketingId=${initialValues?.id}`
: `/marketing/add/delivery-orders?marketingId=${initialValues?.id}`
? 'lti.marketing.delivery_order.update'
: 'lti.marketing.delivery_order.create'
}
>
<Icon icon='mdi:truck' width={24} height={24} />
{initialValues?.latest_approval?.step_number == 3
? 'Edit '
: 'Tambah '}
Delivery Order
</Button>
<Button
color='success'
href={
initialValues?.latest_approval?.step_number == 3
? `/marketing/detail/delivery-orders/edit?marketingId=${initialValues?.id}`
: `/marketing/add/delivery-orders?marketingId=${initialValues?.id}`
}
>
<Icon icon='mdi:truck' width={24} height={24} />
{initialValues?.latest_approval?.step_number == 3
? 'Edit '
: 'Tambah '}
Delivery Order
</Button>
</RequirePermission>
)}
</div>
@@ -413,19 +427,23 @@ const MarketingDetail = ({
)}
<div className='flex flex-row gap-3'>
{initialValues?.latest_approval?.step_number != 3 && (
<Button
color='warning'
type='button'
href={`/marketing/detail/${initialValues?.latest_approval.step_number == 3 ? 'delivery-orders' : 'sales-orders'}/edit?marketingId=${initialValues?.id}`}
>
<Icon icon='mdi:pencil' width={24} height={24} />
Edit
</Button>
<RequirePermission permissions='lti.marketing.sales_order.update'>
<Button
color='warning'
type='button'
href={`/marketing/detail/${initialValues?.latest_approval?.step_number == 3 ? 'delivery-orders' : 'sales-orders'}/edit?marketingId=${initialValues?.id}`}
>
<Icon icon='mdi:pencil' width={24} height={24} />
Edit
</Button>
</RequirePermission>
)}
<Button color='error' onClick={deleteClickHandler}>
<Icon icon='mdi:delete' width={24} height={24} />
Hapus
</Button>
<RequirePermission permissions='lti.marketing.sales_order.delete'>
<Button color='error' onClick={deleteClickHandler}>
<Icon icon='mdi:delete' width={24} height={24} />
Hapus
</Button>
</RequirePermission>
</div>
</div>
<ConfirmationModal
@@ -47,6 +47,7 @@ import DeliveryOrderProductTable from '@/components/pages/marketing/form/table-v
import DeliveryOrderProductForm from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct';
import { SalesOrderProductFormValues } from '@/components/pages/marketing/form/repeater/sales-order/SalesOrderProduct.schema';
import { DeliveryOrderProductFormValues } from '@/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.schema';
import RequirePermission from '@/components/helper/RequirePermission';
const MemoizedSalesOrderProductTable = memo(SalesOrderProductTable);
const MemoizedSalesOrderProductForm = memo(SalesOrderProductForm);
@@ -575,7 +576,7 @@ const MarketingForm = ({
wrapper: 'bg-white w-full',
}}
>
<div className='grid grid-cols-2 gap-3 mt-3'>
<div className='grid sm:grid-cols-2 gap-3 mt-3'>
<SelectInput
label='Pelanggan'
options={customerOptions}
@@ -650,7 +651,7 @@ const MarketingForm = ({
)}
{/* Input Notes */}
<div className='grid grid-cols-2 gap-3'>
<div className='grid sm:grid-cols-2 gap-3'>
<DebouncedTextArea
required
name='notes'
@@ -689,15 +690,17 @@ const MarketingForm = ({
{/* Actions button */}
{formType == 'edit' && (
<div className='flex flex-row justify-start'>
<Button
type='button'
color='error'
onClick={handleDelete}
isLoading={isLoading}
>
<Icon icon='mdi:trash' width={24} height={24} />
Hapus
</Button>
<RequirePermission permissions='lti.marketing.sales_order.delete'>
<Button
type='button'
color='error'
onClick={handleDelete}
isLoading={isLoading}
>
<Icon icon='mdi:trash' width={24} height={24} />
Hapus
</Button>
</RequirePermission>
</div>
)}
@@ -174,26 +174,13 @@ const DeliveryOrderProductForm = ({
}}
onReset={handleResetForm}
>
{/* <small className='block text-blue-500'>
{JSON.stringify(exisitingValues)}
</small>
<small className='block text-emerald-500'>
{JSON.stringify(formik.values)}
</small> */}
{/* <small className='block text-red-500'>
{JSON.stringify(formik.errors)}
</small>
<div className='hidden'>
{JSON.stringify(formik.values.marketing_product)}
</div> */}
{formikErrorMessage && (
<div onClick={() => setFormErrorMessage('')} className='my-3 w-full'>
<Alert color='error'>{formikErrorMessage}</Alert>
</div>
)}
<div className='grid grid-cols-2 gap-4'>
<div className='grid sm:grid-cols-2 gap-4'>
<SelectInput
options={options}
label='Produk'
@@ -11,7 +11,7 @@ import SelectInput, {
useSelect,
} from '@/components/input/SelectInput';
import { Kandang } from '@/types/api/master-data/kandang';
import { KandangApi } from '@/services/api/master-data';
import { KandangApi, WarehouseApi } from '@/services/api/master-data';
import { ProductWarehouse } from '@/types/api/inventory/product-warehouse';
import { ProductWarehouseApi } from '@/services/api/inventory';
import NumberInput from '@/components/input/NumberInput';
@@ -61,7 +61,7 @@ const SalesOrderProductForm = ({
const {
options: kandangSourceOptions,
isLoadingOptions: isLoadingKandangSourceOptions,
} = useSelect<Kandang>(KandangApi.basePath, 'id', 'name');
} = useSelect<Kandang>(WarehouseApi.basePath, 'id', 'name');
const {
options: warehouseSourceOptions,
@@ -183,7 +183,7 @@ const SalesOrderProductForm = ({
{/* <small className='block text-rose-500'>
{JSON.stringify(formik.errors)}
</small> */}
<div className='grid grid-cols-2 gap-4 z-200'>
<div className='grid sm:grid-cols-2 gap-4 z-200'>
<PatternInput
name='vehicle_number'
label='No. Polisi'
@@ -15,6 +15,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission';
import { Area } from '@/types/api/master-data/area';
import { AreaApi } from '@/services/api/master-data';
@@ -34,40 +35,46 @@ const RowOptionsMenu = ({
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<Button
href={`/master-data/area/detail/?areaId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
<Button
href={`/master-data/area/detail/edit/?areaId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
<RequirePermission permissions='lti.master.area.detail'>
<Button
href={`/master-data/area/detail/?areaId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
/>
Delete
</Button>
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.area.update'>
<Button
href={`/master-data/area/detail/edit/?areaId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.area.delete'>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button>
</RequirePermission>
</RowOptionsMenuWrapper>
);
};
@@ -192,15 +199,19 @@ const AreasTable = () => {
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
<div className='w-full flex flex-row'>
<Button
href='/master-data/area/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
<div className='w-full flex flex-row'>
<RequirePermission permissions='lti.master.area.create'>
<Button
href='/master-data/area/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
</RequirePermission>
</div>
</div>
<DebouncedTextInput
@@ -10,6 +10,7 @@ import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import RequirePermission from '@/components/helper/RequirePermission';
import {
AreaFormSchema,
@@ -160,36 +161,40 @@ const AreaForm = ({ type = 'add', initialValues }: AreaFormProps) => {
<div className='flex flex-row justify-between gap-2 flex-wrap'>
{type !== 'add' && (
<div className='flex flex-row justify-start gap-2'>
<Button
type='button'
color='error'
onClick={deleteAreaClickHandler}
className='px-4'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Delete
</Button>
{type !== 'edit' && (
<RequirePermission permissions='lti.master.area.delete'>
<Button
type='button'
color='warning'
href={`/master-data/area/detail/edit/?areaId=${initialValues?.id}`}
color='error'
onClick={deleteAreaClickHandler}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
Delete
</Button>
</RequirePermission>
{type !== 'edit' && (
<RequirePermission permissions='lti.master.area.update'>
<Button
type='button'
color='warning'
href={`/master-data/area/detail/edit/?areaId=${initialValues?.id}`}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
</Button>
</RequirePermission>
)}
</div>
)}
@@ -15,6 +15,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission';
import { Bank } from '@/types/api/master-data/bank';
import { BankApi } from '@/services/api/master-data';
@@ -34,40 +35,46 @@ const RowOptionsMenu = ({
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<Button
href={`/master-data/bank/detail/?bankId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
<Button
href={`/master-data/bank/detail/edit/?bankId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
<RequirePermission permissions='lti.master.banks.detail'>
<Button
href={`/master-data/bank/detail/?bankId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
/>
Delete
</Button>
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.banks.update'>
<Button
href={`/master-data/bank/detail/edit/?bankId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.banks.delete'>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button>
</RequirePermission>
</RowOptionsMenuWrapper>
);
};
@@ -205,15 +212,17 @@ const BanksTable = () => {
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
<div className='w-full flex flex-row'>
<Button
href='/master-data/bank/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
<RequirePermission permissions='lti.master.banks.create'>
<Button
href='/master-data/bank/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
</RequirePermission>
</div>
<DebouncedTextInput
@@ -10,6 +10,7 @@ import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import RequirePermission from '@/components/helper/RequirePermission';
import {
BankFormSchema,
@@ -208,36 +209,40 @@ const BankForm = ({ type = 'add', initialValues }: BankFormProps) => {
<div className='flex flex-row justify-between gap-2 flex-wrap'>
{type !== 'add' && (
<div className='flex flex-row justify-start gap-2'>
<Button
type='button'
color='error'
onClick={deleteBankClickHandler}
className='px-4'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Delete
</Button>
{type !== 'edit' && (
<RequirePermission permissions='lti.master.banks.delete'>
<Button
type='button'
color='warning'
href={`/master-data/bank/detail/edit/?bankId=${initialValues?.id}`}
color='error'
onClick={deleteBankClickHandler}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
Delete
</Button>
</RequirePermission>
{type !== 'edit' && (
<RequirePermission permissions='lti.master.banks.update'>
<Button
type='button'
color='warning'
href={`/master-data/bank/detail/edit/?bankId=${initialValues?.id}`}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
</Button>
</RequirePermission>
)}
</div>
)}
@@ -9,6 +9,7 @@ import Table from '@/components/Table';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission';
import { ROWS_OPTIONS } from '@/config/constant';
import { isResponseSuccess } from '@/lib/api-helper';
import { cn } from '@/lib/helper';
@@ -32,38 +33,44 @@ const RowOptionsMenu = ({
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<Button
href={`/master-data/customer/detail/?customerId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
<Button
href={`/master-data/customer/detail/edit/?customerId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
<RequirePermission permissions='lti.master.customer.detail'>
<Button
href={`/master-data/customer/detail/?customerId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
/>
Delete
</Button>
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.customer.update'>
<Button
href={`/master-data/customer/detail/edit/?customerId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.customer.delete'>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button>
</RequirePermission>
</RowOptionsMenuWrapper>
);
};
@@ -200,15 +207,17 @@ const CustomersTable = () => {
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
<div className='w-full flex flex-row'>
<Button
href='/master-data/customer/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
<RequirePermission permissions='lti.master.customer.create'>
<Button
href='/master-data/customer/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
</RequirePermission>
</div>
<DebouncedTextInput
@@ -27,6 +27,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
import useSWR from 'swr';
import { UserApi } from '@/services/api/user';
import { TYPE_OPTIONS } from '@/config/constant';
import RequirePermission from '@/components/helper/RequirePermission';
interface CustomerFormProps {
formType?: 'add' | 'edit' | 'detail';
@@ -319,36 +320,40 @@ const CustomerForm = ({
<div className='flex flex-row justify-between gap-2 flex-wrap'>
{formType !== 'add' && (
<div className='flex flex-row justify-start gap-2'>
<Button
type='button'
color='error'
onClick={deleteCustomerHandler}
className='px-4'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Delete
</Button>
{formType !== 'edit' && (
<RequirePermission permissions='lti.master.customer.delete'>
<Button
type='button'
color='warning'
href={`/master-data/customer/detail/edit/?customerId=${initialValues?.id}`}
color='error'
onClick={deleteCustomerHandler}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
Delete
</Button>
</RequirePermission>
{formType !== 'edit' && (
<RequirePermission permissions='lti.master.customer.update'>
<Button
type='button'
color='warning'
href={`/master-data/customer/detail/edit/?customerId=${initialValues?.id}`}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
</Button>
</RequirePermission>
)}
</div>
)}
@@ -15,6 +15,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission';
import { Fcr } from '@/types/api/master-data/fcr';
import { FcrApi } from '@/services/api/master-data';
@@ -34,40 +35,46 @@ const RowOptionsMenu = ({
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<Button
href={`/master-data/fcr/detail/?fcrId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
<Button
href={`/master-data/fcr/detail/edit/?fcrId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
<RequirePermission permissions='lti.master.fcr.detail'>
<Button
href={`/master-data/fcr/detail/?fcrId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
/>
Delete
</Button>
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.fcr.update'>
<Button
href={`/master-data/fcr/detail/edit/?fcrId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.fcr.delete'>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button>
</RequirePermission>
</RowOptionsMenuWrapper>
);
};
@@ -192,15 +199,17 @@ const FcrsTable = () => {
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
<div className='w-full flex flex-row'>
<Button
href='/master-data/fcr/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
<RequirePermission permissions='lti.master.fcr.create'>
<Button
href='/master-data/fcr/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
</RequirePermission>
</div>
<DebouncedTextInput
@@ -10,6 +10,7 @@ import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import RequirePermission from '@/components/helper/RequirePermission';
import {
FcrFormSchema,
@@ -296,36 +297,40 @@ const FcrForm = ({ type = 'add', initialValues }: FcrFormProps) => {
<div className='flex flex-row justify-between gap-2 flex-wrap'>
{type !== 'add' && (
<div className='flex flex-row justify-start gap-2'>
<Button
type='button'
color='error'
onClick={deleteFcrClickHandler}
className='px-4'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Delete
</Button>
{type !== 'edit' && (
<RequirePermission permissions='lti.master.fcr.delete'>
<Button
type='button'
color='warning'
href={`/master-data/fcr/detail/edit/?fcrId=${initialValues?.id}`}
color='error'
onClick={deleteFcrClickHandler}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
Delete
</Button>
</RequirePermission>
{type !== 'edit' && (
<RequirePermission permissions='lti.master.fcr.update'>
<Button
type='button'
color='warning'
href={`/master-data/fcr/detail/edit/?fcrId=${initialValues?.id}`}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
</Button>
</RequirePermission>
)}
</div>
)}
@@ -13,6 +13,7 @@ import { useModal } from '@/components/Modal';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission';
import toast from 'react-hot-toast';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
@@ -32,48 +33,54 @@ const RowsOptions = ({
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<Button
href={`/master-data/flock/detail/edit/?flockId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon
icon='material-symbols:edit-outline'
width={16}
height={16}
<RequirePermission permissions='lti.master.flocks.update'>
<Button
href={`/master-data/flock/detail/edit/?flockId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
/>
Edit
</Button>
<Button
href={`/master-data/flock/detail/?flockId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon
icon='mdi:eye-outline'
width={16}
height={16}
>
<Icon
icon='material-symbols:edit-outline'
width={16}
height={16}
className='justify-start text-sm'
/>
Edit
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.flocks.detail'>
<Button
href={`/master-data/flock/detail/?flockId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
/>
Detail
</Button>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button>
>
<Icon
icon='mdi:eye-outline'
width={16}
height={16}
className='justify-start text-sm'
/>
Detail
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.flocks.delete'>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button>
</RequirePermission>
</RowOptionsMenuWrapper>
);
};
@@ -196,15 +203,17 @@ const FlockTable = () => {
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
<div className='w-full flex flex-row'>
<Button
href='/master-data/flock/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
<RequirePermission permissions='lti.master.flocks.create'>
<Button
href='/master-data/flock/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
</RequirePermission>
</div>
<DebouncedTextInput
@@ -16,6 +16,7 @@ import { Icon } from '@iconify/react';
import TextInput from '@/components/input/TextInput';
import { cn } from '@/lib/helper';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import RequirePermission from '@/components/helper/RequirePermission';
interface FlockCustomProps {
formType?: 'add' | 'edit' | 'detail';
@@ -130,35 +131,39 @@ const FlockForm = ({ formType = 'add', initialValues }: FlockCustomProps) => {
<div className='flex flex-row justify-between gap-2 flex-wrap'>
{formType !== 'add' && (
<div className='flex flex-row justify-start gap-2'>
<Button
type='button'
color='error'
onClick={() => deleteModal.openModal()}
className='px-4'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Delete
</Button>
{formType !== 'edit' && (
<RequirePermission permissions='lti.master.flocks.delete'>
<Button
type='button'
color='warning'
href={`/master-data/flock/detail/edit/?flockId=${initialValues?.id}`}
color='error'
onClick={() => deleteModal.openModal()}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
Delete
</Button>
</RequirePermission>
{formType !== 'edit' && (
<RequirePermission permissions='lti.master.flocks.update'>
<Button
type='button'
color='warning'
href={`/master-data/flock/detail/edit/?flockId=${initialValues?.id}`}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
</Button>
</RequirePermission>
)}
</div>
)}
@@ -20,6 +20,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission';
import { Kandang } from '@/types/api/master-data/kandang';
import { KandangApi } from '@/services/api/master-data';
@@ -39,40 +40,46 @@ const RowOptionsMenu = ({
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<Button
href={`/master-data/kandang/detail/?kandangId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
<Button
href={`/master-data/kandang/detail/edit/?kandangId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
<RequirePermission permissions='lti.master.kandangs.detail'>
<Button
href={`/master-data/kandang/detail/?kandangId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
/>
Delete
</Button>
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.kandangs.update'>
<Button
href={`/master-data/kandang/detail/edit/?kandangId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.kandangs.delete'>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button>
</RequirePermission>
</RowOptionsMenuWrapper>
);
};
@@ -243,15 +250,19 @@ const KandangsTable = () => {
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
<div className='w-full flex flex-row'>
<Button
href='/master-data/kandang/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
<div className='w-full flex flex-row'>
<RequirePermission permissions='lti.master.kandangs.create'>
<Button
href='/master-data/kandang/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
</RequirePermission>
</div>
</div>
<DebouncedTextInput
@@ -12,6 +12,7 @@ import TextInput from '@/components/input/TextInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import RequirePermission from '@/components/helper/RequirePermission';
import {
KandangFormSchema,
@@ -285,36 +286,40 @@ const KandangForm = ({ type = 'add', initialValues }: KandangFormProps) => {
<div className='flex flex-row justify-between gap-2 flex-wrap'>
{type !== 'add' && (
<div className='flex flex-row justify-start gap-2'>
<Button
type='button'
color='error'
onClick={deleteKandangClickHandler}
className='px-4'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Delete
</Button>
{type !== 'edit' && (
<RequirePermission permissions='lti.master.kandangs.delete'>
<Button
type='button'
color='warning'
href={`/master-data/kandang/detail/edit/?kandangId=${initialValues?.id}`}
color='error'
onClick={deleteKandangClickHandler}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
Delete
</Button>
</RequirePermission>
{type !== 'edit' && (
<RequirePermission permissions='lti.master.kandangs.update'>
<Button
type='button'
color='warning'
href={`/master-data/kandang/detail/edit/?kandangId=${initialValues?.id}`}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
</Button>
</RequirePermission>
)}
</div>
)}
@@ -20,6 +20,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission';
import { Location } from '@/types/api/master-data/location';
import { LocationApi } from '@/services/api/master-data';
@@ -39,40 +40,46 @@ const RowOptionsMenu = ({
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<Button
href={`/master-data/location/detail/?locationId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
<Button
href={`/master-data/location/detail/edit/?locationId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
<RequirePermission permissions='lti.master.locations.detail'>
<Button
href={`/master-data/location/detail/?locationId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
/>
Delete
</Button>
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.locations.update'>
<Button
href={`/master-data/location/detail/edit/?locationId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.locations.delete'>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button>
</RequirePermission>
</RowOptionsMenuWrapper>
);
};
@@ -230,15 +237,19 @@ const LocationsTable = () => {
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
<div className='w-full flex flex-row'>
<Button
href='/master-data/location/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
<div className='w-full flex flex-row'>
<RequirePermission permissions='lti.master.locations.create'>
<Button
href='/master-data/location/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
</RequirePermission>
</div>
</div>
<DebouncedTextInput
@@ -12,6 +12,7 @@ import TextInput from '@/components/input/TextInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import RequirePermission from '@/components/helper/RequirePermission';
import {
LocationFormSchema,
@@ -229,36 +230,40 @@ const LocationForm = ({ type = 'add', initialValues }: LocationFormProps) => {
<div className='flex flex-row justify-between gap-2 flex-wrap'>
{type !== 'add' && (
<div className='flex flex-row justify-start gap-2'>
<Button
type='button'
color='error'
onClick={deleteLocationClickHandler}
className='px-4'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Delete
</Button>
{type !== 'edit' && (
<RequirePermission permissions='lti.master.locations.delete'>
<Button
type='button'
color='warning'
href={`/master-data/location/detail/edit/?locationId=${initialValues?.id}`}
color='error'
onClick={deleteLocationClickHandler}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
Delete
</Button>
</RequirePermission>
{type !== 'edit' && (
<RequirePermission permissions='lti.master.locations.update'>
<Button
type='button'
color='warning'
href={`/master-data/location/detail/edit/?locationId=${initialValues?.id}`}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
</Button>
</RequirePermission>
)}
</div>
)}
@@ -20,6 +20,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission';
import { Nonstock } from '@/types/api/master-data/nonstock';
import { NonstockApi } from '@/services/api/master-data';
@@ -39,40 +40,46 @@ const RowOptionsMenu = ({
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<Button
href={`/master-data/nonstock/detail/?nonstockId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
<Button
href={`/master-data/nonstock/detail/edit/?nonstockId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
<RequirePermission permissions='lti.master.nonstocks.detail'>
<Button
href={`/master-data/nonstock/detail/?nonstockId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
/>
Delete
</Button>
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.nonstocks.update'>
<Button
href={`/master-data/nonstock/detail/edit/?nonstockId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.nonstocks.delete'>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button>
</RequirePermission>
</RowOptionsMenuWrapper>
);
};
@@ -242,15 +249,17 @@ const NonstocksTable = () => {
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
<div className='w-full flex flex-row'>
<Button
href='/master-data/nonstock/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
<RequirePermission permissions='lti.master.nonstocks.create'>
<Button
href='/master-data/nonstock/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
</RequirePermission>
</div>
<DebouncedTextInput
@@ -12,6 +12,7 @@ import TextInput from '@/components/input/TextInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import RequirePermission from '@/components/helper/RequirePermission';
import {
NonstockFormSchema,
@@ -78,14 +79,14 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
uomId: initialValues?.uom_id ?? 0,
uom: initialValues?.uom
? {
value: initialValues?.uom.id,
label: initialValues?.uom.name,
value: initialValues?.uom?.id,
label: initialValues?.uom?.name,
}
: null,
supplierIds:
initialValues?.suppliers.map((supplier) => supplier.id) ?? [],
initialValues?.suppliers?.map((supplier) => supplier.id) ?? [],
suppliers:
initialValues?.suppliers.map((supplier) => ({
initialValues?.suppliers?.map((supplier) => ({
value: supplier.id,
label: supplier.name,
})) ?? [],
@@ -298,36 +299,40 @@ const NonstockForm = ({ type = 'add', initialValues }: NonstockFormProps) => {
<div className='flex flex-row justify-between gap-2 flex-wrap'>
{type !== 'add' && (
<div className='flex flex-row justify-start gap-2'>
<Button
type='button'
color='error'
onClick={deleteNonstockClickHandler}
className='px-4'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Delete
</Button>
{type !== 'edit' && (
<RequirePermission permissions='lti.master.nonstocks.delete'>
<Button
type='button'
color='warning'
href={`/master-data/nonstock/detail/edit/?nonstockId=${initialValues?.id}`}
color='error'
onClick={deleteNonstockClickHandler}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
Delete
</Button>
</RequirePermission>
{type !== 'edit' && (
<RequirePermission permissions='lti.master.nonstocks.update'>
<Button
type='button'
color='warning'
href={`/master-data/nonstock/detail/edit/?nonstockId=${initialValues?.id}`}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
</Button>
</RequirePermission>
)}
</div>
)}
@@ -15,6 +15,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission';
import { ProductCategory } from '@/types/api/master-data/product-category';
import { ProductCategoryApi } from '@/services/api/master-data';
@@ -34,38 +35,46 @@ const RowOptionsMenu = ({
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<Button
href={`/master-data/product-category/detail/?productCategoryId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
<Button
href={`/master-data/product-category/detail/edit/?productCategoryId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='mdi:pencil-outline' width={16} height={16} />
Edit
</Button>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='mdi:delete-outline'
width={16}
height={16}
<RequirePermission permissions='lti.master.product_categories.detail'>
<Button
href={`/master-data/product-category/detail/?productCategoryId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
/>
Delete
</Button>
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.product_categories.update'>
<Button
href={`/master-data/product-category/detail/edit/?productCategoryId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='mdi:pencil-outline' width={16} height={16} />
Edit
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.product_categories.delete'>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='mdi:delete-outline'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button>
</RequirePermission>
</RowOptionsMenuWrapper>
);
};
@@ -193,15 +202,17 @@ const ProductCategoryTable = () => {
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
<div className='w-full flex flex-row'>
<Button
href='/master-data/product-category/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
<RequirePermission permissions='lti.master.product_categories.create'>
<Button
href='/master-data/product-category/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
</RequirePermission>
</div>
<DebouncedTextInput
name='search'
@@ -10,6 +10,7 @@ import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import RequirePermission from '@/components/helper/RequirePermission';
import {
ProductCategoryFormSchema,
@@ -183,36 +184,40 @@ const ProductCategoryForm = ({
<div className='flex flex-row justify-between gap-2 flex-wrap'>
{type !== 'add' && (
<div className='flex flex-row justify-start gap-2'>
<Button
type='button'
color='error'
onClick={deleteProductCategoryClickHandler}
className='px-4'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Delete
</Button>
{type !== 'edit' && (
<RequirePermission permissions='lti.master.product_categories.delete'>
<Button
type='button'
color='warning'
href={`/master-data/product-category/detail/edit/?productCategoryId=${initialValues?.id}`}
color='error'
onClick={deleteProductCategoryClickHandler}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
Delete
</Button>
</RequirePermission>
{type !== 'edit' && (
<RequirePermission permissions='lti.master.product_categories.update'>
<Button
type='button'
color='warning'
href={`/master-data/product-category/detail/edit/?productCategoryId=${initialValues?.id}`}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
</Button>
</RequirePermission>
)}
</div>
)}
@@ -20,6 +20,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission';
import { Product } from '@/types/api/master-data/product';
import { ProductApi } from '@/services/api/master-data';
@@ -38,38 +39,44 @@ const RowOptionsMenu = ({
deleteClickHandler: () => void;
}) => (
<RowOptionsMenuWrapper type={type}>
<Button
href={`/master-data/product/detail/?productId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
<Button
href={`/master-data/product/detail/edit/?productId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
<RequirePermission permissions='lti.master.products.detail'>
<Button
href={`/master-data/product/detail/?productId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
/>
Delete
</Button>
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.products.update'>
<Button
href={`/master-data/product/detail/edit/?productId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.products.delete'>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button>
</RequirePermission>
</RowOptionsMenuWrapper>
);
@@ -273,15 +280,17 @@ const ProductsTable = () => {
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
<div className='w-full flex flex-row'>
<Button
href='/master-data/product/add'
variant='outline'
className='w-full sm:w-fit'
color='primary'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
<RequirePermission permissions='lti.master.products.create'>
<Button
href='/master-data/product/add'
variant='outline'
className='w-full sm:w-fit'
color='primary'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
</RequirePermission>
</div>
<DebouncedTextInput
name='search'
@@ -16,6 +16,7 @@ import SelectInput, {
} from '@/components/input/SelectInput';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import RequirePermission from '@/components/helper/RequirePermission';
import {
ProductFormSchema,
@@ -413,35 +414,39 @@ const ProductForm = ({ type = 'add', initialValues }: ProductFormProps) => {
<div className='flex flex-row justify-between gap-2 flex-wrap'>
{type !== 'add' && (
<div className='flex flex-row justify-start gap-2'>
<Button
type='button'
color='error'
onClick={deleteProductClickHandler}
className='px-4'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Delete
</Button>
{type !== 'edit' && (
<RequirePermission permissions='lti.master.products.delete'>
<Button
type='button'
color='warning'
href={`/master-data/product/detail/edit/?productId=${initialValues?.id}`}
color='error'
onClick={deleteProductClickHandler}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
Delete
</Button>
</RequirePermission>
{type !== 'edit' && (
<RequirePermission permissions='lti.master.products.update'>
<Button
type='button'
color='warning'
href={`/master-data/product/detail/edit/?productId=${initialValues?.id}`}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
</Button>
</RequirePermission>
)}
</div>
)}
@@ -0,0 +1,217 @@
'use client';
import Button from '@/components/Button';
import { FormHeader } from '@/components/helper/form/FormHeader';
import Table, { TABLE_DEFAULT_STYLING } from '@/components/Table';
import { ProductionStandard } from '@/types/api/master-data/production-standard';
import { Icon } from '@iconify/react';
import useSWR from 'swr';
import { ProductionStandardApi } from '@/services/api/master-data';
import { isResponseSuccess } from '@/lib/api-helper';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import { CellContext } from '@tanstack/react-table';
import { useModal } from '@/components/Modal';
import { useState } from 'react';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import toast from 'react-hot-toast';
import { cn } from '@/lib/helper';
import RequirePermission from '@/components/helper/RequirePermission';
const RowOptionsMenu = ({
type = 'dropdown',
props,
deleteClickHandler,
}: {
type: 'dropdown' | 'collapse';
props: CellContext<ProductionStandard, unknown>;
deleteClickHandler: () => void;
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<RequirePermission permissions='lti.master.production_standards.detail'>
<Button
href={`/master-data/production-standard/detail/?productionStandardId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.production_standards.update'>
<Button
href={`/master-data/production-standard/detail/edit/?productionStandardId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.production_standards.delete'>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button>
</RequirePermission>
</RowOptionsMenuWrapper>
);
};
const ProductionStandardTable = () => {
const deleteModal = useModal();
const [selectedProductionStandard, setSelectedProductionStandard] = useState<
ProductionStandard | undefined
>(undefined);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const {
data: productionStandards,
isLoading: productionStandardsLoading,
mutate: refreshProductionStandards,
} = useSWR(
`${ProductionStandardApi.basePath}`,
ProductionStandardApi.getAllFetcher
);
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
await ProductionStandardApi.delete(
selectedProductionStandard?.id as number
);
refreshProductionStandards();
deleteModal.closeModal();
toast.success('Successfully delete Production Standard!');
setIsDeleteLoading(false);
};
return (
<>
<div className='flex flex-col gap-6 p-6'>
<div className='flex flex-row gap-6 justify-start'>
<RequirePermission permissions='lti.master.production_standards.create'>
<Button
href='/master-data/production-standard/add'
variant='outline'
>
<Icon icon='mdi:plus' /> Tambah
</Button>
</RequirePermission>
</div>
<RequirePermission permissions='lti.master.production_standards.list'>
<Table<ProductionStandard>
data={
isResponseSuccess(productionStandards)
? productionStandards.data
: []
}
columns={[
{
header: 'No',
accessorFn: (row, index) => index + 1,
},
{
header: 'Nama',
accessorKey: 'name',
},
{
header: 'Kategori',
accessorFn: (row) => row.project_category,
},
{
header: 'Aksi',
cell: (props) => {
const currentPageSize =
props.table.getPaginationRowModel().rows.length;
const currentPageRows =
props.table.getPaginationRowModel().flatRows;
const currentRowRelativeIndex =
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
const isLast2Rows =
currentRowRelativeIndex > currentPageSize - 2;
const deleteClickHandler = () => {
setSelectedProductionStandard(props.row.original);
deleteModal.openModal();
};
return (
<>
{currentPageSize > 2 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
/>
</RowCollapseOptions>
)}
</>
);
},
},
]}
className={{
headerColumnClassName: cn(
TABLE_DEFAULT_STYLING.headerColumnClassName,
'last:flex last:flex-row last:justify-end'
),
bodyColumnClassName: cn(
TABLE_DEFAULT_STYLING.bodyColumnClassName,
'last:flex last:flex-row last:justify-end'
),
}}
/>
</RequirePermission>
</div>
<RequirePermission permissions='lti.master.production_standards.delete'>
<ConfirmationModal
ref={deleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data Production Standard ini (${selectedProductionStandard?.name})?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
color: 'error',
isLoading: isDeleteLoading,
onClick: confirmationModalDeleteClickHandler,
}}
/>
</RequirePermission>
</>
);
};
export default ProductionStandardTable;
@@ -0,0 +1,95 @@
import * as Yup from 'yup';
// Schema for LAYING category (production_standard_details is required)
const LayingRepeaterFormSchema = Yup.object({
week: Yup.number().required('Minggu wajib diisi!'),
production_standard_uniformity_details: Yup.object({
target_mean_bw: Yup.number().required('Berat rata-rata wajib diisi!'),
max_depletion: Yup.number().required('Maksimal depletion wajib diisi!'),
min_uniformity: Yup.number().required('Minimal uniformitas wajib diisi!'),
feed_intake: Yup.number().required('Pengambilan makanan wajib diisi!'),
}),
production_standard_details: Yup.object({
target_hen_day_production: Yup.number().required(
'Produksi telur per hari wajib diisi!'
),
target_hen_house_production: Yup.number().required(
'Produksi telur per kandang wajib diisi!'
),
target_egg_weight: Yup.number().required('Berat telur wajib diisi!'),
target_egg_mass: Yup.number().required('Massa telur wajib diisi!'),
standard_fcr: Yup.number().required('FCR wajib diisi!'),
}).required(),
});
// Schema for GROWING category (production_standard_details is optional)
const GrowingRepeaterFormSchema = Yup.object({
week: Yup.number().required('Minggu wajib diisi!'),
production_standard_uniformity_details: Yup.object({
target_mean_bw: Yup.number().required('Berat rata-rata wajib diisi!'),
max_depletion: Yup.number().required('Maksimal depletion wajib diisi!'),
min_uniformity: Yup.number().required('Minimal uniformitas wajib diisi!'),
feed_intake: Yup.number().required('Pengambilan makanan wajib diisi!'),
}),
production_standard_details: Yup.object({
target_hen_day_production: Yup.number().optional(),
target_hen_house_production: Yup.number().optional(),
target_egg_weight: Yup.number().optional(),
target_egg_mass: Yup.number().optional(),
standard_fcr: Yup.number().optional(),
}).optional(),
});
// Explicit types for better type inference
export type LayingRepeaterFormValues = Yup.InferType<
typeof LayingRepeaterFormSchema
>;
export type GrowingRepeaterFormValues = Yup.InferType<
typeof GrowingRepeaterFormSchema
>;
// Union type for repeater form values
export type ProductionStandardRepeaterFormSchemaValues =
| LayingRepeaterFormValues
| GrowingRepeaterFormValues;
// Dynamic schema factory for repeater form based on project category
export const createProductionStandardRepeaterFormSchema = (
category: string
) => {
// For LAYING category, production_standard_details is required
if (category === 'LAYING') {
return LayingRepeaterFormSchema;
}
// For GROWING category, production_standard_details is optional
return GrowingRepeaterFormSchema;
};
// Dynamic schema factory for main form based on project category
export const createProductionStandardFormSchema = (category: string) => {
return Yup.object({
name: Yup.string().required('Nama wajib diisi!'),
project_category: Yup.string()
.min(1, 'Kategori proyek wajib diisi!')
.required('Kategori proyek wajib diisi!'),
details: Yup.array().of(
createProductionStandardRepeaterFormSchema(category)
),
});
};
// Static schemas for backward compatibility (default to LAYING)
export const ProductionStandardFormSchema =
createProductionStandardFormSchema('LAYING');
export const UpdateProductionStandardFormSchema = ProductionStandardFormSchema;
export type ProductionStandardFormValues = Yup.InferType<
typeof ProductionStandardFormSchema
>;
export const ProductionStandardRepeaterFormSchema = LayingRepeaterFormSchema;
export const UpdateProductionStandardRepeaterFormSchema =
ProductionStandardRepeaterFormSchema;
@@ -9,6 +9,7 @@ import Table from '@/components/Table';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission';
import { ROWS_OPTIONS } from '@/config/constant';
import { isResponseSuccess } from '@/lib/api-helper';
import { cn } from '@/lib/helper';
@@ -32,48 +33,54 @@ const RowOptions = ({
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<Button
href={`/master-data/supplier/detail/?supplierId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon
icon='mdi:eye-outline'
width={16}
height={16}
<RequirePermission permissions='lti.master.suppliers.detail'>
<Button
href={`/master-data/supplier/detail/?supplierId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
/>
Detail
</Button>
<Button
href={`/master-data/supplier/detail/edit/?supplierId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon
icon='material-symbols:edit-outline'
width={16}
height={16}
>
<Icon
icon='mdi:eye-outline'
width={16}
height={16}
className='justify-start text-sm'
/>
Detail
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.suppliers.update'>
<Button
href={`/master-data/supplier/detail/edit/?supplierId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
/>
Edit
</Button>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button>
>
<Icon
icon='material-symbols:edit-outline'
width={16}
height={16}
className='justify-start text-sm'
/>
Edit
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.suppliers.delete'>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button>
</RequirePermission>
</RowOptionsMenuWrapper>
);
};
@@ -219,15 +226,17 @@ const SuppliersTable = () => {
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
<div className='w-full flex flex-row'>
<Button
href='/master-data/supplier/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
<RequirePermission permissions='lti.master.suppliers.create'>
<Button
href='/master-data/supplier/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
</RequirePermission>
</div>
<DebouncedTextInput
@@ -18,7 +18,7 @@ export const SupplierFormSchema = Yup.object({
value: Yup.string().required(),
label: Yup.string().required(),
}).required('Tipe wajib diisi!'),
hatchery: Yup.string().required('Hatchery wajib diisi!'),
hatchery: Yup.string().optional(),
phone: Yup.string()
.matches(/^[0-9]+$/, 'Nomor telepon hanya boleh berisi angka!')
.min(10, 'Nomor telepon minimal 10 digit!')
@@ -24,6 +24,7 @@ import TextInput from '@/components/input/TextInput';
import TextArea from '@/components/input/TextArea';
import { cn } from '@/lib/helper';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import RequirePermission from '@/components/helper/RequirePermission';
interface SupplierCustomProps {
formType?: 'add' | 'edit' | 'detail';
@@ -141,7 +142,7 @@ const SupplierForm = ({
pic: values.pic,
type: values.type.value,
category: values.category.value,
hatchery: values.hatchery,
hatchery: values.hatchery ?? '',
phone: values.phone,
email: values.email,
address: values.address,
@@ -170,12 +171,12 @@ const SupplierForm = ({
useEffect(() => {
formikSetValues(formikInitialValues);
if (formType != 'add') {
const hatcheryArrays = formikInitialValues.hatchery.split(',');
const hatcheryCreatedOptions = hatcheryArrays.map((item) => ({
const hatcheryArrays = formikInitialValues.hatchery?.split(',');
const hatcheryCreatedOptions = hatcheryArrays?.map((item) => ({
value: item,
label: item,
}));
setHatcheryOptionValues(hatcheryCreatedOptions);
setHatcheryOptionValues(hatcheryCreatedOptions ?? []);
}
}, [formikSetValues, formikInitialValues, setHatcheryOptionValues]);
useEffect(() => {
@@ -301,7 +302,6 @@ const SupplierForm = ({
<SelectInput
isMulti
createables
required
placeholder='Pilih Hatchery'
label='Hatchery'
value={hatcheryOptionsValues}
@@ -406,36 +406,40 @@ const SupplierForm = ({
<div className='flex flex-row justify-between gap-2 flex-wrap'>
{formType !== 'add' && (
<div className='flex flex-row justify-start gap-2'>
<Button
type='button'
color='error'
onClick={deleteSupplierHandler}
className='px-4'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Delete
</Button>
{formType !== 'edit' && (
<RequirePermission permissions='lti.master.suppliers.delete'>
<Button
type='button'
color='warning'
href={`/master-data/supplier/detail/edit/?supplierId=${initialValues?.id}`}
color='error'
onClick={deleteSupplierHandler}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
Delete
</Button>
</RequirePermission>
{formType !== 'edit' && (
<RequirePermission permissions='lti.master.suppliers.update'>
<Button
type='button'
color='warning'
href={`/master-data/supplier/detail/edit/?supplierId=${initialValues?.id}`}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
</Button>
</RequirePermission>
)}
</div>
)}
@@ -15,6 +15,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission';
import { Uom } from '@/types/api/master-data/uom';
import { UomApi } from '@/services/api/master-data';
@@ -34,40 +35,46 @@ const RowOptionsMenu = ({
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<Button
href={`/master-data/uom/detail/?uomId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
<Button
href={`/master-data/uom/detail/edit/?uomId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
<RequirePermission permissions='lti.master.uoms.detail'>
<Button
href={`/master-data/uom/detail/?uomId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
/>
Delete
</Button>
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.uoms.update'>
<Button
href={`/master-data/uom/detail/edit/?uomId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.uoms.delete'>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button>
</RequirePermission>
</RowOptionsMenuWrapper>
);
};
@@ -192,15 +199,17 @@ const UomsTable = () => {
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
<div className='w-full flex flex-row'>
<Button
href='/master-data/uom/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
<RequirePermission permissions='lti.master.uoms.create'>
<Button
href='/master-data/uom/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
</RequirePermission>
</div>
<DebouncedTextInput
@@ -10,6 +10,7 @@ import Button from '@/components/Button';
import TextInput from '@/components/input/TextInput';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import RequirePermission from '@/components/helper/RequirePermission';
import {
UomFormSchema,
@@ -160,36 +161,40 @@ const UomForm = ({ type = 'add', initialValues }: UomFormProps) => {
<div className='flex flex-row justify-between gap-2 flex-wrap'>
{type !== 'add' && (
<div className='flex flex-row justify-start gap-2'>
<Button
type='button'
color='error'
onClick={deleteUomClickHandler}
className='px-4'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Delete
</Button>
{type !== 'edit' && (
<RequirePermission permissions='lti.master.uoms.delete'>
<Button
type='button'
color='warning'
href={`/master-data/uom/detail/edit/?uomId=${initialValues?.id}`}
color='error'
onClick={deleteUomClickHandler}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
Delete
</Button>
</RequirePermission>
{type !== 'edit' && (
<RequirePermission permissions='lti.master.uoms.update'>
<Button
type='button'
color='warning'
href={`/master-data/uom/detail/edit/?uomId=${initialValues?.id}`}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
</Button>
</RequirePermission>
)}
</div>
)}
@@ -20,6 +20,7 @@ import SelectInput, { OptionType } from '@/components/input/SelectInput';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import RequirePermission from '@/components/helper/RequirePermission';
import { Warehouse } from '@/types/api/master-data/warehouse';
import { WarehouseApi } from '@/services/api/master-data';
@@ -39,40 +40,46 @@ const RowOptionsMenu = ({
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<Button
href={`/master-data/warehouse/detail/?warehouseId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
<Button
href={`/master-data/warehouse/detail/edit/?warehouseId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
<RequirePermission permissions='lti.master.warehouses.detail'>
<Button
href={`/master-data/warehouse/detail/?warehouseId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
/>
Delete
</Button>
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.warehouses.update'>
<Button
href={`/master-data/warehouse/detail/edit/?warehouseId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='material-symbols:edit-outline' width={16} height={16} />
Edit
</Button>
</RequirePermission>
<RequirePermission permissions='lti.master.warehouses.delete'>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button>
</RequirePermission>
</RowOptionsMenuWrapper>
);
};
@@ -270,15 +277,17 @@ const WarehousesTable = () => {
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
<div className='w-full flex flex-row'>
<Button
href='/master-data/warehouse/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
<RequirePermission permissions='lti.master.warehouses.create'>
<Button
href='/master-data/warehouse/add'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
</RequirePermission>
</div>
<DebouncedTextInput
@@ -12,6 +12,7 @@ import TextInput from '@/components/input/TextInput';
import SelectInput, { OptionType } from '@/components/input/SelectInput';
import { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import RequirePermission from '@/components/helper/RequirePermission';
import {
WarehouseFormSchema,
@@ -435,36 +436,40 @@ const WarehouseForm = ({ type = 'add', initialValues }: WarehouseFormProps) => {
<div className='flex flex-row justify-between gap-2 flex-wrap'>
{type !== 'add' && (
<div className='flex flex-row justify-start gap-2'>
<Button
type='button'
color='error'
onClick={deleteWarehouseClickHandler}
className='px-4'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Delete
</Button>
{type !== 'edit' && (
<RequirePermission permissions='lti.master.warehouses.delete'>
<Button
type='button'
color='warning'
href={`/master-data/warehouse/detail/edit/?warehouseId=${initialValues?.id}`}
color='error'
onClick={deleteWarehouseClickHandler}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
icon='material-symbols:delete-outline-rounded'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
Delete
</Button>
</RequirePermission>
{type !== 'edit' && (
<RequirePermission permissions='lti.master.warehouses.update'>
<Button
type='button'
color='warning'
href={`/master-data/warehouse/detail/edit/?warehouseId=${initialValues?.id}`}
className='px-4'
>
<Icon
icon='material-symbols:edit-outline'
width={24}
height={24}
className='justify-start text-sm'
/>
Edit
</Button>
</RequirePermission>
)}
</div>
)}
@@ -1,324 +0,0 @@
'use client';
import Button from '@/components/Button';
import DebouncedTextInput from '@/components/input/DebouncedTextInput';
import { OptionType } from '@/components/input/SelectInput';
import Modal, { useModal } from '@/components/Modal';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import Table from '@/components/Table';
import RowCollapseOptions from '@/components/table/RowCollapseOptions';
import RowDropdownOptions from '@/components/table/RowDropdownOptions';
import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper';
import { TableRowSizeSelector } from '@/components/table/TableRowSizeSelector';
import { ROWS_OPTIONS } from '@/config/constant';
import { isResponseSuccess } from '@/lib/api-helper';
import { cn, formatNumber } from '@/lib/helper';
import { ChickinApi } from '@/services/api/production/chickin';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { Chickin } from '@/types/api/production/chickin';
import { Icon } from '@iconify/react';
import { CellContext, SortingState } from '@tanstack/react-table';
import { useState } from 'react';
import useSWR from 'swr';
const ChickinTable = () => {
const {
state: tableFilterState,
updateFilter,
setPage,
setPageSize,
toQueryString: getTableFilterQueryString,
} = useTableFilter({
initial: {
search: '',
},
paramMap: {
page: 'page',
pageSize: 'limit',
search: 'search',
},
});
const [sorting, setSorting] = useState<SortingState>([]);
const [selectedChickin, setSelectedChickin] = useState<Chickin | undefined>(
undefined
);
const [isDeleteLoading, setIsDeleteLoading] = useState(false);
const deleteModal = useModal();
const chickinModal = useModal();
// Data Fetching
const {
data: chickins,
isLoading,
mutate: refreshChickins,
} = useSWR(
`${ChickinApi.basePath}${getTableFilterQueryString()}`,
ChickinApi.getAllFetcher
);
const searchChangeHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
updateFilter('search', event.target.value);
setPage(1);
};
const pageSizeChangeHandler = (val: OptionType | OptionType[] | null) => {
const newVal = val as OptionType;
setPageSize(newVal.value as number);
setPage(1);
};
const confirmationModalDeleteClickHandler = async () => {
setIsDeleteLoading(true);
try {
await ChickinApi.delete(selectedChickin?.id as number);
refreshChickins();
deleteModal.closeModal();
} finally {
setIsDeleteLoading(false);
}
};
return (
<>
<div className='flex flex-col gap-4'>
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col sm:flex-row justify-between items-end sm:items-center gap-2'>
<Button
href='/production/project-flock/chickin/add?projectFlockId=1'
variant='outline'
color='primary'
className='w-full sm:w-fit'
>
<Icon icon='uil:plus' width={24} height={24} />
Tambah
</Button>
<DebouncedTextInput
name='search'
placeholder='Cari Chickin'
value={tableFilterState.search}
onChange={searchChangeHandler}
className={{ wrapper: 'sm:max-w-3xs' }}
/>
</div>
<TableRowSizeSelector
value={tableFilterState.pageSize}
onChange={pageSizeChangeHandler}
options={ROWS_OPTIONS}
/>
</div>
</div>
<Table<Chickin>
data={isResponseSuccess(chickins) ? chickins?.data : []}
columns={[
{
header: '#',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
accessorFn: (row) => row.project_flock_kandang?.kandang.name,
header: 'Kandang',
},
{
accessorFn: (row) => row.quantity,
header: 'Jumlah Chickin',
cell: (props) => {
if (props.row.original.quantity) {
return formatNumber(props.row.original.quantity);
} else {
return '-';
}
},
},
{
accessorFn: (row) => row.chick_in_date,
header: 'Tanggal Chickin',
cell: (props) => {
if (props.row.original.chick_in_date) {
return new Date(
props.row.original.chick_in_date
).toLocaleDateString('id-ID');
} else {
return '-';
}
},
},
{
accessorFn: (row) => row.note,
header: 'Catatan',
},
{
header: 'Aksi',
cell: (props) => {
const currentPageSize =
props.table.getPaginationRowModel().rows.length;
const currentPageRows =
props.table.getPaginationRowModel().flatRows;
const currentRowRelativeIndex =
currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2;
const deleteClickHandler = () => {
setSelectedChickin(props.row.original);
deleteModal.openModal();
};
const editClickHandler = () => {
setSelectedChickin(props.row.original);
chickinModal.openModal();
};
return (
<>
{currentPageSize > 2 && (
<RowDropdownOptions isLast2Rows={isLast2Rows}>
<RowOptionsMenu
type='dropdown'
props={props}
deleteClickHandler={deleteClickHandler}
editClickHandler={editClickHandler}
/>
</RowDropdownOptions>
)}
{currentPageSize <= 2 && (
<RowCollapseOptions>
<RowOptionsMenu
type='collapse'
props={props}
deleteClickHandler={deleteClickHandler}
editClickHandler={editClickHandler}
/>
</RowCollapseOptions>
)}
</>
);
},
},
]}
pageSize={tableFilterState.pageSize}
page={isResponseSuccess(chickins) ? chickins?.meta?.page : 0}
totalItems={
isResponseSuccess(chickins) ? chickins?.meta?.total_results : 0
}
onPageChange={setPage}
isLoading={isLoading}
sorting={sorting}
setSorting={setSorting}
className={{
containerClassName: cn({
'mb-20':
isResponseSuccess(chickins) && chickins?.data?.length === 0,
}),
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
}}
/>
<ConfirmationModal
ref={deleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data Chickin ini?`}
secondaryButton={{
text: 'Tidak',
}}
primaryButton={{
text: 'Ya',
onClick: confirmationModalDeleteClickHandler,
isLoading: isDeleteLoading,
color: 'error',
}}
/>
<Modal ref={chickinModal.ref}>
<div className='flex flex-row justify-between items-center'>
<h1 className='text-xl font-semibold text-center mb-6'>
Chickin Kandang -{' '}
{selectedChickin?.project_flock_kandang &&
selectedChickin?.project_flock_kandang.kandang?.name}
</h1>
<Button
color='error'
variant='link'
onClick={chickinModal.closeModal}
>
<Icon
className='text-black'
icon='uil:times'
width={24}
height={24}
/>
</Button>
</div>
{/* <ChickinForm
initialValues={selectedChickin}
formType='edit'
afterSubmit={() => {
refreshChickins();
chickinModal.closeModal();
}}
/> */}
</Modal>
</>
);
};
const RowOptionsMenu = ({
type = 'dropdown',
props,
editClickHandler,
deleteClickHandler,
}: {
type: 'dropdown' | 'collapse';
props: CellContext<Chickin, unknown>;
editClickHandler: () => void;
deleteClickHandler: () => void;
}) => {
return (
<RowOptionsMenuWrapper type={type}>
<Button
href={`/production/project-flock/chickin/detail?chickinId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
<Button
variant='ghost'
color='warning'
className='justify-start text-sm'
onClick={editClickHandler}
>
<Icon icon='mdi:pencil-outline' width={16} height={16} />
Edit
</Button>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='justify-start text-sm text-error focus-visible:text-error-content hover:text-error-content'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
className='justify-start text-sm'
/>
Delete
</Button>
</RowOptionsMenuWrapper>
);
};
export default ChickinTable;
@@ -17,6 +17,7 @@ import DrawerHeader from '@/components/helper/drawer/DrawerHeader';
import { Icon } from '@iconify/react';
import Badge from '@/components/Badge';
import { CHICKINS_APPROVAL_LINE } from '@/config/approval-line';
import RequirePermission from '@/components/helper/RequirePermission';
const ChickinFormKandang = ({
formType = 'add',
initialValues,
@@ -144,17 +145,24 @@ const ChickinFormKandang = ({
<h2 className='text-xl font-semibold'>Informasi Chick In</h2>
{/* Badge Row */}
<div className='flex flex-row gap-2'>
<Badge
variant='soft'
color={'success'}
className={{
badge: 'rounded-lg px-2',
}}
>
<Icon icon='mdi:circle' width={12} height={12} color={'success'} />{' '}
Perlu Chick In ({initialValues.available_qtys?.length ?? 0})
</Badge>
<div className='divider divider-horizontal p-0 m-0'></div>
<RequirePermission permissions='lti.production.chickins.create'>
<Badge
variant='soft'
color={'success'}
className={{
badge: 'rounded-lg px-2',
}}
>
<Icon
icon='mdi:circle'
width={12}
height={12}
color={'success'}
/>{' '}
Perlu Chick In ({initialValues.available_qtys?.length ?? 0})
</Badge>
<div className='divider divider-horizontal p-0 m-0'></div>
</RequirePermission>
<Badge
color='neutral'
variant='soft'
@@ -176,11 +184,13 @@ const ChickinFormKandang = ({
afterSubmit={afterSubmitFormChickin}
/>
)}
<ChickinFormView
initialValues={initialValues}
formType={formType}
afterSubmit={afterSubmitFormChickin}
/>
<RequirePermission permissions='lti.production.chickins.create'>
<ChickinFormView
initialValues={initialValues}
formType={formType}
afterSubmit={afterSubmitFormChickin}
/>
</RequirePermission>
</>
);
};
@@ -1,6 +1,7 @@
import Alert from '@/components/Alert';
import Button from '@/components/Button';
import Card from '@/components/Card';
import RequirePermission from '@/components/helper/RequirePermission';
import { useModal } from '@/components/Modal';
import ConfirmationModalWithNotes from '@/components/modal/ConfirmationModalWithNotes';
import PillBadge from '@/components/PillBadge';
@@ -146,14 +147,16 @@ const ChickinLogsView = ({
)}
{initialValues?.approval?.step_number <= 2 && (
<Button
color='success'
onClick={handleClickApprove}
className='w-full'
>
<Icon width={24} height={24} icon='material-symbols:check' />
Approve Semua Chick In
</Button>
<RequirePermission permissions='lti.production.chickins.approve'>
<Button
color='success'
onClick={handleClickApprove}
className='w-full'
>
<Icon width={24} height={24} icon='material-symbols:check' />
Approve Semua Chick In
</Button>
</RequirePermission>
)}
{chickinErrorMessage && (
@@ -25,6 +25,8 @@ import { ChangeEventHandler, useEffect, useMemo, useState } from 'react';
import toast from 'react-hot-toast';
import useSWR from 'swr';
import RequirePermission from '@/components/helper/RequirePermission';
const RowOptionsMenu = ({
type = 'dropdown',
props,
@@ -46,50 +48,58 @@ const RowOptionsMenu = ({
)}
>
<div className='flex flex-col gap-1'>
<Button
href={`/production/project-flock/detail?projectFlockId=${props.row.original.id}`}
variant='ghost'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
{props.row.original.approval.step_name === 'Aktif' && (
<RequirePermission permissions='lti.production.project_flocks.detail'>
<Button
href={`/production/project-flock/chickin/add?projectFlockId=${props.row.original.id}`}
href={`/production/project-flock/detail?projectFlockId=${props.row.original.id}`}
variant='ghost'
color='success'
color='primary'
className='justify-start text-sm'
>
<Icon icon='mdi:home-import-outline' width={16} height={16} />
Chickin
<Icon icon='mdi:eye-outline' width={16} height={16} />
Detail
</Button>
</RequirePermission>
{props.row.original.approval.step_name === 'Aktif' && (
<RequirePermission permissions='lti.production.chickins.create'>
<Button
href={`/production/project-flock/chickin/add?projectFlockId=${props.row.original.id}`}
variant='ghost'
color='success'
className='justify-start text-sm'
>
<Icon icon='mdi:home-import-outline' width={16} height={16} />
Chickin
</Button>
</RequirePermission>
)}
{props.row.original.approval.step_name === 'Pengajuan' && (
<Button
href={`/production/project-flock/detail/edit?projectFlockId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='mdi:pencil-outline' width={16} height={16} />
Edit
</Button>
<RequirePermission permissions='lti.production.project_flocks.update'>
<Button
href={`/production/project-flock/detail/edit?projectFlockId=${props.row.original.id}`}
variant='ghost'
color='warning'
className='justify-start text-sm'
>
<Icon icon='mdi:pencil-outline' width={16} height={16} />
Edit
</Button>
</RequirePermission>
)}
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit justify-start text-sm'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
/>
Delete
</Button>
<RequirePermission permissions='lti.production.project_flocks.delete'>
<Button
onClick={deleteClickHandler}
variant='ghost'
color='error'
className='text-error hover:text-inherit justify-start text-sm'
>
<Icon
icon='material-symbols:delete-outline-rounded'
width={16}
height={16}
/>
Delete
</Button>
</RequirePermission>
</div>
</div>
);
@@ -283,44 +293,20 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
return (
<>
<div className='min-h-screen w-full p-0 sm:p-4'>
<div className='min-h-screen w-full p-4'>
<div className='flex flex-col gap-2 mb-4'>
<div className='w-full flex flex-col justify-between items-end gap-2'>
<div className='flex flex-col sm:flex-row gap-3 w-full'>
<Button
color='primary'
className='w-full sm:w-fit'
href='/production/project-flock/add'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
{/* <Button
variant='outline'
color='success'
onClick={() => {
setApprovalAction('APPROVED');
confirmModal.openModal();
}}
disabled={selectedRowIds.length === 0}
className='w-full sm:w-fit'
>
<Icon icon='material-symbols:check' width={24} height={24} />
Approve
</Button>
<Button
variant='outline'
color='error'
onClick={() => {
setApprovalAction('REJECTED');
confirmModal.openModal();
}}
disabled={selectedRowIds.length === 0}
className='w-full sm:w-fit'
>
<Icon icon='mdi:times' width={24} height={24} />
Reject
</Button> */}
<RequirePermission permissions='lti.production.project_flocks.create'>
<Button
color='primary'
className='w-full sm:w-fit'
href='/production/project-flock/add'
>
<Icon icon='ic:round-plus' width={24} height={24} />
Tambah
</Button>
</RequirePermission>
<div className='ms-auto w-full sm:w-auto'>
<DebouncedTextInput
name='search'
@@ -539,49 +525,6 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
cell: (props) =>
formatDate(props.row.original.created_at, 'MMM DD, YYYY'),
},
// {
// header: 'Aksi',
// cell: (props) => {
// const currentPageSize =
// props.table.getPaginationRowModel().rows.length;
// const currentPageRows =
// props.table.getPaginationRowModel().flatRows;
// const currentRowRelativeIndex =
// currentPageRows.findIndex((r) => r.id === props.row.id) + 1;
// const isLast2Rows =
// currentRowRelativeIndex > currentPageSize - 2;
// const deleteClickHandler = () => {
// setSelectedProjectFlock(props.row.original);
// deleteModal.openModal();
// };
// return (
// <>
// {currentPageSize > 2 && (
// <RowDropdownOptions isLast2Rows={isLast2Rows}>
// <RowOptionsMenu
// type='dropdown'
// props={props}
// deleteClickHandler={deleteClickHandler}
// />
// </RowDropdownOptions>
// )}
// {currentPageSize <= 2 && (
// <RowCollapseOptions>
// <RowOptionsMenu
// type='collapse'
// props={props}
// deleteClickHandler={deleteClickHandler}
// />
// </RowCollapseOptions>
// )}
// </>
// );
// },
// },
]}
pageSize={tableFilterState.pageSize}
page={
@@ -630,6 +573,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
);
setRowSelection({});
},
permissions: 'lti.production.project_flocks.detail',
},
{
action: 'DELETE',
@@ -639,6 +583,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
onClick: () => {
deleteModal.openModal();
},
permissions: 'lti.production.project_flocks.delete',
},
]}
approvals={[
@@ -651,6 +596,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
confirmModal.openModal();
},
disabled: !canApprove,
permissions: 'lti.production.project_flocks.approve',
},
{
icon: 'mdi:times',
@@ -660,6 +606,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
setApprovalAction('REJECTED');
confirmModal.openModal();
},
permissions: 'lti.production.project_flocks.approve',
},
]}
selectedRowIds={selectedRowIds}
@@ -671,7 +618,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
<ConfirmationModal
ref={deleteModal.ref}
type='error'
text={`Apakah anda yakin ingin menghapus data Project Flock ini (${selectedProjectFlock?.flock_name})?`}
text={`Apakah anda yakin ingin menghapus data Project Flock ini (${selectedRowIds?.length} data)?`}
secondaryButton={{
text: 'Tidak',
}}
@@ -686,7 +633,7 @@ const ProjectFlockTable = ({ refresh }: { refresh?: () => void }) => {
<ConfirmationModalWithNotes
ref={confirmModal.ref}
type={approvalAction == 'APPROVED' ? 'success' : 'error'}
text={`Apakah anda yakin ingin ${approvalAction == 'APPROVED' ? 'approve' : 'reject'} data Project Flock ini (${selectedRowIds.length} data)?`}
text={`Apakah anda yakin ingin ${approvalAction == 'APPROVED' ? 'approve' : 'reject'} data Project Flock ini (${selectedRowIds?.length} data)?`}
secondaryButton={{
text: 'Tidak',
}}
@@ -1,640 +0,0 @@
'use client';
import Badge from '@/components/Badge';
import Button from '@/components/Button';
import Card from '@/components/Card';
import SelectInput, {
OptionType,
useSelect,
} from '@/components/input/SelectInput';
import PillBadge from '@/components/PillBadge';
import Table from '@/components/Table';
import { isResponseSuccess } from '@/lib/api-helper';
import { cn, formatDate, formatTitleCase } from '@/lib/helper';
import { ProjectFlockApi } from '@/services/api/production/project-flock';
import { ProjectFlockKandangApi } from '@/services/api/production';
import { useTableFilter } from '@/services/hooks/useTableFilter';
import { ProjectFlock } from '@/types/api/production/project-flock';
import { ProjectFlockKandang } from '@/types/api/production/project-flock-kandang';
import { Icon } from '@iconify/react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import useSWR from 'swr';
import { FormHeader } from '@/components/helper/form/FormHeader';
import Link from 'next/link';
const ProjectFlockChickinDetail = ({
projectFlockId,
}: {
projectFlockId: number | undefined;
}) => {
const router = useRouter();
// Tables Props
const { state: tableFilterState } = useTableFilter({
initial: { search: '' },
paramMap: { page: 'page', pageSize: 'limit' },
});
// States
const [searchProjectFlock, setSearchProjectFlock] = useState('');
const [selectedProjectFlock, setSelectedProjectFlock] =
useState<OptionType | null>(null);
const [projectFlock, setProjectFlock] = useState<ProjectFlock>();
// Fetch Data
const { data: listProjectFlockKandang } = useSWR(
`${ProjectFlockKandangApi.basePath}?${new URLSearchParams({
search: searchProjectFlock,
project_flock_id:
projectFlock?.id?.toString() ?? projectFlockId?.toString() ?? '',
}).toString()}`,
ProjectFlockKandangApi.getAllFetcher
);
const {
options: options,
isLoadingOptions: isLoadingListProjectFlock,
rawData: listProjectFlock,
} = useSelect<ProjectFlock>(
ProjectFlockApi.basePath,
'id',
'flock_name',
'',
{
search: searchProjectFlock,
}
);
// Handle Function
const handleChickinClick = async (
projectFlockKandang: ProjectFlockKandang
) => {
router.push(
`/production/project-flock/chickin/add/kandang?projectFlockKandangId=${projectFlockKandang.id}&projectFlockId=${projectFlockId ?? selectedProjectFlock?.value}`
);
};
const handleChangeProjectFlock = (val: OptionType | null) => {
setSelectedProjectFlock(val);
if (isResponseSuccess(listProjectFlock) && val) {
const selected = listProjectFlock.data.find(
(pf) => pf.id === Number(val.value)
);
setProjectFlock(selected);
} else {
setProjectFlock(undefined);
}
if (projectFlockId) {
router.push('/production/project-flock/chickin/add');
}
if (!val && projectFlockId) {
router.push('/production/project-flock/chickin/add');
}
};
useEffect(() => {
if (projectFlockId && isResponseSuccess(listProjectFlock)) {
setProjectFlock(
listProjectFlock.data.find((pf) => pf.id === Number(projectFlockId))
);
}
}, [projectFlockId, listProjectFlock]);
return (
<>
{/* Header */}
<div className='flex flex-row justify-between items-center px-4 py-4'>
<div className='flex flex-row items-center h-full gap-2'>
<Link
href={`/production/project-flock/detail?projectFlockId=${projectFlock?.id}`}
className='hover:text-gray-400'
>
<Icon icon='mdi:arrow-left' width={24} height={24} />
</Link>
<div className='divider divider-horizontal p-0 m-0'></div>
<div className='text-sm text-neutral'>
Chick In {projectFlock?.flock_name}
</div>
</div>
</div>
{/* <FormHeader
title={`Chick In ${projectFlock?.flock_name ?? 'Project Flock'}`}
backUrl={`/production/project-flock/detail?projectFlockId=${projectFlock?.id}`}
/> */}
{/* <div className='flex flex-col gap-4 w-full my-4'>
<div className='max-w-full sm:max-w-1/2 md:max-w-3/5 lg:max-w-2/5'>
<SelectInput
required
label='Ganti Project Flock'
placeholder='Pilih Project Flock'
options={options}
onInputChange={(val) => {
setSearchProjectFlock(val);
}}
isLoading={isLoadingListProjectFlock}
value={
projectFlock
? {
label: `${projectFlock?.flock_name}`,
value: projectFlock?.id,
}
: null
}
onChange={(val) => {
handleChangeProjectFlock(val as OptionType | null);
}}
isSearchable
isClearable
startAdornment={
projectFlock && (
<Badge
variant='soft'
color='success'
size='sm'
className={{
badge: 'whitespace-nowrap font-semibold',
}}
>
Periode {projectFlock?.period}
</Badge>
)
}
/>
</div>
</div> */}
{/* Informasi Umum */}
{projectFlock && (
<div className='border-t-1 border-gray-300'>
<div className='p-4 flex flex-col gap-4'>
<h2 className='text-2xl font-semibold'>Informasi Umum</h2>
{/* Badge Row */}
<div className='flex flex-row gap-2'>
<Badge
variant='soft'
color={
projectFlock.approval.step_number == 1
? 'neutral'
: projectFlock.approval.step_number == 2
? 'success'
: projectFlock.approval.step_number >= 3
? 'error'
: undefined
}
className={{
badge: 'rounded-lg px-2',
}}
>
<Icon
icon='mdi:circle'
width={12}
height={12}
color={
projectFlock.approval.step_number == 1
? 'neutral'
: projectFlock.approval.step_number == 2
? 'success'
: projectFlock.approval.step_number >= 3
? 'error'
: undefined
}
/>{' '}
{projectFlock.approval.step_name}
</Badge>
<div className='divider divider-horizontal p-0 m-0'></div>
<Badge
color='neutral'
variant='soft'
className={{ badge: 'rounded-lg px-2' }}
>
<Icon icon='mdi:bookmark' width={12} height={12} />
{` ${formatTitleCase(projectFlock.category)}`}
</Badge>
</div>
{/* Information Grid */}
<div className='grid grid-cols-3 gap-4'>
<div className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2'>
<Icon width={14} height={14} icon='mdi:account' /> Submitted
</div>
<div className='col-span-2'>
<Badge
variant='soft'
color='neutral'
className={{
badge: 'rounded-lg px-2',
}}
>
<Icon icon='mdi:account-circle' width={14} height={14} />{' '}
{projectFlock.created_user.name}
</Badge>
</div>
<div className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2'>
<Icon width={14} height={14} icon={'mdi:clock'} /> History
</div>
<div className='col-span-2'>
<Button variant='outline' className='py-1 text-sm'>
See History{' '}
<Icon
icon='mdi:arrow-top-right-thin'
width={11}
height={11}
/>
</Button>
</div>
{/* BARIS 1 */}
<div
className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2
relative
before:content-[""] before:absolute before:left-[5px] before:top-[90%] before:bottom-[-100%] before:w-[1px] before:border-1 before:border-dashed before:border-gray-400'
>
<Icon width={14} height={14} icon='mdi:circle-slice-8' /> Area
</div>
<div className='col-span-2'>{projectFlock.area.name}</div>
{/* BARIS 2 */}
<div
className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2
relative
before:content-[""] before:absolute before:left-[5px] before:top-[90%] before:bottom-[-100%] before:w-[1px] before:border-1 before:border-dashed before:border-gray-400'
>
<Icon width={14} height={14} icon='mdi:circle-slice-8' /> Lokasi
</div>
<div className='col-span-2'>{projectFlock.location.name}</div>
<div
className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2
relative
before:content-[""] before:absolute before:left-[5px] before:top-[90%] before:bottom-[-100%] before:w-[1px] before:border-1 before:border-dashed before:border-gray-400'
>
<Icon width={14} height={14} icon='mdi:circle-slice-8' /> FCR
</div>
<div className='col-span-2'>{projectFlock.fcr.name}</div>
{/* BARIS 3 (Terakhir - TIDAK PERLU garis di bawahnya) */}
<div className='col-span-1 flex flex-row items-center text-gray-400 font-semibold gap-2'>
<Icon width={14} height={14} icon='mdi:circle-slice-8' />{' '}
Kategori
</div>
<div className='col-span-2'>
{formatTitleCase(projectFlock.category)}
</div>
</div>
</div>
</div>
)}
{/* <Card
title='Informasi Flock'
className={{
wrapper: 'w-full bg-white mb-3',
}}
>
<Table<ProjectFlock>
emptyContent={
<div className='w-full p-5 text-center'>
<span className='text-lg opacity-50'>
Pilih project flock terlebih dahulu...
</span>
</div>
}
data={projectFlock ? [projectFlock] : []}
columns={[
{
header: 'ID',
accessorKey: 'id',
},
{
header: 'Area',
accessorKey: 'area.name',
},
{
header: 'Lokasi',
accessorKey: 'location.name',
},
{
header: 'Nama Flock',
accessorKey: 'flock_name',
},
{
header: 'Kategori',
accessorKey: 'category',
},
{
header: 'Status',
accessorKey: 'status',
cell: (props) => {
return props.row.original.approval?.step_name ? (
<PillBadge
color={(() => {
switch (
props.row.original.approval?.step_name.toUpperCase()
) {
case 'AKTIF':
return 'red';
case 'PENGAJUAN':
return 'green';
default:
return 'gray';
}
})()}
content={props.row.original.approval?.step_name
.toLowerCase()
.replace(/_/g, ' ')
.replace(/\b\w/g, (char) => char.toUpperCase())}
/>
) : (
'-'
);
},
},
{
header: 'FCR Layer',
accessorKey: 'fcr.name',
},
]}
page={undefined}
className={{
containerClassName: cn({
'mb-20': projectFlock && projectFlock.kandangs?.length === 0,
}),
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
paginationClassName: 'hidden',
}}
/>
</Card> */}
{/* Card Kandangs */}
<div className='border-t-1 border-gray-300'>
<div className='p-4 flex flex-col gap-4'>
<h2 className='text-2xl font-semibold'>Daftar Kandang</h2>
{isResponseSuccess(listProjectFlock) ? (
<>
{/* Badge Row */}
<div className='flex flex-row gap-2'>
<Badge
variant='soft'
color={'success'}
className={{
badge: 'rounded-lg px-2',
}}
>
<Icon
icon='mdi:circle'
width={12}
height={12}
color={'success'}
/>{' '}
Disetujui (
{isResponseSuccess(listProjectFlockKandang) &&
listProjectFlockKandang.data.filter(
(k) => k.approval?.step_number == 1
).length}
)
</Badge>
<div className='divider divider-horizontal p-0 m-0'></div>
<Badge
variant='soft'
color={'neutral'}
className={{
badge: 'rounded-lg px-2',
}}
>
<Icon
icon='mdi:circle'
width={12}
height={12}
color={'neutral'}
/>{' '}
Pengajuan (
{isResponseSuccess(listProjectFlockKandang) &&
listProjectFlockKandang.data.filter(
(k) => k.approval?.step_number == 2
).length}
)
</Badge>
<div className='divider divider-horizontal p-0 m-0'></div>
<Badge
color='error'
variant='soft'
className={{ badge: 'rounded-lg px-2' }}
>
<Icon
icon={`mdi:circle`}
width={12}
height={12}
color='error'
/>
Belum Chickin (
{isResponseSuccess(listProjectFlockKandang) &&
listProjectFlockKandang.data.filter(
(k) => k.approval == null
).length}
)
</Badge>
</div>
{/* Card Kandang */}
<Card
variant='bordered'
className={{
wrapper: 'w-full',
body: 'p-3',
}}
>
<div className='flex flex-col gap-6'>
{isResponseSuccess(listProjectFlockKandang) &&
listProjectFlockKandang.data.map((kandang) => (
<div
key={kandang.id}
className='flex flex-row justify-between items-center'
>
<div className='flex flex-row gap-2 items-center cursor-pointer text-gray-400'>
<Badge
variant='soft'
color={
kandang.approval
? kandang.approval.step_number == 1
? 'success'
: 'neutral'
: 'error'
}
className={{
badge: 'rounded-lg px-2',
}}
>
<Icon
icon='mdi:circle'
width={12}
height={12}
color={
kandang.approval
? kandang.approval.step_number == 1
? 'success'
: 'neutral'
: 'gray'
}
/>
</Badge>
<span className='font-semibold'>
{kandang.kandang.name}
</span>
</div>
<Button
variant='outline'
className='py-1 text-sm'
onClick={() => {
handleChickinClick(kandang);
}}
disabled={projectFlock?.approval?.step_number === 1}
>
Chick In{' '}
<Icon
icon='mdi:arrow-top-right-thin'
width={11}
height={11}
/>
</Button>
</div>
))}
</div>
</Card>
</>
) : (
<div className='w-full p-5 text-center'>
<span className='text-lg opacity-50'>
Pilih project flock terlebih dahulu...
</span>
</div>
)}
</div>
</div>
{/* <Card
title='Daftar Kandang'
className={{
wrapper: 'w-full bg-white',
}}
>
<Table<ProjectFlockKandang>
emptyContent={
<div className='w-full p-5 text-center'>
<span className='text-lg opacity-50'>
Pilih project flock terlebih dahulu...
</span>
</div>
}
data={
projectFlock && isResponseSuccess(listProjectFlockKandang)
? listProjectFlockKandang.data
: []
}
columns={[
{
header: '#',
cell: (props) =>
tableFilterState.pageSize * (tableFilterState.page - 1) +
props.row.index +
1,
},
{
accessorFn: (row) => row?.project_flock?.area?.name,
header: 'Area',
},
{
accessorFn: (row) => row?.project_flock?.location?.name,
header: 'Lokasi',
},
{
accessorKey: 'kandang.name',
header: 'Kandang',
},
{
accessorKey: 'kandang.capacity',
header: 'Kapasitas',
},
{
accessorFn: () => projectFlock?.period,
header: 'Periode',
},
{
accessorKey: 'approval.step_name',
header: 'Status',
cell: (props) => {
return props.row.original.approval?.step_name ? (
<PillBadge
color={(() => {
switch (
props.row.original.approval?.step_name.toUpperCase()
) {
case 'DISETUJUI':
return 'green';
case 'PENGAJUAN':
return 'yellow';
default:
return 'gray';
}
})()}
content={props.row.original.approval?.step_name
.toLowerCase()
.replace(/_/g, ' ')
.replace(/\b\w/g, (char) => char.toUpperCase())}
/>
) : projectFlock?.approval?.step_number === 1 ? (
<PillBadge color='red' content={'Tidak Dapat Chick In'} />
) : (
<PillBadge color='gray' content={'Belum Chick In'} />
);
},
},
{
header: 'Aksi',
cell: (props) => {
return (
<>
<Button
color='success'
variant='outline'
onClick={() => {
handleChickinClick(props.row.original);
}}
className='p-1'
disabled={projectFlock?.approval?.step_number === 1}
>
<Icon
icon='mdi:home-import-outline'
width={18}
height={18}
/>
Chickin
</Button>
</>
);
},
},
]}
page={undefined}
className={{
containerClassName: cn({
'mb-20': projectFlock && projectFlock.kandangs?.length === 0,
}),
tableWrapperClassName: 'overflow-x-auto min-h-full!',
tableClassName: 'font-inter w-full table-auto min-h-full!',
headerRowClassName: 'border-b border-b-gray-200',
headerColumnClassName:
'px-6 py-3 text-xs font-semibold text-gray-500 last:flex last:flex-row last:justify-end',
bodyRowClassName: 'border-b border-b-gray-200',
bodyColumnClassName:
'px-6 py-3 last:flex last:flex-row last:justify-end',
paginationClassName: 'hidden',
}}
/>
</Card> */}
</>
);
};
export default ProjectFlockChickinDetail;

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