mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-25 15:55:48 +00:00
Compare commits
98 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b13794ee5 | |||
| 8e88355894 | |||
| 1b98e5d4d8 | |||
| d8daf09844 | |||
| 0bb9aee139 | |||
| 306b8d3bf3 | |||
| 2bf764a05c | |||
| 6c3285f624 | |||
| afb79b0589 | |||
| 4f571f1c16 | |||
| 81ca60a09b | |||
| 23453eb8f5 | |||
| 3dc32da834 | |||
| f089492830 | |||
| 6eaa92dfd4 | |||
| 8d668429e1 | |||
| 8d1a3b665e | |||
| 7b99bae197 | |||
| dad169d854 | |||
| f985c041e4 | |||
| 5326eff293 | |||
| d66bd8c606 | |||
| ea5ad20684 | |||
| 23ee8828f0 | |||
| 0dd2edfe01 | |||
| 6edc278bdf | |||
| f81c49becb | |||
| 03a9451fc8 | |||
| cc0b051a0a | |||
| 865438e3fb | |||
| d39b71e759 | |||
| 5e6b03ef08 | |||
| c291ba3246 | |||
| ab2e7db9d0 | |||
| 13c1a82142 | |||
| 6185fafb57 | |||
| 2ab7c10d5d | |||
| bc6ebcfeda | |||
| 10fb9fc990 | |||
| 28639516d5 | |||
| 2bf0f2874e | |||
| a81a61135f | |||
| d2e88c2061 | |||
| 8f4f3d93b8 | |||
| 7daca04cc1 | |||
| 2c5168badf | |||
| 2712821f4e | |||
| 7e73c99074 | |||
| 0f86ba0f2d | |||
| 10a37fde75 | |||
| cd3a5ad441 | |||
| cd42bd6bc0 | |||
| ea7f8a68f4 | |||
| e0371b0884 | |||
| 432d837aaf | |||
| 77b05c6440 | |||
| 731bec5a94 | |||
| 6ea25aa3b1 | |||
| d4f4505405 | |||
| bd653851e2 | |||
| b9b349aa7a | |||
| 6ec6323bbc | |||
| c44e63bd2b | |||
| 500c30c2bc | |||
| 507c4005af | |||
| d49bca1d40 | |||
| 663c1dea14 | |||
| 9e0d3e2bbf | |||
| 4be719b9d8 | |||
| 1152cd2bef | |||
| 4ddd1dc8e3 | |||
| 8c95dc8327 | |||
| df6c1ae49d | |||
| 42a56a08d7 | |||
| 6ed7dcfa6d | |||
| dda29e10d1 | |||
| 9d9b9d93db | |||
| f41899dbc9 | |||
| 36ff6d04ee | |||
| de63b6721a | |||
| a200dac23c | |||
| fcfd2fb576 | |||
| 2c28d0a831 | |||
| addfaff692 | |||
| ecdbb764d5 | |||
| a3be3de338 | |||
| 9e895af62a | |||
| 1f9992c1c8 | |||
| 574fb3b371 | |||
| 4643a39c3e | |||
| 88b8767ca4 | |||
| de19cc5de2 | |||
| b9dad3094c | |||
| ff427d13cc | |||
| 8295943b82 | |||
| a3169d582d | |||
| dd6c6263e7 | |||
| 5d03b68576 |
+1
-1
@@ -1,3 +1,3 @@
|
||||
npm run format
|
||||
npm run lint
|
||||
npm run build
|
||||
npx tsc --noEmit
|
||||
Generated
+383
-12
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1,7 @@
|
||||
import FormFinanceInjection from '@/components/pages/finance/add/injection/FormFinanceInjection';
|
||||
|
||||
const FinanceAddInjectionPage = () => {
|
||||
return <FormFinanceInjection type='add' />;
|
||||
};
|
||||
|
||||
export default FinanceAddInjectionPage;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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'
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
+49
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
+1
-14
@@ -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;
|
||||
+95
@@ -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;
|
||||
+1329
File diff suppressed because it is too large
Load Diff
@@ -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
Reference in New Issue
Block a user