mirror of
https://gitlab.com/mbugroup/lti-web-client.git
synced 2026-05-20 13:32:00 +00:00
feat(FE-390): slicing UI and API integration for production dashboard
This commit is contained in:
Generated
+383
-12
@@ -25,6 +25,7 @@
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"react-number-format": "^5.4.4",
|
||||
"react-select": "^5.10.2",
|
||||
"recharts": "^3.6.0",
|
||||
"swr": "^2.3.6",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"use-debounce": "^10.0.6",
|
||||
@@ -1450,6 +1451,42 @@
|
||||
"@react-pdf/stylesheet": "^6.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
||||
"integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@standard-schema/utils": "^0.3.0",
|
||||
"immer": "^11.0.0",
|
||||
"redux": "^5.0.1",
|
||||
"redux-thunk": "^3.1.0",
|
||||
"reselect": "^5.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
||||
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-redux": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit/node_modules/immer": {
|
||||
"version": "11.1.3",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz",
|
||||
"integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/@rtsao/scc": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||
@@ -1464,6 +1501,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@standard-schema/utils": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||
@@ -1804,6 +1853,69 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-color": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-ease": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-interpolate": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-color": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-path": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-scale": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-time": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-shape": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
|
||||
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-path": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-time": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-timer": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
@@ -1871,7 +1983,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@@ -1902,6 +2013,12 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/use-sync-external-store": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.46.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz",
|
||||
@@ -1948,7 +2065,6 @@
|
||||
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.46.2",
|
||||
"@typescript-eslint/types": "8.46.2",
|
||||
@@ -2472,7 +2588,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -3138,8 +3253,128 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"internmap": "1 - 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-ease": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-format": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
|
||||
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-interpolate": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-path": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-scale": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2.10.0 - 3",
|
||||
"d3-format": "1 - 3",
|
||||
"d3-interpolate": "1.2.0 - 3",
|
||||
"d3-time": "2.1.1 - 3",
|
||||
"d3-time-format": "2 - 4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-shape": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-path": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time-format": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-time": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-timer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/daisyui": {
|
||||
"version": "5.5.8",
|
||||
@@ -3245,6 +3480,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js-light": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/deep-is": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||
@@ -3587,6 +3828,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/es-toolkit": {
|
||||
"version": "1.43.0",
|
||||
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.43.0.tgz",
|
||||
"integrity": "sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"docs",
|
||||
"benchmarks"
|
||||
]
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
@@ -3605,7 +3856,6 @@
|
||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -3779,7 +4029,6 @@
|
||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.9",
|
||||
@@ -4026,6 +4275,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
@@ -4651,6 +4906,16 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
|
||||
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||
@@ -4698,6 +4963,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/internmap": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/iobuffer": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz",
|
||||
@@ -5244,7 +5518,6 @@
|
||||
"resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.4.tgz",
|
||||
"integrity": "sha512-dc6oQ8y37rRcHn316s4ngz/nOjayLF/FFxBF4V9zamQKRqXxyiH1zagkCdktdWhtoQId5K20xt1lB90XzkB+hQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4",
|
||||
"fast-png": "^6.2.0",
|
||||
@@ -6345,7 +6618,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -6376,7 +6648,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
@@ -6440,6 +6711,29 @@
|
||||
"react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-redux": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.2.25 || ^19",
|
||||
"react": "^18.0 || ^19",
|
||||
"redux": "^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"redux": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-select": {
|
||||
"version": "5.10.2",
|
||||
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.10.2.tgz",
|
||||
@@ -6477,6 +6771,51 @@
|
||||
"react-dom": ">=16.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/recharts": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.6.0.tgz",
|
||||
"integrity": "sha512-L5bjxvQRAe26RlToBAziKUB7whaGKEwD3znoM6fz3DrTowCIC/FnJYnuq1GEzB8Zv2kdTfaxQfi5GoH0tBinyg==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"www"
|
||||
],
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "1.x.x || 2.x.x",
|
||||
"clsx": "^2.1.1",
|
||||
"decimal.js-light": "^2.5.1",
|
||||
"es-toolkit": "^1.39.3",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"immer": "^10.1.1",
|
||||
"react-redux": "8.x.x || 9.x.x",
|
||||
"reselect": "5.1.1",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"use-sync-external-store": "^1.2.2",
|
||||
"victory-vendor": "^37.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/redux": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"redux": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/reflect.getprototypeof": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
||||
@@ -6543,6 +6882,12 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/reselect": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.11",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
|
||||
@@ -7263,6 +7608,12 @@
|
||||
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tiny-invariant": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tiny-warning": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||
@@ -7310,7 +7661,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -7478,7 +7828,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -7635,6 +7984,28 @@
|
||||
"base64-arraybuffer": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/victory-vendor": {
|
||||
"version": "37.3.6",
|
||||
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
|
||||
"integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
|
||||
"license": "MIT AND ISC",
|
||||
"dependencies": {
|
||||
"@types/d3-array": "^3.0.3",
|
||||
"@types/d3-ease": "^3.0.0",
|
||||
"@types/d3-interpolate": "^3.0.1",
|
||||
"@types/d3-scale": "^4.0.2",
|
||||
"@types/d3-shape": "^3.1.0",
|
||||
"@types/d3-time": "^3.0.0",
|
||||
"@types/d3-timer": "^3.0.0",
|
||||
"d3-array": "^3.1.6",
|
||||
"d3-ease": "^3.0.1",
|
||||
"d3-interpolate": "^3.0.1",
|
||||
"d3-scale": "^4.0.2",
|
||||
"d3-shape": "^3.1.0",
|
||||
"d3-time": "^3.0.0",
|
||||
"d3-timer": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/vite-compatible-readable-stream": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/vite-compatible-readable-stream/-/vite-compatible-readable-stream-3.6.1.tgz",
|
||||
|
||||
+2
-1
@@ -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",
|
||||
@@ -50,4 +51,4 @@
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import DashboardProduction from '@/components/pages/dashboard/DashboardProduction';
|
||||
|
||||
const Dashboard = () => {
|
||||
return (
|
||||
<section className='w-full p-4'>
|
||||
<h1 className='text-3xl font-bold text-primary'>Dashboard</h1>
|
||||
</section>
|
||||
);
|
||||
return <DashboardProduction />;
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
'use client';
|
||||
|
||||
import Button from '@/components/Button';
|
||||
import Card from '@/components/Card';
|
||||
import { Icon } from '@iconify/react';
|
||||
import ProductionLineChart from '@/components/pages/dashboard/chart/ProductionLineChart';
|
||||
import StandardLineChart from '@/components/pages/dashboard/chart/StandardLineChart';
|
||||
import EggWeightBarChart from '@/components/pages/dashboard/chart/EggWeightBarChart';
|
||||
import FCRBarChart from '@/components/pages/dashboard/chart/FCRBarChart';
|
||||
import ProductionStat from '@/components/pages/dashboard/chart/ProductionStat';
|
||||
import Modal, { useModal } from '@/components/Modal';
|
||||
import DateInput from '@/components/input/DateInput';
|
||||
import SelectInput, {
|
||||
OptionType,
|
||||
useSelect,
|
||||
} from '@/components/input/SelectInput';
|
||||
import { RadioGroup } from '@/components/input/RadioInput';
|
||||
import { useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { DashboardApi } from '@/services/api/dashboard';
|
||||
import { useFormik } from 'formik';
|
||||
import dashboardProductionFilterSchema from '@/components/pages/dashboard/filter/DashboardProductionFilter.schema';
|
||||
import { ProjectFlockApi } from '@/services/api/production';
|
||||
import { ProductionStandardApi } from '@/services/api/master-data';
|
||||
|
||||
const DashboardProduction = () => {
|
||||
const filterModal = useModal();
|
||||
const [selectedPeriod, setSelectedPeriod] = useState('daily');
|
||||
const [selectedStandards, setSelectedStandards] = useState<string[]>([
|
||||
'hen_day',
|
||||
'hen_house',
|
||||
]);
|
||||
const [endpointUrl, setEndpointUrl] = useState('/dashboard');
|
||||
|
||||
// ===== FETCH DATA =====
|
||||
const {
|
||||
data: dashboardProductionResponse,
|
||||
isLoading: isLoadingDashboardProductionData,
|
||||
error: dashboardProductionError,
|
||||
} = useSWR(endpointUrl, () =>
|
||||
DashboardApi.getDashboardProductionFetcher(endpointUrl)
|
||||
);
|
||||
|
||||
const dashboardProductionData =
|
||||
dashboardProductionResponse?.status === 'success'
|
||||
? dashboardProductionResponse.data
|
||||
: undefined;
|
||||
|
||||
// ===== SELECT =====
|
||||
const { options: flockOptions, isLoadingOptions: isLoadingFlockOptions } =
|
||||
useSelect(ProjectFlockApi.basePath, 'id', 'flock_name', '', {
|
||||
limit: 'limit',
|
||||
category: 'LAYING',
|
||||
});
|
||||
const {
|
||||
options: standardProductionOptions,
|
||||
isLoadingOptions: isLoadingStandardProductionOptions,
|
||||
} = useSelect(ProductionStandardApi.basePath, 'id', 'name', '', {
|
||||
limit: 'limit',
|
||||
});
|
||||
|
||||
// ===== FORMIK =====
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
flock: [] as OptionType[],
|
||||
standard_production_id: [] as OptionType[],
|
||||
standard_productions: [] as OptionType[],
|
||||
period: selectedPeriod,
|
||||
},
|
||||
validationSchema: dashboardProductionFilterSchema,
|
||||
onSubmit: (values) => {
|
||||
console.log(values);
|
||||
// Build URL with query parameters
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (values.startDate) params.set('startDate', values.startDate);
|
||||
if (values.endDate) params.set('endDate', values.endDate);
|
||||
|
||||
if (values.flock && values.flock.length > 0) {
|
||||
const flockIds = values.flock
|
||||
.map((f: OptionType) => f.value || f)
|
||||
.join(',');
|
||||
params.set('flock', flockIds);
|
||||
}
|
||||
|
||||
if (
|
||||
values.standard_production_id &&
|
||||
values.standard_production_id.length > 0
|
||||
) {
|
||||
const standardIds = values.standard_production_id
|
||||
.map((s: OptionType) => s.value || s)
|
||||
.join(',');
|
||||
params.set('standard_production_id', standardIds);
|
||||
}
|
||||
|
||||
if (selectedStandards.length > 0) {
|
||||
params.set('standards', selectedStandards.join(','));
|
||||
}
|
||||
|
||||
params.set('period', selectedPeriod);
|
||||
|
||||
const newUrl = `/dashboard?${params.toString()}`;
|
||||
setEndpointUrl(newUrl);
|
||||
|
||||
// Close modal after applying filter
|
||||
filterModal.closeModal();
|
||||
},
|
||||
});
|
||||
|
||||
const handleResetFilter = () => {
|
||||
formik.resetForm();
|
||||
setSelectedPeriod('daily');
|
||||
setSelectedStandards(['hen_day', 'hen_house']);
|
||||
setEndpointUrl('/dashboard');
|
||||
};
|
||||
|
||||
if (isLoadingDashboardProductionData) {
|
||||
return (
|
||||
<div className='w-full min-h-screen flex items-center justify-center'>
|
||||
<span className='loading loading-spinner loading-xl'></span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<section className='w-full p-4 space-y-6'>
|
||||
<div className='flex flex-col sm:flex-row items-center justify-between gap-4'>
|
||||
<h1 className='text-3xl font-bold text-primary'>Dashboard</h1>
|
||||
<div className='flex flex-row justify-end gap-2'>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='min-w-28 rounded-lg'
|
||||
onClick={() => filterModal.openModal()}
|
||||
>
|
||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||
Filter
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
color='neutral'
|
||||
className='min-w-28 rounded-lg'
|
||||
>
|
||||
<Icon icon='heroicons:arrow-down-tray' width={20} height={20} />
|
||||
Export
|
||||
<Icon icon='heroicons:chevron-down' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dashboard Statistics */}
|
||||
<ProductionStat data={dashboardProductionData?.statistics_data} />
|
||||
|
||||
{/* Charts Grid */}
|
||||
<div className='grid grid-cols-1 gap-4'>
|
||||
{/* Production Line Chart */}
|
||||
<Card
|
||||
variant='bordered'
|
||||
className={{ wrapper: 'w-full', body: 'p-6' }}
|
||||
>
|
||||
<ProductionLineChart
|
||||
period={
|
||||
selectedPeriod as 'daily' | 'weekly' | 'monthly' | 'yearly'
|
||||
}
|
||||
data={dashboardProductionData?.production_charts}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Standard Line Chart */}
|
||||
<Card
|
||||
variant='bordered'
|
||||
className={{ wrapper: 'w-full', body: 'p-6' }}
|
||||
>
|
||||
<StandardLineChart
|
||||
selectedStandards={selectedStandards}
|
||||
data={dashboardProductionData?.standard_productions}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Bar Charts Grid - 2 columns */}
|
||||
<div className='grid grid-cols-1 lg:grid-cols-2 gap-4'>
|
||||
{/* FCR Bar Chart */}
|
||||
<Card
|
||||
variant='bordered'
|
||||
className={{ wrapper: 'w-full', body: 'p-6' }}
|
||||
>
|
||||
<FCRBarChart data={dashboardProductionData?.fcr_data} />
|
||||
</Card>
|
||||
|
||||
{/* Egg Weight Bar Chart */}
|
||||
<Card
|
||||
variant='bordered'
|
||||
className={{ wrapper: 'w-full', body: 'p-6' }}
|
||||
>
|
||||
<EggWeightBarChart data={dashboardProductionData?.egg_weights} />
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<Modal
|
||||
ref={filterModal.ref}
|
||||
className={{
|
||||
modal: 'p-0',
|
||||
modalBox: 'p-0 rounded-xl',
|
||||
}}
|
||||
>
|
||||
<div className='space-y-6'>
|
||||
{/* Modal Header */}
|
||||
<div className='flex items-center justify-between gap-2 py-3 border-b border-gray-300'>
|
||||
<div className='flex items-center gap-2 ms-4'>
|
||||
<Icon icon='heroicons:funnel' width={20} height={20} />
|
||||
<h3 className='font-semibold'>Filter Data</h3>
|
||||
</div>
|
||||
<Button
|
||||
variant='link'
|
||||
onClick={() => filterModal.closeModal()}
|
||||
className='text-gray-500 hover:text-gray-700 me-4 '
|
||||
>
|
||||
<Icon icon='heroicons:x-mark' width={20} height={20} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<form className='space-y-4' onSubmit={formik.handleSubmit}>
|
||||
{/* Rentang Waktu */}
|
||||
<div className='px-4'>
|
||||
<label className='flex items-center gap-2 mb-3'>
|
||||
<Icon icon='heroicons:calendar' width={20} height={20} />
|
||||
Rentang Waktu
|
||||
</label>
|
||||
<div className='flex items-center gap-2'>
|
||||
<DateInput
|
||||
name='startDate'
|
||||
placeholder='Tanggal Mulai'
|
||||
value={formik.values.startDate}
|
||||
errorMessage={formik.errors.startDate}
|
||||
onChange={formik.handleChange}
|
||||
className={{
|
||||
inputWrapper: 'rounded-lg',
|
||||
}}
|
||||
isError={
|
||||
Boolean(formik.errors.startDate) &&
|
||||
Boolean(formik.touched.startDate)
|
||||
}
|
||||
/>
|
||||
<span className='hidden md:block text-center'>—</span>
|
||||
<DateInput
|
||||
name='endDate'
|
||||
placeholder='Tanggal Akhir'
|
||||
value={formik.values.endDate}
|
||||
errorMessage={formik.errors.endDate}
|
||||
onChange={formik.handleChange}
|
||||
className={{
|
||||
inputWrapper: 'rounded-lg',
|
||||
}}
|
||||
isError={
|
||||
Boolean(formik.errors.endDate) &&
|
||||
Boolean(formik.touched.endDate)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Flock */}
|
||||
<div className='px-4'>
|
||||
<SelectInput
|
||||
label='Flock'
|
||||
value={formik.values.flock}
|
||||
onChange={(selected) => formik.setFieldValue('flock', selected)}
|
||||
errorMessage={formik.errors.flock as string}
|
||||
options={flockOptions}
|
||||
isLoading={isLoadingFlockOptions}
|
||||
isMulti
|
||||
isError={
|
||||
Boolean(formik.errors.flock) && Boolean(formik.touched.flock)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Production */}
|
||||
<div className='px-4'>
|
||||
<SelectInput
|
||||
label='Standard Produksi'
|
||||
value={formik.values.standard_production_id}
|
||||
onChange={(selected) =>
|
||||
formik.setFieldValue('standard_production_id', selected)
|
||||
}
|
||||
errorMessage={formik.errors.standard_production_id as string}
|
||||
options={standardProductionOptions}
|
||||
isLoading={isLoadingStandardProductionOptions}
|
||||
isMulti
|
||||
isError={
|
||||
Boolean(formik.errors.standard_production_id) &&
|
||||
Boolean(formik.touched.standard_production_id)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Standard */}
|
||||
<div className='px-4'>
|
||||
<SelectInput
|
||||
label='Standard'
|
||||
value={selectedStandards.map((s) => ({
|
||||
value: s,
|
||||
label:
|
||||
s === 'hen_day'
|
||||
? 'Hen Day'
|
||||
: s === 'hen_house'
|
||||
? 'Hen House'
|
||||
: s === 'uniformity'
|
||||
? 'Uniformity'
|
||||
: s === 'egg_weight'
|
||||
? 'Egg Weight'
|
||||
: 'Egg Mass',
|
||||
}))}
|
||||
options={[
|
||||
{ value: 'hen_day', label: 'Hen Day' },
|
||||
{ value: 'hen_house', label: 'Hen House' },
|
||||
{ value: 'uniformity', label: 'Uniformity' },
|
||||
{ value: 'egg_weight', label: 'Egg Weight' },
|
||||
{ value: 'egg_mass', label: 'Egg Mass' },
|
||||
]}
|
||||
isMulti
|
||||
onChange={(selected: OptionType | OptionType[] | null) => {
|
||||
const values = Array.isArray(selected)
|
||||
? selected.map((item) => String(item.value))
|
||||
: [];
|
||||
setSelectedStandards(
|
||||
values.length > 0 ? values : ['hen_day']
|
||||
);
|
||||
}}
|
||||
isError={
|
||||
Boolean(formik.errors.standard_productions) &&
|
||||
Boolean(formik.touched.standard_productions)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Periode Perbandingan */}
|
||||
<div className='px-4'>
|
||||
<label className='block mb-3'>Periode Perbandingan</label>
|
||||
<div className='grid grid-cols-4 gap-2'>
|
||||
<Button
|
||||
variant={selectedPeriod === 'daily' ? 'active' : 'soft'}
|
||||
type='button'
|
||||
className='rounded-lg'
|
||||
onClick={() => setSelectedPeriod('daily')}
|
||||
>
|
||||
Harian
|
||||
</Button>
|
||||
<Button
|
||||
variant={selectedPeriod === 'weekly' ? 'active' : 'soft'}
|
||||
type='button'
|
||||
className='rounded-lg'
|
||||
onClick={() => setSelectedPeriod('weekly')}
|
||||
>
|
||||
Mingguan
|
||||
</Button>
|
||||
<Button
|
||||
variant={selectedPeriod === 'monthly' ? 'active' : 'soft'}
|
||||
type='button'
|
||||
className='rounded-lg'
|
||||
onClick={() => setSelectedPeriod('monthly')}
|
||||
>
|
||||
Bulanan
|
||||
</Button>
|
||||
<Button
|
||||
variant={selectedPeriod === 'yearly' ? 'active' : 'soft'}
|
||||
type='button'
|
||||
className='rounded-lg'
|
||||
onClick={() => setSelectedPeriod('yearly')}
|
||||
>
|
||||
Tahunan
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className='flex justify-between gap-4 py-4 mt-8 border-t border-gray-300 bg-gray-100'>
|
||||
<Button
|
||||
type='reset'
|
||||
variant='soft'
|
||||
className='ms-4 min-w-36 rounded-lg'
|
||||
onClick={handleResetFilter}
|
||||
>
|
||||
Reset Filter
|
||||
</Button>
|
||||
<Button type='submit' className='me-4 min-w-36 rounded-lg'>
|
||||
Terapkan Filter
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardProduction;
|
||||
@@ -0,0 +1,89 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
Cell,
|
||||
} from 'recharts';
|
||||
import { DashboardProductionEggWeights } from '@/types/api/dashboard/dashboard-production';
|
||||
|
||||
interface EggWeightBarChartProps {
|
||||
data?: DashboardProductionEggWeights[];
|
||||
}
|
||||
|
||||
const EggWeightBarChart = ({ data }: EggWeightBarChartProps) => {
|
||||
// Show loading state if no data
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<div className='w-full h-full'>
|
||||
<h3 className='text-lg font-semibold mb-4'>
|
||||
Rata-rata Berat Telur (EW)
|
||||
</h3>
|
||||
<div className='flex items-center justify-center h-[350px]'>
|
||||
<p className='text-gray-500'>Memuat data...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='w-full h-full'>
|
||||
<h3 className='text-lg font-semibold mb-4'>Rata-rata Berat Telur (EW)</h3>
|
||||
<ResponsiveContainer width='100%' height={350}>
|
||||
<BarChart
|
||||
data={data}
|
||||
margin={{
|
||||
top: 5,
|
||||
right: 30,
|
||||
left: 0,
|
||||
bottom: 5,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid strokeDasharray='3 3' stroke='#e5e7eb' />
|
||||
<XAxis
|
||||
dataKey='flock.name'
|
||||
tick={{ fontSize: 12 }}
|
||||
tickLine={false}
|
||||
axisLine={{ stroke: '#e5e7eb' }}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fontSize: 12 }}
|
||||
tickLine={false}
|
||||
axisLine={{ stroke: '#e5e7eb' }}
|
||||
domain={[0, 'auto']}
|
||||
label={{
|
||||
value: 'Berat (gram)',
|
||||
angle: -90,
|
||||
position: 'insideLeft',
|
||||
style: { fontSize: 12 },
|
||||
}}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
padding: '8px 12px',
|
||||
}}
|
||||
formatter={(value: number | undefined) =>
|
||||
value !== undefined ? [`${value} gram`, ''] : ['', '']
|
||||
}
|
||||
cursor={{ fill: 'rgba(59, 130, 246, 0.1)' }}
|
||||
/>
|
||||
<Bar dataKey='weight' radius={[8, 8, 0, 0]}>
|
||||
{data.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill='#3b82f6' />
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EggWeightBarChart;
|
||||
@@ -0,0 +1,97 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
Cell,
|
||||
} from 'recharts';
|
||||
import { DashboardProductionFcrData } from '@/types/api/dashboard/dashboard-production';
|
||||
|
||||
interface FCRBarChartProps {
|
||||
data?: DashboardProductionFcrData[];
|
||||
}
|
||||
|
||||
// Alternating colors: green and red
|
||||
const colors = ['#10b981', '#ef4444'];
|
||||
|
||||
const FCRBarChart = ({ data }: FCRBarChartProps) => {
|
||||
// Show loading state if no data
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<div className='w-full h-full'>
|
||||
<h3 className='text-lg font-semibold mb-4'>
|
||||
Feed Conversion Ratio (FCR)
|
||||
</h3>
|
||||
<div className='flex items-center justify-center h-[350px]'>
|
||||
<p className='text-gray-500'>Memuat data...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='w-full h-full'>
|
||||
<h3 className='text-lg font-semibold mb-4'>
|
||||
Feed Conversion Ratio (FCR)
|
||||
</h3>
|
||||
<ResponsiveContainer width='100%' height={350}>
|
||||
<BarChart
|
||||
data={data}
|
||||
margin={{
|
||||
top: 5,
|
||||
right: 30,
|
||||
left: 0,
|
||||
bottom: 5,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid strokeDasharray='3 3' stroke='#e5e7eb' />
|
||||
<XAxis
|
||||
dataKey='flock.name'
|
||||
tick={{ fontSize: 12 }}
|
||||
tickLine={false}
|
||||
axisLine={{ stroke: '#e5e7eb' }}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fontSize: 12 }}
|
||||
tickLine={false}
|
||||
axisLine={{ stroke: '#e5e7eb' }}
|
||||
domain={[0, 'auto']}
|
||||
label={{
|
||||
value: 'FCR',
|
||||
angle: -90,
|
||||
position: 'insideLeft',
|
||||
style: { fontSize: 12 },
|
||||
}}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
padding: '8px 12px',
|
||||
}}
|
||||
formatter={(value: number | undefined) =>
|
||||
value !== undefined ? [value.toFixed(2), 'FCR'] : ['', '']
|
||||
}
|
||||
cursor={{ fill: 'rgba(16, 185, 129, 0.1)' }}
|
||||
/>
|
||||
<Bar dataKey='fcr' radius={[8, 8, 0, 0]}>
|
||||
{data.map((entry, index) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
fill={colors[index % colors.length]}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FCRBarChart;
|
||||
@@ -0,0 +1,357 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
} from 'recharts';
|
||||
|
||||
// Sample data in API format
|
||||
const sampleApiData: ProductionChartItem[] = [
|
||||
{
|
||||
date: '2025-12-01T00:00:00Z',
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-002', data: 88 },
|
||||
{ id: 2, name: 'Flock A-001', data: 92 },
|
||||
{ id: 3, name: 'Flock B-001', data: 90 },
|
||||
{ id: 4, name: 'Flock B-002', data: 85 },
|
||||
],
|
||||
},
|
||||
{
|
||||
date: '2025-12-03T00:00:00Z',
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-002', data: 85 },
|
||||
{ id: 2, name: 'Flock A-001', data: 95 },
|
||||
{ id: 3, name: 'Flock B-001', data: 93 },
|
||||
{ id: 4, name: 'Flock B-002', data: 87 },
|
||||
],
|
||||
},
|
||||
{
|
||||
date: '2025-12-05T00:00:00Z',
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-002', data: 82 },
|
||||
{ id: 2, name: 'Flock A-001', data: 98 },
|
||||
{ id: 3, name: 'Flock B-001', data: 91 },
|
||||
{ id: 4, name: 'Flock B-002', data: 84 },
|
||||
],
|
||||
},
|
||||
{
|
||||
date: '2025-12-07T00:00:00Z',
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-002', data: 80 },
|
||||
{ id: 2, name: 'Flock A-001', data: 89 },
|
||||
{ id: 3, name: 'Flock B-001', data: 88 },
|
||||
{ id: 4, name: 'Flock B-002', data: 82 },
|
||||
],
|
||||
},
|
||||
{
|
||||
date: '2025-12-08T00:00:00Z',
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-002', data: 83 },
|
||||
{ id: 2, name: 'Flock A-001', data: 92 },
|
||||
{ id: 3, name: 'Flock B-001', data: 95 },
|
||||
{ id: 4, name: 'Flock B-002', data: 85 },
|
||||
],
|
||||
},
|
||||
{
|
||||
date: '2025-12-11T00:00:00Z',
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-002', data: 81 },
|
||||
{ id: 2, name: 'Flock A-001', data: 88 },
|
||||
{ id: 3, name: 'Flock B-001', data: 92 },
|
||||
{ id: 4, name: 'Flock B-002', data: 83 },
|
||||
],
|
||||
},
|
||||
{
|
||||
date: '2025-12-13T00:00:00Z',
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-002', data: 84 },
|
||||
{ id: 2, name: 'Flock A-001', data: 90 },
|
||||
{ id: 3, name: 'Flock B-001', data: 89 },
|
||||
{ id: 4, name: 'Flock B-002', data: 86 },
|
||||
],
|
||||
},
|
||||
{
|
||||
date: '2025-12-15T00:00:00Z',
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-002', data: 82 },
|
||||
{ id: 2, name: 'Flock A-001', data: 94 },
|
||||
{ id: 3, name: 'Flock B-001', data: 96 },
|
||||
{ id: 4, name: 'Flock B-002', data: 84 },
|
||||
],
|
||||
},
|
||||
{
|
||||
date: '2025-12-17T00:00:00Z',
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-002', data: 80 },
|
||||
{ id: 2, name: 'Flock A-001', data: 91 },
|
||||
{ id: 3, name: 'Flock B-001', data: 93 },
|
||||
{ id: 4, name: 'Flock B-002', data: 82 },
|
||||
],
|
||||
},
|
||||
{
|
||||
date: '2025-12-19T00:00:00Z',
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-002', data: 79 },
|
||||
{ id: 2, name: 'Flock A-001', data: 88 },
|
||||
{ id: 3, name: 'Flock B-001', data: 90 },
|
||||
{ id: 4, name: 'Flock B-002', data: 81 },
|
||||
],
|
||||
},
|
||||
{
|
||||
date: '2025-12-21T00:00:00Z',
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-002', data: 81 },
|
||||
{ id: 2, name: 'Flock A-001', data: 97 },
|
||||
{ id: 3, name: 'Flock B-001', data: 92 },
|
||||
{ id: 4, name: 'Flock B-002', data: 83 },
|
||||
],
|
||||
},
|
||||
{
|
||||
date: '2025-12-23T00:00:00Z',
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-002', data: 83 },
|
||||
{ id: 2, name: 'Flock A-001', data: 95 },
|
||||
{ id: 3, name: 'Flock B-001', data: 98 },
|
||||
{ id: 4, name: 'Flock B-002', data: 85 },
|
||||
],
|
||||
},
|
||||
{
|
||||
date: '2025-12-25T00:00:00Z',
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-002', data: 80 },
|
||||
{ id: 2, name: 'Flock A-001', data: 89 },
|
||||
{ id: 3, name: 'Flock B-001', data: 94 },
|
||||
{ id: 4, name: 'Flock B-002', data: 82 },
|
||||
],
|
||||
},
|
||||
{
|
||||
date: '2025-12-27T00:00:00Z',
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-002', data: 82 },
|
||||
{ id: 2, name: 'Flock A-001', data: 93 },
|
||||
{ id: 3, name: 'Flock B-001', data: 96 },
|
||||
{ id: 4, name: 'Flock B-002', data: 84 },
|
||||
],
|
||||
},
|
||||
{
|
||||
date: '2025-12-28T00:00:00Z',
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-002', data: 85 },
|
||||
{ id: 2, name: 'Flock A-001', data: 96 },
|
||||
{ id: 3, name: 'Flock B-001', data: 95 },
|
||||
{ id: 4, name: 'Flock B-002', data: 87 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Helper function to format date based on period
|
||||
const formatDateByPeriod = (
|
||||
dateString: string,
|
||||
period: 'daily' | 'weekly' | 'monthly' | 'yearly'
|
||||
): string => {
|
||||
const date = new Date(dateString);
|
||||
const monthNames = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'Mei',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Agu',
|
||||
'Sep',
|
||||
'Okt',
|
||||
'Nov',
|
||||
'Des',
|
||||
];
|
||||
|
||||
switch (period) {
|
||||
case 'daily':
|
||||
// Format: "1 Des"
|
||||
return `${date.getDate()} ${monthNames[date.getMonth()]}`;
|
||||
|
||||
case 'weekly':
|
||||
// Format: "Week 1 Des"
|
||||
const weekNumber = Math.ceil(date.getDate() / 7);
|
||||
return `Week ${weekNumber} ${monthNames[date.getMonth()]}`;
|
||||
|
||||
case 'monthly':
|
||||
// Format: "Des"
|
||||
return monthNames[date.getMonth()];
|
||||
|
||||
case 'yearly':
|
||||
// Format: "2025"
|
||||
return date.getFullYear().toString();
|
||||
|
||||
default:
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
// Type definitions for API data
|
||||
interface FlockData {
|
||||
id: number;
|
||||
name: string;
|
||||
data: number;
|
||||
}
|
||||
|
||||
interface ProductionChartItem {
|
||||
date: string;
|
||||
flocks: FlockData[];
|
||||
}
|
||||
|
||||
interface ProductionChartsData {
|
||||
production_charts: ProductionChartItem[];
|
||||
}
|
||||
|
||||
// Transform API data to Recharts format
|
||||
const transformProductionData = (apiData: ProductionChartItem[]) => {
|
||||
return apiData.map((item) => {
|
||||
const transformed: Record<string, string | number> = {
|
||||
date: item.date.split('T')[0], // Extract YYYY-MM-DD from ISO string
|
||||
};
|
||||
|
||||
// Add each flock's data as a property
|
||||
item.flocks.forEach((flock) => {
|
||||
transformed[flock.name] = flock.data;
|
||||
});
|
||||
|
||||
return transformed;
|
||||
});
|
||||
};
|
||||
|
||||
interface ProductionLineChartProps {
|
||||
period?: 'daily' | 'weekly' | 'monthly' | 'yearly';
|
||||
data?: ProductionChartItem[]; // Optional API data
|
||||
}
|
||||
|
||||
const ProductionLineChart = ({
|
||||
period = 'daily',
|
||||
data: apiData,
|
||||
}: ProductionLineChartProps) => {
|
||||
// State to track which lines are hidden
|
||||
const [hiddenLines, setHiddenLines] = useState<string[]>([]);
|
||||
|
||||
// Use API data if provided, otherwise use sample data
|
||||
const chartData = apiData
|
||||
? transformProductionData(apiData)
|
||||
: transformProductionData(sampleApiData);
|
||||
|
||||
// Handle legend click to show/hide lines
|
||||
const handleLegendClick = (dataKey: string) => {
|
||||
setHiddenLines((prev) =>
|
||||
prev.includes(dataKey)
|
||||
? prev.filter((key) => key !== dataKey)
|
||||
: [...prev, dataKey]
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='w-full h-full'>
|
||||
<h3 className='text-lg font-semibold mb-4'>
|
||||
Performa Produksi per Flock
|
||||
</h3>
|
||||
<ResponsiveContainer width='100%' height={400}>
|
||||
<LineChart
|
||||
data={chartData}
|
||||
margin={{
|
||||
top: 5,
|
||||
right: 30,
|
||||
left: 0,
|
||||
bottom: 5,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid strokeDasharray='3 3' stroke='#e5e7eb' />
|
||||
<XAxis
|
||||
dataKey='date'
|
||||
tick={{ fontSize: 12 }}
|
||||
tickLine={false}
|
||||
axisLine={{ stroke: '#e5e7eb' }}
|
||||
tickFormatter={(value) => formatDateByPeriod(value, period)}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fontSize: 12 }}
|
||||
tickLine={false}
|
||||
axisLine={{ stroke: '#e5e7eb' }}
|
||||
domain={[0, 100]}
|
||||
label={{
|
||||
value: 'Persentase (%)',
|
||||
angle: -90,
|
||||
position: 'insideLeft',
|
||||
style: { fontSize: 12 },
|
||||
}}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
padding: '8px 12px',
|
||||
}}
|
||||
labelFormatter={(value) =>
|
||||
formatDateByPeriod(value as string, period)
|
||||
}
|
||||
/>
|
||||
<Legend
|
||||
wrapperStyle={{
|
||||
paddingTop: '20px',
|
||||
}}
|
||||
iconType='circle'
|
||||
onClick={(e) => {
|
||||
if (e.dataKey) handleLegendClick(e.dataKey as string);
|
||||
}}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
<Line
|
||||
type='monotone'
|
||||
dataKey='Flock A-002'
|
||||
stroke='#3b82f6'
|
||||
strokeWidth={2}
|
||||
dot={{ r: 4, fill: '#3b82f6' }}
|
||||
activeDot={{ r: 6 }}
|
||||
hide={hiddenLines.includes('Flock A-002')}
|
||||
/>
|
||||
<Line
|
||||
type='monotone'
|
||||
dataKey='Flock A-001'
|
||||
stroke='#10b981'
|
||||
strokeWidth={2}
|
||||
dot={{ r: 4, fill: '#10b981' }}
|
||||
activeDot={{ r: 6 }}
|
||||
hide={hiddenLines.includes('Flock A-001')}
|
||||
/>
|
||||
<Line
|
||||
type='monotone'
|
||||
dataKey='Flock B-001'
|
||||
stroke='#f59e0b'
|
||||
strokeWidth={2}
|
||||
dot={{ r: 4, fill: '#f59e0b' }}
|
||||
activeDot={{ r: 6 }}
|
||||
hide={hiddenLines.includes('Flock B-001')}
|
||||
/>
|
||||
<Line
|
||||
type='monotone'
|
||||
dataKey='Flock B-002'
|
||||
stroke='#ef4444'
|
||||
strokeWidth={2}
|
||||
dot={{ r: 4, fill: '#ef4444' }}
|
||||
activeDot={{ r: 6 }}
|
||||
hide={hiddenLines.includes('Flock B-002')}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductionLineChart;
|
||||
|
||||
// Export types for external use
|
||||
export type { FlockData, ProductionChartItem, ProductionChartsData };
|
||||
@@ -0,0 +1,107 @@
|
||||
import Card from '@/components/Card';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { DashboardProductionStatisticsData } from '@/types/api/dashboard/dashboard-production';
|
||||
import { formatCurrency } from '@/lib/helper';
|
||||
|
||||
interface ProductionStatProps {
|
||||
data?: DashboardProductionStatisticsData[];
|
||||
}
|
||||
|
||||
const ProductionStat = ({ data }: ProductionStatProps) => {
|
||||
// Helper function to get icon based on title
|
||||
const getIcon = (title: string) => {
|
||||
if (title.toLowerCase().includes('keuangan'))
|
||||
return 'heroicons:currency-dollar';
|
||||
if (title.toLowerCase().includes('penjualan'))
|
||||
return 'heroicons:arrow-trending-up';
|
||||
if (title.toLowerCase().includes('pembelian'))
|
||||
return 'heroicons:shopping-cart';
|
||||
if (title.toLowerCase().includes('overhead')) return 'heroicons:calculator';
|
||||
return 'heroicons:chart-bar';
|
||||
};
|
||||
|
||||
// Helper function to get icon background color
|
||||
const getIconBgColor = (title: string) => {
|
||||
if (title.toLowerCase().includes('keuangan')) return 'bg-blue-500';
|
||||
if (title.toLowerCase().includes('penjualan')) return 'bg-green-500';
|
||||
if (title.toLowerCase().includes('pembelian')) return 'bg-orange-500';
|
||||
if (title.toLowerCase().includes('overhead')) return 'bg-purple-500';
|
||||
return 'bg-gray-500';
|
||||
};
|
||||
|
||||
// Show loading state if no data
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<section className='grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4'>
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<Card
|
||||
key={i}
|
||||
variant='bordered'
|
||||
className={{ wrapper: 'w-full', body: 'p-4' }}
|
||||
>
|
||||
<div className='animate-pulse'>
|
||||
<div className='h-4 bg-gray-200 rounded w-1/2 mb-2'></div>
|
||||
<div className='h-6 bg-gray-200 rounded w-3/4 mb-1'></div>
|
||||
<div className='h-4 bg-gray-200 rounded w-1/3'></div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className='grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4'>
|
||||
{data.map((stat, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
variant='bordered'
|
||||
className={{ wrapper: 'w-full', body: 'p-4' }}
|
||||
>
|
||||
<div className='flex items-start justify-between'>
|
||||
<div className='flex-1'>
|
||||
<p className='text-sm text-gray-600 mb-2'>{stat.title}</p>
|
||||
<p className='text-xl font-bold text-gray-900 mb-1'>
|
||||
{formatCurrency(stat.value)}
|
||||
</p>
|
||||
<p
|
||||
className={`text-sm flex items-center gap-1 ${
|
||||
stat.changeType === 'increase'
|
||||
? 'text-green-600'
|
||||
: 'text-red-600'
|
||||
}`}
|
||||
>
|
||||
<Icon
|
||||
icon={
|
||||
stat.changeType === 'increase'
|
||||
? 'heroicons:arrow-trending-up'
|
||||
: 'heroicons:arrow-trending-down'
|
||||
}
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
{stat.change > 0 ? '+' : ''}
|
||||
{stat.change}% vs{' '}
|
||||
{stat.period === 'monthly' ? 'bulan lalu' : 'periode lalu'}
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex-shrink-0'>
|
||||
<div
|
||||
className={`w-12 h-12 rounded-lg ${getIconBgColor(stat.title)} flex items-center justify-center`}
|
||||
>
|
||||
<Icon
|
||||
icon={getIcon(stat.title)}
|
||||
width={24}
|
||||
height={24}
|
||||
className='text-white'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductionStat;
|
||||
@@ -0,0 +1,691 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
} from 'recharts';
|
||||
|
||||
// Type definitions for API data
|
||||
interface FlockData {
|
||||
id: number;
|
||||
name: string;
|
||||
data: number;
|
||||
}
|
||||
|
||||
interface StandardData {
|
||||
name: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface StandardChartItem {
|
||||
week: number;
|
||||
standards: StandardData[];
|
||||
flocks: FlockData[];
|
||||
}
|
||||
|
||||
// Sample data in API format
|
||||
const sampleApiData: StandardChartItem[] = [
|
||||
{
|
||||
week: 18,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 40 },
|
||||
{ name: 'hen_house', value: 38 },
|
||||
{ name: 'uniformity', value: 85 },
|
||||
{ name: 'egg_weight', value: 52 },
|
||||
{ name: 'egg_mass', value: 20 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 38 },
|
||||
{ id: 2, name: 'Flock A-002', data: 37 },
|
||||
{ id: 3, name: 'Flock B-001', data: 39 },
|
||||
{ id: 4, name: 'Flock B-002', data: 36 },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 20,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 45 },
|
||||
{ name: 'hen_house', value: 43 },
|
||||
{ name: 'uniformity', value: 86 },
|
||||
{ name: 'egg_weight', value: 54 },
|
||||
{ name: 'egg_mass', value: 24 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 43 },
|
||||
{ id: 2, name: 'Flock A-002', data: 42 },
|
||||
{ id: 3, name: 'Flock B-001', data: 44 },
|
||||
{ id: 4, name: 'Flock B-002', data: 41 },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 22,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 48 },
|
||||
{ name: 'hen_house', value: 46 },
|
||||
{ name: 'uniformity', value: 87 },
|
||||
{ name: 'egg_weight', value: 55 },
|
||||
{ name: 'egg_mass', value: 26 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 47 },
|
||||
{ id: 2, name: 'Flock A-002', data: 46 },
|
||||
{ id: 3, name: 'Flock B-001', data: 48 },
|
||||
{ id: 4, name: 'Flock B-002', data: 45 },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 24,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 50 },
|
||||
{ name: 'hen_house', value: 48 },
|
||||
{ name: 'uniformity', value: 88 },
|
||||
{ name: 'egg_weight', value: 56 },
|
||||
{ name: 'egg_mass', value: 28 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 49 },
|
||||
{ id: 2, name: 'Flock A-002', data: 48 },
|
||||
{ id: 3, name: 'Flock B-001', data: 50 },
|
||||
{ id: 4, name: 'Flock B-002', data: 47 },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 26,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 52 },
|
||||
{ name: 'hen_house', value: 50 },
|
||||
{ name: 'uniformity', value: 89 },
|
||||
{ name: 'egg_weight', value: 57 },
|
||||
{ name: 'egg_mass', value: 30 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 50 },
|
||||
{ id: 2, name: 'Flock A-002', data: 49 },
|
||||
{ id: 3, name: 'Flock B-001', data: 51 },
|
||||
{ id: 4, name: 'Flock B-002', data: 48 },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 28,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 55 },
|
||||
{ name: 'hen_house', value: 53 },
|
||||
{ name: 'uniformity', value: 90 },
|
||||
{ name: 'egg_weight', value: 58 },
|
||||
{ name: 'egg_mass', value: 32 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 53 },
|
||||
{ id: 2, name: 'Flock A-002', data: 52 },
|
||||
{ id: 3, name: 'Flock B-001', data: 54 },
|
||||
{ id: 4, name: 'Flock B-002', data: 51 },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 30,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 58 },
|
||||
{ name: 'hen_house', value: 56 },
|
||||
{ name: 'uniformity', value: 91 },
|
||||
{ name: 'egg_weight', value: 59 },
|
||||
{ name: 'egg_mass', value: 34 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 55 },
|
||||
{ id: 2, name: 'Flock A-002', data: 54 },
|
||||
{ id: 3, name: 'Flock B-001', data: 56 },
|
||||
{ id: 4, name: 'Flock B-002', data: 53 },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 32,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 60 },
|
||||
{ name: 'hen_house', value: 58 },
|
||||
{ name: 'uniformity', value: 92 },
|
||||
{ name: 'egg_weight', value: 60 },
|
||||
{ name: 'egg_mass', value: 36 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 58 },
|
||||
{ id: 2, name: 'Flock A-002', data: 57 },
|
||||
{ id: 3, name: 'Flock B-001', data: 59 },
|
||||
{ id: 4, name: 'Flock B-002', data: 56 },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 34,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 62 },
|
||||
{ name: 'hen_house', value: 60 },
|
||||
{ name: 'uniformity', value: 92 },
|
||||
{ name: 'egg_weight', value: 61 },
|
||||
{ name: 'egg_mass', value: 38 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 60 },
|
||||
{ id: 2, name: 'Flock A-002', data: 59 },
|
||||
{ id: 3, name: 'Flock B-001', data: 61 },
|
||||
{ id: 4, name: 'Flock B-002', data: 58 },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 36,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 64 },
|
||||
{ name: 'hen_house', value: 62 },
|
||||
{ name: 'uniformity', value: 93 },
|
||||
{ name: 'egg_weight', value: 62 },
|
||||
{ name: 'egg_mass', value: 40 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 62 },
|
||||
{ id: 2, name: 'Flock A-002', data: 61 },
|
||||
{ id: 3, name: 'Flock B-001', data: 63 },
|
||||
{ id: 4, name: 'Flock B-002', data: 60 },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 38,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 66 },
|
||||
{ name: 'hen_house', value: 64 },
|
||||
{ name: 'uniformity', value: 93 },
|
||||
{ name: 'egg_weight', value: 63 },
|
||||
{ name: 'egg_mass', value: 42 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 64 },
|
||||
{ id: 2, name: 'Flock A-002', data: 63 },
|
||||
{ id: 3, name: 'Flock B-001', data: 65 },
|
||||
{ id: 4, name: 'Flock B-002', data: 62 },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 40,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 68 },
|
||||
{ name: 'hen_house', value: 66 },
|
||||
{ name: 'uniformity', value: 94 },
|
||||
{ name: 'egg_weight', value: 64 },
|
||||
{ name: 'egg_mass', value: 44 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 66 },
|
||||
{ id: 2, name: 'Flock A-002', data: 65 },
|
||||
{ id: 3, name: 'Flock B-001', data: 67 },
|
||||
{ id: 4, name: 'Flock B-002', data: 64 },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 42,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 70 },
|
||||
{ name: 'hen_house', value: 68 },
|
||||
{ name: 'uniformity', value: 94 },
|
||||
{ name: 'egg_weight', value: 65 },
|
||||
{ name: 'egg_mass', value: 46 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 68 },
|
||||
{ id: 2, name: 'Flock A-002', data: 67 },
|
||||
{ id: 3, name: 'Flock B-001', data: 69 },
|
||||
{ id: 4, name: 'Flock B-002', data: 66 },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 44,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 72 },
|
||||
{ name: 'hen_house', value: 70 },
|
||||
{ name: 'uniformity', value: 95 },
|
||||
{ name: 'egg_weight', value: 66 },
|
||||
{ name: 'egg_mass', value: 48 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 70 },
|
||||
{ id: 2, name: 'Flock A-002', data: 69 },
|
||||
{ id: 3, name: 'Flock B-001', data: 71 },
|
||||
{ id: 4, name: 'Flock B-002', data: 68 },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 46,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 74 },
|
||||
{ name: 'hen_house', value: 72 },
|
||||
{ name: 'uniformity', value: 95 },
|
||||
{ name: 'egg_weight', value: 67 },
|
||||
{ name: 'egg_mass', value: 50 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 72 },
|
||||
{ id: 2, name: 'Flock A-002', data: 71 },
|
||||
{ id: 3, name: 'Flock B-001', data: 73 },
|
||||
{ id: 4, name: 'Flock B-002', data: 70 },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 48,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 76 },
|
||||
{ name: 'hen_house', value: 74 },
|
||||
{ name: 'uniformity', value: 95 },
|
||||
{ name: 'egg_weight', value: 68 },
|
||||
{ name: 'egg_mass', value: 52 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 74 },
|
||||
{ id: 2, name: 'Flock A-002', data: 73 },
|
||||
{ id: 3, name: 'Flock B-001', data: 75 },
|
||||
{ id: 4, name: 'Flock B-002', data: 72 },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 50,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 78 },
|
||||
{ name: 'hen_house', value: 76 },
|
||||
{ name: 'uniformity', value: 96 },
|
||||
{ name: 'egg_weight', value: 69 },
|
||||
{ name: 'egg_mass', value: 54 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 76 },
|
||||
{ id: 2, name: 'Flock A-002', data: 75 },
|
||||
{ id: 3, name: 'Flock B-001', data: 77 },
|
||||
{ id: 4, name: 'Flock B-002', data: 74 },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 52,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 80 },
|
||||
{ name: 'hen_house', value: 78 },
|
||||
{ name: 'uniformity', value: 96 },
|
||||
{ name: 'egg_weight', value: 70 },
|
||||
{ name: 'egg_mass', value: 56 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 78 },
|
||||
{ id: 2, name: 'Flock A-002', data: 77 },
|
||||
{ id: 3, name: 'Flock B-001', data: 79 },
|
||||
{ id: 4, name: 'Flock B-002', data: 76 },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 54,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 82 },
|
||||
{ name: 'hen_house', value: 80 },
|
||||
{ name: 'uniformity', value: 96 },
|
||||
{ name: 'egg_weight', value: 71 },
|
||||
{ name: 'egg_mass', value: 58 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 80 },
|
||||
{ id: 2, name: 'Flock A-002', data: 79 },
|
||||
{ id: 3, name: 'Flock B-001', data: 81 },
|
||||
{ id: 4, name: 'Flock B-002', data: 78 },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 56,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 84 },
|
||||
{ name: 'hen_house', value: 82 },
|
||||
{ name: 'uniformity', value: 97 },
|
||||
{ name: 'egg_weight', value: 72 },
|
||||
{ name: 'egg_mass', value: 60 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 82 },
|
||||
{ id: 2, name: 'Flock A-002', data: 81 },
|
||||
{ id: 3, name: 'Flock B-001', data: 83 },
|
||||
{ id: 4, name: 'Flock B-002', data: 80 },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 58,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 86 },
|
||||
{ name: 'hen_house', value: 84 },
|
||||
{ name: 'uniformity', value: 97 },
|
||||
{ name: 'egg_weight', value: 73 },
|
||||
{ name: 'egg_mass', value: 62 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 84 },
|
||||
{ id: 2, name: 'Flock A-002', data: 83 },
|
||||
{ id: 3, name: 'Flock B-001', data: 85 },
|
||||
{ id: 4, name: 'Flock B-002', data: 82 },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 60,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 88 },
|
||||
{ name: 'hen_house', value: 86 },
|
||||
{ name: 'uniformity', value: 97 },
|
||||
{ name: 'egg_weight', value: 74 },
|
||||
{ name: 'egg_mass', value: 64 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 86 },
|
||||
{ id: 2, name: 'Flock A-002', data: 85 },
|
||||
{ id: 3, name: 'Flock B-001', data: 87 },
|
||||
{ id: 4, name: 'Flock B-002', data: 84 },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 62,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 90 },
|
||||
{ name: 'hen_house', value: 88 },
|
||||
{ name: 'uniformity', value: 98 },
|
||||
{ name: 'egg_weight', value: 75 },
|
||||
{ name: 'egg_mass', value: 66 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 88 },
|
||||
{ id: 2, name: 'Flock A-002', data: 87 },
|
||||
{ id: 3, name: 'Flock B-001', data: 89 },
|
||||
{ id: 4, name: 'Flock B-002', data: 86 },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 64,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 92 },
|
||||
{ name: 'hen_house', value: 90 },
|
||||
{ name: 'uniformity', value: 98 },
|
||||
{ name: 'egg_weight', value: 76 },
|
||||
{ name: 'egg_mass', value: 68 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 90 },
|
||||
{ id: 2, name: 'Flock A-002', data: 89 },
|
||||
{ id: 3, name: 'Flock B-001', data: 91 },
|
||||
{ id: 4, name: 'Flock B-002', data: 88 },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 66,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 94 },
|
||||
{ name: 'hen_house', value: 92 },
|
||||
{ name: 'uniformity', value: 98 },
|
||||
{ name: 'egg_weight', value: 77 },
|
||||
{ name: 'egg_mass', value: 70 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 92 },
|
||||
{ id: 2, name: 'Flock A-002', data: 91 },
|
||||
{ id: 3, name: 'Flock B-001', data: 93 },
|
||||
{ id: 4, name: 'Flock B-002', data: 90 },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 68,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 95 },
|
||||
{ name: 'hen_house', value: 93 },
|
||||
{ name: 'uniformity', value: 98 },
|
||||
{ name: 'egg_weight', value: 78 },
|
||||
{ name: 'egg_mass', value: 72 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 93 },
|
||||
{ id: 2, name: 'Flock A-002', data: 92 },
|
||||
{ id: 3, name: 'Flock B-001', data: 94 },
|
||||
{ id: 4, name: 'Flock B-002', data: 91 },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 70,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 96 },
|
||||
{ name: 'hen_house', value: 94 },
|
||||
{ name: 'uniformity', value: 99 },
|
||||
{ name: 'egg_weight', value: 79 },
|
||||
{ name: 'egg_mass', value: 74 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 94 },
|
||||
{ id: 2, name: 'Flock A-002', data: 93 },
|
||||
{ id: 3, name: 'Flock B-001', data: 95 },
|
||||
{ id: 4, name: 'Flock B-002', data: 92 },
|
||||
],
|
||||
},
|
||||
{
|
||||
week: 72,
|
||||
standards: [
|
||||
{ name: 'hen_day', value: 97 },
|
||||
{ name: 'hen_house', value: 95 },
|
||||
{ name: 'uniformity', value: 99 },
|
||||
{ name: 'egg_weight', value: 80 },
|
||||
{ name: 'egg_mass', value: 76 },
|
||||
],
|
||||
flocks: [
|
||||
{ id: 1, name: 'Flock A-001', data: 95 },
|
||||
{ id: 2, name: 'Flock A-002', data: 94 },
|
||||
{ id: 3, name: 'Flock B-001', data: 96 },
|
||||
{ id: 4, name: 'Flock B-002', data: 93 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Transform API data to Recharts format
|
||||
const transformStandardData = (
|
||||
apiData: StandardChartItem[],
|
||||
selectedStandards: string[] = [
|
||||
'hen_day',
|
||||
'hen_house',
|
||||
'uniformity',
|
||||
'egg_weight',
|
||||
'egg_mass',
|
||||
]
|
||||
) => {
|
||||
return apiData.map((item) => {
|
||||
const transformed: Record<string, number> = {
|
||||
week: item.week,
|
||||
};
|
||||
|
||||
// Add selected standards as properties
|
||||
selectedStandards.forEach((standardName) => {
|
||||
const standardData = item.standards.find((s) => s.name === standardName);
|
||||
if (standardData) {
|
||||
transformed[standardName] = standardData.value;
|
||||
}
|
||||
});
|
||||
|
||||
// Add each flock's data as a property
|
||||
item.flocks.forEach((flock) => {
|
||||
transformed[flock.name] = flock.data;
|
||||
});
|
||||
|
||||
return transformed;
|
||||
});
|
||||
};
|
||||
|
||||
interface StandardLineChartProps {
|
||||
data?: StandardChartItem[];
|
||||
selectedStandards?: string[];
|
||||
}
|
||||
|
||||
const StandardLineChart = ({
|
||||
data: apiData,
|
||||
selectedStandards = [
|
||||
'hen_day',
|
||||
'hen_house',
|
||||
'uniformity',
|
||||
'egg_weight',
|
||||
'egg_mass',
|
||||
],
|
||||
}: StandardLineChartProps) => {
|
||||
// State to track which lines are hidden
|
||||
const [hiddenLines, setHiddenLines] = useState<string[]>([]);
|
||||
|
||||
// Use API data if provided, otherwise use sample data
|
||||
const chartData = apiData
|
||||
? transformStandardData(apiData, selectedStandards)
|
||||
: transformStandardData(sampleApiData, selectedStandards);
|
||||
|
||||
// Handle legend click to show/hide lines
|
||||
const handleLegendClick = (dataKey: string) => {
|
||||
setHiddenLines((prev) =>
|
||||
prev.includes(dataKey)
|
||||
? prev.filter((key) => key !== dataKey)
|
||||
: [...prev, dataKey]
|
||||
);
|
||||
};
|
||||
|
||||
// Standard line colors mapping
|
||||
const standardColors: Record<string, string> = {
|
||||
hen_day: '#94a3b8',
|
||||
hen_house: '#64748b',
|
||||
uniformity: '#475569',
|
||||
egg_weight: '#334155',
|
||||
egg_mass: '#1e293b',
|
||||
};
|
||||
|
||||
// Standard names mapping for display
|
||||
const standardLabels: Record<string, string> = {
|
||||
hen_day: 'Hen Day',
|
||||
hen_house: 'Hen House',
|
||||
uniformity: 'Uniformity',
|
||||
egg_weight: 'Egg Weight',
|
||||
egg_mass: 'Egg Mass',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='w-full h-full'>
|
||||
<h3 className='text-lg font-semibold mb-4'>
|
||||
Perbandingan Henday per Umur
|
||||
</h3>
|
||||
<ResponsiveContainer width='100%' height={400}>
|
||||
<LineChart
|
||||
data={chartData}
|
||||
margin={{
|
||||
top: 5,
|
||||
right: 30,
|
||||
left: 0,
|
||||
bottom: 5,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid strokeDasharray='3 3' stroke='#e5e7eb' />
|
||||
<XAxis
|
||||
dataKey='week'
|
||||
tick={{ fontSize: 12 }}
|
||||
tickLine={false}
|
||||
axisLine={{ stroke: '#e5e7eb' }}
|
||||
label={{
|
||||
value: 'Umur (minggu)',
|
||||
position: 'insideBottom',
|
||||
offset: -5,
|
||||
style: { fontSize: 12 },
|
||||
}}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fontSize: 12 }}
|
||||
tickLine={false}
|
||||
axisLine={{ stroke: '#e5e7eb' }}
|
||||
domain={[0, 100]}
|
||||
label={{
|
||||
value: 'Henday (%)',
|
||||
angle: -90,
|
||||
position: 'insideLeft',
|
||||
style: { fontSize: 12 },
|
||||
}}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid #e5e7eb',
|
||||
borderRadius: '8px',
|
||||
padding: '8px 12px',
|
||||
}}
|
||||
formatter={(value: number | undefined) =>
|
||||
value !== undefined ? [`${value}%`, ''] : ['', '']
|
||||
}
|
||||
labelFormatter={(label) => `Minggu ${label}`}
|
||||
/>
|
||||
<Legend
|
||||
wrapperStyle={{
|
||||
paddingTop: '20px',
|
||||
}}
|
||||
iconType='circle'
|
||||
onClick={(e) => {
|
||||
if (e.dataKey) handleLegendClick(e.dataKey as string);
|
||||
}}
|
||||
style={{ cursor: 'pointer' }}
|
||||
/>
|
||||
{/* Dynamic Standard Lines */}
|
||||
{selectedStandards.map((standardName) => (
|
||||
<Line
|
||||
key={standardName}
|
||||
type='monotone'
|
||||
dataKey={standardName}
|
||||
name={standardLabels[standardName] || standardName}
|
||||
stroke={standardColors[standardName] || '#94a3b8'}
|
||||
strokeWidth={2}
|
||||
strokeDasharray='5 5'
|
||||
dot={{ r: 3, fill: standardColors[standardName] || '#94a3b8' }}
|
||||
activeDot={{ r: 5 }}
|
||||
hide={hiddenLines.includes(standardName)}
|
||||
/>
|
||||
))}
|
||||
{/* Flock Lines */}
|
||||
<Line
|
||||
type='monotone'
|
||||
dataKey='Flock A-002'
|
||||
stroke='#3b82f6'
|
||||
strokeWidth={2}
|
||||
dot={{ r: 4, fill: '#3b82f6' }}
|
||||
activeDot={{ r: 6 }}
|
||||
hide={hiddenLines.includes('Flock A-002')}
|
||||
/>
|
||||
<Line
|
||||
type='monotone'
|
||||
dataKey='Flock A-001'
|
||||
stroke='#10b981'
|
||||
strokeWidth={2}
|
||||
dot={{ r: 4, fill: '#10b981' }}
|
||||
activeDot={{ r: 6 }}
|
||||
hide={hiddenLines.includes('Flock A-001')}
|
||||
/>
|
||||
<Line
|
||||
type='monotone'
|
||||
dataKey='Flock B-001'
|
||||
stroke='#f59e0b'
|
||||
strokeWidth={2}
|
||||
dot={{ r: 4, fill: '#f59e0b' }}
|
||||
activeDot={{ r: 6 }}
|
||||
hide={hiddenLines.includes('Flock B-001')}
|
||||
/>
|
||||
<Line
|
||||
type='monotone'
|
||||
dataKey='Flock B-002'
|
||||
stroke='#ef4444'
|
||||
strokeWidth={2}
|
||||
dot={{ r: 4, fill: '#ef4444' }}
|
||||
activeDot={{ r: 6 }}
|
||||
hide={hiddenLines.includes('Flock B-002')}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StandardLineChart;
|
||||
|
||||
// Export types for external use
|
||||
export type { FlockData, StandardData, StandardChartItem };
|
||||
@@ -0,0 +1,16 @@
|
||||
import * as yup from 'yup';
|
||||
|
||||
const dashboardProductionFilterSchema = yup.object({
|
||||
startDate: yup.string().optional(),
|
||||
endDate: yup.string().optional(),
|
||||
flock: yup.array().optional(),
|
||||
standard_production_id: yup.array().optional(),
|
||||
standard_productions: yup.array().optional(),
|
||||
period: yup.string().optional(),
|
||||
});
|
||||
|
||||
export type DashboardProductionFilterValues = yup.InferType<
|
||||
typeof dashboardProductionFilterSchema
|
||||
>;
|
||||
|
||||
export default dashboardProductionFilterSchema;
|
||||
@@ -308,7 +308,7 @@ export const FINANCE_INITIAL_BALANCE_TYPE_OPTIONS = [
|
||||
{ label: 'Saldo Awal Negatif', value: 'NEGATIVE' },
|
||||
];
|
||||
|
||||
export const FINANCE_TRANSACTION_STATUS = ['PENJUALAN', 'PEMBELIAN'];
|
||||
export const FINANCE_TRANSACTION_STATUS = ['PENJUALAN', 'PEMBELIAN', 'BIAYA'];
|
||||
|
||||
export const FINANCE_INITIAL_BALANCE_STATUS = ['SALDO_AWAL'];
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Dummy data for DashboardProduction
|
||||
* Generated from: dashboard.production.dummy.json
|
||||
*
|
||||
* This file is auto-generated. Do not edit manually.
|
||||
*/
|
||||
|
||||
import { DashboardProductionStatisticsData, DashboardProductionProductionChartsFlocks, DashboardProductionProductionCharts, DashboardProductionStandardProductionsStandards, DashboardProductionStandardProductions, DashboardProductionFcrDataFlock, DashboardProductionEggWeights, DashboardProductionFcrData, DashboardProduction } from '../../types/api/dashboard/dashboard-production';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import dummyData from './dashboard.production.dummy.json';
|
||||
|
||||
/**
|
||||
* Get dummy DashboardProduction data
|
||||
* @returns Promise with BaseApiResponse containing DashboardProduction
|
||||
*/
|
||||
export async function getDummySingle(): Promise<BaseApiResponse<DashboardProduction>> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
code: 200,
|
||||
status: 'success',
|
||||
message: 'Data retrieved successfully',
|
||||
data: dummyData as unknown as DashboardProduction,
|
||||
});
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { BaseApiService } from '@/services/api/base';
|
||||
import { BaseApiResponse } from '@/types/api/api-general';
|
||||
import { DashboardProduction } from '@/types/api/dashboard/dashboard-production';
|
||||
import { getDummySingle } from '@/dummy/dashboard/dashboard.production.dummy';
|
||||
|
||||
class DashboardService extends BaseApiService<
|
||||
DashboardProduction,
|
||||
unknown,
|
||||
unknown
|
||||
> {
|
||||
constructor(basePath: string) {
|
||||
super(basePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch dashboard production data
|
||||
* @param endpoint - The endpoint URL with query parameters
|
||||
* @returns Promise with BaseApiResponse containing DashboardProduction
|
||||
*
|
||||
* Note: Currently using dummy data. When real API is ready,
|
||||
* uncomment the line below and remove getDummySingle() call:
|
||||
* return await this.customRequest<BaseApiResponse<DashboardProduction>>(endpoint);
|
||||
*/
|
||||
async getDashboardProductionFetcher(
|
||||
endpoint: string
|
||||
): Promise<BaseApiResponse<DashboardProduction>> {
|
||||
// For now, we're using dummy data regardless of the endpoint
|
||||
// The endpoint parameter is kept for future API integration
|
||||
console.log('Fetching dashboard data with endpoint:', endpoint);
|
||||
return await getDummySingle();
|
||||
}
|
||||
}
|
||||
|
||||
export const DashboardApi = new DashboardService('/dashboard');
|
||||
@@ -0,0 +1,52 @@
|
||||
export interface DashboardProduction {
|
||||
statistics_data: DashboardProductionStatisticsData[];
|
||||
production_charts: DashboardProductionProductionCharts[];
|
||||
standard_productions: DashboardProductionStandardProductions[];
|
||||
egg_weights: DashboardProductionEggWeights[];
|
||||
fcr_data: DashboardProductionFcrData[];
|
||||
}
|
||||
|
||||
export interface DashboardProductionFcrData {
|
||||
flock: DashboardProductionFcrDataFlock;
|
||||
fcr: number;
|
||||
}
|
||||
|
||||
export interface DashboardProductionEggWeights {
|
||||
flock: DashboardProductionFcrDataFlock;
|
||||
weight: number;
|
||||
}
|
||||
|
||||
export interface DashboardProductionStandardProductions {
|
||||
week: number;
|
||||
standards: DashboardProductionStandardProductionsStandards[];
|
||||
flocks: DashboardProductionProductionChartsFlocks[];
|
||||
}
|
||||
|
||||
export interface DashboardProductionProductionCharts {
|
||||
date: string;
|
||||
flocks: DashboardProductionProductionChartsFlocks[];
|
||||
}
|
||||
|
||||
export interface DashboardProductionStatisticsData {
|
||||
title: string;
|
||||
value: number;
|
||||
change: number;
|
||||
period: string;
|
||||
changeType: string;
|
||||
}
|
||||
|
||||
export interface DashboardProductionFcrDataFlock {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface DashboardProductionStandardProductionsStandards {
|
||||
name: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface DashboardProductionProductionChartsFlocks {
|
||||
id: number;
|
||||
name: string;
|
||||
data: number;
|
||||
}
|
||||
Reference in New Issue
Block a user