diff --git a/package-lock.json b/package-lock.json
index c29a16a6..d7ffd3eb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 61cc5776..319f0e3d 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx
index 4f2c344e..426cf6b9 100644
--- a/src/app/dashboard/page.tsx
+++ b/src/app/dashboard/page.tsx
@@ -1,9 +1,7 @@
+import DashboardProduction from '@/components/pages/dashboard/DashboardProduction';
+
const Dashboard = () => {
- return (
-
- );
+ return ;
};
export default Dashboard;
diff --git a/src/components/pages/closing/ClosingSapronakCalculationTable.tsx b/src/components/pages/closing/ClosingSapronakCalculationTable.tsx
index ea27fd80..b6703549 100644
--- a/src/components/pages/closing/ClosingSapronakCalculationTable.tsx
+++ b/src/components/pages/closing/ClosingSapronakCalculationTable.tsx
@@ -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
? () => (
- {formatNumber(total.qty_masuk)}
+ {formatNumber(total?.qty_masuk)}
)
: '',
@@ -66,7 +66,7 @@ const ClosingSapronakCalculationTable = ({
footer: total
? () => (
- {formatNumber(total.qty_keluar)}
+ {formatNumber(total?.qty_keluar)}
)
: '',
@@ -78,7 +78,7 @@ const ClosingSapronakCalculationTable = ({
footer: total
? () => (
- {formatNumber(total.qty_pakai)}
+ {formatNumber(total?.qty_pakai)}
)
: '',
@@ -102,7 +102,7 @@ const ClosingSapronakCalculationTable = ({
footer: total
? () => (
- {formatCurrency(total.harga_beli_per_qty)}
+ {formatCurrency(total?.harga_beli_per_qty)}
)
: '',
@@ -114,7 +114,7 @@ const ClosingSapronakCalculationTable = ({
footer: total
? () => (
- {formatCurrency(total.total_harga)}
+ {formatCurrency(total?.total_harga)}
)
: '',
@@ -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 = ({
data={
isResponseSuccess(sapronakCalculation)
- ? (sapronakCalculation.data?.doc_broiler.rows ?? [])
+ ? (sapronakCalculation.data?.doc_broiler?.rows ?? [])
: []
}
columns={docBroilerColumns}
@@ -189,7 +189,7 @@ const ClosingSapronakCalculationTable = ({
data={
isResponseSuccess(sapronakCalculation)
- ? (sapronakCalculation.data?.ovk.rows ?? [])
+ ? (sapronakCalculation.data?.ovk?.rows ?? [])
: []
}
columns={ovkColumns}
@@ -212,7 +212,7 @@ const ClosingSapronakCalculationTable = ({
data={
isResponseSuccess(sapronakCalculation)
- ? (sapronakCalculation.data?.pakan.rows ?? [])
+ ? (sapronakCalculation.data?.pakan?.rows ?? [])
: []
}
columns={pakanColumns}
diff --git a/src/components/pages/dashboard/DashboardProduction.tsx b/src/components/pages/dashboard/DashboardProduction.tsx
new file mode 100644
index 00000000..fb8190aa
--- /dev/null
+++ b/src/components/pages/dashboard/DashboardProduction.tsx
@@ -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([
+ '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 (
+
+
+
+ );
+ }
+ return (
+ <>
+
+
+
Dashboard
+
+
+
+
+
+
+ {/* Dashboard Statistics */}
+
+
+ {/* Charts Grid */}
+
+ {/* Production Line Chart */}
+
+
+
+
+ {/* Standard Line Chart */}
+
+
+
+
+ {/* Bar Charts Grid - 2 columns */}
+
+ {/* FCR Bar Chart */}
+
+
+
+
+ {/* Egg Weight Bar Chart */}
+
+
+
+
+
+
+
+
+ {/* Modal Header */}
+
+
+
+
Filter Data
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default DashboardProduction;
diff --git a/src/components/pages/dashboard/chart/EggWeightBarChart.tsx b/src/components/pages/dashboard/chart/EggWeightBarChart.tsx
new file mode 100644
index 00000000..7a9a02c6
--- /dev/null
+++ b/src/components/pages/dashboard/chart/EggWeightBarChart.tsx
@@ -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 (
+
+
+ Rata-rata Berat Telur (EW)
+
+
+
+ );
+ }
+
+ return (
+
+
Rata-rata Berat Telur (EW)
+
+
+
+
+
+
+ value !== undefined ? [`${value} gram`, ''] : ['', '']
+ }
+ cursor={{ fill: 'rgba(59, 130, 246, 0.1)' }}
+ />
+
+ {data.map((entry, index) => (
+ |
+ ))}
+
+
+
+
+ );
+};
+
+export default EggWeightBarChart;
diff --git a/src/components/pages/dashboard/chart/FCRBarChart.tsx b/src/components/pages/dashboard/chart/FCRBarChart.tsx
new file mode 100644
index 00000000..2647c7f7
--- /dev/null
+++ b/src/components/pages/dashboard/chart/FCRBarChart.tsx
@@ -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 (
+
+
+ Feed Conversion Ratio (FCR)
+
+
+
+ );
+ }
+
+ return (
+
+
+ Feed Conversion Ratio (FCR)
+
+
+
+
+
+
+
+ value !== undefined ? [value.toFixed(2), 'FCR'] : ['', '']
+ }
+ cursor={{ fill: 'rgba(16, 185, 129, 0.1)' }}
+ />
+
+ {data.map((entry, index) => (
+ |
+ ))}
+
+
+
+
+ );
+};
+
+export default FCRBarChart;
diff --git a/src/components/pages/dashboard/chart/ProductionLineChart.tsx b/src/components/pages/dashboard/chart/ProductionLineChart.tsx
new file mode 100644
index 00000000..470e09c9
--- /dev/null
+++ b/src/components/pages/dashboard/chart/ProductionLineChart.tsx
@@ -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 = {
+ 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([]);
+
+ // 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 (
+
+
+ Performa Produksi per Flock
+
+
+
+
+ formatDateByPeriod(value, period)}
+ />
+
+
+ formatDateByPeriod(value as string, period)
+ }
+ />
+
+
+
+ );
+};
+
+export default ProductionLineChart;
+
+// Export types for external use
+export type { FlockData, ProductionChartItem, ProductionChartsData };
diff --git a/src/components/pages/dashboard/chart/ProductionStat.tsx b/src/components/pages/dashboard/chart/ProductionStat.tsx
new file mode 100644
index 00000000..7e299223
--- /dev/null
+++ b/src/components/pages/dashboard/chart/ProductionStat.tsx
@@ -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 (
+
+ {[1, 2, 3, 4].map((i) => (
+
+
+
+ ))}
+
+ );
+ }
+
+ return (
+
+ {data.map((stat, index) => (
+
+
+
+
{stat.title}
+
+ {formatCurrency(stat.value)}
+
+
+
+ {stat.change > 0 ? '+' : ''}
+ {stat.change}% vs{' '}
+ {stat.period === 'monthly' ? 'bulan lalu' : 'periode lalu'}
+
+
+
+
+
+ ))}
+
+ );
+};
+
+export default ProductionStat;
diff --git a/src/components/pages/dashboard/chart/StandardLineChart.tsx b/src/components/pages/dashboard/chart/StandardLineChart.tsx
new file mode 100644
index 00000000..18bcabf6
--- /dev/null
+++ b/src/components/pages/dashboard/chart/StandardLineChart.tsx
@@ -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 = {
+ 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([]);
+
+ // 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 = {
+ hen_day: '#94a3b8',
+ hen_house: '#64748b',
+ uniformity: '#475569',
+ egg_weight: '#334155',
+ egg_mass: '#1e293b',
+ };
+
+ // Standard names mapping for display
+ const standardLabels: Record = {
+ hen_day: 'Hen Day',
+ hen_house: 'Hen House',
+ uniformity: 'Uniformity',
+ egg_weight: 'Egg Weight',
+ egg_mass: 'Egg Mass',
+ };
+
+ return (
+
+
+ Perbandingan Henday per Umur
+
+
+
+
+
+
+
+ value !== undefined ? [`${value}%`, ''] : ['', '']
+ }
+ labelFormatter={(label) => `Minggu ${label}`}
+ />
+
+
+
+ );
+};
+
+export default StandardLineChart;
+
+// Export types for external use
+export type { FlockData, StandardData, StandardChartItem };
diff --git a/src/components/pages/dashboard/filter/DashboardProductionFilter.schema.ts b/src/components/pages/dashboard/filter/DashboardProductionFilter.schema.ts
new file mode 100644
index 00000000..4ed86a48
--- /dev/null
+++ b/src/components/pages/dashboard/filter/DashboardProductionFilter.schema.ts
@@ -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;
diff --git a/src/components/pages/expense/ExpenseRealizationContent.tsx b/src/components/pages/expense/ExpenseRealizationContent.tsx
index c69f089f..ccd57ec3 100644
--- a/src/components/pages/expense/ExpenseRealizationContent.tsx
+++ b/src/components/pages/expense/ExpenseRealizationContent.tsx
@@ -16,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;
@@ -103,24 +103,32 @@ const ExpenseRealizationContent = ({
initialValues?.realization_docs.length > 0 && (
{initialValues?.realization_docs.map(
- (realizationDocument, realizationDocumentIdx) => (
- -
-
- {realizationDocument.path}{' '}
-
-
-
- )
+ (realizationDocument, realizationDocumentIdx) => {
+ const path = realizationDocument.path.startsWith(
+ '/'
+ )
+ ? realizationDocument.path.slice(1)
+ : realizationDocument.path;
+ const documentUrl = `${S3_PUBLIC_BASE_URL}/${path}`;
+ return (
+ -
+
+ {realizationDocument.path}{' '}
+
+
+
+ );
+ }
)}
)}
@@ -211,7 +219,7 @@ const ExpenseRealizationContent = ({
let expenseGrandTotal = 0;
kandangExpense.pengajuans?.forEach(
- (item) => (expenseGrandTotal += item.price)
+ (item) => (expenseGrandTotal += item.qty * item.price)
);
return (
@@ -273,7 +281,7 @@ const ExpenseRealizationContent = ({
let expenseGrandTotal = 0;
kandangExpense.realisasi?.forEach(
- (item) => (expenseGrandTotal += item.price)
+ (item) => (expenseGrandTotal += item.qty * item.price)
);
return (
diff --git a/src/components/pages/expense/ExpenseRequestContent.tsx b/src/components/pages/expense/ExpenseRequestContent.tsx
index b937c5bc..2b9086e0 100644
--- a/src/components/pages/expense/ExpenseRequestContent.tsx
+++ b/src/components/pages/expense/ExpenseRequestContent.tsx
@@ -27,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';
@@ -408,9 +408,13 @@ const ExpenseRequestContent = ({
Kandang |
: |
- {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(', ')
+ : '-'}
|
@@ -448,7 +452,14 @@ const ExpenseRequestContent = ({
| Nominal Biaya |
: |
- {formatCurrency(initialValues?.grand_total ?? 0)} |
+
+ {formatCurrency(
+ initialValues?.latest_approval.step_number === 4 ||
+ initialValues?.latest_approval.step_number === 5
+ ? (initialValues?.total_realisasi ?? 0)
+ : (initialValues?.total_pengajuan ?? 0)
+ )}
+ |
| Status Pencairan |
@@ -482,24 +493,32 @@ const ExpenseRequestContent = ({
initialValues?.documents.length > 0 && (
{initialValues?.documents.map(
- (requestDocument, requestDocumentIdx) => (
- -
-
- {requestDocument.path}{' '}
-
-
-
- )
+ (requestDocument, requestDocumentIdx) => {
+ const path = requestDocument.path.startsWith(
+ '/'
+ )
+ ? requestDocument.path.slice(1)
+ : requestDocument.path;
+ const documentUrl = `${S3_PUBLIC_BASE_URL}/${path}`;
+ return (
+ -
+
+ {requestDocument.path}{' '}
+
+
+
+ );
+ }
)}
)}
@@ -558,7 +577,7 @@ const ExpenseRequestContent = ({
let expenseGrandTotal = 0;
kandangExpense.pengajuans?.forEach(
- (item) => (expenseGrandTotal += item.price)
+ (item) => (expenseGrandTotal += item.qty * item.price)
);
return (
@@ -573,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'}`}
diff --git a/src/components/pages/expense/form/ExpenseKandangsTable.tsx b/src/components/pages/expense/form/ExpenseKandangsTable.tsx
index b3c9f46d..7d7f76ca 100644
--- a/src/components/pages/expense/form/ExpenseKandangsTable.tsx
+++ b/src/components/pages/expense/form/ExpenseKandangsTable.tsx
@@ -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([]);
const [rowSelection, setRowSelection] = useState>(
- convertRowSelectionArrToObj(selectedKandangs.map((item) => item.id))
+ convertRowSelectionArrToObj(
+ selectedKandangs
+ .map((item) => item.id)
+ .filter((id): id is number => id !== undefined)
+ )
);
const kandangsColumns: ColumnDef[] = [
diff --git a/src/components/pages/expense/form/ExpenseRealizationForm.schema.ts b/src/components/pages/expense/form/ExpenseRealizationForm.schema.ts
index 77db761c..1f3682ea 100644
--- a/src/components/pages/expense/form/ExpenseRealizationForm.schema.ts
+++ b/src/components/pages/expense/form/ExpenseRealizationForm.schema.ts
@@ -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 ({
- 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) => {
diff --git a/src/components/pages/expense/form/ExpenseRealizationForm.tsx b/src/components/pages/expense/form/ExpenseRealizationForm.tsx
index d1c7c5f2..6526b1c1 100644
--- a/src/components/pages/expense/form/ExpenseRealizationForm.tsx
+++ b/src/components/pages/expense/form/ExpenseRealizationForm.tsx
@@ -150,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,
@@ -177,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);
};
@@ -338,7 +355,10 @@ const ExpenseRealizationForm = ({
)}
;
+ 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(NonstockApi.basePath, 'id', 'name');
+ } = useSelect(
+ NonstockApi.basePath,
+ 'id',
+ 'name',
+ 'search',
+ supplierId ? { supplier_id: String(supplierId) } : undefined
+ );
const nonstockChangeHandler = (
kandangExpenseIdx: number,
@@ -82,140 +93,159 @@ const ExpenseRealizationKandangDetailExpense: React.FC<
- {formik.values.realizations.length === 0 && (
+ {!formik.values.supplier?.value && (
- Pilih kandang terlebih dahulu!
+ Pilih supplier terlebih dahulu!
)}
- {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 && (
+
+
+ Belum ada item biaya. Silakan pilih lokasi terlebih dahulu.
+
+
+ )}
- return (
- kandangName?.name && (
-
-
-
- Biaya {kandangName?.name}
-
+ {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;
-
-
-
-
- | Nonstock |
- Total Kuantitas |
- Harga Satuan |
- Catatan |
-
-
+ return (
+ (kandangName?.name || !kandangExpense.kandang_id) && (
+
+
+
+ {kandangName?.name
+ ? `Biaya ${kandangName.name}`
+ : location?.label
+ ? `Biaya ${location.label}`
+ : 'Biaya Umum'}
+
-
- {kandangExpense.cost_items.map(
- (expenseItem, expenseIdx) => (
-
- |
- {
- nonstockChangeHandler(
- kandangExpenseIdx,
- expenseIdx,
- val
- );
- }}
- options={nonstockOptions}
- isLoading={isLoadingNonstockOptions}
- onInputChange={setNonstockInputValue}
- className={{ wrapper: 'min-w-48' }}
- isDisabled
- />
- |
-
-
-
- |
-
-
-
- Rp
-
- }
- className={{ wrapper: 'min-w-24' }}
- />
- |
-
-
-
- |
+
+
+
+
+ | Nonstock |
+ Total Kuantitas |
+ Harga Satuan |
+ Catatan |
- )
- )}
-
-
+
+
+
+ {kandangExpense.cost_items.map(
+ (expenseItem, expenseIdx) => (
+
+ |
+ {
+ nonstockChangeHandler(
+ kandangExpenseIdx,
+ expenseIdx,
+ val
+ );
+ }}
+ options={nonstockOptions}
+ isLoading={isLoadingNonstockOptions}
+ onInputChange={setNonstockInputValue}
+ className={{ wrapper: 'min-w-48' }}
+ isDisabled
+ />
+ |
+
+
+
+ |
+
+
+
+ Rp
+
+ }
+ className={{ wrapper: 'min-w-24' }}
+ />
+ |
+
+
+
+ |
+
+ )
+ )}
+
+
+
+
-
-
- )
- );
- })}
+ )
+ );
+ }
+ )}
);
diff --git a/src/components/pages/expense/form/ExpenseRequestForm.schema.ts b/src/components/pages/expense/form/ExpenseRequestForm.schema.ts
index 7758df83..71357361 100644
--- a/src/components/pages/expense/form/ExpenseRequestForm.schema.ts
+++ b/src/components/pages/expense/form/ExpenseRequestForm.schema.ts
@@ -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 =
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 =
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
diff --git a/src/components/pages/expense/form/ExpenseRequestForm.tsx b/src/components/pages/expense/form/ExpenseRequestForm.tsx
index 71160785..60e55397 100644
--- a/src/components/pages/expense/form/ExpenseRequestForm.tsx
+++ b/src/components/pages/expense/form/ExpenseRequestForm.tsx
@@ -108,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) {
@@ -130,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;
+ }
),
};
@@ -179,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,
@@ -208,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);
};
@@ -454,7 +484,10 @@ const ExpenseRequestForm = ({
)}
;
+ 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(NonstockApi.basePath, 'id', 'name');
+ } = useSelect(
+ NonstockApi.basePath,
+ 'id',
+ 'name',
+ 'search',
+ supplierId ? { supplier_id: String(supplierId) } : undefined
+ );
const nonstockChangeHandler = (
kandangExpenseIdx: number,
@@ -113,41 +124,57 @@ const ExpenseRequestKandangDetailExpense: React.FC<
- {(formik.values.expense_nonstocks.length === 0 ||
- !formik.values.supplier?.value) && (
+ {!formik.values.supplier?.value && (
- Pilih kandang terlebih dahulu!
+ Pilih supplier terlebih dahulu!
)}
+ {formik.values.expense_nonstocks.length === 0 &&
+ formik.values.supplier?.value && (
+
+
+ Belum ada item biaya. Silakan pilih lokasi terlebih dahulu.
+
+
+ )}
+
{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) && (
- Biaya {kandangName?.name}
+ Biaya {kandangName?.name || location?.label || 'Umum'}
- | Nonstock |
- Total Kuantitas |
- Harga Satuan |
+
+ Nonstock
+ |
+
+ Total Kuantitas
+ |
+
+ Harga Satuan
+ |
Catatan |
{type !== 'detail' && Aksi | }
diff --git a/src/components/pages/expense/pdf/ExpensePDF.tsx b/src/components/pages/expense/pdf/ExpensePDF.tsx
index 5b107127..ef1c7d8b 100644
--- a/src/components/pages/expense/pdf/ExpensePDF.tsx
+++ b/src/components/pages/expense/pdf/ExpensePDF.tsx
@@ -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}
>
- {kandangExpense.name}
+ {kandangExpense.kandang_id && kandangExpense.name
+ ? `Biaya ${kandangExpense.name}`
+ : `Biaya ${expense?.location.name || 'Umum'}`}
@@ -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}
>
- {kandangExpense.name}
+ {kandangExpense.kandang_id && kandangExpense.name
+ ? `Biaya ${kandangExpense.name}`
+ : `Biaya ${expense?.location.name || 'Umum'}`}
diff --git a/src/components/pages/finance/add/FormFinanceAdd.tsx b/src/components/pages/finance/add/FormFinanceAdd.tsx
index 9b8259be..f7fdf446 100644
--- a/src/components/pages/finance/add/FormFinanceAdd.tsx
+++ b/src/components/pages/finance/add/FormFinanceAdd.tsx
@@ -39,6 +39,12 @@ interface FormFinanceAddProps {
initialValues?: Finance;
}
+interface PartyCommonProps {
+ id: number;
+ name: string;
+ account_number: string;
+}
+
const FormFinanceAdd = ({
type = 'add',
initialValues,
@@ -52,10 +58,12 @@ const FormFinanceAdd = ({
FINANCE_PARTY_TYPE_OPTIONS.find(
(option) => option.value === initialValues?.party.type
) || null,
- party_id_option: {
- label: initialValues?.party.name || '',
- value: initialValues?.party.id || 0,
- },
+ 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(
@@ -97,16 +105,19 @@ const FormFinanceAdd = ({
});
// ===== Options =====
- const { options: partyOptions, isLoadingOptions: isLoadingPartyOptions } =
- useSelect(
- formik.values.party_type_option?.value === 'CUSTOMER'
- ? CustomerApi.basePath
- : SupplierApi.basePath,
- 'id',
- 'name',
- '',
- { limit: 'limit' }
- );
+ const {
+ options: partyOptions,
+ isLoadingOptions: isLoadingPartyOptions,
+ rawData: partyRawData,
+ } = useSelect(
+ formik.values.party_type_option?.value === 'CUSTOMER'
+ ? CustomerApi.basePath
+ : SupplierApi.basePath,
+ 'id',
+ 'name',
+ '',
+ { limit: 'limit' }
+ );
const {
options: bankOptions,
rawData: bankRawData,
@@ -204,6 +215,14 @@ const FormFinanceAdd = ({
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(
@@ -312,6 +331,7 @@ const FormFinanceAdd = ({
: ''
}
required
+ readOnly
/>
Edit
diff --git a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx
index f169eb3c..a0eed811 100644
--- a/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx
+++ b/src/components/pages/marketing/form/repeater/delivery-order/DeliverOrderProduct.tsx
@@ -174,19 +174,6 @@ const DeliveryOrderProductForm = ({
}}
onReset={handleResetForm}
>
- {/*
- {JSON.stringify(exisitingValues)}
-
-
- {JSON.stringify(formik.values)}
- */}
- {/*
- {JSON.stringify(formik.errors)}
-
-
- {JSON.stringify(formik.values.marketing_product)}
-
*/}
-
{formikErrorMessage && (
setFormErrorMessage('')} className='my-3 w-full'>
{formikErrorMessage}
diff --git a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx
index ad50a927..75aa3ba6 100644
--- a/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx
+++ b/src/components/pages/marketing/form/repeater/sales-order/SalesOrderProductForm.tsx
@@ -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
(KandangApi.basePath, 'id', 'name');
+ } = useSelect(WarehouseApi.basePath, 'id', 'name');
const {
options: warehouseSourceOptions,
diff --git a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx
index 47875902..af72f22f 100644
--- a/src/components/pages/master-data/nonstock/form/NonstockForm.tsx
+++ b/src/components/pages/master-data/nonstock/form/NonstockForm.tsx
@@ -79,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,
})) ?? [],
diff --git a/src/components/pages/master-data/production-standard/form/ProductionStandardForm.schema.ts b/src/components/pages/master-data/production-standard/form/ProductionStandardForm.schema.ts
index 6fc3799b..13183e71 100644
--- a/src/components/pages/master-data/production-standard/form/ProductionStandardForm.schema.ts
+++ b/src/components/pages/master-data/production-standard/form/ProductionStandardForm.schema.ts
@@ -18,6 +18,7 @@ const LayingRepeaterFormSchema = Yup.object({
),
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(),
});
@@ -35,6 +36,7 @@ const GrowingRepeaterFormSchema = Yup.object({
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(),
});
@@ -68,7 +70,9 @@ export const createProductionStandardRepeaterFormSchema = (
export const createProductionStandardFormSchema = (category: string) => {
return Yup.object({
name: Yup.string().required('Nama wajib diisi!'),
- project_category: Yup.string().required('Kategori proyek wajib diisi!'),
+ project_category: Yup.string()
+ .min(1, 'Kategori proyek wajib diisi!')
+ .required('Kategori proyek wajib diisi!'),
details: Yup.array().of(
createProductionStandardRepeaterFormSchema(category)
),
diff --git a/src/components/pages/master-data/production-standard/form/ProductionStandardForm.tsx b/src/components/pages/master-data/production-standard/form/ProductionStandardForm.tsx
index 99edb852..640ded51 100644
--- a/src/components/pages/master-data/production-standard/form/ProductionStandardForm.tsx
+++ b/src/components/pages/master-data/production-standard/form/ProductionStandardForm.tsx
@@ -29,6 +29,8 @@ import toast from 'react-hot-toast';
import ConfirmationModal from '@/components/modal/ConfirmationModal';
import { useModal } from '@/components/Modal';
import RequirePermission from '@/components/helper/RequirePermission';
+import Tooltip from '@/components/Tooltip';
+import Alert from '@/components/Alert';
type TableRowsType = {
customRow: boolean;
@@ -41,6 +43,7 @@ type ProductionDetailsErrors = {
target_hen_house_production?: string;
target_egg_weight?: string;
target_egg_mass?: string;
+ standard_fcr?: string;
};
type ProductionDetailsTouched = {
@@ -48,6 +51,7 @@ type ProductionDetailsTouched = {
target_hen_house_production?: boolean;
target_egg_weight?: boolean;
target_egg_mass?: boolean;
+ standard_fcr?: boolean;
};
const getProductionDetailsError = (
@@ -91,6 +95,9 @@ const convertPayloadToNumberTypes = (payload: ProductionStandardFormValues) => {
target_egg_mass: Number(
detail.production_standard_details.target_egg_mass
),
+ standard_fcr: Number(
+ detail.production_standard_details.standard_fcr
+ ),
}
: undefined,
production_standard_uniformity_details: {
@@ -131,6 +138,9 @@ const convertStandardValueToFormValues = (
target_egg_mass: Number(
detail.egg_production_standard_detail.target_egg_mass
),
+ standard_fcr: Number(
+ detail.egg_production_standard_detail.standard_fcr
+ ),
}
: undefined,
production_standard_uniformity_details: {
@@ -175,13 +185,15 @@ const ProductionStandardForm = ({
} = useFormStore();
// ===== Formik =====
+ // Initial values - only recalculate when initialValue changes (for edit/detail mode)
+ // For add mode, we load from cache via useEffect instead to avoid race conditions
const formikInitialValues = useMemo(() => {
- // For add mode, merge cached data with initial values
- if (formType === 'add' && formData) {
+ if (formType === 'add') {
+ // Don't use formData here - will be loaded via useEffect
return {
- name: formData.name || '',
- project_category: formData.project_category || '',
- details: formData.details || [],
+ name: '',
+ project_category: '',
+ details: [],
} as ProductionStandardFormValues;
}
@@ -190,10 +202,11 @@ const ProductionStandardForm = ({
project_category: initialValue?.project_category || '',
details: convertStandardValueToFormValues(initialValue?.details || []),
} as ProductionStandardFormValues;
- }, [initialValue, formData, formType]);
+ }, [initialValue, formType]);
const formik = useFormik({
initialValues: formikInitialValues as ProductionStandardFormValues,
- enableReinitialize: true,
+ // Only enable reinitialize for edit/detail mode, not add mode
+ enableReinitialize: formType !== 'add',
onSubmit: (values) => {
switch (formType) {
case 'add':
@@ -222,6 +235,7 @@ const ProductionStandardForm = ({
target_hen_house_production: '' as unknown as number,
target_egg_weight: '' as unknown as number,
target_egg_mass: '' as unknown as number,
+ standard_fcr: '' as unknown as number,
},
production_standard_uniformity_details: {
target_mean_bw: '' as unknown as number,
@@ -255,36 +269,38 @@ const ProductionStandardForm = ({
const { setValues: repeaterFormikSetValues } = repeaterFormik;
// ===== Effect =====
- // Load initial values only when component mounts or when initialValue changes (for edit mode)
- // This allows:
- // 1. Add mode: Load cached data from formData store
- // 2. Edit mode: Load existing data from initialValue
- // We use initialValue?.id as dependency to avoid infinite loops
+ // Load cached data only once on mount for add mode
+ const [isInitialized, setIsInitialized] = useState(false);
+
useEffect(() => {
- if (formType === 'add' && formData) {
- // For add mode, load from cache
+ if (formType === 'add' && formData && !isInitialized) {
+ // For add mode, load from cache only on initial mount
formikSetValues({
name: formData.name || '',
project_category: formData.project_category || '',
details: formData.details || [],
} as ProductionStandardFormValues);
- } else if (formType === 'detail' && initialValue) {
- // For detail mode, load from initialValue and convert the details
+ setIsInitialized(true);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []); // Only run once on mount
+
+ // For edit/detail mode, update when initialValue changes
+ useEffect(() => {
+ if (formType === 'detail' && initialValue) {
formikSetValues({
name: initialValue.name || '',
project_category: initialValue.project_category || '',
details: convertStandardValueToFormValues(initialValue.details || []),
} as ProductionStandardFormValues);
} else if (formType === 'edit' && initialValue) {
- // For edit mode, load from initialValue and convert the details
formikSetValues({
name: initialValue.name || '',
project_category: initialValue.project_category || '',
details: convertStandardValueToFormValues(initialValue.details || []),
} as ProductionStandardFormValues);
}
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [formData, initialValue?.id]); // Trigger when formData or initialValue.id changes
+ }, [initialValue?.id, formType]);
// ===== Data Table =====
const tableRows = useMemo(() => {
@@ -323,11 +339,6 @@ const ProductionStandardForm = ({
}, [formik.values.details]);
const columns = useMemo[]>(() => {
const baseColumns: ColumnDef[] = [
- {
- header: 'No',
- accessorFn: (row, index) => index + 1,
- enableSorting: false,
- },
{
header: 'Minggu',
accessorKey: 'week',
@@ -363,6 +374,12 @@ const ProductionStandardForm = ({
row.production_standard_details?.target_egg_mass,
enableSorting: false,
},
+ {
+ header: 'FCR',
+ accessorFn: (row) =>
+ row.production_standard_details?.standard_fcr,
+ enableSorting: false,
+ },
]
: [];
@@ -407,6 +424,7 @@ const ProductionStandardForm = ({
variant='outline'
color='warning'
className='p-2'
+ type='button'
onClick={() => handleEditClick(row.row.original.week)}
>
@@ -415,6 +433,7 @@ const ProductionStandardForm = ({
variant='outline'
color='error'
className='p-2'
+ type='button'
onClick={() => handleRemoveRow(row.row.original.week)}
>
@@ -430,7 +449,7 @@ const ProductionStandardForm = ({
...uniformityColumns,
...(formType !== 'detail' ? [actionColumn] : []),
];
- }, [formik.values.project_category, formType]);
+ }, [formik.values, formType]);
// ===== Handler =====
const handleAddRow = async (
@@ -488,9 +507,11 @@ const ProductionStandardForm = ({
setIsAddingRow(false);
};
- const handleRemoveRow = (week: number) => {
- const newValues = (formik.values.details || []).filter(
- (detail) => detail.week !== week
+ const handleRemoveRow = async (week: number) => {
+ // Access formik.values directly to get the latest values
+ const currentDetails = formik.values.details || [];
+ const newValues = currentDetails.filter(
+ (detail) => Number(detail.week) !== Number(week)
);
const updatedFormValues = {
@@ -671,6 +692,7 @@ const ProductionStandardForm = ({
target_hen_house_production: 0,
target_egg_weight: 0,
target_egg_mass: 0,
+ standard_fcr: 0,
},
}));
}
@@ -745,6 +767,7 @@ const ProductionStandardForm = ({
}
required
isDisabled={formType === 'detail'}
+ isClearable
/>
@@ -803,7 +826,7 @@ const ProductionStandardForm = ({
className={cn(
'grid gap-4 items-start',
formik.values.project_category === 'LAYING'
- ? 'grid-cols-9'
+ ? 'grid-cols-10'
: 'grid-cols-5'
)}
>
@@ -962,6 +985,41 @@ const ProductionStandardForm = ({
)
}
/>
+
+ gr
+
+ }
+ errorMessage={getProductionDetailsError(
+ repeaterFormik.errors
+ .production_standard_details,
+ 'standard_fcr'
+ )}
+ isError={
+ Boolean(
+ getProductionDetailsError(
+ repeaterFormik.errors
+ .production_standard_details,
+ 'standard_fcr'
+ )
+ ) &&
+ getProductionDetailsTouched(
+ repeaterFormik.touched
+ .production_standard_details,
+ 'standard_fcr'
+ )
+ }
+ />
>
)}
Batal
)}
-
+
+
{/* Should not be absolute */}