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/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 */}