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 ( -
-

Dashboard

-
- ); + 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

+
+ +
+ +
+ {/* Rentang Waktu */} +
+ +
+ + + +
+
+ + {/* Flock */} +
+ formik.setFieldValue('flock', selected)} + errorMessage={formik.errors.flock as string} + options={flockOptions} + isLoading={isLoadingFlockOptions} + isMulti + isError={ + Boolean(formik.errors.flock) && Boolean(formik.touched.flock) + } + /> +
+ + {/* Production */} +
+ + 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) + } + /> +
+ + {/* Standard */} +
+ ({ + 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) + } + /> +
+ + {/* Periode Perbandingan */} +
+ +
+ + + + +
+
+ + {/* Action Buttons */} +
+ + +
+
+
+
+ + ); +}; + +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) +

+
+

Memuat data...

+
+
+ ); + } + + 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) +

+
+

Memuat data...

+
+
+ ); + } + + 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) + } + /> + { + if (e.dataKey) handleLegendClick(e.dataKey as string); + }} + style={{ cursor: 'pointer' }} + /> + + + + + + +
+ ); +}; + +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}`} + /> + { + if (e.dataKey) handleLegendClick(e.dataKey as string); + }} + style={{ cursor: 'pointer' }} + /> + {/* Dynamic Standard Lines */} + {selectedStandards.map((standardName) => ( + + ))} + {/* Flock Lines */} + + + + + + +
+ ); +}; + +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 */}