From 907afbb062ab9c6f0a5f2327b69615b013cbff37 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 23 Dec 2025 15:51:26 +0700 Subject: [PATCH 001/124] chore(FE): Add Recharts deps and run tsc in pre-commit --- .husky/pre-commit | 2 +- package-lock.json | 387 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 1 + 3 files changed, 388 insertions(+), 2 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index 3782914b..58b97721 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,3 +1,3 @@ npm run format npm run lint -npm run build \ No newline at end of file +npx tsc --noEmit diff --git a/package-lock.json b/package-lock.json index c29a16a6..43e4a964 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.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.0.tgz", + "integrity": "sha512-dlzb07f5LDY+tzs+iLCSXV2yuhaYfezqyZQc+n6baLECWkOMEWxkECAOnXL0ba7lsA25fM9b2jtzpu/uxo1a7g==", + "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", @@ -1902,6 +2014,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", @@ -3141,6 +3259,127 @@ "license": "MIT", "peer": true }, + "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", "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.5.8.tgz", @@ -3245,6 +3484,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 +3832,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", @@ -4026,6 +4281,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 +4912,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 +4969,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", @@ -6428,7 +6708,8 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-number-format": { "version": "5.4.4", @@ -6440,6 +6721,30 @@ "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", + "peer": true, + "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 +6782,52 @@ "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", + "peer": true + }, + "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 +6894,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 +7620,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", @@ -7635,6 +7998,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", From 035482accc51dc7ae5372ee71baa979d1518bc5d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 23 Dec 2025 16:02:59 +0700 Subject: [PATCH 002/124] feat(FE-317): Add Uniformity menu item to main drawer --- src/config/constant.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/config/constant.ts b/src/config/constant.ts index ebb890a2..954d21a4 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -35,6 +35,11 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ link: '/marketing', icon: 'heroicons-outline:currency-dollar', }, + { + text: 'Uniformity', + link: '/uniformity', + icon: 'heroicons-outline:scale', + }, { text: 'Biaya Operasional', link: '/expense', From 398282b3bf4e0667a98cbb7bbc10e576973c7883 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 23 Dec 2025 16:09:29 +0700 Subject: [PATCH 003/124] feat(FE-316): Add Uniformity page components --- .../pages/uniformity/UniformityChart.tsx | 164 ++++++++++++++++++ .../uniformity/UniformityPageWrapper.tsx | 59 +++++++ .../pages/uniformity/UniformityStat.tsx | 93 ++++++++++ .../pages/uniformity/UniformityTable.tsx | 49 ++++++ 4 files changed, 365 insertions(+) create mode 100644 src/components/pages/uniformity/UniformityChart.tsx create mode 100644 src/components/pages/uniformity/UniformityPageWrapper.tsx create mode 100644 src/components/pages/uniformity/UniformityStat.tsx create mode 100644 src/components/pages/uniformity/UniformityTable.tsx diff --git a/src/components/pages/uniformity/UniformityChart.tsx b/src/components/pages/uniformity/UniformityChart.tsx new file mode 100644 index 00000000..576e47d8 --- /dev/null +++ b/src/components/pages/uniformity/UniformityChart.tsx @@ -0,0 +1,164 @@ +import { + Bar, + BarChart, + Rectangle, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; +import Card from '@/components/Card'; + +interface Payload { + value?: number; + name?: string; + dataKey?: string | number; +} + +interface CustomTooltipProps { + active?: boolean; + payload?: readonly Payload[]; + label?: string | number; +} + +const UniformityChart = () => { + // #region Sample data + const data = [ + { + name: '48-52', + uv: 80, + }, + { + name: '52-56', + uv: 120, + }, + { + name: '56-60', + uv: 160, + }, + { + name: '60-64', + uv: 200, + }, + { + name: '64-68', + uv: 160, + }, + { + name: '68-72', + uv: 120, + }, + { + name: '72-76', + uv: 80, + }, + { + name: '76-80', + uv: 120, + }, + { + name: '84-88', + uv: 160, + }, + { + name: '88-92', + uv: 200, + }, + { + name: '92-96', + uv: 160, + }, + ]; + + const margin = { + top: 20, + right: 30, + left: 20, + bottom: 5, + }; + // #endregion + + function getIntroOfPage(label: string): string { + if (label === 'Page A') { + return "Page A is about men's clothing"; + } + if (label === 'Page B') { + return "Page B is about women's dress"; + } + if (label === 'Page C') { + return "Page C is about women's bag"; + } + if (label === 'Page D') { + return 'Page D is about household goods'; + } + if (label === 'Page E') { + return 'Page E is about food'; + } + if (label === 'Page F') { + return 'Page F is about baby food'; + } + return ''; + } + + function CustomTooltip({ payload, label, active }: CustomTooltipProps) { + if (active && payload && payload.length && label !== undefined) { + const labelStr = String(label); + return ( +
+

{`${labelStr} : ${payload[0].value}`}

+

{getIntroOfPage(labelStr)}

+

+ Anything you want can be displayed here. +

+
+ ); + } + + return null; + } + + return ( +
+ +
+ + + + + + + } + /> + + +
+
+ +
+ Weekly Performance Content +
+
+
+ ); +}; + +export default UniformityChart; diff --git a/src/components/pages/uniformity/UniformityPageWrapper.tsx b/src/components/pages/uniformity/UniformityPageWrapper.tsx new file mode 100644 index 00000000..4141d094 --- /dev/null +++ b/src/components/pages/uniformity/UniformityPageWrapper.tsx @@ -0,0 +1,59 @@ +'use client'; + +import { usePathname, useRouter } from 'next/navigation'; +import Drawer from '@/components/Drawer'; +import React, { ReactNode } from 'react'; +import UniformityTable from '@/components/pages/uniformity/UniformityTable'; +import { useUiStore } from '@/stores/ui/ui.store'; + +export default function UniformityPageWrapper({ + children, +}: { + children: ReactNode; +}) { + const pathname = usePathname(); + const router = useRouter(); + const toggleValidate = useUiStore((s) => s.toggleValidate); + + const isAdd = pathname.includes('/add'); + const isEdit = pathname.includes('/detail/edit'); + const isDetail = pathname.includes('/detail'); + + const isOpen = isAdd || isEdit || isDetail; + + const handleBackdropClick = () => { + const unsub = useUiStore.getState().subscribeIsValid((isValid) => { + if (isValid) { + router.push('/uniformity'); + } + }); + + toggleValidate(); + + setTimeout(() => { + unsub?.(); + }, 100); + }; + + return ( + <> +
+ !isOpen && router.push('/uniformity')} + /> +
+ + { + if (!v) router.push('/uniformity'); + }} + closeOnBackdropClick={isDetail ? true : false} + onBackdropClick={handleBackdropClick} + variant='right' + zIndex='99999' + sidebarContent={isOpen &&
{children}
} + /> + + ); +} diff --git a/src/components/pages/uniformity/UniformityStat.tsx b/src/components/pages/uniformity/UniformityStat.tsx new file mode 100644 index 00000000..e7603e16 --- /dev/null +++ b/src/components/pages/uniformity/UniformityStat.tsx @@ -0,0 +1,93 @@ +import Badge from '@/components/Badge'; +import Card from '@/components/Card'; +import { Icon } from '@iconify/react'; +import { formatNumber } from '@/lib/helper'; + +const UniformityStat = () => { + const statisticsData = [ + { + title: 'Total Population', + value: 1908978, + icon: 'heroicons-outline:inbox-stack', + change: '15.5%', + changeType: 'increase', + }, + { + title: 'Total Uniformity', + value: 954489, + icon: 'heroicons-outline:scale', + change: '50%', + changeType: 'decrease', + }, + { + title: 'Total Depletion', + value: 954489, + icon: 'heroicons-outline:inbox-stack', + change: '15.5%', + changeType: 'increase', + }, + { + title: 'Total Production', + value: 2534, + icon: 'heroicons-outline:inbox-stack', + change: '15.5%', + changeType: 'increase', + }, + ]; + + return ( +
+ {statisticsData.map((stat, index) => ( + +
+ + From last month + + + + {stat.change} + +
+ + } + > +
+
+ +
+
+ {stat.title} + + {formatNumber(stat.value)} + +
+
+
+ ))} +
+ ); +}; + +export default UniformityStat; diff --git a/src/components/pages/uniformity/UniformityTable.tsx b/src/components/pages/uniformity/UniformityTable.tsx new file mode 100644 index 00000000..d5caa220 --- /dev/null +++ b/src/components/pages/uniformity/UniformityTable.tsx @@ -0,0 +1,49 @@ +'use client'; + +import Button from '@/components/Button'; +import UniformityChart from '@/components/pages/uniformity/UniformityChart'; +import UniformityStat from '@/components/pages/uniformity/UniformityStat'; +import { Icon } from '@iconify/react'; + +const UniformityTable = ({ refresh }: { refresh?: () => void }) => { + return ( + <> +
+ + +
+ + + +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
Uniformity Table Component
+
+ + ); +}; + +export default UniformityTable; From 33b8d0a8b0c2f31b91029afcb49c78e698d4f91b Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 23 Dec 2025 16:10:12 +0700 Subject: [PATCH 004/124] feat(FE-316): Add Uniformity page and layout components --- src/app/uniformity/layout.tsx | 10 ++++++++++ src/app/uniformity/page.tsx | 5 +++++ 2 files changed, 15 insertions(+) create mode 100644 src/app/uniformity/layout.tsx create mode 100644 src/app/uniformity/page.tsx diff --git a/src/app/uniformity/layout.tsx b/src/app/uniformity/layout.tsx new file mode 100644 index 00000000..dd239577 --- /dev/null +++ b/src/app/uniformity/layout.tsx @@ -0,0 +1,10 @@ +import { ReactNode } from 'react'; +import UniformityPageWrapper from '@/components/pages/uniformity/UniformityPageWrapper'; + +export default function UniformityLayout({ + children, +}: { + children: ReactNode; +}) { + return {children}; +} diff --git a/src/app/uniformity/page.tsx b/src/app/uniformity/page.tsx new file mode 100644 index 00000000..dd0e4466 --- /dev/null +++ b/src/app/uniformity/page.tsx @@ -0,0 +1,5 @@ +const Uniformity = () => { + return <>; +}; + +export default Uniformity; From 09dd907f884fe7278b61c88ec78622f262367509 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 23 Dec 2025 16:11:02 +0700 Subject: [PATCH 005/124] feat(FE-316): Add Uniformity page and form component --- src/app/uniformity/add/page.tsx | 9 ++++ .../uniformity/form/UniformityForm.schema.ts | 0 .../pages/uniformity/form/UniformityForm.tsx | 53 +++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 src/app/uniformity/add/page.tsx create mode 100644 src/components/pages/uniformity/form/UniformityForm.schema.ts create mode 100644 src/components/pages/uniformity/form/UniformityForm.tsx diff --git a/src/app/uniformity/add/page.tsx b/src/app/uniformity/add/page.tsx new file mode 100644 index 00000000..9931eebe --- /dev/null +++ b/src/app/uniformity/add/page.tsx @@ -0,0 +1,9 @@ +'use client'; + +import UniformityForm from '@/components/pages/uniformity/form/UniformityForm'; + +const AddUniformity = () => { + return ; +}; + +export default AddUniformity; diff --git a/src/components/pages/uniformity/form/UniformityForm.schema.ts b/src/components/pages/uniformity/form/UniformityForm.schema.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx new file mode 100644 index 00000000..02fa4207 --- /dev/null +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -0,0 +1,53 @@ +'use client'; + +import { useEffect } from 'react'; +import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; +import { useUiStore } from '@/stores/ui/ui.store'; + +interface UniformityFormProps { + formType?: 'add' | 'edit'; +} + +const UniformityForm = ({ formType = 'add' }: UniformityFormProps) => { + const subscribeValidate = useUiStore((s) => s.subscribeValidate); + const setIsValid = useUiStore((s) => s.setIsValid); + + useEffect(() => { + const unsub = subscribeValidate(() => { + setIsValid(true); + }); + + return unsub; + }, []); + + return ( + <> +
+ {/* Header */} + + {/* Form Section */} +
+
+

Informasi Umum

+
{ + e.preventDefault(); + }} + >
+
+
+ + ); +}; + +export default UniformityForm; From f23a0144b076d64fe327eb453f643765a6dd24ae Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 23 Dec 2025 16:40:37 +0700 Subject: [PATCH 006/124] refactor(FE-316): Replace tooltip with custom uniformity design --- .../pages/uniformity/UniformityChart.tsx | 46 +++++++------------ 1 file changed, 16 insertions(+), 30 deletions(-) diff --git a/src/components/pages/uniformity/UniformityChart.tsx b/src/components/pages/uniformity/UniformityChart.tsx index 576e47d8..04ea0ca9 100644 --- a/src/components/pages/uniformity/UniformityChart.tsx +++ b/src/components/pages/uniformity/UniformityChart.tsx @@ -22,7 +22,7 @@ interface CustomTooltipProps { } const UniformityChart = () => { - // #region Sample data + // #start Uniformity Sample data const data = [ { name: '48-52', @@ -76,39 +76,20 @@ const UniformityChart = () => { left: 20, bottom: 5, }; - // #endregion - - function getIntroOfPage(label: string): string { - if (label === 'Page A') { - return "Page A is about men's clothing"; - } - if (label === 'Page B') { - return "Page B is about women's dress"; - } - if (label === 'Page C') { - return "Page C is about women's bag"; - } - if (label === 'Page D') { - return 'Page D is about household goods'; - } - if (label === 'Page E') { - return 'Page E is about food'; - } - if (label === 'Page F') { - return 'Page F is about baby food'; - } - return ''; - } + // #end Uniformity Sample data function CustomTooltip({ payload, label, active }: CustomTooltipProps) { if (active && payload && payload.length && label !== undefined) { const labelStr = String(label); return ( -
-

{`${labelStr} : ${payload[0].value}`}

-

{getIntroOfPage(labelStr)}

-

- Anything you want can be displayed here. +

+

Uniformity 2025

+

+

+
+ {payload[0].value} of Birds +
+ {labelStr}

); @@ -129,7 +110,12 @@ const UniformityChart = () => { - + Date: Tue, 23 Dec 2025 17:52:29 +0700 Subject: [PATCH 007/124] refactor(FE-316): Fix header markup and comment out UniformityStat --- src/components/pages/uniformity/UniformityChart.tsx | 4 ++-- src/components/pages/uniformity/UniformityTable.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/pages/uniformity/UniformityChart.tsx b/src/components/pages/uniformity/UniformityChart.tsx index 04ea0ca9..1779d5de 100644 --- a/src/components/pages/uniformity/UniformityChart.tsx +++ b/src/components/pages/uniformity/UniformityChart.tsx @@ -84,13 +84,13 @@ const UniformityChart = () => { return (

Uniformity 2025

-

+

{payload[0].value} of Birds
{labelStr} -

+
); } diff --git a/src/components/pages/uniformity/UniformityTable.tsx b/src/components/pages/uniformity/UniformityTable.tsx index d5caa220..dbf5163f 100644 --- a/src/components/pages/uniformity/UniformityTable.tsx +++ b/src/components/pages/uniformity/UniformityTable.tsx @@ -29,11 +29,11 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
-
+ {/*
-
+
*/}
From 5dd64b99076ec51f3bb19100b485241e73467c77 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 23 Dec 2025 18:33:47 +0700 Subject: [PATCH 008/124] feat(FE-316): Add Gauge and Detail Card to Uniformity Chart --- .../pages/uniformity/UniformityChart.tsx | 106 +++++++++++++++++- 1 file changed, 102 insertions(+), 4 deletions(-) diff --git a/src/components/pages/uniformity/UniformityChart.tsx b/src/components/pages/uniformity/UniformityChart.tsx index 1779d5de..b89ce504 100644 --- a/src/components/pages/uniformity/UniformityChart.tsx +++ b/src/components/pages/uniformity/UniformityChart.tsx @@ -1,6 +1,10 @@ +import React from 'react'; import { Bar, BarChart, + Cell, + Pie, + PieChart, Rectangle, ResponsiveContainer, Tooltip, @@ -8,6 +12,8 @@ import { YAxis, } from 'recharts'; import Card from '@/components/Card'; +import { Icon } from '@iconify/react'; +import { formatNumber } from '@/lib/helper'; interface Payload { value?: number; @@ -21,8 +27,65 @@ interface CustomTooltipProps { label?: string | number; } +interface GaugeChartProps { + value: number; + label: string; +} + +const GaugeChart: React.FC = ({ value, label }) => { + const numberOfSegments = 50; + const filledSegments = Math.round((value / 100) * numberOfSegments); + + const data = Array.from({ length: numberOfSegments }, (_, index) => ({ + name: index, + value: 1, + filled: index < filledSegments, + })); + + const activeColor = '#1890ff'; + const inactiveColor = '#f0f0f0'; + + return ( +
+ + + + {data.map((entry, index) => ( + + ))} + + + +
+ + {value}% + +
+ + {label} + +
+
+
+ ); +}; + const UniformityChart = () => { - // #start Uniformity Sample data const data = [ { name: '48-52', @@ -76,7 +139,6 @@ const UniformityChart = () => { left: 20, bottom: 5, }; - // #end Uniformity Sample data function CustomTooltip({ payload, label, active }: CustomTooltipProps) { if (active && payload && payload.length && label !== undefined) { @@ -139,8 +201,44 @@ const UniformityChart = () => { title='Weekly Performance' className={{ wrapper: 'xl:col-span-1 w-full' }} > -
- Weekly Performance Content +
+
+ +
+ +
+
+ +
+
+
+ Kandang Cirangga + + + Week 2 + +
+
+ + {formatNumber(512)} + + From + + {formatNumber(1024)} + +
+
+
+
From 0774200aa5a66657a0f500ad27eb33b16602c7f7 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 23 Dec 2025 18:52:05 +0700 Subject: [PATCH 009/124] refactor(FE-316): Extract Uniformity charts into components --- .../pages/uniformity/UniformityChart.tsx | 202 +++--------------- .../uniformity/chart/UniformityBarChart.tsx | 91 ++++++++ .../uniformity/chart/UniformityGaugeChart.tsx | 108 ++++++++++ 3 files changed, 230 insertions(+), 171 deletions(-) create mode 100644 src/components/pages/uniformity/chart/UniformityBarChart.tsx create mode 100644 src/components/pages/uniformity/chart/UniformityGaugeChart.tsx diff --git a/src/components/pages/uniformity/UniformityChart.tsx b/src/components/pages/uniformity/UniformityChart.tsx index b89ce504..ba598a59 100644 --- a/src/components/pages/uniformity/UniformityChart.tsx +++ b/src/components/pages/uniformity/UniformityChart.tsx @@ -1,92 +1,25 @@ import React from 'react'; -import { - Bar, - BarChart, - Cell, - Pie, - PieChart, - Rectangle, - ResponsiveContainer, - Tooltip, - XAxis, - YAxis, -} from 'recharts'; import Card from '@/components/Card'; -import { Icon } from '@iconify/react'; -import { formatNumber } from '@/lib/helper'; +import UniformityBarChart from './chart/UniformityBarChart'; +import UniformityGaugeChart from './chart/UniformityGaugeChart'; -interface Payload { - value?: number; - name?: string; - dataKey?: string | number; +interface BarChartData { + name: string; + uv: number; } -interface CustomTooltipProps { - active?: boolean; - payload?: readonly Payload[]; - label?: string | number; -} - -interface GaugeChartProps { +interface GaugeChartData { value: number; label: string; + kandang?: string; + week?: string; + currentValue?: number; + totalValue?: number; } -const GaugeChart: React.FC = ({ value, label }) => { - const numberOfSegments = 50; - const filledSegments = Math.round((value / 100) * numberOfSegments); - - const data = Array.from({ length: numberOfSegments }, (_, index) => ({ - name: index, - value: 1, - filled: index < filledSegments, - })); - - const activeColor = '#1890ff'; - const inactiveColor = '#f0f0f0'; - - return ( -
- - - - {data.map((entry, index) => ( - - ))} - - - -
- - {value}% - -
- - {label} - -
-
-
- ); -}; - const UniformityChart = () => { - const data = [ + // TODO: Replace with actual API call + const barChartData: BarChartData[] = [ { name: '48-52', uv: 80, @@ -133,113 +66,40 @@ const UniformityChart = () => { }, ]; - const margin = { - top: 20, - right: 30, - left: 20, - bottom: 5, + // TODO: Replace with actual API call + const gaugeChartData: GaugeChartData = { + value: 52, + label: 'Uniformity', + kandang: 'Kandang Cirangga', + week: 'Week 2', + currentValue: 512, + totalValue: 1024, }; - function CustomTooltip({ payload, label, active }: CustomTooltipProps) { - if (active && payload && payload.length && label !== undefined) { - const labelStr = String(label); - return ( -
-

Uniformity 2025

-
-
-
- {payload[0].value} of Birds -
- {labelStr} -
-
- ); - } - - return null; - } - return ( -
+
- - - - - - - } - /> - - +
-
-
- -
- -
-
- -
-
-
- Kandang Cirangga - - - Week 2 - -
-
- - {formatNumber(512)} - - From - - {formatNumber(1024)} - -
-
-
-
-
+
); diff --git a/src/components/pages/uniformity/chart/UniformityBarChart.tsx b/src/components/pages/uniformity/chart/UniformityBarChart.tsx new file mode 100644 index 00000000..b37fccf2 --- /dev/null +++ b/src/components/pages/uniformity/chart/UniformityBarChart.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import { + Bar, + BarChart, + Rectangle, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; + +interface Payload { + value?: number; + name?: string; + dataKey?: string | number; +} + +interface CustomTooltipProps { + active?: boolean; + payload?: readonly Payload[]; + label?: string | number; +} + +interface BarChartData { + name: string; + uv: number; +} + +interface UniformityBarChartProps { + data: BarChartData[]; +} + +function CustomTooltip({ payload, label, active }: CustomTooltipProps) { + if (active && payload && payload.length && label !== undefined) { + const labelStr = String(label); + return ( +
+

Uniformity 2025

+
+
+
+ {payload[0].value} of Birds +
+ {labelStr} +
+
+ ); + } + + return null; +} + +const UniformityBarChart: React.FC = ({ data }) => { + const margin = { + top: 20, + right: 30, + left: 20, + bottom: 5, + }; + + return ( + + + + + + + } + /> + + + ); +}; + +export default UniformityBarChart; diff --git a/src/components/pages/uniformity/chart/UniformityGaugeChart.tsx b/src/components/pages/uniformity/chart/UniformityGaugeChart.tsx new file mode 100644 index 00000000..eda3d0ab --- /dev/null +++ b/src/components/pages/uniformity/chart/UniformityGaugeChart.tsx @@ -0,0 +1,108 @@ +import React from 'react'; +import { Cell, Pie, PieChart, ResponsiveContainer } from 'recharts'; +import Card from '@/components/Card'; +import { Icon } from '@iconify/react'; +import { formatNumber } from '@/lib/helper'; + +interface UniformityGaugeChartProps { + value: number; + label: string; + kandang?: string; + week?: string; + currentValue?: number; + totalValue?: number; +} + +const UniformityGaugeChart: React.FC = ({ + value, + label, + kandang, + week, + currentValue, + totalValue, +}) => { + const numberOfSegments = 50; + const filledSegments = Math.round((value / 100) * numberOfSegments); + + const data = Array.from({ length: numberOfSegments }, (_, index) => ({ + name: index, + value: 1, + filled: index < filledSegments, + })); + + const activeColor = '#1890ff'; + const inactiveColor = '#f0f0f0'; + + return ( +
+
+
+ + + + {data.map((entry, index) => ( + + ))} + + + +
+ + {value}% + +
+ + {label} + +
+
+
+
+ +
+
+ +
+
+
+ {kandang} + + + {week} + +
+
+ + {formatNumber(currentValue ?? 0)} + + From + {formatNumber(totalValue ?? 0)} +
+
+
+
+
+ ); +}; + +export default UniformityGaugeChart; From 414d6173416bea8da985c8199290bb0d10b25028 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 23 Dec 2025 20:10:28 +0700 Subject: [PATCH 010/124] refactor(FE-316): Adjust UniformityChart responsive grid --- src/components/pages/uniformity/UniformityChart.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/pages/uniformity/UniformityChart.tsx b/src/components/pages/uniformity/UniformityChart.tsx index ba598a59..06db65d7 100644 --- a/src/components/pages/uniformity/UniformityChart.tsx +++ b/src/components/pages/uniformity/UniformityChart.tsx @@ -77,11 +77,14 @@ const UniformityChart = () => { }; return ( -
+
@@ -90,7 +93,10 @@ const UniformityChart = () => { Date: Tue, 23 Dec 2025 21:21:25 +0700 Subject: [PATCH 011/124] feat(FE-317): Add Uniformity API service and types --- src/services/api/uniformity.ts | 23 +++++++++++++++++++++++ src/types/api/uniformity/uniformity.d.ts | 12 ++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 src/services/api/uniformity.ts create mode 100644 src/types/api/uniformity/uniformity.d.ts diff --git a/src/services/api/uniformity.ts b/src/services/api/uniformity.ts new file mode 100644 index 00000000..4cdca280 --- /dev/null +++ b/src/services/api/uniformity.ts @@ -0,0 +1,23 @@ +import { BaseApiService } from '@/services/api/base'; +import { BaseApiResponse } from '@/types/api/api-general'; +import { Uniformity } from '@/types/api/uniformity/uniformity'; + +export class UniformityApiService extends BaseApiService< + Uniformity, + unknown, + unknown +> { + constructor(basePath: string) { + super(basePath); + } + + async getUniformity(): Promise | undefined> { + return await this.customRequest>(''); + } +} + +// export const UniformityApi = new UniformityApiService('uniformity'); + +export const UniformityApi = new UniformityApiService( + 'http://localhost:4010/api/uniformity' +); diff --git a/src/types/api/uniformity/uniformity.d.ts b/src/types/api/uniformity/uniformity.d.ts new file mode 100644 index 00000000..29479dbf --- /dev/null +++ b/src/types/api/uniformity/uniformity.d.ts @@ -0,0 +1,12 @@ +import { Location } from '@/types/api/location/location'; +import { Kandang } from '@/types/api/kandang/kandang'; +import { BaseMetadata } from '../api-general'; + +export type Uniformity = BaseMetadata & { + id: number; + location: Location; + project_flock_kandang_id: number; + kandang: Kandang; + week: number; + status: 'CREATED' | 'APPROVED' | 'REJECTED'; +}; From 3a2fac013e0fe6b4a67865e3d876a1c41153caa3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 23 Dec 2025 21:57:44 +0700 Subject: [PATCH 012/124] feat(FE-317): Add uniformity field to Uniformity type --- src/types/api/uniformity/uniformity.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types/api/uniformity/uniformity.d.ts b/src/types/api/uniformity/uniformity.d.ts index 29479dbf..2540afd0 100644 --- a/src/types/api/uniformity/uniformity.d.ts +++ b/src/types/api/uniformity/uniformity.d.ts @@ -9,4 +9,5 @@ export type Uniformity = BaseMetadata & { kandang: Kandang; week: number; status: 'CREATED' | 'APPROVED' | 'REJECTED'; + uniformity: number; }; From cb78ec4990251f915b2d095607c7128a0a17269f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 23 Dec 2025 21:58:28 +0700 Subject: [PATCH 013/124] feat(FE-316,317): Enhance UniformityTable with selection and actions --- .../pages/uniformity/UniformityTable.tsx | 380 +++++++++++++++++- 1 file changed, 370 insertions(+), 10 deletions(-) diff --git a/src/components/pages/uniformity/UniformityTable.tsx b/src/components/pages/uniformity/UniformityTable.tsx index dbf5163f..91816751 100644 --- a/src/components/pages/uniformity/UniformityTable.tsx +++ b/src/components/pages/uniformity/UniformityTable.tsx @@ -1,18 +1,182 @@ 'use client'; +import React, { useCallback, useState, useEffect } from 'react'; +import useSWR from 'swr'; +import { Icon } from '@iconify/react'; +import { SortingState, CellContext } from '@tanstack/react-table'; +import { cn } from '@/lib/helper'; import Button from '@/components/Button'; import UniformityChart from '@/components/pages/uniformity/UniformityChart'; import UniformityStat from '@/components/pages/uniformity/UniformityStat'; -import { Icon } from '@iconify/react'; +import { useTableFilter } from '@/services/hooks/useTableFilter'; +import { UniformityApi } from '@/services/api/uniformity'; +import { type Uniformity } from '@/types/api/uniformity/uniformity'; +import { isResponseSuccess } from '@/lib/api-helper'; +import Table from '@/components/Table'; +import Badge from '@/components/Badge'; +import CheckboxInput from '@/components/input/CheckboxInput'; +import RowDropdownOptions from '@/components/table/RowDropdownOptions'; +import RowCollapseOptions from '@/components/table/RowCollapseOptions'; +import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; +import { useModal } from '@/components/Modal'; +import ConfirmationModal from '@/components/modal/ConfirmationModal'; +import toast from 'react-hot-toast'; +import Card from '@/components/Card'; + +const RowOptionsMenu = ({ + type = 'dropdown', + props, + deleteClickHandler, +}: { + type: 'dropdown' | 'collapse'; + props: CellContext; + deleteClickHandler: () => void; +}) => { + return ( + + + + + + ); +}; const UniformityTable = ({ refresh }: { refresh?: () => void }) => { + const { + state: tableFilterState, + setPage, + toQueryString: getTableFilterQueryString, + } = useTableFilter({ + initial: { + search: '', + }, + paramMap: { + page: 'page', + pageSize: 'limit', + search: 'search', + }, + }); + + const [sorting, setSorting] = useState([]); + const [rowSelection, setRowSelection] = useState>({}); + const [selectedUniformity, setSelectedUniformity] = useState< + Uniformity | undefined + >(undefined); + const [isDeleteLoading, setIsDeleteLoading] = useState(false); + + const singleDeleteModal = useModal(); + + const { + data: uniformities, + isLoading, + mutate: refreshUniformities, + } = useSWR( + `${UniformityApi.basePath}${getTableFilterQueryString()}`, + UniformityApi.getAllFetcher + ); + + const isUniformityLocked = useCallback((uniformity: Uniformity): boolean => { + return uniformity.status === 'APPROVED' || uniformity.status === 'REJECTED'; + }, []); + + const singleDeleteHandler = async () => { + setIsDeleteLoading(true); + + await UniformityApi.delete(selectedUniformity?.id as number); + refreshUniformities(); + + singleDeleteModal.closeModal(); + toast.success('Successfully delete Uniformity!'); + setIsDeleteLoading(false); + }; + + useEffect(() => { + if (isResponseSuccess(uniformities) && uniformities.data) { + const newSelection: Record = {}; + + Object.entries(rowSelection).forEach(([rowId, isSelected]) => { + if (isSelected) { + const uniformity = uniformities.data.find( + (r) => r.id === parseInt(rowId) + ); + if (uniformity && !isUniformityLocked(uniformity)) { + newSelection[rowId] = true; + } + } + }); + + if ( + Object.keys(newSelection).length !== Object.keys(rowSelection).length + ) { + setRowSelection(newSelection); + } + } + }, [uniformities, rowSelection, isUniformityLocked, setRowSelection]); + + const getStatusColor = (status: string) => { + switch (status) { + case 'APPROVED': + return 'success'; + case 'REJECTED': + return 'error'; + case 'CREATED': + return 'info'; + default: + return 'info'; + } + }; + + const getStatusText = (status: string) => { + switch (status) { + case 'APPROVED': + return 'Disetujui'; + case 'REJECTED': + return 'Ditolak'; + case 'CREATED': + return 'Pengajuan'; + default: + return status; + } + }; + return ( <>
- +
+ +
diff --git a/src/components/pages/uniformity/UniformityPageWrapper.tsx b/src/components/pages/uniformity/UniformityPageWrapper.tsx index 4141d094..3871e487 100644 --- a/src/components/pages/uniformity/UniformityPageWrapper.tsx +++ b/src/components/pages/uniformity/UniformityPageWrapper.tsx @@ -14,6 +14,7 @@ export default function UniformityPageWrapper({ const pathname = usePathname(); const router = useRouter(); const toggleValidate = useUiStore((s) => s.toggleValidate); + const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen); const isAdd = pathname.includes('/add'); const isEdit = pathname.includes('/detail/edit'); @@ -25,6 +26,7 @@ export default function UniformityPageWrapper({ const unsub = useUiStore.getState().subscribeIsValid((isValid) => { if (isValid) { router.push('/uniformity'); + setExpandedDrawerOpen(false); } }); @@ -46,13 +48,16 @@ export default function UniformityPageWrapper({ { - if (!v) router.push('/uniformity'); + if (!v) { + router.push('/uniformity'); + setExpandedDrawerOpen(false); + } }} closeOnBackdropClick={isDetail ? true : false} onBackdropClick={handleBackdropClick} variant='right' zIndex='99999' - sidebarContent={isOpen &&
{children}
} + sidebarContent={isOpen ?
{children}
: null} /> ); diff --git a/src/components/pages/uniformity/form/ExpandedDrawerForm.tsx b/src/components/pages/uniformity/form/ExpandedDrawerForm.tsx new file mode 100644 index 00000000..cd27a45d --- /dev/null +++ b/src/components/pages/uniformity/form/ExpandedDrawerForm.tsx @@ -0,0 +1,33 @@ +'use client'; + +import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; +import { useUiStore } from '@/stores/ui/ui.store'; + +const ExpandedDrawerForm = () => { + const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen); + + const handleClose = () => { + setExpandedDrawerOpen(false); + }; + + return ( +
+ {/* Header */} + + + {/* Form Section */} +
+
+
+ ); +}; + +export default ExpandedDrawerForm; diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx index 02fa4207..74138b5b 100644 --- a/src/components/pages/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -3,6 +3,9 @@ import { useEffect } from 'react'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import { useUiStore } from '@/stores/ui/ui.store'; +import Button from '@/components/Button'; +import { Icon } from '@iconify/react'; +import ExpandedDrawerForm from '@/components/pages/uniformity/form/ExpandedDrawerForm'; interface UniformityFormProps { formType?: 'add' | 'edit'; @@ -11,6 +14,8 @@ interface UniformityFormProps { const UniformityForm = ({ formType = 'add' }: UniformityFormProps) => { const subscribeValidate = useUiStore((s) => s.subscribeValidate); const setIsValid = useUiStore((s) => s.setIsValid); + const expandedDrawerOpen = useUiStore((s) => s.expandedDrawerOpen); + const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen); useEffect(() => { const unsub = subscribeValidate(() => { @@ -20,8 +25,13 @@ const UniformityForm = ({ formType = 'add' }: UniformityFormProps) => { return unsub; }, []); + const handleOpenExpandedDrawer = () => { + setExpandedDrawerOpen(true); + }; + return ( - <> +
+ {/* Primary Drawer Content */}
{/* Header */} { onSubmit={(e) => { e.preventDefault(); }} - > + > + +
- + + {/* Expanded Drawer - shown when open */} + {expandedDrawerOpen && } +
); }; diff --git a/src/stores/ui/slices/drawer.slice.ts b/src/stores/ui/slices/drawer.slice.ts index b92b60c3..150eff4d 100644 --- a/src/stores/ui/slices/drawer.slice.ts +++ b/src/stores/ui/slices/drawer.slice.ts @@ -37,4 +37,11 @@ export const createDrawerUISlice: StateCreator< callback(Boolean(state.isValid)); }); }, + + expandedDrawerOpen: false, + setExpandedDrawerOpen: (open: boolean) => set({ expandedDrawerOpen: open }), + toggleExpandedDrawer: () => { + const current = get().expandedDrawerOpen; + set({ expandedDrawerOpen: !current }); + }, }); diff --git a/src/types/stores.d.ts b/src/types/stores.d.ts index 37b252fe..5b4c7c6a 100644 --- a/src/types/stores.d.ts +++ b/src/types/stores.d.ts @@ -10,6 +10,9 @@ type DrawerUISlice = { isValid: boolean; setIsValid: (v: boolean) => void; subscribeIsValid: (callback: (isValid: boolean) => void) => () => void; + expandedDrawerOpen: boolean; + setExpandedDrawerOpen: (open: boolean) => void; + toggleExpandedDrawer: () => void; }; export type UIStore = MainUiSlice & DrawerUISlice; From 8c21883aa9d8f6fb51f279f3638de627269a3a5a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 26 Dec 2025 08:38:54 +0700 Subject: [PATCH 026/124] refactor(FE-316): Move UniformityStat to chart folder --- src/components/pages/uniformity/UniformityTable.tsx | 6 +++--- .../pages/uniformity/{ => chart}/UniformityStat.tsx | 0 2 files changed, 3 insertions(+), 3 deletions(-) rename src/components/pages/uniformity/{ => chart}/UniformityStat.tsx (100%) diff --git a/src/components/pages/uniformity/UniformityTable.tsx b/src/components/pages/uniformity/UniformityTable.tsx index da7da693..65decf22 100644 --- a/src/components/pages/uniformity/UniformityTable.tsx +++ b/src/components/pages/uniformity/UniformityTable.tsx @@ -7,7 +7,7 @@ import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; import { cn } from '@/lib/helper'; import Button from '@/components/Button'; import UniformityChart from '@/components/pages/uniformity/UniformityChart'; -import UniformityStat from '@/components/pages/uniformity/UniformityStat'; +// import UniformityStat from '@/components/pages/uniformity/chart/UniformityStat'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { UniformityApi } from '@/services/api/uniformity'; import { type Uniformity } from '@/types/api/uniformity/uniformity'; @@ -367,9 +367,9 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
-
+ {/*
-
+
*/}
diff --git a/src/components/pages/uniformity/UniformityStat.tsx b/src/components/pages/uniformity/chart/UniformityStat.tsx similarity index 100% rename from src/components/pages/uniformity/UniformityStat.tsx rename to src/components/pages/uniformity/chart/UniformityStat.tsx From f5f154883b818785dfd8cf3bd50ed18b62412b73 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 26 Dec 2025 09:49:18 +0700 Subject: [PATCH 027/124] feat(FE-438): Add UniformityBarChartSkeleton and use it --- .../pages/uniformity/UniformityChart.tsx | 142 ++++++++++-------- .../skeleton/UniformityBarChartSkeleton.tsx | 104 +++++++++++++ 2 files changed, 184 insertions(+), 62 deletions(-) create mode 100644 src/components/pages/uniformity/skeleton/UniformityBarChartSkeleton.tsx diff --git a/src/components/pages/uniformity/UniformityChart.tsx b/src/components/pages/uniformity/UniformityChart.tsx index 1db4d0bf..fb65e088 100644 --- a/src/components/pages/uniformity/UniformityChart.tsx +++ b/src/components/pages/uniformity/UniformityChart.tsx @@ -2,6 +2,7 @@ import React from 'react'; import Card from '@/components/Card'; import UniformityBarChart from '@/components/pages/uniformity/chart/UniformityBarChart'; import UniformityGaugeChart from '@/components/pages/uniformity/chart/UniformityGaugeChart'; +import UniformityBarChartSkeleton from './skeleton/UniformityBarChartSkeleton'; interface BarChartData { name: string; @@ -20,50 +21,50 @@ interface GaugeChartData { const UniformityChart = () => { // TODO: Replace with actual API call const barChartData: BarChartData[] = [ - { - name: '48-52', - uv: 80, - }, - { - name: '52-56', - uv: 120, - }, - { - name: '56-60', - uv: 160, - }, - { - name: '60-64', - uv: 200, - }, - { - name: '64-68', - uv: 160, - }, - { - name: '68-72', - uv: 120, - }, - { - name: '72-76', - uv: 80, - }, - { - name: '76-80', - uv: 120, - }, - { - name: '84-88', - uv: 160, - }, - { - name: '88-92', - uv: 200, - }, - { - name: '92-96', - uv: 160, - }, + // { + // name: '48-52', + // uv: 80, + // }, + // { + // name: '52-56', + // uv: 120, + // }, + // { + // name: '56-60', + // uv: 160, + // }, + // { + // name: '60-64', + // uv: 200, + // }, + // { + // name: '64-68', + // uv: 160, + // }, + // { + // name: '68-72', + // uv: 120, + // }, + // { + // name: '72-76', + // uv: 80, + // }, + // { + // name: '76-80', + // uv: 120, + // }, + // { + // name: '84-88', + // uv: 160, + // }, + // { + // name: '88-92', + // uv: 200, + // }, + // { + // name: '92-96', + // uv: 160, + // }, ]; // TODO: Replace with actual API call @@ -87,26 +88,43 @@ const UniformityChart = () => { }} >
- + {barChartData.length === 0 ? ( + + ) : ( + + )}
- - - + {gaugeChartData.value === 0 ? ( + + + + ) : ( + + + + )} ); }; diff --git a/src/components/pages/uniformity/skeleton/UniformityBarChartSkeleton.tsx b/src/components/pages/uniformity/skeleton/UniformityBarChartSkeleton.tsx new file mode 100644 index 00000000..4983c84e --- /dev/null +++ b/src/components/pages/uniformity/skeleton/UniformityBarChartSkeleton.tsx @@ -0,0 +1,104 @@ +import Button from '@/components/Button'; +import { Icon } from '@iconify/react'; + +const LeftLegend = () => { + return ( + <> +
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + ); +}; + +const ChartArea = () => { + const ranges = [ + '48-52', + '52-56', + '56-60', + '60-64', + '64-68', + '68-72', + '72-76', + ]; + + return ( + <> +
+
+
+ {[...Array(5)].map((_, index) => ( +
+ ))} +
+ +
+ {ranges.map((range) => ( +
+ ))} +
+ +
+
+
+
+
+ + ); +}; + +const EmptyState = () => { + return ( + <> +
+
+ +
+ + No Filters Selected + + + Please choose filters to narrow down your results and make your search + easier. + +
+ + ); +}; + +const UniformityBarChartSkeleton = () => { + return ( +
+
+ + +
+ +
+ ); +}; + +export default UniformityBarChartSkeleton; From d9322cc17d7cc157116e8970112537963ea8e4d6 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 26 Dec 2025 10:13:17 +0700 Subject: [PATCH 028/124] refactor(FE-438): Make left legend skeleton DRY and widen gaps --- .../skeleton/UniformityBarChartSkeleton.tsx | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/src/components/pages/uniformity/skeleton/UniformityBarChartSkeleton.tsx b/src/components/pages/uniformity/skeleton/UniformityBarChartSkeleton.tsx index 4983c84e..d0a950b5 100644 --- a/src/components/pages/uniformity/skeleton/UniformityBarChartSkeleton.tsx +++ b/src/components/pages/uniformity/skeleton/UniformityBarChartSkeleton.tsx @@ -4,21 +4,17 @@ import { Icon } from '@iconify/react'; const LeftLegend = () => { return ( <> -
+
-
-
-
-
-
-
-
-
-
-
-
-
+ {[...Array(5)].map((_, index) => ( +
+
+
+ ))}
); @@ -37,8 +33,8 @@ const ChartArea = () => { return ( <> -
-
+
+
{[...Array(5)].map((_, index) => (
{ ))}
-
+
{ranges.map((range) => (
))}
-
+
@@ -92,7 +88,7 @@ const EmptyState = () => { const UniformityBarChartSkeleton = () => { return (
-
+
From ae00f49e643ab28d5ef504fd0558022c12228871 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 26 Dec 2025 10:36:29 +0700 Subject: [PATCH 029/124] feat(FE-438): Add gauge skeleton and use in UniformityChart --- .../pages/uniformity/UniformityChart.tsx | 20 +++-- .../skeleton/UniformityGaugeChartSkeleton.tsx | 81 +++++++++++++++++++ 2 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 src/components/pages/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx diff --git a/src/components/pages/uniformity/UniformityChart.tsx b/src/components/pages/uniformity/UniformityChart.tsx index fb65e088..2b104623 100644 --- a/src/components/pages/uniformity/UniformityChart.tsx +++ b/src/components/pages/uniformity/UniformityChart.tsx @@ -2,7 +2,8 @@ import React from 'react'; import Card from '@/components/Card'; import UniformityBarChart from '@/components/pages/uniformity/chart/UniformityBarChart'; import UniformityGaugeChart from '@/components/pages/uniformity/chart/UniformityGaugeChart'; -import UniformityBarChartSkeleton from './skeleton/UniformityBarChartSkeleton'; +import UniformityBarChartSkeleton from '@/components/pages/uniformity/skeleton/UniformityBarChartSkeleton'; +import UniformityGaugeChartSkeleton from '@/components/pages/uniformity/skeleton/UniformityGaugeChartSkeleton'; interface BarChartData { name: string; @@ -69,14 +70,23 @@ const UniformityChart = () => { // TODO: Replace with actual API call const gaugeChartData: GaugeChartData = { - value: 52, - label: 'Uniformity', + value: 0, + label: '', kandang: 'Kandang Cirangga', week: 'Week 2', currentValue: 512, totalValue: 1024, }; + // const gaugeChartData: GaugeChartData = { + // value: 52, + // label: 'Uniformity', + // kandang: 'Kandang Cirangga', + // week: 'Week 2', + // currentValue: 512, + // totalValue: 1024, + // }; + return (
{ title='Weekly Performance ⓘ' className={{ wrapper: 'xl:col-span-1 2xl:col-span-1 w-full', - body: 'h-96', + body: 'h-110', }} > - + ) : ( = ({}) => { + const numberOfSegments = 50; + const value = 0; + const filledSegments = Math.round((value / 100) * numberOfSegments); + + const data = Array.from({ length: numberOfSegments }, (_, index) => ({ + name: index, + value: 1, + filled: index < filledSegments, + })); + + const activeColor = '#1890ff'; + const inactiveColor = '#f0f0f0'; + + return ( +
+
+
+ + + + {data.map((entry, index) => ( + + ))} + + + +
+
+ +
+ + No Filters Selected + + + Please choose filters to narrow down your results and make your + search easier. + +
+
+
+
+ ); +}; + +export default UniformityGaugeChartSkeleton; From 5f3c3be1f3ddcd131aa07276eef734f50c7b9c36 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 26 Dec 2025 10:55:24 +0700 Subject: [PATCH 030/124] refactor(FE-438): Use skeleton class in UniformityBarChartSkeleton --- .../uniformity/skeleton/UniformityBarChartSkeleton.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/pages/uniformity/skeleton/UniformityBarChartSkeleton.tsx b/src/components/pages/uniformity/skeleton/UniformityBarChartSkeleton.tsx index d0a950b5..4cf706f7 100644 --- a/src/components/pages/uniformity/skeleton/UniformityBarChartSkeleton.tsx +++ b/src/components/pages/uniformity/skeleton/UniformityBarChartSkeleton.tsx @@ -4,7 +4,7 @@ import { Icon } from '@iconify/react'; const LeftLegend = () => { return ( <> -
+
{[...Array(5)].map((_, index) => ( @@ -12,7 +12,7 @@ const LeftLegend = () => { key={`grid-${index}`} className='shrink-0 flex flex-col justify-center mb-10' > -
+
))}
@@ -49,14 +49,13 @@ const ChartArea = () => { {ranges.map((range) => (
))}
-
+
From f1227c9dcb9f44e3e13b865767c61710fd956334 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 26 Dec 2025 10:58:27 +0700 Subject: [PATCH 031/124] feat(FE-438): Add UniformityTable skeleton for empty state --- .../pages/uniformity/UniformityTable.tsx | 2 ++ .../skeleton/UniformityTableSkeleton.tsx | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 src/components/pages/uniformity/skeleton/UniformityTableSkeleton.tsx diff --git a/src/components/pages/uniformity/UniformityTable.tsx b/src/components/pages/uniformity/UniformityTable.tsx index 65decf22..4ff61785 100644 --- a/src/components/pages/uniformity/UniformityTable.tsx +++ b/src/components/pages/uniformity/UniformityTable.tsx @@ -22,6 +22,7 @@ import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import toast from 'react-hot-toast'; import Card from '@/components/Card'; +import UniformityTableSkeleton from './skeleton/UniformityTableSkeleton'; const statusColorMap: Record = { APPROVED: 'bg-[#00D39033]', @@ -414,6 +415,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { bodyColumnClassName: 'px-6 py-3 last:flex last:flex-row last:justify-end', }} + emptyContent={} /> { + return ( +
+
+ +
+ + No Data Available + + + There is no uniformity data displayed. Enter uniformity check data to + get started. + +
+ ); +}; + +export default UniformityTableSkeleton; From 580c35766720fc1511506aefc4175d4fa501d091 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 26 Dec 2025 16:08:04 +0700 Subject: [PATCH 032/124] feat(FE-316): Add Uniformity form with validation and upload --- .../uniformity/form/UniformityForm.schema.ts | 86 +++++ .../pages/uniformity/form/UniformityForm.tsx | 344 +++++++++++++++++- src/services/api/uniformity.ts | 26 +- src/types/api/uniformity/uniformity.d.ts | 8 + 4 files changed, 447 insertions(+), 17 deletions(-) diff --git a/src/components/pages/uniformity/form/UniformityForm.schema.ts b/src/components/pages/uniformity/form/UniformityForm.schema.ts index e69de29b..87e79aa3 100644 --- a/src/components/pages/uniformity/form/UniformityForm.schema.ts +++ b/src/components/pages/uniformity/form/UniformityForm.schema.ts @@ -0,0 +1,86 @@ +import * as Yup from 'yup'; +import { Uniformity } from '@/types/api/uniformity/uniformity'; + +type UniformityFormSchemaType = { + date: string; + location?: { + value: number; + label: string; + } | null; + location_id: number; + project_flock_kandang_id: number; + kandang?: { + value: number; + label: string; + } | null; + kandang_id: number; + files: File | undefined; +}; + +const FileSchema = Yup.mixed() + .test('fileSize', 'Ukuran file maksimal 2 MB', (value): boolean => { + if (!value) return true; + if (value instanceof File) return value.size <= 2 * 1024 * 1024; + return false; + }) + .test('fileType', 'Format file harus Excel', (value): boolean => { + if (!value) return true; + if (value instanceof File) { + const allowedTypes = [ + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'text/csv', + ]; + return allowedTypes.includes(value.type); + } + return false; + }); + +export const UniformityFormSchema: Yup.ObjectSchema = + Yup.object({ + date: Yup.string().required('Tanggal wajib diisi!'), + location: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + location_id: Yup.number() + .required('Location wajib diisi!') + .typeError('Location wajib diisi!'), + project_flock_kandang_id: Yup.number() + .required('Project flock kandang wajib diisi!') + .typeError('Project flock kandang wajib diisi!'), + kandang: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + kandang_id: Yup.number() + .required('Kandang wajib diisi!') + .typeError('Kandang wajib diisi!'), + files: FileSchema.required('File wajib diisi!'), + }); + +export type UniformityFormValues = Yup.InferType; + +export const getUniformityFormInitialValues = ( + initialValues?: Uniformity +): UniformityFormValues => { + return { + date: initialValues?.week ? '' : '', + location: initialValues?.location + ? { + value: initialValues.location.id, + label: initialValues.location.name, + } + : null, + location_id: initialValues?.location?.id ?? 0, + project_flock_kandang_id: initialValues?.project_flock_kandang_id ?? 0, + kandang: initialValues?.kandang + ? { + value: initialValues.kandang.id, + label: initialValues.kandang.name, + } + : null, + kandang_id: initialValues?.kandang?.id ?? 0, + files: undefined, + }; +}; diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx index 74138b5b..33bcb7cc 100644 --- a/src/components/pages/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -1,22 +1,191 @@ 'use client'; -import { useEffect } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useFormik } from 'formik'; +import useSWR from 'swr'; +import { useRouter } from 'next/navigation'; +import { Icon } from '@iconify/react'; +import { toast } from 'react-hot-toast'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import { useUiStore } from '@/stores/ui/ui.store'; import Button from '@/components/Button'; -import { Icon } from '@iconify/react'; +import DateInput from '@/components/input/DateInput'; + +import SelectInput, { + OptionType, + useSelect, +} from '@/components/input/SelectInput'; +import FileInput from '@/components/input/FileInput'; + +import { + UniformityFormSchema, + UniformityFormValues, + getUniformityFormInitialValues, +} from '@/components/pages/uniformity/form/UniformityForm.schema'; +import { LocationApi, KandangApi } from '@/services/api/master-data'; +import { UniformityApi } from '@/services/api/uniformity'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { + Uniformity, + CreateUniformityPayload, +} from '@/types/api/uniformity/uniformity'; import ExpandedDrawerForm from '@/components/pages/uniformity/form/ExpandedDrawerForm'; interface UniformityFormProps { formType?: 'add' | 'edit'; + initialValues?: Uniformity; } -const UniformityForm = ({ formType = 'add' }: UniformityFormProps) => { +const UniformityForm = ({ + formType = 'add', + initialValues, +}: UniformityFormProps) => { + const router = useRouter(); const subscribeValidate = useUiStore((s) => s.subscribeValidate); const setIsValid = useUiStore((s) => s.setIsValid); const expandedDrawerOpen = useUiStore((s) => s.expandedDrawerOpen); const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen); + const [locationSelectInputValue, setLocationSelectInputValue] = useState(''); + const [uniformityFormErrorMessage, setUniformityFormErrorMessage] = + useState(''); + + // ===== SELECT INPUT DATA ===== + const { + setInputValue: setKandangSelectInputValue, + options: kandangOptions, + isLoadingOptions: isLoadingKandangs, + } = useSelect(KandangApi.basePath, 'id', 'name', 'search'); + + // ===== FORM CONFIGURATION ===== + const formikInitialValues = useMemo( + () => getUniformityFormInitialValues(initialValues), + [initialValues] + ); + + const formik = useFormik({ + initialValues: formikInitialValues, + validationSchema: UniformityFormSchema, + validateOnChange: true, + validateOnBlur: true, + validateOnMount: false, + enableReinitialize: true, + onSubmit: async (values) => { + const formData = new FormData(); + formData.append('date', values.date); + formData.append('location_id', values.location_id.toString()); + formData.append( + 'project_flock_kandang_id', + values.project_flock_kandang_id.toString() + ); + formData.append('kandang_id', values.kandang_id.toString()); + + if (values.files) { + formData.append('files[]', values.files); + } + + const res = await UniformityApi.create( + formData as unknown as CreateUniformityPayload + ); + + if (isResponseError(res)) { + setUniformityFormErrorMessage(res.message); + return; + } + + toast.success(res?.message as string); + router.push('/uniformity'); + }, + }); + + // ===== API DATA FETCHING ===== + const locationsUrl = useMemo(() => { + const params = new URLSearchParams({ + search: locationSelectInputValue, + }); + return `${LocationApi.basePath}?${params.toString()}`; + }, [locationSelectInputValue]); + + const { data: locations, isLoading: isLoadingLocations } = useSWR( + locationsUrl, + LocationApi.getAllFetcher + ); + + const locationOptions = useMemo(() => { + if (!locations || !isResponseSuccess(locations)) return []; + return ( + locations.data.map((location) => ({ + value: location.id, + label: location.name, + })) || [] + ); + }, [locations]); + + // ===== FORM HANDLERS ===== + const handleLocationChange = useCallback( + (val: OptionType | OptionType[] | null) => { + const location = val as OptionType | null; + formik.setFieldTouched('location', true); + formik.setFieldValue('location', location); + formik.setFieldTouched('location_id', true); + formik.setFieldValue('location_id', (location as OptionType)?.value || 0); + }, + [formik] + ); + + const handleKandangChange = useCallback( + (val: OptionType | OptionType[] | null) => { + const kandang = val as OptionType | null; + formik.setFieldTouched('kandang', true); + formik.setFieldValue('kandang', kandang); + formik.setFieldTouched('kandang_id', true); + formik.setFieldValue('kandang_id', (kandang as OptionType)?.value || 0); + }, + [formik] + ); + + const handleFileChange = useCallback( + (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + + if (!file) { + formik.setFieldValue('files', undefined); + return; + } + + if (file.size > 2 * 1024 * 1024) { + toast.error(`Ukuran file ${file.name} maksimal 2 MB!`); + return; + } + + const allowedTypes = [ + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'text/csv', + ]; + + if (!allowedTypes.includes(file.type)) { + toast.error(`Format file ${file.name} harus Excel atau CSV!`); + return; + } + + formik.setFieldValue('files', file); + }, + [formik] + ); + + const handleDateChange = useCallback( + (e: React.ChangeEvent) => { + formik.setFieldValue('date', e.target.value); + }, + [formik] + ); + + const handleRemoveFile = useCallback(() => { + formik.setFieldValue('files', undefined); + }, [formik]); + + // ===== EFFECTS ===== useEffect(() => { const unsub = subscribeValidate(() => { setIsValid(true); @@ -31,9 +200,7 @@ const UniformityForm = ({ formType = 'add' }: UniformityFormProps) => { return (
- {/* Primary Drawer Content */}
- {/* Header */} { subtitleClassName='text-sm text-neutral' showDivider /> - {/* Form Section */} +
-

Informasi Umum

-
{ - e.preventDefault(); - }} - > +

Informasi Umum

+ + + {uniformityFormErrorMessage && ( +
+ + {uniformityFormErrorMessage} +
+ )} + + + + + + { + const option = val as OptionType | null; + formik.setFieldValue( + 'project_flock_kandang_id', + option?.value || 0 + ); + }} + options={[ + { value: 1, label: '1' }, + { value: 2, label: '2' }, + { value: 3, label: '3' }, + ]} + isError={ + formik.touched.project_flock_kandang_id && + Boolean(formik.errors.project_flock_kandang_id) + } + errorMessage={formik.errors.project_flock_kandang_id as string} + isClearable + className={{ wrapper: 'w-full' }} + /> + + + +
+ + + {formik.values.files && ( +
+ +
+
+ + + {formik.values.files.name} + + + ({(formik.values.files.size / 1024).toFixed(2)} KB) + +
+ +
+
+ )} +
+ + + + + {formType === 'add' && ( - + )}
- {/* Expanded Drawer - shown when open */} {expandedDrawerOpen && }
); diff --git a/src/services/api/uniformity.ts b/src/services/api/uniformity.ts index 4cdca280..e732d48b 100644 --- a/src/services/api/uniformity.ts +++ b/src/services/api/uniformity.ts @@ -1,10 +1,13 @@ import { BaseApiService } from '@/services/api/base'; import { BaseApiResponse } from '@/types/api/api-general'; -import { Uniformity } from '@/types/api/uniformity/uniformity'; +import { + CreateUniformityPayload, + Uniformity, +} from '@/types/api/uniformity/uniformity'; export class UniformityApiService extends BaseApiService< Uniformity, - unknown, + CreateUniformityPayload, unknown > { constructor(basePath: string) { @@ -14,6 +17,25 @@ export class UniformityApiService extends BaseApiService< async getUniformity(): Promise | undefined> { return await this.customRequest>(''); } + + async createUniformity( + payload: CreateUniformityPayload + ): Promise | undefined> { + const formData = new FormData(); + formData.append('date', payload.date); + formData.append('location_id', payload.location_id.toString()); + formData.append( + 'project_flock_kandang_id', + payload.project_flock_kandang_id.toString() + ); + formData.append('kandang_id', payload.kandang_id.toString()); + + if (payload.files) { + formData.append('files[]', payload.files); + } + + return await this.create(formData as unknown as CreateUniformityPayload); + } } // export const UniformityApi = new UniformityApiService('uniformity'); diff --git a/src/types/api/uniformity/uniformity.d.ts b/src/types/api/uniformity/uniformity.d.ts index 97d2463a..8815b198 100644 --- a/src/types/api/uniformity/uniformity.d.ts +++ b/src/types/api/uniformity/uniformity.d.ts @@ -11,3 +11,11 @@ export type Uniformity = BaseMetadata & { status: 'CREATED' | 'APPROVED' | 'REJECTED'; uniformity: number; }; + +export type CreateUniformityPayload = { + date: string; + location_id: number; + project_flock_kandang_id: number; + kandang_id: number; + files: File; +}; From 31a98286612d52cc0396c809c4845c8ad5c36916 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 26 Dec 2025 16:15:33 +0700 Subject: [PATCH 033/124] refactor(FE-316): Use useSelect for location options --- .../pages/uniformity/form/UniformityForm.tsx | 36 +++++-------------- 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx index 33bcb7cc..0cfc6213 100644 --- a/src/components/pages/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useFormik } from 'formik'; -import useSWR from 'swr'; import { useRouter } from 'next/navigation'; import { Icon } from '@iconify/react'; import { toast } from 'react-hot-toast'; @@ -24,7 +23,7 @@ import { } from '@/components/pages/uniformity/form/UniformityForm.schema'; import { LocationApi, KandangApi } from '@/services/api/master-data'; import { UniformityApi } from '@/services/api/uniformity'; -import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { isResponseError } from '@/lib/api-helper'; import { Uniformity, CreateUniformityPayload, @@ -46,11 +45,16 @@ const UniformityForm = ({ const expandedDrawerOpen = useUiStore((s) => s.expandedDrawerOpen); const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen); - const [locationSelectInputValue, setLocationSelectInputValue] = useState(''); const [uniformityFormErrorMessage, setUniformityFormErrorMessage] = useState(''); // ===== SELECT INPUT DATA ===== + const { + setInputValue: setLocationSelectInputValue, + options: locationOptions, + isLoadingOptions: isLoadingLocations, + } = useSelect(LocationApi.basePath, 'id', 'name', 'search'); + const { setInputValue: setKandangSelectInputValue, options: kandangOptions, @@ -98,29 +102,6 @@ const UniformityForm = ({ }, }); - // ===== API DATA FETCHING ===== - const locationsUrl = useMemo(() => { - const params = new URLSearchParams({ - search: locationSelectInputValue, - }); - return `${LocationApi.basePath}?${params.toString()}`; - }, [locationSelectInputValue]); - - const { data: locations, isLoading: isLoadingLocations } = useSWR( - locationsUrl, - LocationApi.getAllFetcher - ); - - const locationOptions = useMemo(() => { - if (!locations || !isResponseSuccess(locations)) return []; - return ( - locations.data.map((location) => ({ - value: location.id, - label: location.name, - })) || [] - ); - }, [locations]); - // ===== FORM HANDLERS ===== const handleLocationChange = useCallback( (val: OptionType | OptionType[] | null) => { @@ -185,7 +166,7 @@ const UniformityForm = ({ formik.setFieldValue('files', undefined); }, [formik]); - // ===== EFFECTS ===== + // ===== SIDE EFFECTS ===== useEffect(() => { const unsub = subscribeValidate(() => { setIsValid(true); @@ -194,6 +175,7 @@ const UniformityForm = ({ return unsub; }, []); + // ===== EVENT HANDLERS ===== const handleOpenExpandedDrawer = () => { setExpandedDrawerOpen(true); }; From 97c59174012db9342d9683c1a724b56df351fbc6 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 26 Dec 2025 16:21:42 +0700 Subject: [PATCH 034/124] refactor(FE-438): Unsubscribe immediately after validation --- src/components/pages/uniformity/UniformityPageWrapper.tsx | 7 +++---- src/components/pages/uniformity/form/UniformityForm.tsx | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/pages/uniformity/UniformityPageWrapper.tsx b/src/components/pages/uniformity/UniformityPageWrapper.tsx index 3871e487..4e71dc9c 100644 --- a/src/components/pages/uniformity/UniformityPageWrapper.tsx +++ b/src/components/pages/uniformity/UniformityPageWrapper.tsx @@ -26,15 +26,14 @@ export default function UniformityPageWrapper({ const unsub = useUiStore.getState().subscribeIsValid((isValid) => { if (isValid) { router.push('/uniformity'); + unsub?.(); setExpandedDrawerOpen(false); + } else { + unsub?.(); } }); toggleValidate(); - - setTimeout(() => { - unsub?.(); - }, 100); }; return ( diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx index 0cfc6213..da4d7e4f 100644 --- a/src/components/pages/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -40,6 +40,7 @@ const UniformityForm = ({ initialValues, }: UniformityFormProps) => { const router = useRouter(); + const subscribeValidate = useUiStore((s) => s.subscribeValidate); const setIsValid = useUiStore((s) => s.setIsValid); const expandedDrawerOpen = useUiStore((s) => s.expandedDrawerOpen); From 517e8c758c8500ef6155bac3653cd68f46932091 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 26 Dec 2025 17:12:40 +0700 Subject: [PATCH 035/124] refactor(FE-438): Add Project Flock selection and lookup --- .../uniformity/form/UniformityForm.schema.ts | 25 +- .../pages/uniformity/form/UniformityForm.tsx | 231 +++++++++++++++--- 2 files changed, 215 insertions(+), 41 deletions(-) diff --git a/src/components/pages/uniformity/form/UniformityForm.schema.ts b/src/components/pages/uniformity/form/UniformityForm.schema.ts index 87e79aa3..819c6b7c 100644 --- a/src/components/pages/uniformity/form/UniformityForm.schema.ts +++ b/src/components/pages/uniformity/form/UniformityForm.schema.ts @@ -8,7 +8,12 @@ type UniformityFormSchemaType = { label: string; } | null; location_id: number; - project_flock_kandang_id: number; + project_flock?: { + value: number; + label: string; + } | null; + project_flock_id: number; + project_flock_kandang_id: number | null; kandang?: { value: number; label: string; @@ -46,9 +51,14 @@ export const UniformityFormSchema: Yup.ObjectSchema = location_id: Yup.number() .required('Location wajib diisi!') .typeError('Location wajib diisi!'), - project_flock_kandang_id: Yup.number() - .required('Project flock kandang wajib diisi!') - .typeError('Project flock kandang wajib diisi!'), + project_flock: Yup.object({ + value: Yup.number().min(1).required(), + label: Yup.string().required(), + }).nullable(), + project_flock_id: Yup.number() + .required('Project flock wajib diisi!') + .typeError('Project flock wajib diisi!'), + project_flock_kandang_id: Yup.number().optional().nullable().default(null), kandang: Yup.object({ value: Yup.number().min(1).required(), label: Yup.string().required(), @@ -73,6 +83,13 @@ export const getUniformityFormInitialValues = ( } : null, location_id: initialValues?.location?.id ?? 0, + project_flock: initialValues?.project_flock + ? { + value: initialValues.project_flock.id, + label: initialValues.project_flock.flock_name, + } + : null, + project_flock_id: initialValues?.project_flock?.id ?? 0, project_flock_kandang_id: initialValues?.project_flock_kandang_id ?? 0, kandang: initialValues?.kandang ? { diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx index da4d7e4f..7c8ba5d9 100644 --- a/src/components/pages/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -21,14 +21,22 @@ import { UniformityFormValues, getUniformityFormInitialValues, } from '@/components/pages/uniformity/form/UniformityForm.schema'; -import { LocationApi, KandangApi } from '@/services/api/master-data'; +import { LocationApi } from '@/services/api/master-data'; +import { + ProjectFlockApi, + ProjectFlockKandangApi, +} from '@/services/api/production'; import { UniformityApi } from '@/services/api/uniformity'; -import { isResponseError } from '@/lib/api-helper'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { Uniformity, CreateUniformityPayload, } from '@/types/api/uniformity/uniformity'; +import { type BaseApiResponse } from '@/types/api/api-general'; +import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock'; +import { Kandang } from '@/types/api/master-data/kandang'; import ExpandedDrawerForm from '@/components/pages/uniformity/form/ExpandedDrawerForm'; +import useSWR from 'swr'; interface UniformityFormProps { formType?: 'add' | 'edit'; @@ -50,17 +58,142 @@ const UniformityForm = ({ useState(''); // ===== SELECT INPUT DATA ===== + const [selectedLocation, setSelectedLocation] = useState( + null + ); + + const [projectFlockSearchValue, setProjectFlockSearchValue] = useState(''); + const [selectedProjectFlock, setSelectedProjectFlock] = + useState(null); + + const [selectedKandang, setSelectedKandang] = useState( + null + ); + const { setInputValue: setLocationSelectInputValue, options: locationOptions, isLoadingOptions: isLoadingLocations, } = useSelect(LocationApi.basePath, 'id', 'name', 'search'); - const { - setInputValue: setKandangSelectInputValue, - options: kandangOptions, - isLoadingOptions: isLoadingKandangs, - } = useSelect(KandangApi.basePath, 'id', 'name', 'search'); + // ===== FETCH PROJECT FLOCKS DATA ===== + const projectFlocksUrl = useMemo(() => { + const params = new URLSearchParams({ + search: projectFlockSearchValue || '', + limit: '100', + }); + if (selectedLocation) { + params.append('location_id', selectedLocation.value.toString()); + } + return `${ProjectFlockApi.basePath}?${params.toString()}`; + }, [projectFlockSearchValue, selectedLocation]); + + const { data: projectFlocksData, isLoading: isLoadingProjectFlocks } = useSWR( + projectFlocksUrl, + ProjectFlockApi.getAllFetcher + ); + + const projectFlocksDataList = + projectFlocksData?.status === 'success' + ? projectFlocksData.data + : undefined; + + // ===== PROJECT FLOCK OPTIONS ===== + const projectFlockOptions = useMemo(() => { + let options: OptionType[] = []; + + if (isResponseSuccess(projectFlocksData)) { + const flockOptions = + projectFlocksData?.data.map((projectFlock) => ({ + value: projectFlock.id, + label: projectFlock.flock_name || '', + })) || []; + options = options.concat(flockOptions); + } + + return options; + }, [projectFlocksData]); + + // ===== APPROVED PROJECT FLOCK KANDANGS ===== + const approvedProjectFlockKandangsUrl = useMemo(() => { + const params = new URLSearchParams({ + step_name: 'Disetujui', + limit: '100', + }); + return `${ProjectFlockKandangApi.basePath}?${params.toString()}`; + }, []); + + const { data: approvedProjectFlockKandangsData } = useSWR( + approvedProjectFlockKandangsUrl, + ProjectFlockKandangApi.getAllFetcher + ); + + const approvedProjectFlockKandangs = useMemo(() => { + if (!isResponseSuccess(approvedProjectFlockKandangsData)) return []; + return approvedProjectFlockKandangsData.data; + }, [approvedProjectFlockKandangsData]); + + // ===== KANDANG OPTIONS (FILTERED BY SELECTED PROJECT FLOCK) ===== + const kandangOptions = useMemo(() => { + let options: OptionType[] = []; + + if (selectedProjectFlock && projectFlocksDataList) { + const selectedProjectFlockData = projectFlocksDataList.find( + (pf) => pf.id === selectedProjectFlock.value + ); + + if (selectedProjectFlockData?.kandangs) { + const approvedKandangIds = approvedProjectFlockKandangs + .filter((pfk) => pfk.project_flock_id === selectedProjectFlock.value) + .map((pfk) => pfk.kandang_id); + + const kandangOpts = selectedProjectFlockData.kandangs + .filter((kandang: Kandang) => { + if (formType === 'add') { + return approvedKandangIds.includes(kandang.id); + } + return true; + }) + .map((kandang: Kandang) => ({ + value: kandang.id, + label: kandang.name || '', + })); + options = options.concat(kandangOpts); + } + } + + return options; + }, [ + selectedProjectFlock, + projectFlocksDataList, + approvedProjectFlockKandangs, + formType, + ]); + + // ===== PROJECT FLOCK KANDANG LOOKUP ===== + const projectFlockKandangLookupUrl = useMemo(() => { + if (!selectedProjectFlock || !selectedKandang) return null; + const params = new URLSearchParams({ + project_flock_id: selectedProjectFlock.value.toString(), + kandang_id: selectedKandang.value.toString(), + }); + return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`; + }, [selectedProjectFlock, selectedKandang]); + + const { data: projectFlockKandangLookupData } = useSWR( + projectFlockKandangLookupUrl, + projectFlockKandangLookupUrl + ? () => + ProjectFlockApi.getAllFetcher( + projectFlockKandangLookupUrl + ) as Promise> + : null + ); + + const projectFlockKandangLookup = + projectFlockKandangLookupData?.status === 'success' + ? projectFlockKandangLookupData.data + : undefined; // ===== FORM CONFIGURATION ===== const formikInitialValues = useMemo( @@ -76,14 +209,22 @@ const UniformityForm = ({ validateOnMount: false, enableReinitialize: true, onSubmit: async (values) => { + const projectFlockKandangId = projectFlockKandangLookup?.id; + + if (!projectFlockKandangId) { + setUniformityFormErrorMessage( + 'Project Flock Kandang tidak ditemukan. Silakan pilih Project Flock dan Kandang yang valid.' + ); + return; + } + const formData = new FormData(); formData.append('date', values.date); formData.append('location_id', values.location_id.toString()); formData.append( 'project_flock_kandang_id', - values.project_flock_kandang_id.toString() + projectFlockKandangId.toString() ); - formData.append('kandang_id', values.kandang_id.toString()); if (values.files) { formData.append('files[]', values.files); @@ -111,6 +252,35 @@ const UniformityForm = ({ formik.setFieldValue('location', location); formik.setFieldTouched('location_id', true); formik.setFieldValue('location_id', (location as OptionType)?.value || 0); + + setSelectedLocation(location); + setSelectedProjectFlock(null); + setSelectedKandang(null); + + formik.setFieldValue('project_flock', null); + formik.setFieldValue('project_flock_id', 0); + formik.setFieldValue('kandang', null); + formik.setFieldValue('kandang_id', 0); + }, + [formik] + ); + + const handleProjectFlockChange = useCallback( + (val: OptionType | OptionType[] | null) => { + const projectFlock = val as OptionType | null; + formik.setFieldTouched('project_flock', true); + formik.setFieldValue('project_flock', projectFlock); + formik.setFieldTouched('project_flock_id', true); + formik.setFieldValue( + 'project_flock_id', + (projectFlock as OptionType)?.value || 0 + ); + + setSelectedProjectFlock(projectFlock); + setSelectedKandang(null); + + formik.setFieldValue('kandang', null); + formik.setFieldValue('kandang_id', 0); }, [formik] ); @@ -122,6 +292,8 @@ const UniformityForm = ({ formik.setFieldValue('kandang', kandang); formik.setFieldTouched('kandang_id', true); formik.setFieldValue('kandang_id', (kandang as OptionType)?.value || 0); + + setSelectedKandang(kandang); }, [formik] ); @@ -225,7 +397,7 @@ const UniformityForm = ({ { - const option = val as OptionType | null; - formik.setFieldValue( - 'project_flock_kandang_id', - option?.value || 0 - ); - }} - options={[ - { value: 1, label: '1' }, - { value: 2, label: '2' }, - { value: 3, label: '3' }, - ]} + label='Project Flock' + placeholder='Pilih Project Flock...' + value={formik.values.project_flock} + onChange={handleProjectFlockChange} + options={projectFlockOptions} + onInputChange={setProjectFlockSearchValue} + isLoading={isLoadingProjectFlocks} + isDisabled={!formik.values.location_id} isError={ - formik.touched.project_flock_kandang_id && - Boolean(formik.errors.project_flock_kandang_id) + formik.touched.project_flock_id && + Boolean(formik.errors.project_flock_id) } - errorMessage={formik.errors.project_flock_kandang_id as string} + errorMessage={formik.errors.project_flock_id as string} isClearable className={{ wrapper: 'w-full' }} /> @@ -280,8 +438,7 @@ const UniformityForm = ({ value={formik.values.kandang} onChange={handleKandangChange} options={kandangOptions} - onInputChange={setKandangSelectInputValue} - isLoading={isLoadingKandangs} + isDisabled={!formik.values.project_flock_id} isError={ formik.touched.kandang_id && Boolean(formik.errors.kandang_id) } From f58cb4380176521688b33476247b59acfab6d4cf Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 26 Dec 2025 19:17:12 +0700 Subject: [PATCH 036/124] refacotr(FE-438): Unsubscribe validate subscription and close drawer --- src/components/pages/uniformity/form/UniformityForm.tsx | 7 +++++-- src/types/stores.d.ts | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx index 7c8ba5d9..fab49ba5 100644 --- a/src/components/pages/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -345,8 +345,11 @@ const UniformityForm = ({ setIsValid(true); }); - return unsub; - }, []); + return () => { + unsub(); + useUiStore.getState().setExpandedDrawerOpen(false); + }; + }, [subscribeValidate, setIsValid]); // ===== EVENT HANDLERS ===== const handleOpenExpandedDrawer = () => { diff --git a/src/types/stores.d.ts b/src/types/stores.d.ts index 5b4c7c6a..0521b40e 100644 --- a/src/types/stores.d.ts +++ b/src/types/stores.d.ts @@ -6,7 +6,7 @@ type MainUiSlice = { type DrawerUISlice = { triggerValidate: boolean; toggleValidate: () => void; - subscribeValidate: (callback: () => void) => void; + subscribeValidate: (callback: () => void) => () => void; isValid: boolean; setIsValid: (v: boolean) => void; subscribeIsValid: (callback: (isValid: boolean) => void) => () => void; From e6a38c3f654ad01a0fb999284877b3d8be995a14 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 26 Dec 2025 20:29:18 +0700 Subject: [PATCH 037/124] refactor(FE-438): Scope Drawer classes to sm breakpoint --- src/components/Drawer.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Drawer.tsx b/src/components/Drawer.tsx index 3e2a71a2..20f5e1d5 100644 --- a/src/components/Drawer.tsx +++ b/src/components/Drawer.tsx @@ -61,14 +61,14 @@ const Drawer = ({ } else if (variant === 'right') { return { ...baseClassNames, - drawer: cn(baseClassNames.drawer, 'drawer-end'), + drawer: cn(baseClassNames.drawer, 'sm:drawer-end'), drawerSide: cn( baseClassNames.drawerSide, - 'border-l border-solid border-gray-200 drawer-side w-screen top-0 right-0 fixed z-21' + 'border-l border-solid border-gray-200 sm:drawer-side w-screen top-0 right-0 fixed z-21' ), drawerSidebarContent: cn( baseClassNames.drawerSidebarContent, - 'w-full min-w-120 sm:w-fit' + 'w-full sm:min-w-120 sm:w-fit' ), }; } else if (variant === 'left') { @@ -80,7 +80,7 @@ const Drawer = ({ ), drawerSidebarContent: cn( baseClassNames.drawerSidebarContent, - 'w-full min-w-120 sm:w-fit' + 'w-full sm:min-w-120 sm:w-fit' ), }; } From 1843a47d5966e12cd0bcd698285625b4eeac607d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Fri, 26 Dec 2025 21:50:59 +0700 Subject: [PATCH 038/124] refactor(FE-438): Replace FileInput with custom file upload UI --- .../pages/uniformity/form/UniformityForm.tsx | 142 ++++++++++++------ 1 file changed, 99 insertions(+), 43 deletions(-) diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx index fab49ba5..919b28ff 100644 --- a/src/components/pages/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useFormik } from 'formik'; import { useRouter } from 'next/navigation'; import { Icon } from '@iconify/react'; @@ -14,7 +14,6 @@ import SelectInput, { OptionType, useSelect, } from '@/components/input/SelectInput'; -import FileInput from '@/components/input/FileInput'; import { UniformityFormSchema, @@ -57,6 +56,8 @@ const UniformityForm = ({ const [uniformityFormErrorMessage, setUniformityFormErrorMessage] = useState(''); + const fileInputRef = useRef(null); + // ===== SELECT INPUT DATA ===== const [selectedLocation, setSelectedLocation] = useState( null @@ -451,50 +452,105 @@ const UniformityForm = ({ />
- - - {formik.values.files && ( -
- -
-
- - - {formik.values.files.name} + +
+ document.getElementById('file-upload-input')?.click() + } + > + {formik.values.files ? ( +
+
+ +
+ + {formik.values.files.name} + +
+ ) : ( + <> +
+
+ +
+ + Drag file to this area to upload - - ({(formik.values.files.size / 1024).toFixed(2)} KB) + + Upload data file (*.csv)
- -
-
- )} + +
+
+ + Templates + +
+
+ +
+ +
+ + )} +
+ +
) : ( <> -
+
- + {!isNextStep && ( + + )} - - {formType === 'add' && ( - - )} diff --git a/src/stores/ui/slices/drawer.slice.ts b/src/stores/ui/slices/drawer.slice.ts index 15ee1bed..382eaff2 100644 --- a/src/stores/ui/slices/drawer.slice.ts +++ b/src/stores/ui/slices/drawer.slice.ts @@ -45,4 +45,7 @@ export const createDrawerUISlice: StateCreator< expandedDrawerContent: null as ReactNode | null, setExpandedDrawerContent: (content: ReactNode) => set({ expandedDrawerContent: content }), + + isNextStep: false, + setIsNextStep: (isNextStep: boolean) => set({ isNextStep }), }); diff --git a/src/types/stores.d.ts b/src/types/stores.d.ts index 80815ada..7bfa63cd 100644 --- a/src/types/stores.d.ts +++ b/src/types/stores.d.ts @@ -16,6 +16,8 @@ type DrawerUISlice = { setExpandedDrawerOpen: (open: boolean) => void; expandedDrawerContent: ReactNode | null; setExpandedDrawerContent: (content: ReactNode) => void; + isNextStep: boolean; + setIsNextStep: (v: boolean) => void; }; export type UIStore = MainUiSlice & DrawerUISlice; From 0d77aa4a5f5b281369b2f7992f99b77f1773dfaf Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 27 Dec 2025 08:57:01 +0700 Subject: [PATCH 047/124] feat(FE-438): Display date and week in Uniformity table --- src/components/pages/uniformity/UniformityTable.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/pages/uniformity/UniformityTable.tsx b/src/components/pages/uniformity/UniformityTable.tsx index 4ff61785..d616a4a4 100644 --- a/src/components/pages/uniformity/UniformityTable.tsx +++ b/src/components/pages/uniformity/UniformityTable.tsx @@ -4,7 +4,7 @@ import React, { useCallback, useState, useEffect, useMemo } from 'react'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; -import { cn } from '@/lib/helper'; +import { cn, formatDate } from '@/lib/helper'; import Button from '@/components/Button'; import UniformityChart from '@/components/pages/uniformity/UniformityChart'; // import UniformityStat from '@/components/pages/uniformity/chart/UniformityStat'; @@ -261,7 +261,8 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { { accessorKey: 'week', header: 'Tanggal (Week)', - cell: (props) => `Week ${props.row.original.week}`, + cell: (props) => + `${formatDate(props.row.original.date, 'DD MMM YYYY')} (${props.row.original.week})`, }, { accessorKey: 'status', From 751c27b73ed8cd92dfbcf9c25915979712e91e26 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 27 Dec 2025 09:05:12 +0700 Subject: [PATCH 048/124] feat(FE-316): Add delete button to ExpandedDrawerForm header --- .../pages/uniformity/form/ExpandedDrawerForm.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/pages/uniformity/form/ExpandedDrawerForm.tsx b/src/components/pages/uniformity/form/ExpandedDrawerForm.tsx index 74c9b15c..bbad8e6e 100644 --- a/src/components/pages/uniformity/form/ExpandedDrawerForm.tsx +++ b/src/components/pages/uniformity/form/ExpandedDrawerForm.tsx @@ -1,5 +1,8 @@ 'use client'; +import { Icon } from '@iconify/react'; +import Button from '@/components/Button'; +import Tooltip from '@/components/Tooltip'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import { useUiStore } from '@/stores/ui/ui.store'; @@ -23,7 +26,13 @@ const ExpandedDrawerForm = () => { subtitle='Add Body Weight' subtitleClassName='text-sm text-neutral' showDivider - /> + > + + {/* Form Section */}
From 4f22024c82045aab1438e602a2b3586e3f024f09 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 27 Dec 2025 09:12:31 +0700 Subject: [PATCH 049/124] refactor(FE-316): Rename ExpandedDrawerForm to UniformityPreviewForm --- src/components/pages/uniformity/form/UniformityForm.tsx | 4 ++-- .../{ExpandedDrawerForm.tsx => UniformityPreviewForm.tsx} | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename src/components/pages/uniformity/form/{ExpandedDrawerForm.tsx => UniformityPreviewForm.tsx} (94%) diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx index 609a4150..caf415a9 100644 --- a/src/components/pages/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -34,7 +34,7 @@ import { import { type BaseApiResponse } from '@/types/api/api-general'; import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock'; import { Kandang } from '@/types/api/master-data/kandang'; -import ExpandedDrawerForm from '@/components/pages/uniformity/form/ExpandedDrawerForm'; +import UniformityPreviewForm from '@/components/pages/uniformity/form/UniformityPreviewForm'; import useSWR from 'swr'; import { cn } from '@/lib/helper'; @@ -360,7 +360,7 @@ const UniformityForm = ({ useEffect(() => { if (expandedDrawerOpen) { - setExpandedDrawerContent(); + setExpandedDrawerContent(); } else { setExpandedDrawerContent(null); } diff --git a/src/components/pages/uniformity/form/ExpandedDrawerForm.tsx b/src/components/pages/uniformity/form/UniformityPreviewForm.tsx similarity index 94% rename from src/components/pages/uniformity/form/ExpandedDrawerForm.tsx rename to src/components/pages/uniformity/form/UniformityPreviewForm.tsx index bbad8e6e..aeb90bef 100644 --- a/src/components/pages/uniformity/form/ExpandedDrawerForm.tsx +++ b/src/components/pages/uniformity/form/UniformityPreviewForm.tsx @@ -6,7 +6,7 @@ import Tooltip from '@/components/Tooltip'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import { useUiStore } from '@/stores/ui/ui.store'; -const ExpandedDrawerForm = () => { +const UniformityPreviewForm = () => { const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen); const setIsNextStep = useUiStore((s) => s.setIsNextStep); @@ -41,4 +41,4 @@ const ExpandedDrawerForm = () => { ); }; -export default ExpandedDrawerForm; +export default UniformityPreviewForm; From e4d75dad685afad451d6a40246b3093d03e4d087 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 27 Dec 2025 09:17:09 +0700 Subject: [PATCH 050/124] refactor(FE-316): Rename CreateUniformityPayload to VerifyUniformityPayload --- src/components/pages/uniformity/form/UniformityForm.tsx | 4 ++-- src/services/api/uniformity.ts | 8 ++++---- src/types/api/uniformity/uniformity.d.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx index caf415a9..a4ab72bc 100644 --- a/src/components/pages/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -29,7 +29,7 @@ import { UniformityApi } from '@/services/api/uniformity'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { Uniformity, - CreateUniformityPayload, + VerifyUniformityPayload, } from '@/types/api/uniformity/uniformity'; import { type BaseApiResponse } from '@/types/api/api-general'; import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock'; @@ -237,7 +237,7 @@ const UniformityForm = ({ } const res = await UniformityApi.create( - formData as unknown as CreateUniformityPayload + formData as unknown as VerifyUniformityPayload ); if (isResponseError(res)) { diff --git a/src/services/api/uniformity.ts b/src/services/api/uniformity.ts index ebe4093a..aa0b765e 100644 --- a/src/services/api/uniformity.ts +++ b/src/services/api/uniformity.ts @@ -1,13 +1,13 @@ import { BaseApiService } from '@/services/api/base'; import { BaseApiResponse } from '@/types/api/api-general'; import { - CreateUniformityPayload, + VerifyUniformityPayload, Uniformity, } from '@/types/api/uniformity/uniformity'; export class UniformityApiService extends BaseApiService< Uniformity, - CreateUniformityPayload, + VerifyUniformityPayload, unknown > { constructor(basePath: string) { @@ -19,7 +19,7 @@ export class UniformityApiService extends BaseApiService< } async createUniformity( - payload: CreateUniformityPayload + payload: VerifyUniformityPayload ): Promise | undefined> { const formData = new FormData(); formData.append('date', payload.date); @@ -32,7 +32,7 @@ export class UniformityApiService extends BaseApiService< formData.append('file', payload.files); } - return await this.create(formData as unknown as CreateUniformityPayload); + return await this.create(formData as unknown as VerifyUniformityPayload); } } diff --git a/src/types/api/uniformity/uniformity.d.ts b/src/types/api/uniformity/uniformity.d.ts index 3c94c23b..d59520cd 100644 --- a/src/types/api/uniformity/uniformity.d.ts +++ b/src/types/api/uniformity/uniformity.d.ts @@ -12,7 +12,7 @@ export type Uniformity = BaseMetadata & { uniformity: number; }; -export type CreateUniformityPayload = { +export type VerifyUniformityPayload = { date: string; project_flock_kandang_id: number; files: File; From 5f68c05acc8a64e17cdd9e63f188676a0ebf23be Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 27 Dec 2025 18:05:25 +0700 Subject: [PATCH 051/124] refactor(FE-316,438): Wrap Uniformity actions with permission checks --- .../pages/uniformity/UniformityTable.tsx | 79 +++++++++++-------- .../pages/uniformity/form/UniformityForm.tsx | 28 ++++--- .../uniformity/form/UniformityPreviewForm.tsx | 17 ++-- src/config/constant.ts | 1 + src/config/route-permission.ts | 6 ++ 5 files changed, 79 insertions(+), 52 deletions(-) diff --git a/src/components/pages/uniformity/UniformityTable.tsx b/src/components/pages/uniformity/UniformityTable.tsx index d616a4a4..0f5de463 100644 --- a/src/components/pages/uniformity/UniformityTable.tsx +++ b/src/components/pages/uniformity/UniformityTable.tsx @@ -23,6 +23,7 @@ import ConfirmationModal from '@/components/modal/ConfirmationModal'; import toast from 'react-hot-toast'; import Card from '@/components/Card'; import UniformityTableSkeleton from './skeleton/UniformityTableSkeleton'; +import RequirePermission from '@/components/helper/RequirePermission'; const statusColorMap: Record = { APPROVED: 'bg-[#00D39033]', @@ -78,38 +79,44 @@ const RowOptionsMenu = ({ return ( - - - + > + + Detail + + + + + + + + ); }; @@ -348,10 +355,12 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { <>
- + + +
diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx index a4ab72bc..b1387b70 100644 --- a/src/components/pages/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -15,6 +15,8 @@ import SelectInput, { useSelect, } from '@/components/input/SelectInput'; +import RequirePermission from '@/components/helper/RequirePermission'; + import { UniformityFormSchema, UniformityFormValues, @@ -578,18 +580,20 @@ const UniformityForm = ({
{!isNextStep && ( - + + + )}
diff --git a/src/components/pages/uniformity/form/UniformityPreviewForm.tsx b/src/components/pages/uniformity/form/UniformityPreviewForm.tsx index aeb90bef..7cbd86b9 100644 --- a/src/components/pages/uniformity/form/UniformityPreviewForm.tsx +++ b/src/components/pages/uniformity/form/UniformityPreviewForm.tsx @@ -5,6 +5,7 @@ import Button from '@/components/Button'; import Tooltip from '@/components/Tooltip'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import { useUiStore } from '@/stores/ui/ui.store'; +import RequirePermission from '@/components/helper/RequirePermission'; const UniformityPreviewForm = () => { const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen); @@ -27,11 +28,17 @@ const UniformityPreviewForm = () => { subtitleClassName='text-sm text-neutral' showDivider > - + + + {/* Form Section */} diff --git a/src/config/constant.ts b/src/config/constant.ts index fb293c52..07866102 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -46,6 +46,7 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ text: 'Uniformity', link: '/uniformity', icon: 'heroicons-outline:scale', + permission: ['lti.production.uniformity.list'], }, { text: 'Biaya', diff --git a/src/config/route-permission.ts b/src/config/route-permission.ts index ed6d4771..5618c134 100644 --- a/src/config/route-permission.ts +++ b/src/config/route-permission.ts @@ -152,4 +152,10 @@ export const ROUTE_PERMISSIONS: Record = { '/master-data/flock/add/': ['lti.master.flocks.create'], '/master-data/flock/detail/': ['lti.master.flocks.detail'], '/master-data/flock/detail/edit/': ['lti.master.flocks.update'], + + // Uniformity + '/uniformity/': ['lti.production.uniformity.list'], + '/uniformity/add/': ['lti.production.uniformity.create'], + '/uniformity/detail/': ['lti.production.uniformity.detail'], + '/uniformity/detail/edit/': ['lti.production.uniformity.update'], }; From ec8ae7561da2267d6de92b409672899420303141 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 27 Dec 2025 19:09:03 +0700 Subject: [PATCH 052/124] feat(FE-316): Add verifyUniformity and split payload types --- .../pages/uniformity/form/UniformityForm.tsx | 18 ++++------- src/services/api/uniformity.ts | 30 +++++++++++++++---- src/types/api/uniformity/uniformity.d.ts | 8 ++++- 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx index b1387b70..ec8314c3 100644 --- a/src/components/pages/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -227,20 +227,12 @@ const UniformityForm = ({ return; } - const formData = new FormData(); - formData.append('date', values.date); - formData.append( - 'project_flock_kandang_id', - projectFlockKandangId.toString() - ); + const payload: VerifyUniformityPayload = { + project_flock_kandang_id: projectFlockKandangId, + files: values.files as File, + }; - if (values.files) { - formData.append('file', values.files); - } - - const res = await UniformityApi.create( - formData as unknown as VerifyUniformityPayload - ); + const res = await UniformityApi.verifyUniformity(payload); if (isResponseError(res)) { setUniformityFormErrorMessage(res.message); diff --git a/src/services/api/uniformity.ts b/src/services/api/uniformity.ts index aa0b765e..386c1814 100644 --- a/src/services/api/uniformity.ts +++ b/src/services/api/uniformity.ts @@ -3,12 +3,13 @@ import { BaseApiResponse } from '@/types/api/api-general'; import { VerifyUniformityPayload, Uniformity, + CreateUniformityPayload, } from '@/types/api/uniformity/uniformity'; export class UniformityApiService extends BaseApiService< Uniformity, - VerifyUniformityPayload, - unknown + CreateUniformityPayload, + VerifyUniformityPayload > { constructor(basePath: string) { super(basePath); @@ -19,7 +20,7 @@ export class UniformityApiService extends BaseApiService< } async createUniformity( - payload: VerifyUniformityPayload + payload: CreateUniformityPayload ): Promise | undefined> { const formData = new FormData(); formData.append('date', payload.date); @@ -32,12 +33,29 @@ export class UniformityApiService extends BaseApiService< formData.append('file', payload.files); } - return await this.create(formData as unknown as VerifyUniformityPayload); + return await this.create(formData as unknown as CreateUniformityPayload); + } + + async verifyUniformity( + payload: VerifyUniformityPayload + ): Promise | undefined> { + const formData = new FormData(); + formData.append( + 'project_flock_kandang_id', + payload.project_flock_kandang_id.toString() + ); + + if (payload.files) { + formData.append('file', payload.files); + } + + return await this.customRequest>('/verify', { + method: 'POST', + payload: formData as unknown as Record, + }); } } -// export const UniformityApi = new UniformityApiService('uniformity'); - export const UniformityApi = new UniformityApiService( 'http://localhost:4010/api/uniformity' ); diff --git a/src/types/api/uniformity/uniformity.d.ts b/src/types/api/uniformity/uniformity.d.ts index d59520cd..5f889dda 100644 --- a/src/types/api/uniformity/uniformity.d.ts +++ b/src/types/api/uniformity/uniformity.d.ts @@ -10,10 +10,16 @@ export type Uniformity = BaseMetadata & { week: number; status: 'CREATED' | 'APPROVED' | 'REJECTED'; uniformity: number; + date?: string; }; -export type VerifyUniformityPayload = { +export type CreateUniformityPayload = { date: string; project_flock_kandang_id: number; files: File; }; + +export type VerifyUniformityPayload = { + project_flock_kandang_id: number; + files: File; +}; From 549a710a8d02b3ad8fd12fbd4ee34c9326ccce2f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 27 Dec 2025 21:00:07 +0700 Subject: [PATCH 053/124] feat(FE-316): Save and preview uniformity verification --- .../pages/uniformity/form/UniformityForm.tsx | 7 +++ .../uniformity/form/UniformityPreviewForm.tsx | 62 ++++++++++++++++++- src/services/api/uniformity.ts | 14 +++-- src/stores/ui/slices/drawer.slice.ts | 4 ++ src/types/api/uniformity/uniformity.d.ts | 4 ++ src/types/stores.d.ts | 3 + 6 files changed, 88 insertions(+), 6 deletions(-) diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx index ec8314c3..d8c1bda4 100644 --- a/src/components/pages/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -60,6 +60,9 @@ const UniformityForm = ({ ); const isNextStep = useUiStore((s) => s.isNextStep); const setIsNextStep = useUiStore((s) => s.setIsNextStep); + const setVerifyUniformityResult = useUiStore( + (s) => s.setVerifyUniformityResult + ); const [uniformityFormErrorMessage, setUniformityFormErrorMessage] = useState(''); @@ -239,6 +242,10 @@ const UniformityForm = ({ return; } + if (isResponseSuccess(res) && res.data) { + setVerifyUniformityResult(res.data); + } + toast.success(res?.message as string); if (formType === 'add') { diff --git a/src/components/pages/uniformity/form/UniformityPreviewForm.tsx b/src/components/pages/uniformity/form/UniformityPreviewForm.tsx index 7cbd86b9..2e239fbf 100644 --- a/src/components/pages/uniformity/form/UniformityPreviewForm.tsx +++ b/src/components/pages/uniformity/form/UniformityPreviewForm.tsx @@ -1,21 +1,59 @@ 'use client'; +import { useMemo } from 'react'; import { Icon } from '@iconify/react'; +import { ColumnDef } from '@tanstack/react-table'; import Button from '@/components/Button'; import Tooltip from '@/components/Tooltip'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import { useUiStore } from '@/stores/ui/ui.store'; import RequirePermission from '@/components/helper/RequirePermission'; +import Table from '@/components/Table'; + +type BodyWeightData = { + id: string; + number: number; + weight: number; +}; const UniformityPreviewForm = () => { const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen); const setIsNextStep = useUiStore((s) => s.setIsNextStep); + const verifyUniformityResult = useUiStore((s) => s.verifyUniformityResult); const handleClose = () => { setExpandedDrawerOpen(false); setIsNextStep(false); }; + const tableData = useMemo(() => { + if (!verifyUniformityResult) return []; + + return verifyUniformityResult.body_weights.map((weight, index) => ({ + id: `weight-${index}`, + number: index + 1, + weight: weight, + })); + }, [verifyUniformityResult]); + + const columns: ColumnDef[] = useMemo( + () => [ + { + accessorKey: 'number', + header: 'No', + cell: (props) => props.row.original.number, + }, + { + accessorKey: 'weight', + header: 'Weight (g)', + cell: (props) => ( + {props.row.original.weight} + ), + }, + ], + [] + ); + return (
{/* Header */} @@ -43,7 +81,29 @@ const UniformityPreviewForm = () => { {/* Form Section */}
-
+
+ {verifyUniformityResult ? ( +
+ + data={tableData} + columns={columns} + pageSize={20} + className={{ containerClassName: 'mb-10' }} + /> +
+ ) : ( +
+ +

No data available

+

Upload a file to verify uniformity

+
+ )} +
); }; diff --git a/src/services/api/uniformity.ts b/src/services/api/uniformity.ts index 386c1814..2ccd38fd 100644 --- a/src/services/api/uniformity.ts +++ b/src/services/api/uniformity.ts @@ -2,6 +2,7 @@ import { BaseApiService } from '@/services/api/base'; import { BaseApiResponse } from '@/types/api/api-general'; import { VerifyUniformityPayload, + VerifyUniformityResponse, Uniformity, CreateUniformityPayload, } from '@/types/api/uniformity/uniformity'; @@ -38,7 +39,7 @@ export class UniformityApiService extends BaseApiService< async verifyUniformity( payload: VerifyUniformityPayload - ): Promise | undefined> { + ): Promise | undefined> { const formData = new FormData(); formData.append( 'project_flock_kandang_id', @@ -49,10 +50,13 @@ export class UniformityApiService extends BaseApiService< formData.append('file', payload.files); } - return await this.customRequest>('/verify', { - method: 'POST', - payload: formData as unknown as Record, - }); + return await this.customRequest>( + '/verify', + { + method: 'POST', + payload: formData as unknown as Record, + } + ); } } diff --git a/src/stores/ui/slices/drawer.slice.ts b/src/stores/ui/slices/drawer.slice.ts index 382eaff2..c8eb3c8b 100644 --- a/src/stores/ui/slices/drawer.slice.ts +++ b/src/stores/ui/slices/drawer.slice.ts @@ -48,4 +48,8 @@ export const createDrawerUISlice: StateCreator< isNextStep: false, setIsNextStep: (isNextStep: boolean) => set({ isNextStep }), + + verifyUniformityResult: null, + setVerifyUniformityResult: (result) => + set({ verifyUniformityResult: result }), }); diff --git a/src/types/api/uniformity/uniformity.d.ts b/src/types/api/uniformity/uniformity.d.ts index 5f889dda..b8d1b144 100644 --- a/src/types/api/uniformity/uniformity.d.ts +++ b/src/types/api/uniformity/uniformity.d.ts @@ -23,3 +23,7 @@ export type VerifyUniformityPayload = { project_flock_kandang_id: number; files: File; }; + +export type VerifyUniformityResponse = { + body_weights: number[]; +}; diff --git a/src/types/stores.d.ts b/src/types/stores.d.ts index 7bfa63cd..ff18d06a 100644 --- a/src/types/stores.d.ts +++ b/src/types/stores.d.ts @@ -1,4 +1,5 @@ import type { ReactNode } from 'react'; +import type { VerifyUniformityResponse } from '@/types/api/uniformity/uniformity'; type MainUiSlice = { mainDrawerOpen: boolean; @@ -18,6 +19,8 @@ type DrawerUISlice = { setExpandedDrawerContent: (content: ReactNode) => void; isNextStep: boolean; setIsNextStep: (v: boolean) => void; + verifyUniformityResult: VerifyUniformityResponse | null; + setVerifyUniformityResult: (result: VerifyUniformityResponse | null) => void; }; export type UIStore = MainUiSlice & DrawerUISlice; From 819b709f7efdf9eabc83c122260b7fba9db72bb1 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 27 Dec 2025 21:25:06 +0700 Subject: [PATCH 054/124] feat(FE-316): Add Uniformity result drawer and flow --- .../pages/uniformity/form/UniformityForm.tsx | 19 +- .../uniformity/form/UniformityPreviewForm.tsx | 15 +- .../uniformity/form/UniformityResultForm.tsx | 178 ++++++++++++++++++ src/stores/ui/slices/drawer.slice.ts | 6 + src/types/stores.d.ts | 15 +- 5 files changed, 228 insertions(+), 5 deletions(-) create mode 100644 src/components/pages/uniformity/form/UniformityResultForm.tsx diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx index d8c1bda4..f9b22a65 100644 --- a/src/components/pages/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -37,6 +37,7 @@ import { type BaseApiResponse } from '@/types/api/api-general'; import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock'; import { Kandang } from '@/types/api/master-data/kandang'; import UniformityPreviewForm from '@/components/pages/uniformity/form/UniformityPreviewForm'; +import UniformityResultForm from '@/components/pages/uniformity/form/UniformityResultForm'; import useSWR from 'swr'; import { cn } from '@/lib/helper'; @@ -63,6 +64,9 @@ const UniformityForm = ({ const setVerifyUniformityResult = useUiStore( (s) => s.setVerifyUniformityResult ); + const setUniformityFormData = useUiStore((s) => s.setUniformityFormData); + const uniformityStep = useUiStore((s) => s.uniformityStep); + const setUniformityStep = useUiStore((s) => s.setUniformityStep); const [uniformityFormErrorMessage, setUniformityFormErrorMessage] = useState(''); @@ -230,6 +234,12 @@ const UniformityForm = ({ return; } + setUniformityFormData({ + date: values.date, + project_flock_kandang_id: projectFlockKandangId, + files: values.files as File, + }); + const payload: VerifyUniformityPayload = { project_flock_kandang_id: projectFlockKandangId, files: values.files as File, @@ -251,6 +261,7 @@ const UniformityForm = ({ if (formType === 'add') { setIsNextStep(true); setExpandedDrawerOpen(true); + setUniformityStep('preview'); } else { router.push('/uniformity'); } @@ -361,11 +372,15 @@ const UniformityForm = ({ useEffect(() => { if (expandedDrawerOpen) { - setExpandedDrawerContent(); + if (uniformityStep === 'preview') { + setExpandedDrawerContent(); + } else if (uniformityStep === 'result') { + setExpandedDrawerContent(); + } } else { setExpandedDrawerContent(null); } - }, [expandedDrawerOpen, setExpandedDrawerContent]); + }, [expandedDrawerOpen, uniformityStep, setExpandedDrawerContent]); return ( <> diff --git a/src/components/pages/uniformity/form/UniformityPreviewForm.tsx b/src/components/pages/uniformity/form/UniformityPreviewForm.tsx index 2e239fbf..ff526bc0 100644 --- a/src/components/pages/uniformity/form/UniformityPreviewForm.tsx +++ b/src/components/pages/uniformity/form/UniformityPreviewForm.tsx @@ -19,11 +19,17 @@ type BodyWeightData = { const UniformityPreviewForm = () => { const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen); const setIsNextStep = useUiStore((s) => s.setIsNextStep); + const setUniformityStep = useUiStore((s) => s.setUniformityStep); const verifyUniformityResult = useUiStore((s) => s.verifyUniformityResult); const handleClose = () => { setExpandedDrawerOpen(false); setIsNextStep(false); + setUniformityStep('preview'); + }; + + const handleNext = () => { + setUniformityStep('result'); }; const tableData = useMemo(() => { @@ -87,9 +93,14 @@ const UniformityPreviewForm = () => { data={tableData} columns={columns} - pageSize={20} - className={{ containerClassName: 'mb-10' }} + pageSize={15} + className={{ containerClassName: 'mb-5' }} /> + + +
) : (
diff --git a/src/components/pages/uniformity/form/UniformityResultForm.tsx b/src/components/pages/uniformity/form/UniformityResultForm.tsx new file mode 100644 index 00000000..bda0ca09 --- /dev/null +++ b/src/components/pages/uniformity/form/UniformityResultForm.tsx @@ -0,0 +1,178 @@ +'use client'; + +import React, { useMemo } from 'react'; +import { Icon } from '@iconify/react'; +import { ColumnDef } from '@tanstack/react-table'; +import Button from '@/components/Button'; +import Tooltip from '@/components/Tooltip'; +import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; +import { useUiStore } from '@/stores/ui/ui.store'; +import RequirePermission from '@/components/helper/RequirePermission'; +import Table from '@/components/Table'; +import { useRouter } from 'next/navigation'; +import toast from 'react-hot-toast'; +import { UniformityApi } from '@/services/api/uniformity'; +import { isResponseError } from '@/lib/api-helper'; + +type BodyWeightData = { + id: string; + number: number; + weight: number; +}; + +const UniformityResultForm = () => { + const router = useRouter(); + const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen); + const setIsNextStep = useUiStore((s) => s.setIsNextStep); + const setUniformityStep = useUiStore((s) => s.setUniformityStep); + const verifyUniformityResult = useUiStore((s) => s.verifyUniformityResult); + const setVerifyUniformityResult = useUiStore( + (s) => s.setVerifyUniformityResult + ); + const uniformityFormData = useUiStore((s) => s.uniformityFormData); + + const [isSubmitting, setIsSubmitting] = React.useState(false); + + const handleClose = () => { + setExpandedDrawerOpen(false); + setIsNextStep(false); + setUniformityStep('preview'); + setVerifyUniformityResult(null); + }; + + const handleBack = () => { + setUniformityStep('preview'); + }; + + const handleSubmit = async () => { + if (!uniformityFormData || !uniformityFormData.files) { + toast.error('Form data is missing. Please try again.'); + return; + } + + setIsSubmitting(true); + + try { + const payload = { + date: uniformityFormData.date, + project_flock_kandang_id: uniformityFormData.project_flock_kandang_id, + files: uniformityFormData.files, + }; + + const res = await UniformityApi.createUniformity(payload); + + if (isResponseError(res)) { + toast.error(res.message); + return; + } + + toast.success('Uniformity created successfully!'); + + setExpandedDrawerOpen(false); + setIsNextStep(false); + setUniformityStep('preview'); + setVerifyUniformityResult(null); + router.push('/uniformity'); + } finally { + setIsSubmitting(false); + } + }; + + const tableData = useMemo(() => { + if (!verifyUniformityResult) return []; + + return verifyUniformityResult.body_weights.map((weight, index) => ({ + id: `weight-${index}`, + number: index + 1, + weight: weight, + })); + }, [verifyUniformityResult]); + + const columns: ColumnDef[] = useMemo( + () => [ + { + accessorKey: 'number', + header: 'No', + cell: (props) => props.row.original.number, + }, + { + accessorKey: 'weight', + header: 'Weight (g)', + cell: (props) => ( + {props.row.original.weight} + ), + }, + ], + [] + ); + + return ( +
+ {/* Header */} + + + + + + + {/* Form Section */} +
+
+ {verifyUniformityResult ? ( +
+
+ + data={tableData} + columns={columns} + pageSize={15} + className={{ containerClassName: 'mb-5' }} + /> +
+ + {/* Action Buttons */} + + + +
+ ) : ( +
+ +

No data available

+

Upload a file to verify uniformity

+
+ )} +
+
+ ); +}; + +export default UniformityResultForm; diff --git a/src/stores/ui/slices/drawer.slice.ts b/src/stores/ui/slices/drawer.slice.ts index c8eb3c8b..4e93dea9 100644 --- a/src/stores/ui/slices/drawer.slice.ts +++ b/src/stores/ui/slices/drawer.slice.ts @@ -52,4 +52,10 @@ export const createDrawerUISlice: StateCreator< verifyUniformityResult: null, setVerifyUniformityResult: (result) => set({ verifyUniformityResult: result }), + + uniformityStep: 'preview', + setUniformityStep: (step) => set({ uniformityStep: step }), + + uniformityFormData: null, + setUniformityFormData: (data) => set({ uniformityFormData: data }), }); diff --git a/src/types/stores.d.ts b/src/types/stores.d.ts index ff18d06a..de103cfd 100644 --- a/src/types/stores.d.ts +++ b/src/types/stores.d.ts @@ -1,11 +1,20 @@ import type { ReactNode } from 'react'; -import type { VerifyUniformityResponse } from '@/types/api/uniformity/uniformity'; +import type { + VerifyUniformityResponse, + CreateUniformityPayload, +} from '@/types/api/uniformity/uniformity'; type MainUiSlice = { mainDrawerOpen: boolean; setMainDrawerOpen: (open: boolean) => void; }; +type UniformityFormData = { + date: string; + project_flock_kandang_id: number; + files: File | null; +}; + type DrawerUISlice = { triggerValidate: boolean; toggleValidate: () => void; @@ -21,6 +30,10 @@ type DrawerUISlice = { setIsNextStep: (v: boolean) => void; verifyUniformityResult: VerifyUniformityResponse | null; setVerifyUniformityResult: (result: VerifyUniformityResponse | null) => void; + uniformityStep: 'preview' | 'result'; + setUniformityStep: (step: 'preview' | 'result') => void; + uniformityFormData: UniformityFormData | null; + setUniformityFormData: (data: UniformityFormData | null) => void; }; export type UIStore = MainUiSlice & DrawerUISlice; From fd2077c68bd547213e203f027e7f07f8973b46ad Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 27 Dec 2025 21:36:34 +0700 Subject: [PATCH 055/124] refactor(FE-316): Add fileName to Uniformity form data --- src/components/pages/uniformity/form/UniformityForm.tsx | 1 + src/components/pages/uniformity/form/UniformityResultForm.tsx | 2 +- src/types/stores.d.ts | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx index f9b22a65..59521018 100644 --- a/src/components/pages/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -238,6 +238,7 @@ const UniformityForm = ({ date: values.date, project_flock_kandang_id: projectFlockKandangId, files: values.files as File, + fileName: (values.files as File).name, }); const payload: VerifyUniformityPayload = { diff --git a/src/components/pages/uniformity/form/UniformityResultForm.tsx b/src/components/pages/uniformity/form/UniformityResultForm.tsx index bda0ca09..c0137e37 100644 --- a/src/components/pages/uniformity/form/UniformityResultForm.tsx +++ b/src/components/pages/uniformity/form/UniformityResultForm.tsx @@ -114,7 +114,7 @@ const UniformityResultForm = () => { leftIconSize={24} leftIconOnClick={handleBack} leftIconClassName='hover:text-gray-400 cursor-pointer' - subtitle='Uniformity Result' + subtitle={uniformityFormData?.fileName || 'Uniformity Result'} subtitleClassName='text-sm text-neutral' showDivider > diff --git a/src/types/stores.d.ts b/src/types/stores.d.ts index de103cfd..ddf94844 100644 --- a/src/types/stores.d.ts +++ b/src/types/stores.d.ts @@ -13,6 +13,7 @@ type UniformityFormData = { date: string; project_flock_kandang_id: number; files: File | null; + fileName: string | null; }; type DrawerUISlice = { From 45d65024dbc26e9485552125c4a9df605d8420d2 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 27 Dec 2025 21:51:13 +0700 Subject: [PATCH 056/124] refactor(FE-316): Extract uniformity state into separate store --- .../pages/uniformity/form/UniformityForm.tsx | 12 +++-- .../uniformity/form/UniformityPreviewForm.tsx | 7 ++- .../uniformity/form/UniformityResultForm.tsx | 11 ++-- src/stores/ui/slices/drawer.slice.ts | 10 ---- src/stores/uniformity/uniformity.store.ts | 54 +++++++++++++++++++ src/types/stores.d.ts | 17 ------ 6 files changed, 74 insertions(+), 37 deletions(-) create mode 100644 src/stores/uniformity/uniformity.store.ts diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx index 59521018..40587e6f 100644 --- a/src/components/pages/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -7,6 +7,7 @@ import { Icon } from '@iconify/react'; import { toast } from 'react-hot-toast'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import { useUiStore } from '@/stores/ui/ui.store'; +import { useUniformityStore } from '@/stores/uniformity/uniformity.store'; import Button from '@/components/Button'; import DateInput from '@/components/input/DateInput'; @@ -61,12 +62,15 @@ const UniformityForm = ({ ); const isNextStep = useUiStore((s) => s.isNextStep); const setIsNextStep = useUiStore((s) => s.setIsNextStep); - const setVerifyUniformityResult = useUiStore( + + const setVerifyUniformityResult = useUniformityStore( (s) => s.setVerifyUniformityResult ); - const setUniformityFormData = useUiStore((s) => s.setUniformityFormData); - const uniformityStep = useUiStore((s) => s.uniformityStep); - const setUniformityStep = useUiStore((s) => s.setUniformityStep); + const setUniformityFormData = useUniformityStore( + (s) => s.setUniformityFormData + ); + const uniformityStep = useUniformityStore((s) => s.uniformityStep); + const setUniformityStep = useUniformityStore((s) => s.setUniformityStep); const [uniformityFormErrorMessage, setUniformityFormErrorMessage] = useState(''); diff --git a/src/components/pages/uniformity/form/UniformityPreviewForm.tsx b/src/components/pages/uniformity/form/UniformityPreviewForm.tsx index ff526bc0..e8258ba2 100644 --- a/src/components/pages/uniformity/form/UniformityPreviewForm.tsx +++ b/src/components/pages/uniformity/form/UniformityPreviewForm.tsx @@ -7,6 +7,7 @@ import Button from '@/components/Button'; import Tooltip from '@/components/Tooltip'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import { useUiStore } from '@/stores/ui/ui.store'; +import { useUniformityStore } from '@/stores/uniformity/uniformity.store'; import RequirePermission from '@/components/helper/RequirePermission'; import Table from '@/components/Table'; @@ -19,8 +20,10 @@ type BodyWeightData = { const UniformityPreviewForm = () => { const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen); const setIsNextStep = useUiStore((s) => s.setIsNextStep); - const setUniformityStep = useUiStore((s) => s.setUniformityStep); - const verifyUniformityResult = useUiStore((s) => s.verifyUniformityResult); + const setUniformityStep = useUniformityStore((s) => s.setUniformityStep); + const verifyUniformityResult = useUniformityStore( + (s) => s.verifyUniformityResult + ); const handleClose = () => { setExpandedDrawerOpen(false); diff --git a/src/components/pages/uniformity/form/UniformityResultForm.tsx b/src/components/pages/uniformity/form/UniformityResultForm.tsx index c0137e37..153db04d 100644 --- a/src/components/pages/uniformity/form/UniformityResultForm.tsx +++ b/src/components/pages/uniformity/form/UniformityResultForm.tsx @@ -7,6 +7,7 @@ import Button from '@/components/Button'; import Tooltip from '@/components/Tooltip'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import { useUiStore } from '@/stores/ui/ui.store'; +import { useUniformityStore } from '@/stores/uniformity/uniformity.store'; import RequirePermission from '@/components/helper/RequirePermission'; import Table from '@/components/Table'; import { useRouter } from 'next/navigation'; @@ -24,12 +25,14 @@ const UniformityResultForm = () => { const router = useRouter(); const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen); const setIsNextStep = useUiStore((s) => s.setIsNextStep); - const setUniformityStep = useUiStore((s) => s.setUniformityStep); - const verifyUniformityResult = useUiStore((s) => s.verifyUniformityResult); - const setVerifyUniformityResult = useUiStore( + const setUniformityStep = useUniformityStore((s) => s.setUniformityStep); + const verifyUniformityResult = useUniformityStore( + (s) => s.verifyUniformityResult + ); + const setVerifyUniformityResult = useUniformityStore( (s) => s.setVerifyUniformityResult ); - const uniformityFormData = useUiStore((s) => s.uniformityFormData); + const uniformityFormData = useUniformityStore((s) => s.uniformityFormData); const [isSubmitting, setIsSubmitting] = React.useState(false); diff --git a/src/stores/ui/slices/drawer.slice.ts b/src/stores/ui/slices/drawer.slice.ts index 4e93dea9..382eaff2 100644 --- a/src/stores/ui/slices/drawer.slice.ts +++ b/src/stores/ui/slices/drawer.slice.ts @@ -48,14 +48,4 @@ export const createDrawerUISlice: StateCreator< isNextStep: false, setIsNextStep: (isNextStep: boolean) => set({ isNextStep }), - - verifyUniformityResult: null, - setVerifyUniformityResult: (result) => - set({ verifyUniformityResult: result }), - - uniformityStep: 'preview', - setUniformityStep: (step) => set({ uniformityStep: step }), - - uniformityFormData: null, - setUniformityFormData: (data) => set({ uniformityFormData: data }), }); diff --git a/src/stores/uniformity/uniformity.store.ts b/src/stores/uniformity/uniformity.store.ts new file mode 100644 index 00000000..914f398e --- /dev/null +++ b/src/stores/uniformity/uniformity.store.ts @@ -0,0 +1,54 @@ +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { VerifyUniformityResponse } from '@/types/api/uniformity/uniformity'; + +export type UniformityStep = 'preview' | 'result'; + +export type UniformityFormData = { + date: string; + project_flock_kandang_id: number; + files: File | null; + fileName: string; +}; + +type UniformityState = { + // State + uniformityStep: UniformityStep; + verifyUniformityResult: VerifyUniformityResponse | null; + uniformityFormData: UniformityFormData | null; + + // Actions + setUniformityStep: (step: UniformityStep) => void; + setVerifyUniformityResult: (result: VerifyUniformityResponse | null) => void; + setUniformityFormData: (data: UniformityFormData | null) => void; + resetUniformity: () => void; +}; + +export const useUniformityStore = create()( + devtools( + (set) => ({ + // Initial state + uniformityStep: 'preview', + verifyUniformityResult: null, + uniformityFormData: null, + + // Actions + setUniformityStep: (step) => set({ uniformityStep: step }), + + setVerifyUniformityResult: (result) => + set({ verifyUniformityResult: result }), + + setUniformityFormData: (data) => set({ uniformityFormData: data }), + + resetUniformity: () => + set({ + uniformityStep: 'preview', + verifyUniformityResult: null, + uniformityFormData: null, + }), + }), + { + name: 'UniformityStore', + } + ) +); diff --git a/src/types/stores.d.ts b/src/types/stores.d.ts index ddf94844..7bfa63cd 100644 --- a/src/types/stores.d.ts +++ b/src/types/stores.d.ts @@ -1,21 +1,10 @@ import type { ReactNode } from 'react'; -import type { - VerifyUniformityResponse, - CreateUniformityPayload, -} from '@/types/api/uniformity/uniformity'; type MainUiSlice = { mainDrawerOpen: boolean; setMainDrawerOpen: (open: boolean) => void; }; -type UniformityFormData = { - date: string; - project_flock_kandang_id: number; - files: File | null; - fileName: string | null; -}; - type DrawerUISlice = { triggerValidate: boolean; toggleValidate: () => void; @@ -29,12 +18,6 @@ type DrawerUISlice = { setExpandedDrawerContent: (content: ReactNode) => void; isNextStep: boolean; setIsNextStep: (v: boolean) => void; - verifyUniformityResult: VerifyUniformityResponse | null; - setVerifyUniformityResult: (result: VerifyUniformityResponse | null) => void; - uniformityStep: 'preview' | 'result'; - setUniformityStep: (step: 'preview' | 'result') => void; - uniformityFormData: UniformityFormData | null; - setUniformityFormData: (data: UniformityFormData | null) => void; }; export type UIStore = MainUiSlice & DrawerUISlice; From 3c29b8bc7761eb568b23db5b6ed844612126fe81 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 27 Dec 2025 22:32:12 +0700 Subject: [PATCH 057/124] refactor(FE-316): Show selected file name in preview subtitle --- src/components/pages/uniformity/form/UniformityPreviewForm.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/pages/uniformity/form/UniformityPreviewForm.tsx b/src/components/pages/uniformity/form/UniformityPreviewForm.tsx index e8258ba2..c1e921b0 100644 --- a/src/components/pages/uniformity/form/UniformityPreviewForm.tsx +++ b/src/components/pages/uniformity/form/UniformityPreviewForm.tsx @@ -24,6 +24,7 @@ const UniformityPreviewForm = () => { const verifyUniformityResult = useUniformityStore( (s) => s.verifyUniformityResult ); + const uniformityFormData = useUniformityStore((s) => s.uniformityFormData); const handleClose = () => { setExpandedDrawerOpen(false); @@ -71,7 +72,7 @@ const UniformityPreviewForm = () => { leftIconSize={24} leftIconOnClick={handleClose} leftIconClassName='hover:text-gray-400 cursor-pointer' - subtitle='Add Body Weight' + subtitle={uniformityFormData?.fileName || 'Add Body Weight'} subtitleClassName='text-sm text-neutral' showDivider > From fe04bf5692980d5f0ffd008e54ce1deb783f9701 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 27 Dec 2025 22:39:03 +0700 Subject: [PATCH 058/124] refactor(FE-316): Hide back icon and divider in uniformity forms --- .../pages/uniformity/form/UniformityPreviewForm.tsx | 7 ++----- .../pages/uniformity/form/UniformityResultForm.tsx | 7 ++----- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/components/pages/uniformity/form/UniformityPreviewForm.tsx b/src/components/pages/uniformity/form/UniformityPreviewForm.tsx index c1e921b0..27eae139 100644 --- a/src/components/pages/uniformity/form/UniformityPreviewForm.tsx +++ b/src/components/pages/uniformity/form/UniformityPreviewForm.tsx @@ -68,13 +68,10 @@ const UniformityPreviewForm = () => {
{/* Header */} - + {/* Form Section */} -
+
{verifyUniformityResult ? (
diff --git a/src/components/pages/uniformity/form/UniformityResultForm.tsx b/src/components/pages/uniformity/form/UniformityResultForm.tsx index 8d138519..a1780154 100644 --- a/src/components/pages/uniformity/form/UniformityResultForm.tsx +++ b/src/components/pages/uniformity/form/UniformityResultForm.tsx @@ -118,21 +118,15 @@ const UniformityResultForm = () => { subtitleClassName='text-sm text-neutral' showDivider={false} > - - - + {/* Form Section */} -
+
{verifyUniformityResult ? (
From c8f47c741aaaf70fc00bf8ee8a2ee0ecb8031a1e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sat, 27 Dec 2025 23:15:19 +0700 Subject: [PATCH 060/124] feat(FE-316): Add status badges and result summary tables --- .../uniformity/form/UniformityPreviewForm.tsx | 4 +- .../uniformity/form/UniformityResultForm.tsx | 188 +++++++++++++++++- 2 files changed, 183 insertions(+), 9 deletions(-) diff --git a/src/components/pages/uniformity/form/UniformityPreviewForm.tsx b/src/components/pages/uniformity/form/UniformityPreviewForm.tsx index 89749325..edd94a9a 100644 --- a/src/components/pages/uniformity/form/UniformityPreviewForm.tsx +++ b/src/components/pages/uniformity/form/UniformityPreviewForm.tsx @@ -56,9 +56,7 @@ const UniformityPreviewForm = () => { { accessorKey: 'weight', header: 'Weight (g)', - cell: (props) => ( - {props.row.original.weight} - ), + cell: (props) => {props.row.original.weight}, }, ], [] diff --git a/src/components/pages/uniformity/form/UniformityResultForm.tsx b/src/components/pages/uniformity/form/UniformityResultForm.tsx index a1780154..b7644de8 100644 --- a/src/components/pages/uniformity/form/UniformityResultForm.tsx +++ b/src/components/pages/uniformity/form/UniformityResultForm.tsx @@ -14,11 +14,52 @@ import { useRouter } from 'next/navigation'; import toast from 'react-hot-toast'; import { UniformityApi } from '@/services/api/uniformity'; import { isResponseError } from '@/lib/api-helper'; +import Badge from '@/components/Badge'; + +const weightStatusColorMap: Record = { + ideal: 'bg-[#00D39033]', + outside: 'bg-error/10', +}; + +const weightStatusIndicatorColorMap: Record = { + ideal: 'bg-[#008000]', + outside: 'bg-error', +}; + +const weightStatusTextMap: Record = { + ideal: 'Ideal', + outside: 'Outside', +}; + +const getWeightStatusColor = (status: string): string => { + return weightStatusColorMap[status] || 'bg-info'; +}; + +const getWeightStatusIndicatorColor = (status: string): string => { + return weightStatusIndicatorColorMap[status] || 'bg-info'; +}; + +const getWeightStatusText = (status: string): string => { + return weightStatusTextMap[status] || status; +}; type BodyWeightData = { id: string; number: number; weight: number; + status?: 'ideal' | 'outside'; +}; + +type SamplingData = { + id: string; + label: string; + value: string; +}; + +type ResultData = { + id: string; + label: string; + value: string; }; const UniformityResultForm = () => { @@ -81,6 +122,87 @@ const UniformityResultForm = () => { } }; + const samplingTableData: SamplingData[] = useMemo(() => { + if (!verifyUniformityResult) return []; + + return [ + { + id: 'sampling-size', + label: 'Sampling size', + value: `1,150 of Birds`, + }, + { + id: 'mean-weight', + label: 'Mean Weight', + value: `121 g`, + }, + { + id: 'min-limit', + label: 'Min Limit (-10%)', + value: `109 g`, + }, + { + id: 'max-limit', + label: 'Max Limit (+10%)', + value: `133 g`, + }, + ]; + }, [verifyUniformityResult]); + + const columnsSampling: ColumnDef[] = useMemo( + () => [ + { + accessorKey: 'label', + header: 'Label', + cell: (props) => props.row.original.label, + }, + { + accessorKey: 'value', + header: 'Value', + cell: (props) => {props.row.original.value}, + }, + ], + [] + ); + + const resultTableData: ResultData[] = useMemo(() => { + if (!verifyUniformityResult) return []; + + return [ + { + id: 'ideal-birds', + label: 'Ideal Birds', + value: `851 of Birds`, + }, + { + id: 'outside-range', + label: 'Outside Range', + value: `299 of Birds`, + }, + { + id: 'uniformity', + label: 'Uniformity', + value: `74 %`, + }, + ]; + }, [verifyUniformityResult]); + + const resultColumns: ColumnDef[] = useMemo( + () => [ + { + accessorKey: 'label', + header: 'Label', + cell: (props) => props.row.original.label, + }, + { + accessorKey: 'value', + header: 'Value', + cell: (props) => {props.row.original.value}, + }, + ], + [] + ); + const tableData = useMemo(() => { if (!verifyUniformityResult) return []; @@ -91,7 +213,7 @@ const UniformityResultForm = () => { })); }, [verifyUniformityResult]); - const columns: ColumnDef[] = useMemo( + const columnsUniformity: ColumnDef[] = useMemo( () => [ { accessorKey: 'number', @@ -101,9 +223,39 @@ const UniformityResultForm = () => { { accessorKey: 'weight', header: 'Weight (g)', - cell: (props) => ( - {props.row.original.weight} - ), + cell: (props) => {props.row.original.weight}, + }, + { + accessorKey: 'status', + header: 'Status', + cell: (props) => { + const status = props.row.original.status; + return status ? ( +
+ + {getWeightStatusText(status)} + +
+ ) : ( + + Ideal + + ); + }, }, ], [] @@ -130,15 +282,39 @@ const UniformityResultForm = () => {
{verifyUniformityResult ? (
+
+

Sampling and Range

+ + data={samplingTableData} + columns={columnsSampling} + pageSize={4} + className={{ + containerClassName: 'mb-0', + paginationClassName: 'hidden', + }} + /> +
+ +
+

Result

+ + data={resultTableData} + columns={resultColumns} + pageSize={3} + className={{ + containerClassName: 'mb-0', + paginationClassName: 'hidden', + }} + /> +
data={tableData} - columns={columns} + columns={columnsUniformity} pageSize={15} className={{ containerClassName: 'mb-5' }} />
- {/* Action Buttons */}
- {formik.values.files.name} + {formik.values.file.name}
) : ( @@ -585,15 +602,15 @@ const UniformityForm = ({ ref={fileInputRef} type='file' id='file-upload-input' - name='files' + name='file' accept='application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/csv' onChange={handleFileChange} className='hidden' /> - {formik.touched.files && formik.errors.files && ( + {formik.touched.file && formik.errors.file && (

- {formik.errors.files as string} + {formik.errors.file as string}

)}
diff --git a/src/components/pages/uniformity/form/UniformityPreviewForm.tsx b/src/components/pages/uniformity/form/UniformityPreviewForm.tsx index edd94a9a..ff5ffacb 100644 --- a/src/components/pages/uniformity/form/UniformityPreviewForm.tsx +++ b/src/components/pages/uniformity/form/UniformityPreviewForm.tsx @@ -39,10 +39,10 @@ const UniformityPreviewForm = () => { const tableData = useMemo(() => { if (!verifyUniformityResult) return []; - return verifyUniformityResult.body_weights.map((weight, index) => ({ + return verifyUniformityResult.uniformity_details.map((detail, index) => ({ id: `weight-${index}`, number: index + 1, - weight: weight, + weight: detail.weight, })); }, [verifyUniformityResult]); diff --git a/src/components/pages/uniformity/form/UniformityResultForm.tsx b/src/components/pages/uniformity/form/UniformityResultForm.tsx index aa12409b..7b8466fb 100644 --- a/src/components/pages/uniformity/form/UniformityResultForm.tsx +++ b/src/components/pages/uniformity/form/UniformityResultForm.tsx @@ -90,7 +90,7 @@ const UniformityResultForm = () => { }; const handleSubmit = async () => { - if (!uniformityFormData || !uniformityFormData.files) { + if (!uniformityFormData || !uniformityFormData.file) { toast.error('Form data is missing. Please try again.'); return; } @@ -100,8 +100,9 @@ const UniformityResultForm = () => { try { const payload = { date: uniformityFormData.date, + week: uniformityFormData.week, project_flock_kandang_id: uniformityFormData.project_flock_kandang_id, - files: uniformityFormData.files, + file: uniformityFormData.file, }; const res = await UniformityApi.createUniformity(payload); @@ -206,10 +207,11 @@ const UniformityResultForm = () => { const tableData = useMemo(() => { if (!verifyUniformityResult) return []; - return verifyUniformityResult.body_weights.map((weight, index) => ({ - id: `weight-${index}`, + return verifyUniformityResult.uniformity_details.map((detail, index) => ({ + id: `body-weight-${index + 1}`, number: index + 1, - weight: weight, + weight: detail.weight, + status: detail.range.toLowerCase() as 'ideal' | 'outside', })); }, [verifyUniformityResult]); diff --git a/src/services/api/uniformity.ts b/src/services/api/uniformity.ts index 2ccd38fd..7ef16e91 100644 --- a/src/services/api/uniformity.ts +++ b/src/services/api/uniformity.ts @@ -25,13 +25,14 @@ export class UniformityApiService extends BaseApiService< ): Promise | undefined> { const formData = new FormData(); formData.append('date', payload.date); + formData.append('week', payload.week.toString()); formData.append( 'project_flock_kandang_id', payload.project_flock_kandang_id.toString() ); - if (payload.files) { - formData.append('file', payload.files); + if (payload.file) { + formData.append('file', payload.file); } return await this.create(formData as unknown as CreateUniformityPayload); @@ -41,13 +42,15 @@ export class UniformityApiService extends BaseApiService< payload: VerifyUniformityPayload ): Promise | undefined> { const formData = new FormData(); + formData.append('date', payload.date); + formData.append('week', payload.week.toString()); formData.append( 'project_flock_kandang_id', payload.project_flock_kandang_id.toString() ); - if (payload.files) { - formData.append('file', payload.files); + if (payload.file) { + formData.append('file', payload.file); } return await this.customRequest>( @@ -61,5 +64,5 @@ export class UniformityApiService extends BaseApiService< } export const UniformityApi = new UniformityApiService( - 'http://localhost:4010/api/uniformity' + 'http://localhost:4010/api/production/uniformities' ); diff --git a/src/stores/uniformity/uniformity.store.ts b/src/stores/uniformity/uniformity.store.ts index 97162836..082a2d5b 100644 --- a/src/stores/uniformity/uniformity.store.ts +++ b/src/stores/uniformity/uniformity.store.ts @@ -6,8 +6,9 @@ export type UniformityStep = 'preview' | 'result'; export type UniformityFormData = { date: string; + week: number; project_flock_kandang_id: number; - files: File | null; + file: File | null; fileName: string; }; diff --git a/src/types/api/uniformity/uniformity.d.ts b/src/types/api/uniformity/uniformity.d.ts index b8d1b144..1cdefadb 100644 --- a/src/types/api/uniformity/uniformity.d.ts +++ b/src/types/api/uniformity/uniformity.d.ts @@ -1,29 +1,88 @@ +import { BaseMetadata } from '@/types/api/api-general'; import { Location } from '@/types/api/location/location'; +import { ProjectFlock } from '@/types/api/project-flock/project-flock'; import { Kandang } from '@/types/api/kandang/kandang'; -import { BaseMetadata } from '@/types/common/base-metadata'; +import { BaseApproval } from '@/types/api/approval/approval'; +// ==================== GET ALL RESPONSE ==================== export type Uniformity = BaseMetadata & { id: number; - location: Location; project_flock_kandang_id: number; + location: Location; + project_flock: ProjectFlock; + location_name: string; + flock_name: string; kandang: Kandang; + kandang_name: string; + applied_at: string; week: number; status: 'CREATED' | 'APPROVED' | 'REJECTED'; uniformity: number; - date?: string; + cv: number; + chick_qty_of_weight: number; + uniform_qty: number; + mean_up: number; + mean_down: number; + created_at: string; + created_by: number; + latest_approval?: BaseApproval; }; +// ==================== GET ONE RESPONSE ==================== +export type UniformityInfoUmum = { + tanggal: string; + lokasi_farm: string; + project_flock: string; + kandang: string; + file_name: string; +}; + +export type UniformitySampling = { + chick_qty_of_weight: number; + mean_weight: number; + mean_down: number; + mean_up: number; +}; + +export type UniformityResult = { + uniform_qty: number; + outside_qty: number; + uniformity: number; + cv: number; +}; + +export type UniformityDetailItem = { + id: number; + weight: number; + range: 'Ideal' | 'Outside'; +}; + +export type UniformityDetail = BaseMetadata & { + id: number; + info_umum: UniformityInfoUmum; + sampling: UniformitySampling; + result: UniformityResult; + uniformity_details: UniformityDetailItem[]; +}; + +// ==================== VERIFY RESPONSE ==================== +export type VerifyUniformityResponse = { + sampling: UniformitySampling; + result: UniformityResult; + uniformity_details: UniformityDetailItem[]; +}; + +// ==================== PAYLOADS ==================== export type CreateUniformityPayload = { date: string; project_flock_kandang_id: number; - files: File; + file: File; + week: number; }; export type VerifyUniformityPayload = { + date: string; project_flock_kandang_id: number; - files: File; -}; - -export type VerifyUniformityResponse = { - body_weights: number[]; + file: File; + week: number; }; From c55092297425379a64ba64635531cdd9acf03440 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sun, 28 Dec 2025 13:52:59 +0700 Subject: [PATCH 063/124] refactor(FE-316): Use real data and formatting in uniformity results --- .../uniformity/form/UniformityResultForm.tsx | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/components/pages/uniformity/form/UniformityResultForm.tsx b/src/components/pages/uniformity/form/UniformityResultForm.tsx index 7b8466fb..09be7fdd 100644 --- a/src/components/pages/uniformity/form/UniformityResultForm.tsx +++ b/src/components/pages/uniformity/form/UniformityResultForm.tsx @@ -15,6 +15,7 @@ import toast from 'react-hot-toast'; import { UniformityApi } from '@/services/api/uniformity'; import { isResponseError } from '@/lib/api-helper'; import Badge from '@/components/Badge'; +import { formatNumber } from '@/lib/helper'; const weightStatusColorMap: Record = { ideal: 'bg-[#00D39033]', @@ -126,26 +127,28 @@ const UniformityResultForm = () => { const samplingTableData: SamplingData[] = useMemo(() => { if (!verifyUniformityResult) return []; + const { sampling } = verifyUniformityResult; + return [ { id: 'sampling-size', label: 'Sampling size', - value: `1,150 of Birds`, + value: `${formatNumber(sampling.chick_qty_of_weight)} of Birds`, }, { id: 'mean-weight', label: 'Mean Weight', - value: `121 g`, + value: `${sampling.mean_weight} g`, }, { id: 'min-limit', label: 'Min Limit (-10%)', - value: `109 g`, + value: `${sampling.mean_down} g`, }, { id: 'max-limit', label: 'Max Limit (+10%)', - value: `133 g`, + value: `${sampling.mean_up} g`, }, ]; }, [verifyUniformityResult]); @@ -169,21 +172,23 @@ const UniformityResultForm = () => { const resultTableData: ResultData[] = useMemo(() => { if (!verifyUniformityResult) return []; + const { result } = verifyUniformityResult; + return [ { id: 'ideal-birds', label: 'Ideal Birds', - value: `851 of Birds`, + value: `${formatNumber(result.uniform_qty)} of Birds`, }, { id: 'outside-range', label: 'Outside Range', - value: `299 of Birds`, + value: `${formatNumber(result.outside_qty)} of Birds`, }, { id: 'uniformity', label: 'Uniformity', - value: `74 %`, + value: `${result.uniformity} %`, }, ]; }, [verifyUniformityResult]); @@ -250,11 +255,12 @@ const UniformityResultForm = () => { statusIndicator={true} variant='soft' className={{ - badge: `rounded-xl w-full justify-start border border-gray-200 text-black bg-[#00D39033]`, - status: 'bg-[#008000]', + badge: + 'rounded-xl w-full justify-start border border-gray-200 text-black bg-info/10', + status: 'bg-info', }} > - Ideal + Unknown ); }, From b2c09bb7c733ba6cf5aa130e991ba2cdf6530844 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sun, 28 Dec 2025 13:56:32 +0700 Subject: [PATCH 064/124] refactor(FE-316): Reset uniformity state on drawer close --- .../pages/uniformity/form/UniformityForm.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx index 40e4fa22..10e8fda0 100644 --- a/src/components/pages/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -389,6 +389,9 @@ const UniformityForm = ({ unsub(); useUiStore.getState().setExpandedDrawerOpen(false); useUiStore.getState().setExpandedDrawerContent(null); + useUiStore.getState().setIsNextStep(false); + useUniformityStore.getState().setUniformityStep('preview'); + useUniformityStore.getState().setVerifyUniformityResult(null); }; }, [subscribeValidate, setIsValid]); @@ -401,8 +404,18 @@ const UniformityForm = ({ } } else { setExpandedDrawerContent(null); + setIsNextStep(false); + setUniformityStep('preview'); + setVerifyUniformityResult(null); } - }, [expandedDrawerOpen, uniformityStep, setExpandedDrawerContent]); + }, [ + expandedDrawerOpen, + uniformityStep, + setExpandedDrawerContent, + setIsNextStep, + setUniformityStep, + setVerifyUniformityResult, + ]); return ( <> From 8a6f78ef84aa6ba475d68495e97b6dfd3b926d86 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sun, 28 Dec 2025 14:06:30 +0700 Subject: [PATCH 065/124] feat(FE-316): Show details table in success confirmation modal --- .../pages/uniformity/UniformityTable.tsx | 62 +++++++++++++++++-- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/src/components/pages/uniformity/UniformityTable.tsx b/src/components/pages/uniformity/UniformityTable.tsx index fee811d0..dbfcf9f2 100644 --- a/src/components/pages/uniformity/UniformityTable.tsx +++ b/src/components/pages/uniformity/UniformityTable.tsx @@ -462,14 +462,68 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { + > +
+ props.row.original.label, + }, + { + accessorKey: 'value', + header: 'Value', + cell: (props) => {props.row.original.value}, + }, + ]} + pageSize={6} + className={{ + containerClassName: 'mb-0', + paginationClassName: 'hidden', + }} + /> + + ); From c0a818af7e695b50f219507b516be3f7ddff7903 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sun, 28 Dec 2025 14:32:14 +0700 Subject: [PATCH 066/124] feat(FE-438): Add bulk approve/reject/delete and FAB --- .../pages/uniformity/UniformityTable.tsx | 273 ++++++++++-------- src/services/api/uniformity.ts | 50 ++++ 2 files changed, 197 insertions(+), 126 deletions(-) diff --git a/src/components/pages/uniformity/UniformityTable.tsx b/src/components/pages/uniformity/UniformityTable.tsx index dbfcf9f2..05e1fdd9 100644 --- a/src/components/pages/uniformity/UniformityTable.tsx +++ b/src/components/pages/uniformity/UniformityTable.tsx @@ -3,7 +3,7 @@ import React, { useCallback, useState, useEffect, useMemo } from 'react'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; -import { CellContext, ColumnDef, SortingState } from '@tanstack/react-table'; +import { ColumnDef, SortingState } from '@tanstack/react-table'; import { cn, formatDate } from '@/lib/helper'; import Button from '@/components/Button'; import UniformityChart from '@/components/pages/uniformity/UniformityChart'; @@ -15,9 +15,6 @@ import { isResponseSuccess } from '@/lib/api-helper'; import Table from '@/components/Table'; import Badge from '@/components/Badge'; import CheckboxInput from '@/components/input/CheckboxInput'; -import RowDropdownOptions from '@/components/table/RowDropdownOptions'; -import RowCollapseOptions from '@/components/table/RowCollapseOptions'; -import RowOptionsMenuWrapper from '@/components/table/RowOptionsMenuWrapper'; import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import toast from 'react-hot-toast'; @@ -25,6 +22,7 @@ import Card from '@/components/Card'; import UniformityTableSkeleton from './skeleton/UniformityTableSkeleton'; import RequirePermission from '@/components/helper/RequirePermission'; import { useUniformityStore } from '@/stores/uniformity/uniformity.store'; +import FloatingActionsButton from '@/components/FloatingActionsButton'; const statusColorMap: Record = { APPROVED: 'bg-[#00D39033]', @@ -60,68 +58,6 @@ const isUniformityLocked = (uniformity: Uniformity): boolean => { return uniformity.status === 'APPROVED' || uniformity.status === 'REJECTED'; }; -const RowOptionsMenu = ({ - type = 'dropdown', - props, - deleteClickHandler, - setSelectedUniformity, - openModal, -}: { - type: 'dropdown' | 'collapse'; - props: CellContext; - deleteClickHandler: () => void; - setSelectedUniformity: (uniformity: Uniformity) => void; - openModal: () => void; -}) => { - const handleDeleteClick = useCallback(() => { - setSelectedUniformity(props.row.original); - openModal(); - }, [props.row.original, setSelectedUniformity, openModal]); - - return ( - - - - - - - - - - - - ); -}; - const UniformityTable = ({ refresh }: { refresh?: () => void }) => { const isSuccess = useUniformityStore((s) => s.isSuccess); const setIsSuccess = useUniformityStore((s) => s.setIsSuccess); @@ -143,14 +79,41 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState>({}); - const [selectedUniformity, setSelectedUniformity] = useState< - Uniformity | undefined - >(undefined); + const [selectedUniformity] = useState(undefined); const [isDeleteLoading, setIsDeleteLoading] = useState(false); + const [isBulkActionLoading, setIsBulkActionLoading] = useState(false); const singleDeleteModal = useModal(); + const bulkDeleteModal = useModal(); const successModal = useModal(); + const { + data: uniformities, + isLoading, + mutate: refreshUniformities, + } = useSWR( + `${UniformityApi.basePath}${getTableFilterQueryString()}`, + UniformityApi.getAllFetcher + ); + + const selectedRowIds = useMemo(() => { + return Object.keys(rowSelection) + .filter((key) => rowSelection[key]) + .map((key) => parseInt(key)); + }, [rowSelection]); + + const selectedUniformities = useMemo(() => { + if (!isResponseSuccess(uniformities) || !uniformities.data) return []; + return uniformities.data.filter((u) => selectedRowIds.includes(u.id)); + }, [uniformities, selectedRowIds]); + + const canApproveReject = useMemo(() => { + return ( + selectedUniformities.length > 0 && + selectedUniformities.every((u) => u.status === 'CREATED') + ); + }, [selectedUniformities]); + useEffect(() => { if (isSuccess) { successModal.openModal(); @@ -162,15 +125,6 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { setIsSuccess(false); }; - const { - data: uniformities, - isLoading, - mutate: refreshUniformities, - } = useSWR( - `${UniformityApi.basePath}${getTableFilterQueryString()}`, - UniformityApi.getAllFetcher - ); - const singleDeleteHandler = useCallback(async () => { setIsDeleteLoading(true); @@ -182,6 +136,72 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { setIsDeleteLoading(false); }, [selectedUniformity?.id, refreshUniformities, singleDeleteModal]); + const handleBulkDelete = useCallback(() => { + bulkDeleteModal.openModal(); + }, [bulkDeleteModal]); + + const bulkDeleteHandler = useCallback(async () => { + setIsBulkActionLoading(true); + + try { + await UniformityApi.bulkDelete(selectedRowIds); + + setRowSelection({}); + refreshUniformities(); + + bulkDeleteModal.closeModal(); + toast.success( + `Successfully deleted ${selectedRowIds.length} Uniformity data!` + ); + } catch { + toast.error('Failed to delete Uniformity data'); + } finally { + setIsBulkActionLoading(false); + } + }, [selectedRowIds, refreshUniformities, bulkDeleteModal]); + + const handleCloseFab = useCallback(() => { + setRowSelection({}); + }, []); + + const handleBulkApprove = useCallback(async () => { + setIsBulkActionLoading(true); + + try { + await UniformityApi.approve(selectedRowIds); + + setRowSelection({}); + refreshUniformities(); + + toast.success( + `Successfully approved ${selectedRowIds.length} Uniformity data!` + ); + } catch { + toast.error('Failed to approve Uniformity data'); + } finally { + setIsBulkActionLoading(false); + } + }, [selectedRowIds, refreshUniformities]); + + const handleBulkReject = useCallback(async () => { + setIsBulkActionLoading(true); + + try { + await UniformityApi.reject(selectedRowIds); + + setRowSelection({}); + refreshUniformities(); + + toast.success( + `Successfully rejected ${selectedRowIds.length} Uniformity data!` + ); + } catch (error) { + toast.error('Failed to reject Uniformity data'); + } finally { + setIsBulkActionLoading(false); + } + }, [selectedRowIds, refreshUniformities]); + useEffect(() => { if (isResponseSuccess(uniformities) && uniformities.data) { const newSelection: Record = {}; @@ -316,53 +336,6 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { return {uniformity}%; }, }, - { - id: 'actions', - header: 'Aksi', - cell: (props: CellContext) => { - const currentPageSize = - props.table.getPaginationRowModel().rows.length; - const currentPageRows = props.table.getPaginationRowModel().flatRows; - const currentRowRelativeIndex = - currentPageRows.findIndex((r) => r.id === props.row.id) + 1; - - const isLast2Rows = currentRowRelativeIndex > currentPageSize - 2; - - return ( - <> - {currentPageSize > 2 && ( - - { - setSelectedUniformity(props.row.original); - singleDeleteModal.openModal(); - }} - setSelectedUniformity={setSelectedUniformity} - openModal={singleDeleteModal.openModal} - /> - - )} - - {currentPageSize <= 2 && ( - - { - setSelectedUniformity(props.row.original); - singleDeleteModal.openModal(); - }} - setSelectedUniformity={setSelectedUniformity} - openModal={singleDeleteModal.openModal} - /> - - )} - - ); - }, - }, ], [] ); @@ -407,7 +380,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { @@ -459,6 +432,21 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { }} /> + + void }) => { /> + + {/* Floating Actions Button */} + ); diff --git a/src/services/api/uniformity.ts b/src/services/api/uniformity.ts index 7ef16e91..4728e3e7 100644 --- a/src/services/api/uniformity.ts +++ b/src/services/api/uniformity.ts @@ -61,6 +61,56 @@ export class UniformityApiService extends BaseApiService< } ); } + + async approve( + idOrIds: number | number[], + notes?: string + ): Promise | undefined> { + const approvable_ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds]; + return await this.customRequest>( + 'approvals', + { + method: 'POST', + payload: { + action: 'APPROVED', + approvable_ids, + notes, + }, + } + ); + } + + async reject( + idOrIds: number | number[], + notes: string = '' + ): Promise | undefined> { + const approvable_ids = Array.isArray(idOrIds) ? idOrIds : [idOrIds]; + return await this.customRequest>( + 'approvals', + { + method: 'POST', + payload: { + action: 'REJECTED', + approvable_ids, + notes, + }, + } + ); + } + + async bulkDelete( + ids: number[] + ): Promise | undefined> { + return await this.customRequest>( + 'bulk-delete', + { + method: 'POST', + payload: { + ids, + }, + } + ); + } } export const UniformityApi = new UniformityApiService( From b1ccad081d5d579f838225a8cea730d66f92fcb7 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sun, 28 Dec 2025 16:44:01 +0700 Subject: [PATCH 067/124] refactor(FE-438): Add icon position/size and subtitle to modal --- src/components/modal/ConfirmationModal.tsx | 149 +++++++++++++----- .../pages/uniformity/UniformityTable.tsx | 129 ++++++++++++++- 2 files changed, 238 insertions(+), 40 deletions(-) diff --git a/src/components/modal/ConfirmationModal.tsx b/src/components/modal/ConfirmationModal.tsx index 00b63c86..3ed33650 100644 --- a/src/components/modal/ConfirmationModal.tsx +++ b/src/components/modal/ConfirmationModal.tsx @@ -8,10 +8,13 @@ import Button, { ButtonProps } from '@/components/Button'; import { cn } from '@/lib/helper'; +export type IconPosition = 'left' | 'center' | 'right'; + export interface ConfirmationModalProps { ref: RefObject; type?: 'info' | 'success' | 'error'; text?: string; + subtitleText?: string; closeOnBackdrop?: boolean; primaryButton?: ButtonProps & { text?: string; @@ -24,17 +27,22 @@ export interface ConfirmationModalProps { modalBox?: string; }; children?: React.ReactNode; + iconSize?: number; + iconPosition?: IconPosition; } const ConfirmationModal = ({ ref, type = 'info', text, + subtitleText, closeOnBackdrop, primaryButton, secondaryButton, className, children, + iconSize = 64, + iconPosition = 'center', }: ConfirmationModalProps) => { const [isPrimaryButtonLoading, setIsPrimaryButtonLoading] = useState(false); @@ -55,47 +63,112 @@ const ConfirmationModal = ({ return (
-
- {type === 'info' && ( - - )} + {iconPosition === 'center' ? ( + <> +
+ {type === 'info' && ( + + )} - {type === 'success' && ( - - )} + {type === 'success' && ( + + )} - {type === 'error' && ( - - )} -
+ {type === 'error' && ( + + )} +
-

- {text ?? 'Apakah anda yakin ingin melakukan hal ini?'} -

+

+ {text ?? 'Apakah anda yakin ingin melakukan hal ini?'} +

+ + {subtitleText && ( +

+ {subtitleText} +

+ )} + + ) : ( +
+
+ {type === 'info' && ( + + )} + + {type === 'success' && ( + + )} + + {type === 'error' && ( + + )} +
+ +
+

+ {text ?? 'Apakah anda yakin ingin melakukan hal ini?'} +

+ + {subtitleText && ( +

{subtitleText}

+ )} +
+
+ )} {children &&
{children}
} diff --git a/src/components/pages/uniformity/UniformityTable.tsx b/src/components/pages/uniformity/UniformityTable.tsx index 05e1fdd9..5e3a2f7f 100644 --- a/src/components/pages/uniformity/UniformityTable.tsx +++ b/src/components/pages/uniformity/UniformityTable.tsx @@ -420,6 +420,8 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { void }) => { isLoading: isDeleteLoading, onClick: singleDeleteHandler, }} - /> + className={{ + modalBox: 'rounded-2xl', + }} + > + {' '} +
+
props.row.original.label, + }, + { + accessorKey: 'value', + header: 'Value', + cell: (props) => {props.row.original.value}, + }, + ]} + pageSize={6} + className={{ + containerClassName: 'mb-0', + paginationClassName: 'hidden', + }} + /> + + void }) => { isLoading: isBulkActionLoading, onClick: bulkDeleteHandler, }} - /> + className={{ + modalBox: 'rounded-2xl', + }} + > +
+
props.row.original.label, + }, + { + accessorKey: 'value', + header: 'Value', + cell: (props) => {props.row.original.value}, + }, + ]} + pageSize={6} + className={{ + containerClassName: 'mb-0', + paginationClassName: 'hidden', + }} + /> + +
Date: Sun, 28 Dec 2025 16:58:29 +0700 Subject: [PATCH 068/124] feat(FE-438): Add approve/reject flows to UniformityTable --- .../pages/uniformity/UniformityTable.tsx | 300 +++++++++++++++++- 1 file changed, 296 insertions(+), 4 deletions(-) diff --git a/src/components/pages/uniformity/UniformityTable.tsx b/src/components/pages/uniformity/UniformityTable.tsx index 5e3a2f7f..77d0c75c 100644 --- a/src/components/pages/uniformity/UniformityTable.tsx +++ b/src/components/pages/uniformity/UniformityTable.tsx @@ -86,6 +86,10 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { const singleDeleteModal = useModal(); const bulkDeleteModal = useModal(); const successModal = useModal(); + const singleApproveModal = useModal(); + const singleRejectModal = useModal(); + const bulkApproveModal = useModal(); + const bulkRejectModal = useModal(); const { data: uniformities, @@ -136,6 +140,38 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { setIsDeleteLoading(false); }, [selectedUniformity?.id, refreshUniformities, singleDeleteModal]); + const singleApproveHandler = useCallback(async () => { + setIsDeleteLoading(true); + + try { + await UniformityApi.approve([selectedUniformity?.id as number]); + refreshUniformities(); + + singleApproveModal.closeModal(); + toast.success('Successfully approved Uniformity!'); + } catch { + toast.error('Failed to approve Uniformity'); + } finally { + setIsDeleteLoading(false); + } + }, [selectedUniformity?.id, refreshUniformities, singleApproveModal]); + + const singleRejectHandler = useCallback(async () => { + setIsDeleteLoading(true); + + try { + await UniformityApi.reject([selectedUniformity?.id as number]); + refreshUniformities(); + + singleRejectModal.closeModal(); + toast.success('Successfully rejected Uniformity!'); + } catch { + toast.error('Failed to reject Uniformity'); + } finally { + setIsDeleteLoading(false); + } + }, [selectedUniformity?.id, refreshUniformities, singleRejectModal]); + const handleBulkDelete = useCallback(() => { bulkDeleteModal.openModal(); }, [bulkDeleteModal]); @@ -164,7 +200,15 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { setRowSelection({}); }, []); - const handleBulkApprove = useCallback(async () => { + const handleBulkApprove = useCallback(() => { + bulkApproveModal.openModal(); + }, [bulkApproveModal]); + + const handleBulkReject = useCallback(() => { + bulkRejectModal.openModal(); + }, [bulkRejectModal]); + + const bulkApproveHandler = useCallback(async () => { setIsBulkActionLoading(true); try { @@ -173,6 +217,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { setRowSelection({}); refreshUniformities(); + bulkApproveModal.closeModal(); toast.success( `Successfully approved ${selectedRowIds.length} Uniformity data!` ); @@ -181,9 +226,9 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { } finally { setIsBulkActionLoading(false); } - }, [selectedRowIds, refreshUniformities]); + }, [selectedRowIds, refreshUniformities, bulkApproveModal]); - const handleBulkReject = useCallback(async () => { + const bulkRejectHandler = useCallback(async () => { setIsBulkActionLoading(true); try { @@ -192,6 +237,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { setRowSelection({}); refreshUniformities(); + bulkRejectModal.closeModal(); toast.success( `Successfully rejected ${selectedRowIds.length} Uniformity data!` ); @@ -200,7 +246,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { } finally { setIsBulkActionLoading(false); } - }, [selectedRowIds, refreshUniformities]); + }, [selectedRowIds, refreshUniformities, bulkRejectModal]); useEffect(() => { if (isResponseSuccess(uniformities) && uniformities.data) { @@ -638,6 +684,252 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { + +
+
props.row.original.label, + }, + { + accessorKey: 'value', + header: 'Value', + cell: (props) => {props.row.original.value}, + }, + ]} + pageSize={6} + className={{ + containerClassName: 'mb-0', + paginationClassName: 'hidden', + }} + /> + + + + +
+
props.row.original.label, + }, + { + accessorKey: 'value', + header: 'Value', + cell: (props) => {props.row.original.value}, + }, + ]} + pageSize={6} + className={{ + containerClassName: 'mb-0', + paginationClassName: 'hidden', + }} + /> + + + + +
+
({ + id: `bulk-approve-${index}`, + label: `${index + 1}. ${u.location_name}`, + value: `${u.flock_name} - ${u.kandang_name}`, + }))} + columns={[ + { + accessorKey: 'label', + header: 'Label', + cell: (props) => props.row.original.label, + }, + { + accessorKey: 'value', + header: 'Value', + cell: (props) => {props.row.original.value}, + }, + ]} + pageSize={selectedUniformities.length} + className={{ + containerClassName: 'mb-0', + paginationClassName: 'hidden', + }} + /> + + + + +
+
({ + id: `bulk-reject-${index}`, + label: `${index + 1}. ${u.location_name}`, + value: `${u.flock_name} - ${u.kandang_name}`, + }))} + columns={[ + { + accessorKey: 'label', + header: 'Label', + cell: (props) => props.row.original.label, + }, + { + accessorKey: 'value', + header: 'Value', + cell: (props) => {props.row.original.value}, + }, + ]} + pageSize={selectedUniformities.length} + className={{ + containerClassName: 'mb-0', + paginationClassName: 'hidden', + }} + /> + + + {/* Floating Actions Button */} Date: Sun, 28 Dec 2025 17:08:47 +0700 Subject: [PATCH 069/124] refactor(FE-438): Rework confirmation modals and add bulk approve --- .../pages/uniformity/UniformityTable.tsx | 400 +++++++++--------- 1 file changed, 202 insertions(+), 198 deletions(-) diff --git a/src/components/pages/uniformity/UniformityTable.tsx b/src/components/pages/uniformity/UniformityTable.tsx index 77d0c75c..fbeec640 100644 --- a/src/components/pages/uniformity/UniformityTable.tsx +++ b/src/components/pages/uniformity/UniformityTable.tsx @@ -463,155 +463,6 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { emptyContent={} /> - - {' '} -
-
props.row.original.label, - }, - { - accessorKey: 'value', - header: 'Value', - cell: (props) => {props.row.original.value}, - }, - ]} - pageSize={6} - className={{ - containerClassName: 'mb-0', - paginationClassName: 'hidden', - }} - /> - - - - -
-
props.row.original.label, - }, - { - accessorKey: 'value', - header: 'Value', - cell: (props) => {props.row.original.value}, - }, - ]} - pageSize={6} - className={{ - containerClassName: 'mb-0', - paginationClassName: 'hidden', - }} - /> - - - void }) => { + +
+
props.row.original.label, + }, + { + accessorKey: 'value', + header: 'Value', + cell: (props) => {props.row.original.value}, + }, + ]} + pageSize={6} + className={{ + containerClassName: 'mb-0', + paginationClassName: 'hidden', + }} + /> + + + + +
+
props.row.original.label, + }, + { + accessorKey: 'value', + header: 'Value', + cell: (props) => {props.row.original.value}, + }, + ]} + pageSize={6} + className={{ + containerClassName: 'mb-0', + paginationClassName: 'hidden', + }} + /> + + + void }) => { + +
+
({ + id: `bulk-approve-${index}`, + label: `${index + 1}. ${u.location_name}`, + value: `${u.flock_name} - ${u.kandang_name}`, + }))} + columns={[ + { + accessorKey: 'label', + header: 'Label', + cell: (props) => props.row.original.label, + }, + { + accessorKey: 'value', + header: 'Value', + cell: (props) => {props.row.original.value}, + }, + ]} + pageSize={selectedUniformities.length} + className={{ + containerClassName: 'mb-0', + paginationClassName: 'hidden', + }} + /> + + + void }) => { - -
-
({ - id: `bulk-approve-${index}`, - label: `${index + 1}. ${u.location_name}`, - value: `${u.flock_name} - ${u.kandang_name}`, - }))} - columns={[ - { - accessorKey: 'label', - header: 'Label', - cell: (props) => props.row.original.label, - }, - { - accessorKey: 'value', - header: 'Value', - cell: (props) => {props.row.original.value}, - }, - ]} - pageSize={selectedUniformities.length} - className={{ - containerClassName: 'mb-0', - paginationClassName: 'hidden', - }} - /> - - - Date: Sun, 28 Dec 2025 17:46:27 +0700 Subject: [PATCH 070/124] refactor(FE-438): Refactor ConfirmationModal icon and update usages --- src/components/modal/ConfirmationModal.tsx | 142 +++++++++--------- .../pages/uniformity/UniformityTable.tsx | 17 +-- 2 files changed, 73 insertions(+), 86 deletions(-) diff --git a/src/components/modal/ConfirmationModal.tsx b/src/components/modal/ConfirmationModal.tsx index 3ed33650..9cf17008 100644 --- a/src/components/modal/ConfirmationModal.tsx +++ b/src/components/modal/ConfirmationModal.tsx @@ -31,6 +31,68 @@ export interface ConfirmationModalProps { iconPosition?: IconPosition; } +const iconConfig = { + info: { + icon: 'material-symbols:info-outline-rounded', + iconClassName: 'text-info-content', + bgClassName: 'bg-info', + outerRingClassName: 'bg-info/20', + borderClassName: 'border-info', + }, + success: { + icon: 'heroicons:check', + iconClassName: 'text-white', + bgClassName: 'bg-[#00D390]', + outerRingClassName: 'bg-[#00D3901F]', + borderClassName: 'border-[#CCF7EB]', + }, + error: { + icon: 'solar:danger-triangle-linear', + iconClassName: 'text-error-content', + bgClassName: 'bg-[#f03338]', + outerRingClassName: 'bg-[#f3cdcd]', + borderClassName: 'border-[#fff0ef]', + }, +} as const; + +const ConfirmationModalIcon = ({ + type, + size = 24, +}: { + type: 'info' | 'success' | 'error'; + size?: number; +}) => { + const config = iconConfig[type]; + + return ( +
+
+
+
+ +
+
+
+
+ ); +}; + const ConfirmationModal = ({ ref, type = 'info', @@ -41,7 +103,7 @@ const ConfirmationModal = ({ secondaryButton, className, children, - iconSize = 64, + iconSize = 32, iconPosition = 'center', }: ConfirmationModalProps) => { const [isPrimaryButtonLoading, setIsPrimaryButtonLoading] = useState(false); @@ -65,42 +127,8 @@ const ConfirmationModal = ({
{iconPosition === 'center' ? ( <> -
- {type === 'info' && ( - - )} - - {type === 'success' && ( - - )} - - {type === 'error' && ( - - )} +
+

@@ -120,42 +148,8 @@ const ConfirmationModal = ({ 'flex-row-reverse': iconPosition === 'right', })} > -

- {type === 'info' && ( - - )} - - {type === 'success' && ( - - )} - - {type === 'error' && ( - - )} +
+
@@ -176,7 +170,7 @@ const ConfirmationModal = ({ {secondaryButton && secondaryButton.text && (
({ - id: `bulk-approve-${index}`, - label: `${index + 1}. ${u.location_name}`, - value: `${u.flock_name} - ${u.kandang_name}`, - }))} + data={[ + { + id: 'tanggal', + label: 'Tanggal', + value: '28 Desember 2025', + }, + { + id: 'lokasi-farm', + label: 'Lokasi Farm', + value: 'Farm A', + }, + { + id: 'project-flock', + label: 'Project Flock', + value: 'Flock 2025-01', + }, + { + id: 'kandang', + label: 'Kandang', + value: 'Kandang 1', + }, + { + id: 'file-uniformity', + label: 'File Uniformity', + value: 'uniformity_data.xlsx', + }, + { + id: 'status', + label: 'Status', + value: 'Disetujui', + }, + ]} columns={[ { accessorKey: 'label', @@ -796,7 +821,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { cell: (props) => {props.row.original.value}, }, ]} - pageSize={selectedUniformities.length} + pageSize={6} className={{ containerClassName: 'mb-0', paginationClassName: 'hidden', @@ -812,10 +837,10 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { text='Reject This Submission?' subtitleText='Are you sure you want to reject this submission?' secondaryButton={{ - text: 'Tidak', + text: 'Cancel', }} primaryButton={{ - text: 'Ya', + text: 'Reject', color: 'primary', isLoading: isDeleteLoading, onClick: singleRejectHandler, @@ -830,34 +855,32 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { { id: 'tanggal', label: 'Tanggal', - value: selectedUniformity - ? formatDate(selectedUniformity.applied_at, 'DD MMM YYYY') - : '-', + value: '28 Desember 2025', }, { id: 'lokasi-farm', label: 'Lokasi Farm', - value: selectedUniformity?.location_name || '-', + value: 'Farm A', }, { id: 'project-flock', label: 'Project Flock', - value: selectedUniformity?.flock_name || '-', + value: 'Flock 2025-01', }, { id: 'kandang', label: 'Kandang', - value: selectedUniformity?.kandang_name || '-', + value: 'Kandang 1', }, { - id: 'uniformity', - label: 'Uniformity', - value: `${selectedUniformity?.uniformity || 0}%`, + id: 'file-uniformity', + label: 'File Uniformity', + value: 'uniformity_data.xlsx', }, { id: 'status', label: 'Status', - value: getStatusText(selectedUniformity?.status || 'CREATED'), + value: 'Disetujui', }, ]} columns={[ @@ -887,11 +910,11 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { iconPosition='left' text={`Apakah anda yakin ingin menolak ${selectedRowIds.length} data Uniformity yang dipilih?`} secondaryButton={{ - text: 'Tidak', + text: 'Cancel', }} primaryButton={{ - text: 'Ya', - color: 'error', + text: 'Reject', + color: 'primary', isLoading: isBulkActionLoading, onClick: bulkRejectHandler, }} @@ -901,11 +924,38 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { >
({ - id: `bulk-reject-${index}`, - label: `${index + 1}. ${u.location_name}`, - value: `${u.flock_name} - ${u.kandang_name}`, - }))} + data={[ + { + id: 'tanggal', + label: 'Tanggal', + value: '28 Desember 2025', + }, + { + id: 'lokasi-farm', + label: 'Lokasi Farm', + value: 'Farm A', + }, + { + id: 'project-flock', + label: 'Project Flock', + value: 'Flock 2025-01', + }, + { + id: 'kandang', + label: 'Kandang', + value: 'Kandang 1', + }, + { + id: 'file-uniformity', + label: 'File Uniformity', + value: 'uniformity_data.xlsx', + }, + { + id: 'status', + label: 'Status', + value: 'Disetujui', + }, + ]} columns={[ { accessorKey: 'label', @@ -918,7 +968,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { cell: (props) => {props.row.original.value}, }, ]} - pageSize={selectedUniformities.length} + pageSize={6} className={{ containerClassName: 'mb-0', paginationClassName: 'hidden', From 8ec76af0126619cf203b62c852949dacc50ffbdc Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sun, 28 Dec 2025 20:58:59 +0700 Subject: [PATCH 072/124] feat(FE-438): Add Uniformity detail view and navigation --- src/app/uniformity/add/page.tsx | 2 - src/app/uniformity/detail/page.tsx | 47 +++ src/app/uniformity/page.tsx | 4 +- .../pages/uniformity/UniformityTable.tsx | 23 ++ .../uniformity/detail/UniformityDetail.tsx | 283 ++++++++++++++++++ src/services/api/uniformity.ts | 11 +- 6 files changed, 366 insertions(+), 4 deletions(-) create mode 100644 src/app/uniformity/detail/page.tsx create mode 100644 src/components/pages/uniformity/detail/UniformityDetail.tsx diff --git a/src/app/uniformity/add/page.tsx b/src/app/uniformity/add/page.tsx index 9931eebe..7c12cc72 100644 --- a/src/app/uniformity/add/page.tsx +++ b/src/app/uniformity/add/page.tsx @@ -1,5 +1,3 @@ -'use client'; - import UniformityForm from '@/components/pages/uniformity/form/UniformityForm'; const AddUniformity = () => { diff --git a/src/app/uniformity/detail/page.tsx b/src/app/uniformity/detail/page.tsx new file mode 100644 index 00000000..a2e9402e --- /dev/null +++ b/src/app/uniformity/detail/page.tsx @@ -0,0 +1,47 @@ +'use client'; + +import UniformityDetail from '@/components/pages/uniformity/detail/UniformityDetail'; +import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; +import { UniformityApi } from '@/services/api/uniformity'; +import { useRouter, useSearchParams } from 'next/navigation'; +import useSWR from 'swr'; + +const UniformityDetailPage = () => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const uniformityId = searchParams.get('uniformityId'); + + const { data: uniformity, isLoading: isLoadingUniformity } = useSWR( + uniformityId, + (id: string) => UniformityApi.getUniformityDetail(parseInt(id)) + ); + + if (!uniformityId) { + router.back(); + + return ( +
+ +
+ ); + } + + if (!isLoadingUniformity && (!uniformity || isResponseError(uniformity))) { + router.replace('/404'); + return; + } + + return ( +
+ {isLoadingUniformity && ( + + )} + {isResponseSuccess(uniformity) && ( + + )} +
+ ); +}; + +export default UniformityDetailPage; diff --git a/src/app/uniformity/page.tsx b/src/app/uniformity/page.tsx index dd0e4466..24a31482 100644 --- a/src/app/uniformity/page.tsx +++ b/src/app/uniformity/page.tsx @@ -1,5 +1,7 @@ +import UniformityTable from '@/components/pages/uniformity/UniformityTable'; + const Uniformity = () => { - return <>; + return ; }; export default Uniformity; diff --git a/src/components/pages/uniformity/UniformityTable.tsx b/src/components/pages/uniformity/UniformityTable.tsx index 27365e40..efa8dd66 100644 --- a/src/components/pages/uniformity/UniformityTable.tsx +++ b/src/components/pages/uniformity/UniformityTable.tsx @@ -1,6 +1,7 @@ 'use client'; import React, { useCallback, useState, useEffect, useMemo } from 'react'; +import { useRouter } from 'next/navigation'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; import { ColumnDef, SortingState } from '@tanstack/react-table'; @@ -59,6 +60,7 @@ const isUniformityLocked = (uniformity: Uniformity): boolean => { }; const UniformityTable = ({ refresh }: { refresh?: () => void }) => { + const router = useRouter(); const isSuccess = useUniformityStore((s) => s.isSuccess); const setIsSuccess = useUniformityStore((s) => s.setIsSuccess); @@ -200,6 +202,14 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { setRowSelection({}); }, []); + const handleViewDetail = useCallback( + (uniformity: Uniformity) => { + router.push(`/uniformity/detail?uniformityId=${uniformity.id}`); + setRowSelection({}); + }, + [router] + ); + const handleBulkApprove = useCallback(() => { bulkApproveModal.openModal(); }, [bulkApproveModal]); @@ -980,6 +990,19 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { {/* Floating Actions Button */} = ({ + initialValues, +}) => { + const router = useRouter(); + + const handleClose = () => { + router.back(); + }; + + const infoUmumTableData: DetailOptionType[] = useMemo(() => { + if (!initialValues) return []; + + return [ + { + id: 'tanggal', + value: 'tanggal', + label: 'Tanggal', + }, + { + id: 'lokasi-farm', + value: 'lokasi-farm', + label: 'Lokasi Farm', + }, + { + id: 'project-flock', + value: 'project-flock', + label: 'Project Flock', + }, + { + id: 'kandang', + value: 'kandang', + label: 'Kandang', + }, + { + id: 'file-name', + value: 'file-name', + label: 'File Name', + }, + ]; + }, [initialValues]); + + const columnsInfoUmum: ColumnDef[] = useMemo( + () => [ + { + accessorKey: 'label', + header: 'Label', + cell: (props) => props.row.original.label, + }, + { + accessorKey: 'value', + header: 'Value', + cell: (props) => { + const id = props.row.original.id; + const { info_umum } = initialValues!; + + const valueMap: Record = { + tanggal: info_umum.tanggal, + 'lokasi-farm': info_umum.lokasi_farm, + 'project-flock': info_umum.project_flock, + kandang: info_umum.kandang, + 'file-name': info_umum.file_name, + }; + + return {valueMap[id] || '-'}; + }, + }, + ], + [initialValues] + ); + + const samplingTableData: DetailOptionType[] = useMemo(() => { + if (!initialValues) return []; + + return [ + { + id: 'sampling-size', + value: 'sampling-size', + label: 'Sampling size', + }, + { + id: 'mean-weight', + value: 'mean-weight', + label: 'Mean Weight', + }, + { + id: 'min-limit', + value: 'min-limit', + label: 'Min Limit (-10%)', + }, + { + id: 'max-limit', + value: 'max-limit', + label: 'Max Limit (+10%)', + }, + ]; + }, [initialValues]); + + const columnsSampling: ColumnDef[] = useMemo( + () => [ + { + accessorKey: 'label', + header: 'Label', + cell: (props) => props.row.original.label, + }, + { + accessorKey: 'value', + header: 'Value', + cell: (props) => { + const id = props.row.original.id; + const { sampling } = initialValues!; + + const valueMap: Record = { + 'sampling-size': `${formatNumber(sampling.chick_qty_of_weight)} of Birds`, + 'mean-weight': `${formatNumber(sampling.mean_weight)} g`, + 'min-limit': `${formatNumber(sampling.mean_down)} g`, + 'max-limit': `${formatNumber(sampling.mean_up)} g`, + }; + + return {valueMap[id] || '-'}; + }, + }, + ], + [initialValues] + ); + + const resultTableData: DetailOptionType[] = useMemo(() => { + if (!initialValues) return []; + + return [ + { + id: 'ideal-birds', + value: 'ideal-birds', + label: 'Ideal Birds', + }, + { + id: 'outside-range', + value: 'outside-range', + label: 'Outside Range', + }, + { + id: 'uniformity', + value: 'uniformity', + label: 'Uniformity', + }, + { + id: 'cv', + value: 'cv', + label: 'CV', + }, + ]; + }, [initialValues]); + + const columnsResult: ColumnDef[] = useMemo( + () => [ + { + accessorKey: 'label', + header: 'Label', + cell: (props) => props.row.original.label, + }, + { + accessorKey: 'value', + header: 'Value', + cell: (props) => { + const id = props.row.original.id; + const { result } = initialValues!; + + const valueMap: Record = { + 'ideal-birds': `${formatNumber(result.uniform_qty)} of Birds`, + 'outside-range': `${formatNumber(result.outside_qty)} of Birds`, + uniformity: `${result.uniformity} %`, + cv: `${result.cv}`, + }; + + return {valueMap[id] || '-'}; + }, + }, + ], + [initialValues] + ); + + return ( +
+ {/* Header */} + + + + + {/* Form Section */} +
+
+ {initialValues ? ( +
+ {/* Info Umum */} +
+

Informasi Umum

+ + data={infoUmumTableData} + columns={columnsInfoUmum} + pageSize={5} + className={{ + containerClassName: 'mb-0', + paginationClassName: 'hidden', + }} + /> +
+ + {/* Sampling */} +
+

Sampling and Range

+ + data={samplingTableData} + columns={columnsSampling} + pageSize={4} + className={{ + containerClassName: 'mb-0', + paginationClassName: 'hidden', + }} + /> +
+ + {/* Result */} +
+

Result

+ + data={resultTableData} + columns={columnsResult} + pageSize={4} + className={{ + containerClassName: 'mb-0', + paginationClassName: 'hidden', + }} + /> +
+
+ ) : ( +
+ +

No data available

+

Uniformity detail not found

+
+ )} +
+
+ ); +}; + +export default UniformityDetail; diff --git a/src/services/api/uniformity.ts b/src/services/api/uniformity.ts index 4728e3e7..892192b6 100644 --- a/src/services/api/uniformity.ts +++ b/src/services/api/uniformity.ts @@ -1,9 +1,10 @@ import { BaseApiService } from '@/services/api/base'; import { BaseApiResponse } from '@/types/api/api-general'; import { + Uniformity, + UniformityDetail, VerifyUniformityPayload, VerifyUniformityResponse, - Uniformity, CreateUniformityPayload, } from '@/types/api/uniformity/uniformity'; @@ -20,6 +21,14 @@ export class UniformityApiService extends BaseApiService< return await this.customRequest>(''); } + async getUniformityDetail( + id: number + ): Promise | undefined> { + return await this.customRequest>( + `/${id}` + ); + } + async createUniformity( payload: CreateUniformityPayload ): Promise | undefined> { From 2276df2790378bc8cdc61deaabb63ced5797923c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sun, 28 Dec 2025 21:19:38 +0700 Subject: [PATCH 073/124] feat(FE-438): Handle approve/reject via URL and add buttons --- .../pages/uniformity/UniformityTable.tsx | 29 +++- .../uniformity/detail/UniformityDetail.tsx | 160 +++--------------- 2 files changed, 50 insertions(+), 139 deletions(-) diff --git a/src/components/pages/uniformity/UniformityTable.tsx b/src/components/pages/uniformity/UniformityTable.tsx index efa8dd66..97194e98 100644 --- a/src/components/pages/uniformity/UniformityTable.tsx +++ b/src/components/pages/uniformity/UniformityTable.tsx @@ -1,7 +1,7 @@ 'use client'; import React, { useCallback, useState, useEffect, useMemo } from 'react'; -import { useRouter } from 'next/navigation'; +import { useRouter, useSearchParams } from 'next/navigation'; import useSWR from 'swr'; import { Icon } from '@iconify/react'; import { ColumnDef, SortingState } from '@tanstack/react-table'; @@ -61,6 +61,7 @@ const isUniformityLocked = (uniformity: Uniformity): boolean => { const UniformityTable = ({ refresh }: { refresh?: () => void }) => { const router = useRouter(); + const searchParams = useSearchParams(); const isSuccess = useUniformityStore((s) => s.isSuccess); const setIsSuccess = useUniformityStore((s) => s.setIsSuccess); @@ -81,7 +82,9 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { const [sorting, setSorting] = useState([]); const [rowSelection, setRowSelection] = useState>({}); - const [selectedUniformity] = useState(undefined); + const [selectedUniformity, setSelectedUniformity] = useState< + Uniformity | undefined + >(undefined); const [isDeleteLoading, setIsDeleteLoading] = useState(false); const [isBulkActionLoading, setIsBulkActionLoading] = useState(false); @@ -120,6 +123,28 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { ); }, [selectedUniformities]); + useEffect(() => { + const action = searchParams.get('action'); + const id = searchParams.get('id'); + + if (action && id) { + if (isResponseSuccess(uniformities)) { + const uniformity = uniformities.data.find((u) => u.id === parseInt(id)); + if (uniformity) { + setSelectedUniformity(uniformity); + + if (action === 'approve') { + singleApproveModal.openModal(); + } else if (action === 'reject') { + singleRejectModal.openModal(); + } + + router.replace('/uniformity', { scroll: false }); + } + } + } + }, [searchParams, uniformities]); + useEffect(() => { if (isSuccess) { successModal.openModal(); diff --git a/src/components/pages/uniformity/detail/UniformityDetail.tsx b/src/components/pages/uniformity/detail/UniformityDetail.tsx index 57edd666..ef43969e 100644 --- a/src/components/pages/uniformity/detail/UniformityDetail.tsx +++ b/src/components/pages/uniformity/detail/UniformityDetail.tsx @@ -10,6 +10,7 @@ import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import Table from '@/components/Table'; import { formatNumber } from '@/lib/helper'; import { type OptionType } from '@/components/input/SelectInput'; +import RequirePermission from '@/components/helper/RequirePermission'; import { UniformityDetail as UniformityDetailType } from '@/types/api/uniformity/uniformity'; type DetailOptionType = OptionType & { @@ -29,9 +30,19 @@ const UniformityDetail: React.FC = ({ router.back(); }; + const handleApprove = () => { + router.push(`/uniformity?action=approve&id=${initialValues.id}`); + }; + + const handleReject = () => { + router.push(`/uniformity?action=reject&id=${initialValues.id}`); + }; + const infoUmumTableData: DetailOptionType[] = useMemo(() => { if (!initialValues) return []; + const { info_umum } = initialValues; + return [ { id: 'tanggal', @@ -90,116 +101,6 @@ const UniformityDetail: React.FC = ({ [initialValues] ); - const samplingTableData: DetailOptionType[] = useMemo(() => { - if (!initialValues) return []; - - return [ - { - id: 'sampling-size', - value: 'sampling-size', - label: 'Sampling size', - }, - { - id: 'mean-weight', - value: 'mean-weight', - label: 'Mean Weight', - }, - { - id: 'min-limit', - value: 'min-limit', - label: 'Min Limit (-10%)', - }, - { - id: 'max-limit', - value: 'max-limit', - label: 'Max Limit (+10%)', - }, - ]; - }, [initialValues]); - - const columnsSampling: ColumnDef[] = useMemo( - () => [ - { - accessorKey: 'label', - header: 'Label', - cell: (props) => props.row.original.label, - }, - { - accessorKey: 'value', - header: 'Value', - cell: (props) => { - const id = props.row.original.id; - const { sampling } = initialValues!; - - const valueMap: Record = { - 'sampling-size': `${formatNumber(sampling.chick_qty_of_weight)} of Birds`, - 'mean-weight': `${formatNumber(sampling.mean_weight)} g`, - 'min-limit': `${formatNumber(sampling.mean_down)} g`, - 'max-limit': `${formatNumber(sampling.mean_up)} g`, - }; - - return {valueMap[id] || '-'}; - }, - }, - ], - [initialValues] - ); - - const resultTableData: DetailOptionType[] = useMemo(() => { - if (!initialValues) return []; - - return [ - { - id: 'ideal-birds', - value: 'ideal-birds', - label: 'Ideal Birds', - }, - { - id: 'outside-range', - value: 'outside-range', - label: 'Outside Range', - }, - { - id: 'uniformity', - value: 'uniformity', - label: 'Uniformity', - }, - { - id: 'cv', - value: 'cv', - label: 'CV', - }, - ]; - }, [initialValues]); - - const columnsResult: ColumnDef[] = useMemo( - () => [ - { - accessorKey: 'label', - header: 'Label', - cell: (props) => props.row.original.label, - }, - { - accessorKey: 'value', - header: 'Value', - cell: (props) => { - const id = props.row.original.id; - const { result } = initialValues!; - - const valueMap: Record = { - 'ideal-birds': `${formatNumber(result.uniform_qty)} of Birds`, - 'outside-range': `${formatNumber(result.outside_qty)} of Birds`, - uniformity: `${result.uniformity} %`, - cv: `${result.cv}`, - }; - - return {valueMap[id] || '-'}; - }, - }, - ], - [initialValues] - ); - return (
{/* Header */} @@ -233,34 +134,19 @@ const UniformityDetail: React.FC = ({ paginationClassName: 'hidden', }} /> - - {/* Sampling */} -
-

Sampling and Range

- - data={samplingTableData} - columns={columnsSampling} - pageSize={4} - className={{ - containerClassName: 'mb-0', - paginationClassName: 'hidden', - }} - /> -
- - {/* Result */} -
-

Result

- - data={resultTableData} - columns={columnsResult} - pageSize={4} - className={{ - containerClassName: 'mb-0', - paginationClassName: 'hidden', - }} - /> +
+ {/* Approve/Reject Buttons */} + {initialValues.result && ( + +
+ + +
+
+ )}
) : ( From 817f8a70106d3a97c609b65cf6735e92835a88e9 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sun, 28 Dec 2025 21:22:11 +0700 Subject: [PATCH 074/124] refactor(FE-438): Update UniformityDetail header --- .../pages/uniformity/detail/UniformityDetail.tsx | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/components/pages/uniformity/detail/UniformityDetail.tsx b/src/components/pages/uniformity/detail/UniformityDetail.tsx index ef43969e..ed36c072 100644 --- a/src/components/pages/uniformity/detail/UniformityDetail.tsx +++ b/src/components/pages/uniformity/detail/UniformityDetail.tsx @@ -105,17 +105,11 @@ const UniformityDetail: React.FC = ({
{/* Header */} - - + showDivider + /> {/* Form Section */}
From 39f70bd71be70e6fc99b8c0a4d0ebe96f19693fb Mon Sep 17 00:00:00 2001 From: rstubryan Date: Sun, 28 Dec 2025 21:25:45 +0700 Subject: [PATCH 075/124] refactor(FE-438): Remove unused code from UniformityDetail --- .../pages/uniformity/detail/UniformityDetail.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/components/pages/uniformity/detail/UniformityDetail.tsx b/src/components/pages/uniformity/detail/UniformityDetail.tsx index ed36c072..88f1d7fa 100644 --- a/src/components/pages/uniformity/detail/UniformityDetail.tsx +++ b/src/components/pages/uniformity/detail/UniformityDetail.tsx @@ -5,10 +5,8 @@ import { useRouter } from 'next/navigation'; import { Icon } from '@iconify/react'; import { ColumnDef } from '@tanstack/react-table'; import Button from '@/components/Button'; -import Tooltip from '@/components/Tooltip'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import Table from '@/components/Table'; -import { formatNumber } from '@/lib/helper'; import { type OptionType } from '@/components/input/SelectInput'; import RequirePermission from '@/components/helper/RequirePermission'; import { UniformityDetail as UniformityDetailType } from '@/types/api/uniformity/uniformity'; @@ -26,10 +24,6 @@ const UniformityDetail: React.FC = ({ }) => { const router = useRouter(); - const handleClose = () => { - router.back(); - }; - const handleApprove = () => { router.push(`/uniformity?action=approve&id=${initialValues.id}`); }; @@ -41,8 +35,6 @@ const UniformityDetail: React.FC = ({ const infoUmumTableData: DetailOptionType[] = useMemo(() => { if (!initialValues) return []; - const { info_umum } = initialValues; - return [ { id: 'tanggal', From 70d9b4d8ed605f9afa5eb63251dd9fc9707b2408 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 29 Dec 2025 09:23:25 +0700 Subject: [PATCH 076/124] feat(FE-438): Add approval badge to uniformity detail --- .../uniformity/detail/UniformityDetail.tsx | 69 +++++++++++++++++-- src/types/api/uniformity/uniformity.d.ts | 1 + 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/src/components/pages/uniformity/detail/UniformityDetail.tsx b/src/components/pages/uniformity/detail/UniformityDetail.tsx index 88f1d7fa..1ba06b0f 100644 --- a/src/components/pages/uniformity/detail/UniformityDetail.tsx +++ b/src/components/pages/uniformity/detail/UniformityDetail.tsx @@ -7,9 +7,41 @@ import { ColumnDef } from '@tanstack/react-table'; import Button from '@/components/Button'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import Table from '@/components/Table'; +import Badge from '@/components/Badge'; import { type OptionType } from '@/components/input/SelectInput'; import RequirePermission from '@/components/helper/RequirePermission'; import { UniformityDetail as UniformityDetailType } from '@/types/api/uniformity/uniformity'; +import { formatDate } from '@/lib/helper'; + +const statusColorMap: Record = { + APPROVED: 'bg-[#00D39033]', + REJECTED: 'bg-error/10', + CREATED: 'bg-[#f3f3f4]', +}; + +const statusIndicatorColorMap: Record = { + APPROVED: 'bg-[#008000]', + REJECTED: 'bg-error', + CREATED: 'bg-[#D9D9D9]', +}; + +const statusTextMap: Record = { + APPROVED: 'Disetujui', + REJECTED: 'Ditolak', + CREATED: 'Pengajuan', +}; + +const getStatusColor = (status: string): string => { + return statusColorMap[status] || 'bg-info'; +}; + +const getStatusIndicatorColor = (status: string): string => { + return statusIndicatorColorMap[status] || 'bg-info'; +}; + +const getStatusText = (status: string): string => { + return statusTextMap[status] || status; +}; type DetailOptionType = OptionType & { id: string; @@ -59,7 +91,12 @@ const UniformityDetail: React.FC = ({ { id: 'file-name', value: 'file-name', - label: 'File Name', + label: 'File Uniformity', + }, + { + id: 'approval-status', + value: 'approval-status', + label: 'Status', }, ]; }, [initialValues]); @@ -76,16 +113,40 @@ const UniformityDetail: React.FC = ({ header: 'Value', cell: (props) => { const id = props.row.original.id; - const { info_umum } = initialValues!; + const { info_umum, latest_approval } = initialValues!; + + const statusValue = latest_approval?.action ?? '-'; const valueMap: Record = { - tanggal: info_umum.tanggal, + tanggal: formatDate(info_umum.tanggal, 'DD MMMM YYYY'), 'lokasi-farm': info_umum.lokasi_farm, 'project-flock': info_umum.project_flock, kandang: info_umum.kandang, 'file-name': info_umum.file_name, + 'approval-status': statusValue, }; + if (id === 'approval-status') { + const status = latest_approval?.action; + if (status) { + return ( +
+ + {getStatusText(status)} + +
+ ); + } + return -; + } + return {valueMap[id] || '-'}; }, }, @@ -114,7 +175,7 @@ const UniformityDetail: React.FC = ({ data={infoUmumTableData} columns={columnsInfoUmum} - pageSize={5} + pageSize={6} className={{ containerClassName: 'mb-0', paginationClassName: 'hidden', diff --git a/src/types/api/uniformity/uniformity.d.ts b/src/types/api/uniformity/uniformity.d.ts index 1cdefadb..78269ad6 100644 --- a/src/types/api/uniformity/uniformity.d.ts +++ b/src/types/api/uniformity/uniformity.d.ts @@ -63,6 +63,7 @@ export type UniformityDetail = BaseMetadata & { sampling: UniformitySampling; result: UniformityResult; uniformity_details: UniformityDetailItem[]; + latest_approval?: BaseApproval; }; // ==================== VERIFY RESPONSE ==================== From 9f2fcbf1542502489b548a85f1e556b637949dcf Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 29 Dec 2025 12:08:30 +0700 Subject: [PATCH 077/124] refactor(FE-438): Add uniformity details preview drawer --- .../uniformity/detail/UniformityDetail.tsx | 49 ++- .../detail/UniformityDetailsPreview.tsx | 295 ++++++++++++++++++ 2 files changed, 342 insertions(+), 2 deletions(-) create mode 100644 src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx diff --git a/src/components/pages/uniformity/detail/UniformityDetail.tsx b/src/components/pages/uniformity/detail/UniformityDetail.tsx index 1ba06b0f..cae0a1ec 100644 --- a/src/components/pages/uniformity/detail/UniformityDetail.tsx +++ b/src/components/pages/uniformity/detail/UniformityDetail.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useMemo } from 'react'; +import { useMemo, useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { Icon } from '@iconify/react'; import { ColumnDef } from '@tanstack/react-table'; @@ -8,10 +8,13 @@ import Button from '@/components/Button'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import Table from '@/components/Table'; import Badge from '@/components/Badge'; +import Tooltip from '@/components/Tooltip'; import { type OptionType } from '@/components/input/SelectInput'; import RequirePermission from '@/components/helper/RequirePermission'; import { UniformityDetail as UniformityDetailType } from '@/types/api/uniformity/uniformity'; import { formatDate } from '@/lib/helper'; +import { useUiStore } from '@/stores/ui/ui.store'; +import UniformityDetailsPreview from './UniformityDetailsPreview'; const statusColorMap: Record = { APPROVED: 'bg-[#00D39033]', @@ -43,7 +46,7 @@ const getStatusText = (status: string): string => { return statusTextMap[status] || status; }; -type DetailOptionType = OptionType & { +export type DetailOptionType = OptionType & { id: string; }; @@ -55,6 +58,10 @@ const UniformityDetail: React.FC = ({ initialValues, }) => { const router = useRouter(); + const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen); + const setExpandedDrawerContent = useUiStore( + (s) => s.setExpandedDrawerContent + ); const handleApprove = () => { router.push(`/uniformity?action=approve&id=${initialValues.id}`); @@ -64,6 +71,28 @@ const UniformityDetail: React.FC = ({ router.push(`/uniformity?action=reject&id=${initialValues.id}`); }; + const handleViewUniformityDetails = () => { + setExpandedDrawerContent( + + ); + + setTimeout(() => { + setExpandedDrawerOpen(true); + }, 0); + }; + + useEffect(() => { + return () => { + setExpandedDrawerOpen(false); + setExpandedDrawerContent(null); + }; + }, []); + const infoUmumTableData: DetailOptionType[] = useMemo(() => { if (!initialValues) return []; @@ -147,6 +176,22 @@ const UniformityDetail: React.FC = ({ return -; } + if (id === 'file-name') { + return ( +
+ {valueMap[id]} + + + +
+ ); + } + return {valueMap[id] || '-'}; }, }, diff --git a/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx b/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx new file mode 100644 index 00000000..4949e368 --- /dev/null +++ b/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx @@ -0,0 +1,295 @@ +'use client'; + +import React, { useMemo } from 'react'; +import { Icon } from '@iconify/react'; +import { ColumnDef } from '@tanstack/react-table'; +import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; +import { useUiStore } from '@/stores/ui/ui.store'; +import { + UniformityDetailItem, + UniformitySampling, + UniformityResult, + UniformityInfoUmum, +} from '@/types/api/uniformity/uniformity'; +import Table from '@/components/Table'; +import Badge from '@/components/Badge'; +import { formatNumber } from '@/lib/helper'; +import { DetailOptionType } from '@/components/pages/uniformity/detail/UniformityDetail'; + +const weightStatusColorMap: Record = { + ideal: 'bg-[#00D39033]', + outside: 'bg-error/10', +}; + +const weightStatusIndicatorColorMap: Record = { + ideal: 'bg-[#008000]', + outside: 'bg-error', +}; + +const weightStatusTextMap: Record = { + ideal: 'Ideal', + outside: 'Outside', +}; + +const getWeightStatusColor = (status: string): string => { + return weightStatusColorMap[status] || 'bg-info'; +}; + +const getWeightStatusIndicatorColor = (status: string): string => { + return weightStatusIndicatorColorMap[status] || 'bg-info'; +}; + +const getWeightStatusText = (status: string): string => { + return weightStatusTextMap[status] || status; +}; + +type BodyWeightData = { + id: string; + number: number; + weight: number; + status?: 'ideal' | 'outside'; +}; + +interface UniformityDetailsPreviewProps { + info_umum: UniformityInfoUmum; + uniformityDetails: UniformityDetailItem[]; + sampling: UniformitySampling; + result: UniformityResult; +} + +const UniformityDetailsPreview = ({ + info_umum, + uniformityDetails, + sampling, + result, +}: UniformityDetailsPreviewProps) => { + const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen); + + const handleClose = () => { + setExpandedDrawerOpen(false); + }; + + const samplingTableData: DetailOptionType[] = useMemo(() => { + if (!sampling) return []; + + return [ + { + id: 'sampling-size', + label: 'Sampling size', + value: `${formatNumber(sampling.chick_qty_of_weight)} of Birds`, + }, + { + id: 'mean-weight', + label: 'Mean Weight', + value: `${sampling.mean_weight} g`, + }, + { + id: 'min-limit', + label: 'Min Limit (-10%)', + value: `${sampling.mean_down} g`, + }, + { + id: 'max-limit', + label: 'Max Limit (+10%)', + value: `${sampling.mean_up} g`, + }, + ]; + }, [sampling]); + + const columnsSampling: ColumnDef[] = useMemo( + () => [ + { + accessorKey: 'label', + header: 'Label', + cell: (props) => props.row.original.label, + }, + { + accessorKey: 'value', + header: 'Value', + cell: (props) => {props.row.original.value}, + }, + ], + [] + ); + + const resultTableData: DetailOptionType[] = useMemo(() => { + if (!result) return []; + + return [ + { + id: 'ideal-birds', + label: 'Ideal Birds', + value: `${formatNumber(result.uniform_qty)} of Birds`, + }, + { + id: 'outside-range', + label: 'Outside Range', + value: `${formatNumber(result.outside_qty)} of Birds`, + }, + { + id: 'uniformity', + label: 'Uniformity', + value: `${result.uniformity} %`, + }, + { + id: 'cv', + label: 'CV', + value: `${result.cv} %`, + }, + ]; + }, [result]); + + const resultColumns: ColumnDef[] = useMemo( + () => [ + { + accessorKey: 'label', + header: 'Label', + cell: (props) => props.row.original.label, + }, + { + accessorKey: 'value', + header: 'Value', + cell: (props) => {props.row.original.value}, + }, + ], + [] + ); + + const tableData = useMemo(() => { + if (!uniformityDetails) return []; + + return uniformityDetails.map((detail, index) => ({ + id: `body-weight-${index + 1}`, + number: index + 1, + weight: detail.weight, + status: detail.range.toLowerCase() as 'ideal' | 'outside', + })); + }, [uniformityDetails]); + + const columnsUniformity: ColumnDef[] = useMemo( + () => [ + { + accessorKey: 'number', + header: 'No', + cell: (props) => props.row.original.number, + }, + { + accessorKey: 'weight', + header: 'Weight (g)', + cell: (props) => {props.row.original.weight}, + }, + { + accessorKey: 'status', + header: 'Status', + cell: (props) => { + const status = props.row.original.status; + return status ? ( +
+ + {getWeightStatusText(status)} + +
+ ) : ( + + Unknown + + ); + }, + }, + ], + [] + ); + + return ( +
+ {/* Header */} + + + + + {/* Form Section */} +
+
+ {uniformityDetails && uniformityDetails.length > 0 ? ( +
+ {/* Sampling and Range */} +
+

Sampling and Range

+ + data={samplingTableData} + columns={columnsSampling} + pageSize={4} + className={{ + containerClassName: 'mb-0', + paginationClassName: 'hidden', + }} + /> +
+ + {/* Result */} +
+

Result

+ + data={resultTableData} + columns={resultColumns} + pageSize={4} + className={{ + containerClassName: 'mb-0', + paginationClassName: 'hidden', + }} + /> +
+ + {/* Body Weight Details */} +
+ + data={tableData} + columns={columnsUniformity} + pageSize={15} + className={{ containerClassName: 'mb-5' }} + /> +
+
+ ) : ( +
+ +

No data available

+

Uniformity details not found

+
+ )} +
+
+ ); +}; + +export default UniformityDetailsPreview; From cd41d5daab34967d4bafc76c1da7bcbfd41cb2e2 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 29 Dec 2025 13:16:39 +0700 Subject: [PATCH 078/124] refactor(FE-438): Extract uniformity status and weight helpers --- .../pages/uniformity/UniformityTable.tsx | 35 ++---------- .../uniformity/detail/UniformityDetail.tsx | 35 ++---------- .../detail/UniformityDetailsPreview.tsx | 32 ++--------- .../uniformity/form/UniformityResultForm.tsx | 32 ++--------- .../pages/uniformity/uniformity-utils.ts | 56 +++++++++++++++++++ 5 files changed, 76 insertions(+), 114 deletions(-) create mode 100644 src/components/pages/uniformity/uniformity-utils.ts diff --git a/src/components/pages/uniformity/UniformityTable.tsx b/src/components/pages/uniformity/UniformityTable.tsx index 97194e98..72a9d2ad 100644 --- a/src/components/pages/uniformity/UniformityTable.tsx +++ b/src/components/pages/uniformity/UniformityTable.tsx @@ -24,36 +24,11 @@ import UniformityTableSkeleton from './skeleton/UniformityTableSkeleton'; import RequirePermission from '@/components/helper/RequirePermission'; import { useUniformityStore } from '@/stores/uniformity/uniformity.store'; import FloatingActionsButton from '@/components/FloatingActionsButton'; - -const statusColorMap: Record = { - APPROVED: 'bg-[#00D39033]', - REJECTED: 'bg-error/10', - CREATED: 'bg-[#f3f3f4]', -}; - -const statusIndicatorColorMap: Record = { - APPROVED: 'bg-[#008000]', - REJECTED: 'bg-error', - CREATED: 'bg-[#D9D9D9]', -}; - -const statusTextMap: Record = { - APPROVED: 'Disetujui', - REJECTED: 'Ditolak', - CREATED: 'Pengajuan', -}; - -const getStatusColor = (status: string): string => { - return statusColorMap[status] || 'bg-info'; -}; - -const getStatusIndicatorColor = (status: string): string => { - return statusIndicatorColorMap[status] || 'bg-info'; -}; - -const getStatusText = (status: string): string => { - return statusTextMap[status] || status; -}; +import { + getStatusColor, + getStatusIndicatorColor, + getStatusText, +} from '@/components/pages/uniformity/uniformity-utils'; const isUniformityLocked = (uniformity: Uniformity): boolean => { return uniformity.status === 'APPROVED' || uniformity.status === 'REJECTED'; diff --git a/src/components/pages/uniformity/detail/UniformityDetail.tsx b/src/components/pages/uniformity/detail/UniformityDetail.tsx index cae0a1ec..528c268a 100644 --- a/src/components/pages/uniformity/detail/UniformityDetail.tsx +++ b/src/components/pages/uniformity/detail/UniformityDetail.tsx @@ -15,36 +15,11 @@ import { UniformityDetail as UniformityDetailType } from '@/types/api/uniformity import { formatDate } from '@/lib/helper'; import { useUiStore } from '@/stores/ui/ui.store'; import UniformityDetailsPreview from './UniformityDetailsPreview'; - -const statusColorMap: Record = { - APPROVED: 'bg-[#00D39033]', - REJECTED: 'bg-error/10', - CREATED: 'bg-[#f3f3f4]', -}; - -const statusIndicatorColorMap: Record = { - APPROVED: 'bg-[#008000]', - REJECTED: 'bg-error', - CREATED: 'bg-[#D9D9D9]', -}; - -const statusTextMap: Record = { - APPROVED: 'Disetujui', - REJECTED: 'Ditolak', - CREATED: 'Pengajuan', -}; - -const getStatusColor = (status: string): string => { - return statusColorMap[status] || 'bg-info'; -}; - -const getStatusIndicatorColor = (status: string): string => { - return statusIndicatorColorMap[status] || 'bg-info'; -}; - -const getStatusText = (status: string): string => { - return statusTextMap[status] || status; -}; +import { + getStatusColor, + getStatusIndicatorColor, + getStatusText, +} from '@/components/pages/uniformity/uniformity-utils'; export type DetailOptionType = OptionType & { id: string; diff --git a/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx b/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx index 4949e368..8a20b8f8 100644 --- a/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx +++ b/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx @@ -15,33 +15,11 @@ import Table from '@/components/Table'; import Badge from '@/components/Badge'; import { formatNumber } from '@/lib/helper'; import { DetailOptionType } from '@/components/pages/uniformity/detail/UniformityDetail'; - -const weightStatusColorMap: Record = { - ideal: 'bg-[#00D39033]', - outside: 'bg-error/10', -}; - -const weightStatusIndicatorColorMap: Record = { - ideal: 'bg-[#008000]', - outside: 'bg-error', -}; - -const weightStatusTextMap: Record = { - ideal: 'Ideal', - outside: 'Outside', -}; - -const getWeightStatusColor = (status: string): string => { - return weightStatusColorMap[status] || 'bg-info'; -}; - -const getWeightStatusIndicatorColor = (status: string): string => { - return weightStatusIndicatorColorMap[status] || 'bg-info'; -}; - -const getWeightStatusText = (status: string): string => { - return weightStatusTextMap[status] || status; -}; +import { + getWeightStatusColor, + getWeightStatusIndicatorColor, + getWeightStatusText, +} from '@/components/pages/uniformity/uniformity-utils'; type BodyWeightData = { id: string; diff --git a/src/components/pages/uniformity/form/UniformityResultForm.tsx b/src/components/pages/uniformity/form/UniformityResultForm.tsx index 09be7fdd..7be02868 100644 --- a/src/components/pages/uniformity/form/UniformityResultForm.tsx +++ b/src/components/pages/uniformity/form/UniformityResultForm.tsx @@ -16,33 +16,11 @@ import { UniformityApi } from '@/services/api/uniformity'; import { isResponseError } from '@/lib/api-helper'; import Badge from '@/components/Badge'; import { formatNumber } from '@/lib/helper'; - -const weightStatusColorMap: Record = { - ideal: 'bg-[#00D39033]', - outside: 'bg-error/10', -}; - -const weightStatusIndicatorColorMap: Record = { - ideal: 'bg-[#008000]', - outside: 'bg-error', -}; - -const weightStatusTextMap: Record = { - ideal: 'Ideal', - outside: 'Outside', -}; - -const getWeightStatusColor = (status: string): string => { - return weightStatusColorMap[status] || 'bg-info'; -}; - -const getWeightStatusIndicatorColor = (status: string): string => { - return weightStatusIndicatorColorMap[status] || 'bg-info'; -}; - -const getWeightStatusText = (status: string): string => { - return weightStatusTextMap[status] || status; -}; +import { + getWeightStatusColor, + getWeightStatusIndicatorColor, + getWeightStatusText, +} from '@/components/pages/uniformity/uniformity-utils'; type BodyWeightData = { id: string; diff --git a/src/components/pages/uniformity/uniformity-utils.ts b/src/components/pages/uniformity/uniformity-utils.ts new file mode 100644 index 00000000..1c5ee0b5 --- /dev/null +++ b/src/components/pages/uniformity/uniformity-utils.ts @@ -0,0 +1,56 @@ +export const weightStatusColorMap: Record = { + ideal: 'bg-[#00D39033]', + outside: 'bg-error/10', +}; + +export const weightStatusIndicatorColorMap: Record = { + ideal: 'bg-[#008000]', + outside: 'bg-error', +}; + +export const weightStatusTextMap: Record = { + ideal: 'Ideal', + outside: 'Outside', +}; + +export const getWeightStatusColor = (status: string): string => { + return weightStatusColorMap[status] || 'bg-info'; +}; + +export const getWeightStatusIndicatorColor = (status: string): string => { + return weightStatusIndicatorColorMap[status] || 'bg-info'; +}; + +export const getWeightStatusText = (status: string): string => { + return weightStatusTextMap[status] || status; +}; + +export const statusColorMap: Record = { + APPROVED: 'bg-[#00D39033]', + REJECTED: 'bg-error/10', + CREATED: 'bg-[#f3f3f4]', +}; + +export const statusIndicatorColorMap: Record = { + APPROVED: 'bg-[#008000]', + REJECTED: 'bg-error', + CREATED: 'bg-[#D9D9D9]', +}; + +export const statusTextMap: Record = { + APPROVED: 'Disetujui', + REJECTED: 'Ditolak', + CREATED: 'Pengajuan', +}; + +export const getStatusColor = (status: string): string => { + return statusColorMap[status] || 'bg-info'; +}; + +export const getStatusIndicatorColor = (status: string): string => { + return statusIndicatorColorMap[status] || 'bg-info'; +}; + +export const getStatusText = (status: string): string => { + return statusTextMap[status] || status; +}; From 11a63f76b77f19c8624c944002ab1aa98b0ff919 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 29 Dec 2025 13:18:44 +0700 Subject: [PATCH 079/124] refactor(FE-438): Use shared DetailOptionType for result tables --- .../uniformity/form/UniformityResultForm.tsx | 29 +++++-------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/src/components/pages/uniformity/form/UniformityResultForm.tsx b/src/components/pages/uniformity/form/UniformityResultForm.tsx index 7be02868..a3342e9e 100644 --- a/src/components/pages/uniformity/form/UniformityResultForm.tsx +++ b/src/components/pages/uniformity/form/UniformityResultForm.tsx @@ -21,6 +21,7 @@ import { getWeightStatusIndicatorColor, getWeightStatusText, } from '@/components/pages/uniformity/uniformity-utils'; +import { DetailOptionType } from '@/components/pages/uniformity/detail/UniformityDetail'; type BodyWeightData = { id: string; @@ -29,18 +30,6 @@ type BodyWeightData = { status?: 'ideal' | 'outside'; }; -type SamplingData = { - id: string; - label: string; - value: string; -}; - -type ResultData = { - id: string; - label: string; - value: string; -}; - const UniformityResultForm = () => { const router = useRouter(); const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen); @@ -64,10 +53,6 @@ const UniformityResultForm = () => { setVerifyUniformityResult(null); }; - const handleBack = () => { - setUniformityStep('preview'); - }; - const handleSubmit = async () => { if (!uniformityFormData || !uniformityFormData.file) { toast.error('Form data is missing. Please try again.'); @@ -102,7 +87,7 @@ const UniformityResultForm = () => { } }; - const samplingTableData: SamplingData[] = useMemo(() => { + const samplingTableData: DetailOptionType[] = useMemo(() => { if (!verifyUniformityResult) return []; const { sampling } = verifyUniformityResult; @@ -131,7 +116,7 @@ const UniformityResultForm = () => { ]; }, [verifyUniformityResult]); - const columnsSampling: ColumnDef[] = useMemo( + const columnsSampling: ColumnDef[] = useMemo( () => [ { accessorKey: 'label', @@ -147,7 +132,7 @@ const UniformityResultForm = () => { [] ); - const resultTableData: ResultData[] = useMemo(() => { + const resultTableData: DetailOptionType[] = useMemo(() => { if (!verifyUniformityResult) return []; const { result } = verifyUniformityResult; @@ -171,7 +156,7 @@ const UniformityResultForm = () => { ]; }, [verifyUniformityResult]); - const resultColumns: ColumnDef[] = useMemo( + const resultColumns: ColumnDef[] = useMemo( () => [ { accessorKey: 'label', @@ -270,7 +255,7 @@ const UniformityResultForm = () => {

Sampling and Range

- + data={samplingTableData} columns={columnsSampling} pageSize={4} @@ -283,7 +268,7 @@ const UniformityResultForm = () => {

Result

- + data={resultTableData} columns={resultColumns} pageSize={3} From 4ed1e4f8b5448d1bcd71b173fa6c60cf9397dd1c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 29 Dec 2025 13:22:18 +0700 Subject: [PATCH 080/124] refactor(FE-438): Fix Uniformity gauge skeleton sizing --- .../uniformity/skeleton/UniformityGaugeChartSkeleton.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/pages/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx b/src/components/pages/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx index 1e5903a4..02ec70c1 100644 --- a/src/components/pages/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx +++ b/src/components/pages/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx @@ -29,9 +29,9 @@ const UniformityGaugeChartSkeleton: React.FC< return (
-
-
- +
+
+ Date: Mon, 29 Dec 2025 13:30:16 +0700 Subject: [PATCH 081/124] refactor(FE-316, 438): Move uniformity types into shared types file --- .../pages/uniformity/detail/UniformityDetail.tsx | 6 +----- .../uniformity/detail/UniformityDetailsPreview.tsx | 10 ++-------- .../pages/uniformity/form/UniformityPreviewForm.tsx | 7 +------ .../pages/uniformity/form/UniformityResultForm.tsx | 10 ++-------- src/types/api/uniformity/uniformity.d.ts | 12 ++++++++++++ 5 files changed, 18 insertions(+), 27 deletions(-) diff --git a/src/components/pages/uniformity/detail/UniformityDetail.tsx b/src/components/pages/uniformity/detail/UniformityDetail.tsx index 528c268a..1063f993 100644 --- a/src/components/pages/uniformity/detail/UniformityDetail.tsx +++ b/src/components/pages/uniformity/detail/UniformityDetail.tsx @@ -9,7 +9,6 @@ import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; import Table from '@/components/Table'; import Badge from '@/components/Badge'; import Tooltip from '@/components/Tooltip'; -import { type OptionType } from '@/components/input/SelectInput'; import RequirePermission from '@/components/helper/RequirePermission'; import { UniformityDetail as UniformityDetailType } from '@/types/api/uniformity/uniformity'; import { formatDate } from '@/lib/helper'; @@ -20,10 +19,7 @@ import { getStatusIndicatorColor, getStatusText, } from '@/components/pages/uniformity/uniformity-utils'; - -export type DetailOptionType = OptionType & { - id: string; -}; +import { DetailOptionType } from '@/types/api/uniformity/uniformity'; interface UniformityDetailProps { initialValues: UniformityDetailType; diff --git a/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx b/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx index 8a20b8f8..3588b068 100644 --- a/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx +++ b/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx @@ -14,19 +14,13 @@ import { import Table from '@/components/Table'; import Badge from '@/components/Badge'; import { formatNumber } from '@/lib/helper'; -import { DetailOptionType } from '@/components/pages/uniformity/detail/UniformityDetail'; +import { DetailOptionType } from '@/types/api/uniformity/uniformity'; import { getWeightStatusColor, getWeightStatusIndicatorColor, getWeightStatusText, } from '@/components/pages/uniformity/uniformity-utils'; - -type BodyWeightData = { - id: string; - number: number; - weight: number; - status?: 'ideal' | 'outside'; -}; +import { BodyWeightData } from '@/types/api/uniformity/uniformity'; interface UniformityDetailsPreviewProps { info_umum: UniformityInfoUmum; diff --git a/src/components/pages/uniformity/form/UniformityPreviewForm.tsx b/src/components/pages/uniformity/form/UniformityPreviewForm.tsx index ff5ffacb..4520464f 100644 --- a/src/components/pages/uniformity/form/UniformityPreviewForm.tsx +++ b/src/components/pages/uniformity/form/UniformityPreviewForm.tsx @@ -10,12 +10,7 @@ import { useUiStore } from '@/stores/ui/ui.store'; import { useUniformityStore } from '@/stores/uniformity/uniformity.store'; import RequirePermission from '@/components/helper/RequirePermission'; import Table from '@/components/Table'; - -type BodyWeightData = { - id: string; - number: number; - weight: number; -}; +import { BodyWeightData } from '@/types/api/uniformity/uniformity'; const UniformityPreviewForm = () => { const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen); diff --git a/src/components/pages/uniformity/form/UniformityResultForm.tsx b/src/components/pages/uniformity/form/UniformityResultForm.tsx index a3342e9e..6b3a520f 100644 --- a/src/components/pages/uniformity/form/UniformityResultForm.tsx +++ b/src/components/pages/uniformity/form/UniformityResultForm.tsx @@ -21,14 +21,8 @@ import { getWeightStatusIndicatorColor, getWeightStatusText, } from '@/components/pages/uniformity/uniformity-utils'; -import { DetailOptionType } from '@/components/pages/uniformity/detail/UniformityDetail'; - -type BodyWeightData = { - id: string; - number: number; - weight: number; - status?: 'ideal' | 'outside'; -}; +import { DetailOptionType } from '@/types/api/uniformity/uniformity'; +import { BodyWeightData } from '@/types/api/uniformity/uniformity'; const UniformityResultForm = () => { const router = useRouter(); diff --git a/src/types/api/uniformity/uniformity.d.ts b/src/types/api/uniformity/uniformity.d.ts index 78269ad6..cde4415d 100644 --- a/src/types/api/uniformity/uniformity.d.ts +++ b/src/types/api/uniformity/uniformity.d.ts @@ -87,3 +87,15 @@ export type VerifyUniformityPayload = { file: File; week: number; }; + +// ==================== OTHER TYPES ==================== +export type BodyWeightData = { + id: string; + number: number; + weight: number; + status?: 'ideal' | 'outside'; +}; + +export type DetailOptionType = OptionType & { + id: string; +}; From d02f919b7632ca2b4b5a31df2258725902768a5d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 29 Dec 2025 13:37:18 +0700 Subject: [PATCH 082/124] refactor(FE-438): Rename uniformityDetails prop to uniformity_details --- .../pages/uniformity/detail/UniformityDetail.tsx | 2 +- .../uniformity/detail/UniformityDetailsPreview.tsx | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/pages/uniformity/detail/UniformityDetail.tsx b/src/components/pages/uniformity/detail/UniformityDetail.tsx index 1063f993..e010f119 100644 --- a/src/components/pages/uniformity/detail/UniformityDetail.tsx +++ b/src/components/pages/uniformity/detail/UniformityDetail.tsx @@ -46,7 +46,7 @@ const UniformityDetail: React.FC = ({ setExpandedDrawerContent( diff --git a/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx b/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx index 3588b068..c26933d6 100644 --- a/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx +++ b/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx @@ -24,14 +24,14 @@ import { BodyWeightData } from '@/types/api/uniformity/uniformity'; interface UniformityDetailsPreviewProps { info_umum: UniformityInfoUmum; - uniformityDetails: UniformityDetailItem[]; + uniformity_details: UniformityDetailItem[]; sampling: UniformitySampling; result: UniformityResult; } const UniformityDetailsPreview = ({ info_umum, - uniformityDetails, + uniformity_details, sampling, result, }: UniformityDetailsPreviewProps) => { @@ -128,15 +128,15 @@ const UniformityDetailsPreview = ({ ); const tableData = useMemo(() => { - if (!uniformityDetails) return []; + if (!uniformity_details) return []; - return uniformityDetails.map((detail, index) => ({ + return uniformity_details.map((detail, index) => ({ id: `body-weight-${index + 1}`, number: index + 1, weight: detail.weight, status: detail.range.toLowerCase() as 'ideal' | 'outside', })); - }, [uniformityDetails]); + }, [uniformity_details]); const columnsUniformity: ColumnDef[] = useMemo( () => [ @@ -207,7 +207,7 @@ const UniformityDetailsPreview = ({ {/* Form Section */}
- {uniformityDetails && uniformityDetails.length > 0 ? ( + {uniformity_details && uniformity_details.length > 0 ? (
{/* Sampling and Range */}
From 9a1a6a7e41fc3ae57aa1dd8a9012b90abae73f04 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 29 Dec 2025 13:43:28 +0700 Subject: [PATCH 083/124] refactor(FE-316): Add cursor to buttons and enlarge close icon --- src/components/pages/uniformity/detail/UniformityDetail.tsx | 2 +- .../pages/uniformity/detail/UniformityDetailsPreview.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/pages/uniformity/detail/UniformityDetail.tsx b/src/components/pages/uniformity/detail/UniformityDetail.tsx index e010f119..2822fa45 100644 --- a/src/components/pages/uniformity/detail/UniformityDetail.tsx +++ b/src/components/pages/uniformity/detail/UniformityDetail.tsx @@ -153,7 +153,7 @@ const UniformityDetail: React.FC = ({ {valueMap[id]} From 11bd8b27b5d8e47d29a0fbd2122871843f2c1b3e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 29 Dec 2025 13:46:27 +0700 Subject: [PATCH 084/124] refactor(FE-438): Remove CV field from Uniformity preview --- src/app/uniformity/detail/page.tsx | 4 +++- .../pages/uniformity/detail/UniformityDetailsPreview.tsx | 5 ----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/app/uniformity/detail/page.tsx b/src/app/uniformity/detail/page.tsx index a2e9402e..e66d0a40 100644 --- a/src/app/uniformity/detail/page.tsx +++ b/src/app/uniformity/detail/page.tsx @@ -35,7 +35,9 @@ const UniformityDetailPage = () => { return (
{isLoadingUniformity && ( - +
+ +
)} {isResponseSuccess(uniformity) && ( diff --git a/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx b/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx index 6748d370..51f0e3aa 100644 --- a/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx +++ b/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx @@ -103,11 +103,6 @@ const UniformityDetailsPreview = ({ label: 'Uniformity', value: `${result.uniformity} %`, }, - { - id: 'cv', - label: 'CV', - value: `${result.cv} %`, - }, ]; }, [result]); From 5e32724d400fd60f4c26fe5be34dfa7fae02b7bc Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 29 Dec 2025 13:51:32 +0700 Subject: [PATCH 085/124] refactor(FE-438): Show approve/reject only when step is CREATED --- .../uniformity/detail/UniformityDetail.tsx | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/components/pages/uniformity/detail/UniformityDetail.tsx b/src/components/pages/uniformity/detail/UniformityDetail.tsx index 2822fa45..3176f96f 100644 --- a/src/components/pages/uniformity/detail/UniformityDetail.tsx +++ b/src/components/pages/uniformity/detail/UniformityDetail.tsx @@ -198,18 +198,21 @@ const UniformityDetail: React.FC = ({ }} /> -
{/* Approve/Reject Buttons */} - {initialValues.result && ( - -
- - -
-
- )} + {initialValues.result && + initialValues.latest_approval?.step_name === 'CREATED' ? ( + <> +
+ +
+ + +
+
+ + ) : null}
) : ( From 17a6cee1e325b872f87d44e6eb35f04a7ddd83ec Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 29 Dec 2025 14:01:37 +0700 Subject: [PATCH 086/124] refactor(FE-438): Fix Uniformity component sample data and imports --- .../pages/uniformity/UniformityChart.tsx | 110 +++++++++--------- .../pages/uniformity/UniformityTable.tsx | 9 +- .../uniformity/detail/UniformityDetail.tsx | 2 +- 3 files changed, 57 insertions(+), 64 deletions(-) diff --git a/src/components/pages/uniformity/UniformityChart.tsx b/src/components/pages/uniformity/UniformityChart.tsx index 2b104623..12587202 100644 --- a/src/components/pages/uniformity/UniformityChart.tsx +++ b/src/components/pages/uniformity/UniformityChart.tsx @@ -22,71 +22,71 @@ interface GaugeChartData { const UniformityChart = () => { // TODO: Replace with actual API call const barChartData: BarChartData[] = [ - // { - // name: '48-52', - // uv: 80, - // }, - // { - // name: '52-56', - // uv: 120, - // }, - // { - // name: '56-60', - // uv: 160, - // }, - // { - // name: '60-64', - // uv: 200, - // }, - // { - // name: '64-68', - // uv: 160, - // }, - // { - // name: '68-72', - // uv: 120, - // }, - // { - // name: '72-76', - // uv: 80, - // }, - // { - // name: '76-80', - // uv: 120, - // }, - // { - // name: '84-88', - // uv: 160, - // }, - // { - // name: '88-92', - // uv: 200, - // }, - // { - // name: '92-96', - // uv: 160, - // }, + { + name: '48-52', + uv: 80, + }, + { + name: '52-56', + uv: 120, + }, + { + name: '56-60', + uv: 160, + }, + { + name: '60-64', + uv: 200, + }, + { + name: '64-68', + uv: 160, + }, + { + name: '68-72', + uv: 120, + }, + { + name: '72-76', + uv: 80, + }, + { + name: '76-80', + uv: 120, + }, + { + name: '84-88', + uv: 160, + }, + { + name: '88-92', + uv: 200, + }, + { + name: '92-96', + uv: 160, + }, ]; // TODO: Replace with actual API call - const gaugeChartData: GaugeChartData = { - value: 0, - label: '', - kandang: 'Kandang Cirangga', - week: 'Week 2', - currentValue: 512, - totalValue: 1024, - }; - // const gaugeChartData: GaugeChartData = { - // value: 52, - // label: 'Uniformity', + // value: 0, + // label: '', // kandang: 'Kandang Cirangga', // week: 'Week 2', // currentValue: 512, // totalValue: 1024, // }; + const gaugeChartData: GaugeChartData = { + value: 52, + label: 'Uniformity', + kandang: 'Kandang Cirangga', + week: 'Week 2', + currentValue: 512, + totalValue: 1024, + }; + return (
void }) => {
- {/*
- -
*/} - -
-
diff --git a/src/components/pages/uniformity/detail/UniformityDetail.tsx b/src/components/pages/uniformity/detail/UniformityDetail.tsx index 3176f96f..72e7bd9e 100644 --- a/src/components/pages/uniformity/detail/UniformityDetail.tsx +++ b/src/components/pages/uniformity/detail/UniformityDetail.tsx @@ -13,7 +13,7 @@ import RequirePermission from '@/components/helper/RequirePermission'; import { UniformityDetail as UniformityDetailType } from '@/types/api/uniformity/uniformity'; import { formatDate } from '@/lib/helper'; import { useUiStore } from '@/stores/ui/ui.store'; -import UniformityDetailsPreview from './UniformityDetailsPreview'; +import UniformityDetailsPreview from '@/components/pages/uniformity/detail/UniformityDetailsPreview'; import { getStatusColor, getStatusIndicatorColor, From dc4e569453d8cc256a92c4a7db110d1e20354706 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 29 Dec 2025 14:03:14 +0700 Subject: [PATCH 087/124] refactor(FE-438): Remove unused view handler and simplify catch --- src/components/pages/uniformity/UniformityTable.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/components/pages/uniformity/UniformityTable.tsx b/src/components/pages/uniformity/UniformityTable.tsx index e2a9618d..412caeb3 100644 --- a/src/components/pages/uniformity/UniformityTable.tsx +++ b/src/components/pages/uniformity/UniformityTable.tsx @@ -201,14 +201,6 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { setRowSelection({}); }, []); - const handleViewDetail = useCallback( - (uniformity: Uniformity) => { - router.push(`/uniformity/detail?uniformityId=${uniformity.id}`); - setRowSelection({}); - }, - [router] - ); - const handleBulkApprove = useCallback(() => { bulkApproveModal.openModal(); }, [bulkApproveModal]); @@ -250,7 +242,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { toast.success( `Successfully rejected ${selectedRowIds.length} Uniformity data!` ); - } catch (error) { + } catch { toast.error('Failed to reject Uniformity data'); } finally { setIsBulkActionLoading(false); From 39f2fc48a866b8c2aecf0187668d88a73165cd16 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 29 Dec 2025 14:16:28 +0700 Subject: [PATCH 088/124] refactor(FE): Remove unnecessary comments in useEffect --- src/components/pages/uniformity/form/UniformityForm.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx index 10e8fda0..1ffbb969 100644 --- a/src/components/pages/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -368,12 +368,9 @@ const UniformityForm = ({ // ===== SIDE EFFECTS ===== useEffect(() => { - // Calculate week from date whenever date changes (week of the month) if (formik.values.date) { const date = moment(formik.values.date); const weekNumber = date.week() - moment(date).startOf('month').week() + 1; - - // Handle edge case for end of year const adjustedWeekNumber = weekNumber <= 0 ? weekNumber + 52 : weekNumber; formik.setFieldValue('week', adjustedWeekNumber); From ded1cc1f62648bba7d1632f997d409e3441282f4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 29 Dec 2025 14:24:23 +0700 Subject: [PATCH 089/124] refactor(FE-316): Extract uniformity slice and add types --- .../uniformity/slices/uniformity.slice.ts | 58 ++++++++++++++++++ src/stores/uniformity/uniformity.store.ts | 60 ++++--------------- src/types/stores.d.ts | 24 ++++++++ 3 files changed, 92 insertions(+), 50 deletions(-) create mode 100644 src/stores/uniformity/slices/uniformity.slice.ts diff --git a/src/stores/uniformity/slices/uniformity.slice.ts b/src/stores/uniformity/slices/uniformity.slice.ts new file mode 100644 index 00000000..c3b0fbb6 --- /dev/null +++ b/src/stores/uniformity/slices/uniformity.slice.ts @@ -0,0 +1,58 @@ +import { StateCreator } from 'zustand'; +import { VerifyUniformityResponse } from '@/types/api/uniformity/uniformity'; + +export type UniformityStep = 'preview' | 'result'; + +export type UniformityFormData = { + date: string; + week: number; + project_flock_kandang_id: number; + file: File | null; + fileName: string; +}; + +export type UniformitySlice = { + // State + uniformityStep: UniformityStep; + verifyUniformityResult: VerifyUniformityResponse | null; + uniformityFormData: UniformityFormData | null; + isSuccess: boolean; + + // Actions + setUniformityStep: (step: UniformityStep) => void; + setVerifyUniformityResult: (result: VerifyUniformityResponse | null) => void; + setUniformityFormData: (data: UniformityFormData | null) => void; + setIsSuccess: (success: boolean) => void; + resetUniformity: () => void; +}; + +export const createUniformitySlice: StateCreator< + UniformitySlice, + [], + [], + UniformitySlice +> = (set) => ({ + // Initial state + uniformityStep: 'preview', + verifyUniformityResult: null, + uniformityFormData: null, + isSuccess: false, + + // Actions + setUniformityStep: (step) => set({ uniformityStep: step }), + + setVerifyUniformityResult: (result) => + set({ verifyUniformityResult: result }), + + setUniformityFormData: (data) => set({ uniformityFormData: data }), + + setIsSuccess: (success) => set({ isSuccess: success }), + + resetUniformity: () => + set({ + uniformityStep: 'preview', + verifyUniformityResult: null, + uniformityFormData: null, + isSuccess: false, + }), +}); diff --git a/src/stores/uniformity/uniformity.store.ts b/src/stores/uniformity/uniformity.store.ts index 082a2d5b..da8dc4e3 100644 --- a/src/stores/uniformity/uniformity.store.ts +++ b/src/stores/uniformity/uniformity.store.ts @@ -1,58 +1,18 @@ +'use client'; + import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; -import { VerifyUniformityResponse } from '@/types/api/uniformity/uniformity'; +import { + createUniformitySlice, + UniformitySlice, +} from '@/stores/uniformity/slices/uniformity.slice'; -export type UniformityStep = 'preview' | 'result'; +export type UniformityStore = UniformitySlice; -export type UniformityFormData = { - date: string; - week: number; - project_flock_kandang_id: number; - file: File | null; - fileName: string; -}; - -type UniformityState = { - // State - uniformityStep: UniformityStep; - verifyUniformityResult: VerifyUniformityResponse | null; - uniformityFormData: UniformityFormData | null; - isSuccess: boolean; - - // Actions - setUniformityStep: (step: UniformityStep) => void; - setVerifyUniformityResult: (result: VerifyUniformityResponse | null) => void; - setUniformityFormData: (data: UniformityFormData | null) => void; - setIsSuccess: (success: boolean) => void; - resetUniformity: () => void; -}; - -export const useUniformityStore = create()( +export const useUniformityStore = create()( devtools( - (set) => ({ - // Initial state - uniformityStep: 'preview', - verifyUniformityResult: null, - uniformityFormData: null, - isSuccess: false, - - // Actions - setUniformityStep: (step) => set({ uniformityStep: step }), - - setVerifyUniformityResult: (result) => - set({ verifyUniformityResult: result }), - - setUniformityFormData: (data) => set({ uniformityFormData: data }), - - setIsSuccess: (success) => set({ isSuccess: success }), - - resetUniformity: () => - set({ - uniformityStep: 'preview', - verifyUniformityResult: null, - uniformityFormData: null, - isSuccess: false, - }), + (...args) => ({ + ...createUniformitySlice(...args), }), { name: 'UniformityStore', diff --git a/src/types/stores.d.ts b/src/types/stores.d.ts index b9145459..c1281437 100644 --- a/src/types/stores.d.ts +++ b/src/types/stores.d.ts @@ -46,3 +46,27 @@ type ProductionStandardFormSlice = { }; export type FormStore = ProductionStandardFormSlice; + +type UniformityStep = 'preview' | 'result'; + +type UniformityFormData = { + date: string; + week: number; + project_flock_kandang_id: number; + file: File | null; + fileName: string; +}; + +type UniformitySlice = { + uniformityStep: UniformityStep; + verifyUniformityResult: VerifyUniformityResponse | null; + uniformityFormData: UniformityFormData | null; + isSuccess: boolean; + setUniformityStep: (step: UniformityStep) => void; + setVerifyUniformityResult: (result: VerifyUniformityResponse | null) => void; + setUniformityFormData: (data: UniformityFormData | null) => void; + setIsSuccess: (success: boolean) => void; + resetUniformity: () => void; +}; + +export type UniformityStore = UniformitySlice; From 4f168b51c756006c0f8a9a52a647396c728d2ec8 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 29 Dec 2025 14:32:17 +0700 Subject: [PATCH 090/124] refactor(FE-316): Centralize uniformity types and add typings --- .../detail/UniformityDetailsPreview.tsx | 14 +++++----- .../uniformity/form/UniformityForm.schema.ts | 8 ++++++ .../uniformity/form/UniformityPreviewForm.tsx | 17 +++++++----- .../uniformity/form/UniformityResultForm.tsx | 19 ++++++++----- .../uniformity/slices/uniformity.slice.ts | 27 +------------------ src/stores/uniformity/uniformity.store.ts | 6 ++--- src/types/stores.d.ts | 17 ++++-------- 7 files changed, 47 insertions(+), 61 deletions(-) diff --git a/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx b/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx index 51f0e3aa..35dd88ff 100644 --- a/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx +++ b/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx @@ -125,12 +125,14 @@ const UniformityDetailsPreview = ({ const tableData = useMemo(() => { if (!uniformity_details) return []; - return uniformity_details.map((detail, index) => ({ - id: `body-weight-${index + 1}`, - number: index + 1, - weight: detail.weight, - status: detail.range.toLowerCase() as 'ideal' | 'outside', - })); + return uniformity_details.map( + (detail: UniformityDetailItem, index: number) => ({ + id: `body-weight-${index + 1}`, + number: index + 1, + weight: detail.weight, + status: detail.range.toLowerCase() as 'ideal' | 'outside', + }) + ); }, [uniformity_details]); const columnsUniformity: ColumnDef[] = useMemo( diff --git a/src/components/pages/uniformity/form/UniformityForm.schema.ts b/src/components/pages/uniformity/form/UniformityForm.schema.ts index cb891965..6bcc3d95 100644 --- a/src/components/pages/uniformity/form/UniformityForm.schema.ts +++ b/src/components/pages/uniformity/form/UniformityForm.schema.ts @@ -79,6 +79,14 @@ export const UniformityFormSchema: Yup.ObjectSchema = export type UniformityFormValues = Yup.InferType; +export type UniformityFormData = { + date: string; + week: number; + project_flock_kandang_id: number; + file: File | null; + fileName: string; +}; + export const getUniformityFormInitialValues = ( initialValues?: Uniformity ): UniformityFormValues => { diff --git a/src/components/pages/uniformity/form/UniformityPreviewForm.tsx b/src/components/pages/uniformity/form/UniformityPreviewForm.tsx index 4520464f..5ee5f5f6 100644 --- a/src/components/pages/uniformity/form/UniformityPreviewForm.tsx +++ b/src/components/pages/uniformity/form/UniformityPreviewForm.tsx @@ -10,7 +10,10 @@ import { useUiStore } from '@/stores/ui/ui.store'; import { useUniformityStore } from '@/stores/uniformity/uniformity.store'; import RequirePermission from '@/components/helper/RequirePermission'; import Table from '@/components/Table'; -import { BodyWeightData } from '@/types/api/uniformity/uniformity'; +import { + BodyWeightData, + UniformityDetailItem, +} from '@/types/api/uniformity/uniformity'; const UniformityPreviewForm = () => { const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen); @@ -34,11 +37,13 @@ const UniformityPreviewForm = () => { const tableData = useMemo(() => { if (!verifyUniformityResult) return []; - return verifyUniformityResult.uniformity_details.map((detail, index) => ({ - id: `weight-${index}`, - number: index + 1, - weight: detail.weight, - })); + return verifyUniformityResult.uniformity_details.map( + (detail: UniformityDetailItem, index: number) => ({ + id: `weight-${index}`, + number: index + 1, + weight: detail.weight, + }) + ); }, [verifyUniformityResult]); const columns: ColumnDef[] = useMemo( diff --git a/src/components/pages/uniformity/form/UniformityResultForm.tsx b/src/components/pages/uniformity/form/UniformityResultForm.tsx index 6b3a520f..8c38f6be 100644 --- a/src/components/pages/uniformity/form/UniformityResultForm.tsx +++ b/src/components/pages/uniformity/form/UniformityResultForm.tsx @@ -22,7 +22,10 @@ import { getWeightStatusText, } from '@/components/pages/uniformity/uniformity-utils'; import { DetailOptionType } from '@/types/api/uniformity/uniformity'; -import { BodyWeightData } from '@/types/api/uniformity/uniformity'; +import { + BodyWeightData, + UniformityDetailItem, +} from '@/types/api/uniformity/uniformity'; const UniformityResultForm = () => { const router = useRouter(); @@ -169,12 +172,14 @@ const UniformityResultForm = () => { const tableData = useMemo(() => { if (!verifyUniformityResult) return []; - return verifyUniformityResult.uniformity_details.map((detail, index) => ({ - id: `body-weight-${index + 1}`, - number: index + 1, - weight: detail.weight, - status: detail.range.toLowerCase() as 'ideal' | 'outside', - })); + return verifyUniformityResult.uniformity_details.map( + (detail: UniformityDetailItem, index: number) => ({ + id: `body-weight-${index + 1}`, + number: index + 1, + weight: detail.weight, + status: detail.range.toLowerCase() as 'ideal' | 'outside', + }) + ); }, [verifyUniformityResult]); const columnsUniformity: ColumnDef[] = useMemo( diff --git a/src/stores/uniformity/slices/uniformity.slice.ts b/src/stores/uniformity/slices/uniformity.slice.ts index c3b0fbb6..244ca94e 100644 --- a/src/stores/uniformity/slices/uniformity.slice.ts +++ b/src/stores/uniformity/slices/uniformity.slice.ts @@ -1,30 +1,5 @@ +import { UniformitySlice } from '@/types/stores'; import { StateCreator } from 'zustand'; -import { VerifyUniformityResponse } from '@/types/api/uniformity/uniformity'; - -export type UniformityStep = 'preview' | 'result'; - -export type UniformityFormData = { - date: string; - week: number; - project_flock_kandang_id: number; - file: File | null; - fileName: string; -}; - -export type UniformitySlice = { - // State - uniformityStep: UniformityStep; - verifyUniformityResult: VerifyUniformityResponse | null; - uniformityFormData: UniformityFormData | null; - isSuccess: boolean; - - // Actions - setUniformityStep: (step: UniformityStep) => void; - setVerifyUniformityResult: (result: VerifyUniformityResponse | null) => void; - setUniformityFormData: (data: UniformityFormData | null) => void; - setIsSuccess: (success: boolean) => void; - resetUniformity: () => void; -}; export const createUniformitySlice: StateCreator< UniformitySlice, diff --git a/src/stores/uniformity/uniformity.store.ts b/src/stores/uniformity/uniformity.store.ts index da8dc4e3..c8d759d6 100644 --- a/src/stores/uniformity/uniformity.store.ts +++ b/src/stores/uniformity/uniformity.store.ts @@ -2,10 +2,8 @@ import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; -import { - createUniformitySlice, - UniformitySlice, -} from '@/stores/uniformity/slices/uniformity.slice'; +import { createUniformitySlice } from '@/stores/uniformity/slices/uniformity.slice'; +import { UniformitySlice } from '@/types/stores'; export type UniformityStore = UniformitySlice; diff --git a/src/types/stores.d.ts b/src/types/stores.d.ts index c1281437..062045b9 100644 --- a/src/types/stores.d.ts +++ b/src/types/stores.d.ts @@ -47,26 +47,19 @@ type ProductionStandardFormSlice = { export type FormStore = ProductionStandardFormSlice; -type UniformityStep = 'preview' | 'result'; +export type UniformityStep = 'preview' | 'result'; -type UniformityFormData = { - date: string; - week: number; - project_flock_kandang_id: number; - file: File | null; - fileName: string; -}; - -type UniformitySlice = { +export type UniformitySlice = { + // State uniformityStep: UniformityStep; verifyUniformityResult: VerifyUniformityResponse | null; uniformityFormData: UniformityFormData | null; isSuccess: boolean; + + // Actions setUniformityStep: (step: UniformityStep) => void; setVerifyUniformityResult: (result: VerifyUniformityResponse | null) => void; setUniformityFormData: (data: UniformityFormData | null) => void; setIsSuccess: (success: boolean) => void; resetUniformity: () => void; }; - -export type UniformityStore = UniformitySlice; From 34eae71b44496af21c4742d0882eb3ee74974c47 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 29 Dec 2025 14:43:11 +0700 Subject: [PATCH 091/124] refactor(FE-316): Guard unsubscribe call with optional chaining --- src/components/pages/uniformity/form/UniformityForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx index 1ffbb969..d97a6aba 100644 --- a/src/components/pages/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -383,7 +383,7 @@ const UniformityForm = ({ }); return () => { - unsub(); + unsub?.(); useUiStore.getState().setExpandedDrawerOpen(false); useUiStore.getState().setExpandedDrawerContent(null); useUiStore.getState().setIsNextStep(false); From 3279fb30ce1c8a0f08b4bc77881473f6716b2b83 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 29 Dec 2025 14:43:54 +0700 Subject: [PATCH 092/124] refactor(FE-316): Update template label to XLSX --- src/components/pages/uniformity/form/UniformityForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx index d97a6aba..a9bad044 100644 --- a/src/components/pages/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -601,7 +601,7 @@ const UniformityForm = ({ width={18} height={18} /> - Template CSV + Template XLSX
From 3bb5d5e5a53e7025f32a243450d807346e98cfb5 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 29 Dec 2025 14:44:38 +0700 Subject: [PATCH 093/124] refactor(FE-316): Change upload file hint to .xlsx --- src/components/pages/uniformity/form/UniformityForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx index a9bad044..58c9781e 100644 --- a/src/components/pages/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -575,7 +575,7 @@ const UniformityForm = ({ Drag file to this area to upload - Upload data file (*.csv) + Upload data file (*.xlsx)
From 6ad1a3349bb6c6e40e8e5bf178ed58904aa91f6c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 29 Dec 2025 14:53:29 +0700 Subject: [PATCH 094/124] refactor(FE-316): Extract Uniformity confirmation preview component --- .../pages/uniformity/UniformityTable.tsx | 466 ++++-------------- 1 file changed, 109 insertions(+), 357 deletions(-) diff --git a/src/components/pages/uniformity/UniformityTable.tsx b/src/components/pages/uniformity/UniformityTable.tsx index 412caeb3..c1cc12c9 100644 --- a/src/components/pages/uniformity/UniformityTable.tsx +++ b/src/components/pages/uniformity/UniformityTable.tsx @@ -33,6 +33,108 @@ const isUniformityLocked = (uniformity: Uniformity): boolean => { return uniformity.status === 'APPROVED' || uniformity.status === 'REJECTED'; }; +interface UniformityPreviewData { + id: string; + label: string; + value: string; +} + +const UniformityConfirmationPreview = ({ + tanggal = '28 Desember 2025', + lokasiFarm = 'Farm A', + projectFlock = 'Flock 2025-01', + kandang = 'Kandang 1', + fileName = 'uniformity_data.xlsx', + status = 'APPROVED', +}: { + tanggal?: string; + lokasiFarm?: string; + projectFlock?: string; + kandang?: string; + fileName?: string; + status?: string; +}) => { + const data: UniformityPreviewData[] = [ + { + id: 'tanggal', + label: 'Tanggal', + value: tanggal, + }, + { + id: 'lokasi-farm', + label: 'Lokasi Farm', + value: lokasiFarm, + }, + { + id: 'project-flock', + label: 'Project Flock', + value: projectFlock, + }, + { + id: 'kandang', + label: 'Kandang', + value: kandang, + }, + { + id: 'file-uniformity', + label: 'File Uniformity', + value: fileName, + }, + { + id: 'status', + label: 'Status', + value: status, + }, + ]; + + const columns: ColumnDef[] = [ + { + accessorKey: 'label', + header: 'Label', + cell: (props) => props.row.original.label, + }, + { + accessorKey: 'value', + header: 'Value', + cell: (props) => { + const id = props.row.original.id; + const value = props.row.original.value; + + if (id === 'status') { + return ( +
+ + {getStatusText(value)} + +
+ ); + } + + return {value}; + }, + }, + ]; + + return ( +
+ ); +}; + const UniformityTable = ({ refresh }: { refresh?: () => void }) => { const router = useRouter(); const searchParams = useSearchParams(); @@ -475,57 +577,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { }} >
-
props.row.original.label, - }, - { - accessorKey: 'value', - header: 'Value', - cell: (props) => {props.row.original.value}, - }, - ]} - pageSize={6} - className={{ - containerClassName: 'mb-0', - paginationClassName: 'hidden', - }} - /> + @@ -549,57 +601,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { }} >
-
props.row.original.label, - }, - { - accessorKey: 'value', - header: 'Value', - cell: (props) => {props.row.original.value}, - }, - ]} - pageSize={6} - className={{ - containerClassName: 'mb-0', - paginationClassName: 'hidden', - }} - /> + @@ -623,57 +625,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { }} >
-
props.row.original.label, - }, - { - accessorKey: 'value', - header: 'Value', - cell: (props) => {props.row.original.value}, - }, - ]} - pageSize={6} - className={{ - containerClassName: 'mb-0', - paginationClassName: 'hidden', - }} - /> + @@ -697,57 +649,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { }} >
-
props.row.original.label, - }, - { - accessorKey: 'value', - header: 'Value', - cell: (props) => {props.row.original.value}, - }, - ]} - pageSize={6} - className={{ - containerClassName: 'mb-0', - paginationClassName: 'hidden', - }} - /> + @@ -771,57 +673,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { }} >
-
props.row.original.label, - }, - { - accessorKey: 'value', - header: 'Value', - cell: (props) => {props.row.original.value}, - }, - ]} - pageSize={6} - className={{ - containerClassName: 'mb-0', - paginationClassName: 'hidden', - }} - /> + @@ -845,57 +697,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { }} >
-
props.row.original.label, - }, - { - accessorKey: 'value', - header: 'Value', - cell: (props) => {props.row.original.value}, - }, - ]} - pageSize={6} - className={{ - containerClassName: 'mb-0', - paginationClassName: 'hidden', - }} - /> + @@ -918,57 +720,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { }} >
-
props.row.original.label, - }, - { - accessorKey: 'value', - header: 'Value', - cell: (props) => {props.row.original.value}, - }, - ]} - pageSize={6} - className={{ - containerClassName: 'mb-0', - paginationClassName: 'hidden', - }} - /> + From be0bdcd2994bb81d923986f5cf24161edfad7fb3 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 29 Dec 2025 17:52:26 +0700 Subject: [PATCH 095/124] feat(FE-316): Show required data count in upload area --- src/components/pages/uniformity/form/UniformityForm.tsx | 4 +++- src/types/api/production/project-flock.d.ts | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx index 58c9781e..3da378d6 100644 --- a/src/components/pages/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -575,7 +575,9 @@ const UniformityForm = ({ Drag file to this area to upload - Upload data file (*.xlsx) + {projectFlockKandangLookup?.available_quantity + ? `Jumlah data yang dibutuhkan: ${projectFlockKandangLookup.available_quantity.toLocaleString('id-ID')} (2% dari total populasi).` + : 'Upload data file (*.xlsx)'} diff --git a/src/types/api/production/project-flock.d.ts b/src/types/api/production/project-flock.d.ts index 35c42c38..9f128093 100644 --- a/src/types/api/production/project-flock.d.ts +++ b/src/types/api/production/project-flock.d.ts @@ -68,6 +68,7 @@ export type ProjectFlockKandangLookup = { kandang: Kandang; project_flock: ProjectFlock; quantity: number; + available_quantity?: number; }; export type ProjectFlockAvailableQuantity = { From e81c0a3baf271aac5a9af294734f7f43c89dfdf9 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 29 Dec 2025 19:34:17 +0700 Subject: [PATCH 096/124] feat(FE-316): Add uniformity Excel template generator --- .../uniformity/export/UniformityTemplate.tsx | 59 ++++++++++++++++ .../pages/uniformity/form/UniformityForm.tsx | 70 +++++++++++-------- 2 files changed, 101 insertions(+), 28 deletions(-) create mode 100644 src/components/pages/uniformity/export/UniformityTemplate.tsx diff --git a/src/components/pages/uniformity/export/UniformityTemplate.tsx b/src/components/pages/uniformity/export/UniformityTemplate.tsx new file mode 100644 index 00000000..27e3b7e5 --- /dev/null +++ b/src/components/pages/uniformity/export/UniformityTemplate.tsx @@ -0,0 +1,59 @@ +import { formatNumber, formatDate } from '@/lib/helper'; +import { toast } from 'react-hot-toast'; +import * as XLSX from 'xlsx'; +import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock'; + +export const generateUniformityTemplate = ( + availableQuantity: number, + projectFlockKandangLookup: ProjectFlockKandangLookup +) => { + try { + // Calculate 2% sample from total population + const sampleSize = Math.round(availableQuantity * 0.02); + + const data = Array.from({ length: sampleSize }, (_, index) => ({ + NO: index + 1, + BW: '', + })); + + const worksheet = XLSX.utils.json_to_sheet(data, { + header: ['NO', 'BW'], + }); + + worksheet['!cols'] = [{ wch: 10 }, { wch: 15 }]; + + const workbook = XLSX.utils.book_new(); + + const kandangName = projectFlockKandangLookup.kandang?.name || 'Kandang'; + const flockName = projectFlockKandangLookup.project_flock?.flock_name || ''; + const flockPeriod = projectFlockKandangLookup.project_flock?.period || 1; + + const sheetName = + kandangName.length > 31 ? kandangName.substring(0, 31) : kandangName; + XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); + + const sanitizedFlockName = flockName + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); + + const sanitizedKandangName = kandangName + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); + + const filename = `${formatDate( + new Date(), + 'YYYY-MM-DD' + )}-${sanitizedFlockName}-${sanitizedKandangName}-periode-${flockPeriod}-${sampleSize}-data.xlsx`; + + XLSX.writeFile(workbook, filename); + + toast.success( + `Template berhasil dibuat dengan ${formatNumber(sampleSize)} baris data (2% dari ${formatNumber(availableQuantity)} populasi).` + ); + } catch (error) { + console.error('Error generating uniformity template:', error); + toast.error('Gagal membuat template Excel. Silakan coba lagi.'); + } +}; diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx index 3da378d6..a57cc9d4 100644 --- a/src/components/pages/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -40,8 +40,9 @@ import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock' import { Kandang } from '@/types/api/master-data/kandang'; import UniformityPreviewForm from '@/components/pages/uniformity/form/UniformityPreviewForm'; import UniformityResultForm from '@/components/pages/uniformity/form/UniformityResultForm'; +import { generateUniformityTemplate } from '@/components/pages/uniformity/export/UniformityTemplate'; import useSWR from 'swr'; -import { cn } from '@/lib/helper'; +import { cn, formatNumber } from '@/lib/helper'; interface UniformityFormProps { formType?: 'add' | 'edit'; @@ -352,20 +353,31 @@ const UniformityForm = ({ formik.setFieldValue('file', file); }, - [formik] + [] ); const handleDateChange = useCallback( (e: React.ChangeEvent) => { formik.setFieldValue('date', e.target.value); }, - [formik] + [] ); const handleRemoveFile = useCallback(() => { formik.setFieldValue('files', undefined); }, [formik]); + const handleDownloadTemplate = useCallback(() => { + const availableQuantity = projectFlockKandangLookup?.available_quantity; + + if (!availableQuantity || !projectFlockKandangLookup) { + toast.error('Silakan pilih Project Flock dan Kandang terlebih dahulu.'); + return; + } + + generateUniformityTemplate(availableQuantity, projectFlockKandangLookup); + }, [projectFlockKandangLookup]); + // ===== SIDE EFFECTS ===== useEffect(() => { if (formik.values.date) { @@ -576,36 +588,38 @@ const UniformityForm = ({ {projectFlockKandangLookup?.available_quantity - ? `Jumlah data yang dibutuhkan: ${projectFlockKandangLookup.available_quantity.toLocaleString('id-ID')} (2% dari total populasi).` + ? `Jumlah data yang dibutuhkan: ${formatNumber(Math.round(projectFlockKandangLookup.available_quantity * 0.02))} (2% dari ${formatNumber(projectFlockKandangLookup.available_quantity)} populasi).` : 'Upload data file (*.xlsx)'} -
-
- - Templates - -
-
+ {projectFlockKandangLookup?.available_quantity && ( + <> +
+
+ + Templates + +
+
-
- -
+
+ +
+ + )} )} From 1d27781c028edcce7dbc3cb9d19da2b31b94586f Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 29 Dec 2025 19:43:32 +0700 Subject: [PATCH 097/124] feat(FE-316): Add instruction sheet and format data sheet --- .../uniformity/export/UniformityTemplate.tsx | 58 ++++++++++++++++--- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/src/components/pages/uniformity/export/UniformityTemplate.tsx b/src/components/pages/uniformity/export/UniformityTemplate.tsx index 27e3b7e5..3cd3f76b 100644 --- a/src/components/pages/uniformity/export/UniformityTemplate.tsx +++ b/src/components/pages/uniformity/export/UniformityTemplate.tsx @@ -8,29 +8,69 @@ export const generateUniformityTemplate = ( projectFlockKandangLookup: ProjectFlockKandangLookup ) => { try { - // Calculate 2% sample from total population const sampleSize = Math.round(availableQuantity * 0.02); + const kandangName = projectFlockKandangLookup.kandang?.name || 'Kandang'; + const flockName = projectFlockKandangLookup.project_flock?.flock_name || ''; + const flockPeriod = projectFlockKandangLookup.project_flock?.period || 1; + const locationName = + projectFlockKandangLookup.project_flock?.location?.name || ''; + const areaName = projectFlockKandangLookup.project_flock?.area?.name || ''; + + const instructions = [ + ['PETUNJUK PENGISIAN DATA UNIFORMITY'], + [''], + ['INFORMASI PROYEK'], + ['Location', locationName], + ['Area', areaName], + ['Flock Name', flockName], + ['Periode', flockPeriod], + ['Kandang', kandangName], + ['Total Populasi', formatNumber(availableQuantity)], + ['Jumlah Sampel (2%)', formatNumber(sampleSize)], + [''], + ['CARA PENGISIAN:'], + ['1. Pindah ke sheet "Data" untuk mengisi data BW (Body Weight)'], + [ + '2. Kolom NO sudah terisi otomatis dari 1 sampai ' + + formatNumber(sampleSize), + ], + ['3. Isi kolom BW dengan berat badan ayam dalam gram'], + ['4. Pastikan semua baris terisi dengan data yang valid'], + ['5. Simpan file dan upload kembali ke sistem'], + [''], + ['FORMAT DATA:'], + ['• NO: Nomor urut ayam (1, 2, 3, ...)'], + ['• BW: Berat badan dalam gram (contoh: 1500, 1650, 1800)'], + [''], + ['CATATAN:'], + ['• Data yang diisi adalah sampel 2% dari total populasi'], + ['• Pastikan timbangan dalam kondisi baik dan terkalibrasi'], + ['• Lakukan pengukuran pada waktu yang sama setiap hari'], + ]; + + const instructionSheet = XLSX.utils.aoa_to_sheet(instructions); + instructionSheet['!cols'] = [ + { wch: 30 }, // Column A width + { wch: 40 }, // Column B width + ]; const data = Array.from({ length: sampleSize }, (_, index) => ({ NO: index + 1, BW: '', })); - const worksheet = XLSX.utils.json_to_sheet(data, { + const dataWorksheet = XLSX.utils.json_to_sheet(data, { header: ['NO', 'BW'], }); - worksheet['!cols'] = [{ wch: 10 }, { wch: 15 }]; + dataWorksheet['!cols'] = [{ wch: 10 }, { wch: 15 }]; const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, instructionSheet, 'Instruksi'); - const kandangName = projectFlockKandangLookup.kandang?.name || 'Kandang'; - const flockName = projectFlockKandangLookup.project_flock?.flock_name || ''; - const flockPeriod = projectFlockKandangLookup.project_flock?.period || 1; - - const sheetName = + const dataSheetName = kandangName.length > 31 ? kandangName.substring(0, 31) : kandangName; - XLSX.utils.book_append_sheet(workbook, worksheet, sheetName); + XLSX.utils.book_append_sheet(workbook, dataWorksheet, dataSheetName); const sanitizedFlockName = flockName .toLowerCase() From 34ec650a017b11be6367d95fa45b803d180975c4 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 29 Dec 2025 20:42:38 +0700 Subject: [PATCH 098/124] refactor(FE-316): Clarify instructions in uniformity template --- .../uniformity/export/UniformityTemplate.tsx | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/components/pages/uniformity/export/UniformityTemplate.tsx b/src/components/pages/uniformity/export/UniformityTemplate.tsx index 3cd3f76b..f216b0a1 100644 --- a/src/components/pages/uniformity/export/UniformityTemplate.tsx +++ b/src/components/pages/uniformity/export/UniformityTemplate.tsx @@ -14,28 +14,26 @@ export const generateUniformityTemplate = ( const flockPeriod = projectFlockKandangLookup.project_flock?.period || 1; const locationName = projectFlockKandangLookup.project_flock?.location?.name || ''; - const areaName = projectFlockKandangLookup.project_flock?.area?.name || ''; const instructions = [ ['PETUNJUK PENGISIAN DATA UNIFORMITY'], [''], - ['INFORMASI PROYEK'], - ['Location', locationName], - ['Area', areaName], - ['Flock Name', flockName], + ['INFORMASI FLOCK'], + ['Lokasi', locationName], + ['Nama Flock', flockName], ['Periode', flockPeriod], ['Kandang', kandangName], ['Total Populasi', formatNumber(availableQuantity)], ['Jumlah Sampel (2%)', formatNumber(sampleSize)], [''], ['CARA PENGISIAN:'], - ['1. Pindah ke sheet "Data" untuk mengisi data BW (Body Weight)'], + ['1. Pindah ke sheet ke-2 untuk mengisi data BW (Body Weight)'], [ '2. Kolom NO sudah terisi otomatis dari 1 sampai ' + formatNumber(sampleSize), ], ['3. Isi kolom BW dengan berat badan ayam dalam gram'], - ['4. Pastikan semua baris terisi dengan data yang valid'], + ['4. Pastikan baris terisi dengan data yang valid'], ['5. Simpan file dan upload kembali ke sistem'], [''], ['FORMAT DATA:'], @@ -43,9 +41,16 @@ export const generateUniformityTemplate = ( ['• BW: Berat badan dalam gram (contoh: 1500, 1650, 1800)'], [''], ['CATATAN:'], - ['• Data yang diisi adalah sampel 2% dari total populasi'], - ['• Pastikan timbangan dalam kondisi baik dan terkalibrasi'], - ['• Lakukan pengukuran pada waktu yang sama setiap hari'], + [ + '1. File ini dibuat secara otomatis berdasarkan ukuran sampling (2% dari total populasi).', + ], + [ + '2. Jumlah baris sudah ditentukan dan boleh ditambah asal angkanya berurutan.', + ], + ['3. Silakan isi berat badan (gram) untuk setiap ayam yang disampling.'], + [ + '4. Biarkan sel kosong jika data tidak tersedia, jangan dihapus nomornya.', + ], ]; const instructionSheet = XLSX.utils.aoa_to_sheet(instructions); From 9ef232bac5ebfc58f8b5ec2097c26eb0b57bc71e Mon Sep 17 00:00:00 2001 From: rstubryan Date: Mon, 29 Dec 2025 20:50:53 +0700 Subject: [PATCH 099/124] refactor(FE-316): Polish Uniformity template and upload UI --- .../pages/uniformity/export/UniformityTemplate.tsx | 12 ++++++------ .../pages/uniformity/form/UniformityForm.tsx | 11 ++++++++--- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/components/pages/uniformity/export/UniformityTemplate.tsx b/src/components/pages/uniformity/export/UniformityTemplate.tsx index f216b0a1..ef5f40a4 100644 --- a/src/components/pages/uniformity/export/UniformityTemplate.tsx +++ b/src/components/pages/uniformity/export/UniformityTemplate.tsx @@ -27,18 +27,18 @@ export const generateUniformityTemplate = ( ['Jumlah Sampel (2%)', formatNumber(sampleSize)], [''], ['CARA PENGISIAN:'], - ['1. Pindah ke sheet ke-2 untuk mengisi data BW (Body Weight)'], + ['1. Pindah ke sheet ke-2 untuk mengisi data BW (Body Weight).'], [ '2. Kolom NO sudah terisi otomatis dari 1 sampai ' + formatNumber(sampleSize), ], - ['3. Isi kolom BW dengan berat badan ayam dalam gram'], - ['4. Pastikan baris terisi dengan data yang valid'], - ['5. Simpan file dan upload kembali ke sistem'], + ['3. Isi kolom BW dengan berat badan ayam dalam gram.'], + ['4. Pastikan baris terisi dengan data yang valid.'], + ['5. Simpan file dan upload kembali ke sistem.'], [''], ['FORMAT DATA:'], - ['• NO: Nomor urut ayam (1, 2, 3, ...)'], - ['• BW: Berat badan dalam gram (contoh: 1500, 1650, 1800)'], + ['• NO: Nomor urut ayam (1, 2, 3, ...).'], + ['• BW: Berat badan dalam gram (contoh: 1500, 1650, 1800).'], [''], ['CATATAN:'], [ diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx index a57cc9d4..9aa3644c 100644 --- a/src/components/pages/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -565,7 +565,9 @@ const UniformityForm = ({ ) : ( <> -
+
- Drag file to this area to upload + Choose file to upload {projectFlockKandangLookup?.available_quantity @@ -608,7 +610,10 @@ const UniformityForm = ({ type='button' variant='outline' className='btn-sm rounded-2xl shadow-md border border-base-300' - onClick={handleDownloadTemplate} + onClick={(e) => { + e.stopPropagation(); + handleDownloadTemplate(); + }} > Date: Mon, 29 Dec 2025 20:58:12 +0700 Subject: [PATCH 100/124] refactor(FE-316): Clamp subtitle text and update export filename --- .../pages/uniformity/detail/UniformityDetailsPreview.tsx | 2 +- src/components/pages/uniformity/export/UniformityTemplate.tsx | 2 +- src/components/pages/uniformity/form/UniformityPreviewForm.tsx | 2 +- src/components/pages/uniformity/form/UniformityResultForm.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx b/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx index 35dd88ff..d69d6633 100644 --- a/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx +++ b/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx @@ -190,7 +190,7 @@ const UniformityDetailsPreview = ({ + ) : !formik.values.file && !isNextStep ? ( + + ) : null} +
Date: Mon, 29 Dec 2025 22:22:09 +0700 Subject: [PATCH 103/124] feat(FE-438): Load uniformity details on demand --- .../uniformity/detail/UniformityDetail.tsx | 1 + .../detail/UniformityDetailsPreview.tsx | 63 +++++++++++++++---- src/services/api/uniformity.ts | 11 +++- src/types/api/uniformity/uniformity.d.ts | 2 +- 4 files changed, 63 insertions(+), 14 deletions(-) diff --git a/src/components/pages/uniformity/detail/UniformityDetail.tsx b/src/components/pages/uniformity/detail/UniformityDetail.tsx index 72e7bd9e..8cfaa8d4 100644 --- a/src/components/pages/uniformity/detail/UniformityDetail.tsx +++ b/src/components/pages/uniformity/detail/UniformityDetail.tsx @@ -49,6 +49,7 @@ const UniformityDetail: React.FC = ({ uniformity_details={initialValues.uniformity_details} sampling={initialValues.sampling} result={initialValues.result} + uniformityId={initialValues.id} /> ); diff --git a/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx b/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx index d69d6633..e7f7067b 100644 --- a/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx +++ b/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { Icon } from '@iconify/react'; import { ColumnDef } from '@tanstack/react-table'; import DrawerHeader from '@/components/helper/drawer/DrawerHeader'; @@ -21,26 +21,51 @@ import { getWeightStatusText, } from '@/components/pages/uniformity/uniformity-utils'; import { BodyWeightData } from '@/types/api/uniformity/uniformity'; +import Button from '@/components/Button'; +import { UniformityApi } from '@/services/api/uniformity'; +import useSWR from 'swr'; +import { isResponseSuccess } from '@/lib/api-helper'; interface UniformityDetailsPreviewProps { info_umum: UniformityInfoUmum; - uniformity_details: UniformityDetailItem[]; sampling: UniformitySampling; result: UniformityResult; + uniformity_details?: UniformityDetailItem[]; + uniformityId: number; } const UniformityDetailsPreview = ({ info_umum, - uniformity_details, + uniformity_details: initialUniformityDetails, sampling, result, + uniformityId, }: UniformityDetailsPreviewProps) => { const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen); + const [shouldFetchDetails, setShouldFetchDetails] = useState(false); + + const { data: uniformityDetailResponse, isLoading } = useSWR( + shouldFetchDetails + ? `uniformity-detail-${uniformityId}-with-details` + : null, + () => UniformityApi.getUniformityDetail(uniformityId, true) + ); + + const uniformity_details = useMemo(() => { + if (shouldFetchDetails && isResponseSuccess(uniformityDetailResponse)) { + return uniformityDetailResponse.data.uniformity_details; + } + return initialUniformityDetails; + }, [shouldFetchDetails, uniformityDetailResponse, initialUniformityDetails]); const handleClose = () => { setExpandedDrawerOpen(false); }; + const fetchWeightData = () => { + setShouldFetchDetails(true); + }; + const samplingTableData: DetailOptionType[] = useMemo(() => { if (!sampling) return []; @@ -219,7 +244,6 @@ const UniformityDetailsPreview = ({ }} />
- {/* Result */}

Result

@@ -234,15 +258,32 @@ const UniformityDetailsPreview = ({ />
- {/* Body Weight Details */} + {/* Body Weight Details Button */}
- - data={tableData} - columns={columnsUniformity} - pageSize={15} - className={{ containerClassName: 'mb-5' }} - /> +
+ {/*{!uniformity_details || uniformity_details.length === 0 ? ( + <> + ) : null}*/} + + {/* Body Weight Details */} + {uniformity_details && uniformity_details.length > 0 && ( +
+ + data={tableData} + columns={columnsUniformity} + pageSize={15} + className={{ containerClassName: 'mb-5' }} + /> +
+ )} ) : (
diff --git a/src/services/api/uniformity.ts b/src/services/api/uniformity.ts index 892192b6..73d979ca 100644 --- a/src/services/api/uniformity.ts +++ b/src/services/api/uniformity.ts @@ -22,10 +22,17 @@ export class UniformityApiService extends BaseApiService< } async getUniformityDetail( - id: number + id: number, + with_details = false ): Promise | undefined> { return await this.customRequest>( - `/${id}` + `/${id}`, + { + method: 'GET', + params: { + with_details: with_details, + }, + } ); } diff --git a/src/types/api/uniformity/uniformity.d.ts b/src/types/api/uniformity/uniformity.d.ts index cde4415d..3af424ef 100644 --- a/src/types/api/uniformity/uniformity.d.ts +++ b/src/types/api/uniformity/uniformity.d.ts @@ -62,7 +62,7 @@ export type UniformityDetail = BaseMetadata & { info_umum: UniformityInfoUmum; sampling: UniformitySampling; result: UniformityResult; - uniformity_details: UniformityDetailItem[]; + uniformity_details?: UniformityDetailItem[]; latest_approval?: BaseApproval; }; From 2e44371c6c57961e8920c74095ffe2002ffdccce Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 30 Dec 2025 09:05:50 +0700 Subject: [PATCH 104/124] feat(FE-316): Add withpopulation query param to kandang lookup --- src/components/pages/uniformity/form/UniformityForm.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx index b45f9025..9b04c3d0 100644 --- a/src/components/pages/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -199,6 +199,7 @@ const UniformityForm = ({ const params = new URLSearchParams({ project_flock_id: selectedProjectFlock.value.toString(), kandang_id: selectedKandang.value.toString(), + withpopulation: Boolean(true).toString(), }); return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`; }, [selectedProjectFlock, selectedKandang]); From d6849a48d2bb63b02ad41ef6e7885ecac2b2e34a Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 30 Dec 2025 09:21:38 +0700 Subject: [PATCH 105/124] refactor(FE-316): Rename file to documents in uniformity feature --- .../uniformity/detail/UniformityDetail.tsx | 8 +-- .../detail/UniformityDetailsPreview.tsx | 2 +- .../uniformity/form/UniformityForm.schema.ts | 14 +++--- .../pages/uniformity/form/UniformityForm.tsx | 49 +++++++++---------- .../uniformity/form/UniformityResultForm.tsx | 6 +-- src/services/api/uniformity.ts | 15 ++---- src/types/api/uniformity/uniformity.d.ts | 9 ++-- 7 files changed, 46 insertions(+), 57 deletions(-) diff --git a/src/components/pages/uniformity/detail/UniformityDetail.tsx b/src/components/pages/uniformity/detail/UniformityDetail.tsx index 8cfaa8d4..8195fa71 100644 --- a/src/components/pages/uniformity/detail/UniformityDetail.tsx +++ b/src/components/pages/uniformity/detail/UniformityDetail.tsx @@ -90,8 +90,8 @@ const UniformityDetail: React.FC = ({ label: 'Kandang', }, { - id: 'file-name', - value: 'file-name', + id: 'documents-name', + value: 'documents-name', label: 'File Uniformity', }, { @@ -123,7 +123,7 @@ const UniformityDetail: React.FC = ({ 'lokasi-farm': info_umum.lokasi_farm, 'project-flock': info_umum.project_flock, kandang: info_umum.kandang, - 'file-name': info_umum.file_name, + 'documents-name': info_umum.documents_name, 'approval-status': statusValue, }; @@ -148,7 +148,7 @@ const UniformityDetail: React.FC = ({ return -; } - if (id === 'file-name') { + if (id === 'documents-name') { return (
{valueMap[id]} diff --git a/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx b/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx index e7f7067b..2c83ba00 100644 --- a/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx +++ b/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx @@ -214,7 +214,7 @@ const UniformityDetailsPreview = ({ {/* Header */} diff --git a/src/components/pages/uniformity/form/UniformityForm.schema.ts b/src/components/pages/uniformity/form/UniformityForm.schema.ts index 31e9d1bc..5463557a 100644 --- a/src/components/pages/uniformity/form/UniformityForm.schema.ts +++ b/src/components/pages/uniformity/form/UniformityForm.schema.ts @@ -20,16 +20,16 @@ type UniformityFormSchemaType = { label: string; } | null; kandang_id: number; - file: File | undefined; + documents: File | undefined; }; const FileSchema = Yup.mixed() - .test('fileSize', 'Ukuran file maksimal 2 MB', (value): boolean => { + .test('documentsSize', 'Ukuran file maksimal 2 MB', (value): boolean => { if (!value) return true; if (value instanceof File) return value.size <= 2 * 1024 * 1024; return false; }) - .test('fileType', 'Format file harus Excel', (value): boolean => { + .test('documentsType', 'Format file harus Excel', (value): boolean => { if (!value) return true; if (value instanceof File) { const allowedTypes = [ @@ -74,7 +74,7 @@ export const UniformityFormSchema: Yup.ObjectSchema = .min(1, 'Kandang wajib diisi!') .required('Kandang wajib diisi!') .typeError('Kandang wajib diisi!'), - file: FileSchema.required('File wajib diisi!'), + documents: FileSchema.required('File wajib diisi!'), }); export type UniformityFormValues = Yup.InferType; @@ -83,8 +83,8 @@ export type UniformityFormData = { date: string; week: number; project_flock_kandang_id: number; - file: File | null; - file_name: string; + documents: File | null; + documents_name: string; }; export const getUniformityFormInitialValues = ( @@ -115,6 +115,6 @@ export const getUniformityFormInitialValues = ( } : null, kandang_id: initialValues?.kandang?.id ?? 0, - file: undefined, + documents: undefined, }; }; diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx index 9b04c3d0..48a84c70 100644 --- a/src/components/pages/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -246,15 +246,12 @@ const UniformityForm = ({ date: values.date, week: values.week, project_flock_kandang_id: projectFlockKandangId, - file: values.file as File, - file_name: (values.file as File).name, + documents: values.documents as File, + documents_name: (values.documents as File).name, }); const payload: VerifyUniformityPayload = { - date: values.date, - week: values.week, - project_flock_kandang_id: projectFlockKandangId, - file: values.file as File, + documents: values.documents as File, }; const res = await UniformityApi.verifyUniformity(payload); @@ -328,17 +325,17 @@ const UniformityForm = ({ const handleFileChange = useCallback( (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; + const documents = e.target.files?.[0]; - formik.setFieldTouched('file', true); + formik.setFieldTouched('documents', true); - if (!file) { - formik.setFieldValue('file', undefined); + if (!documents) { + formik.setFieldValue('documents', undefined); return; } - if (file.size > 2 * 1024 * 1024) { - toast.error(`Ukuran file ${file.name} maksimal 2 MB!`); + if (documents.size > 2 * 1024 * 1024) { + toast.error(`Ukuran file ${documents.name} maksimal 2 MB!`); return; } @@ -348,12 +345,12 @@ const UniformityForm = ({ 'text/csv', ]; - if (!allowedTypes.includes(file.type)) { - toast.error(`Format file ${file.name} harus Excel atau CSV!`); + if (!allowedTypes.includes(documents.type)) { + toast.error(`Format file ${documents.name} harus Excel atau CSV!`); return; } - formik.setFieldValue('file', file); + formik.setFieldValue('documents', documents); }, [] ); @@ -366,7 +363,7 @@ const UniformityForm = ({ ); const handleRemoveFile = useCallback(() => { - formik.setFieldValue('file', undefined); + formik.setFieldValue('documents', undefined); if (fileInputRef.current) { fileInputRef.current.value = ''; } @@ -531,12 +528,14 @@ const UniformityForm = ({ htmlFor='file-upload-input' className={cn( "w-full text-sm font-normal leading-5 after:content-['*'] after:ml-0.5 after:text-red-500", - formik.touched.file && formik.errors.file && 'text-red-500' + formik.touched.documents && + formik.errors.documents && + 'text-red-500' )} > Upload File - {formik.values.file && !isNextStep ? ( + {formik.values.documents && !isNextStep ? ( - ) : !formik.values.file && !isNextStep ? ( + ) : !formik.values.documents && !isNextStep ? (
- {formik.values.file.name} + {formik.values.documents.name}
) : ( @@ -668,15 +667,15 @@ const UniformityForm = ({ ref={fileInputRef} type='file' id='file-upload-input' - name='file' + name='documents' accept='application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/csv' onChange={handleFileChange} className='hidden' /> - {formik.touched.file && formik.errors.file && ( + {formik.touched.documents && formik.errors.documents && (

- {formik.errors.file as string} + {formik.errors.documents as string}

)} diff --git a/src/components/pages/uniformity/form/UniformityResultForm.tsx b/src/components/pages/uniformity/form/UniformityResultForm.tsx index 67f0026f..776e8dbc 100644 --- a/src/components/pages/uniformity/form/UniformityResultForm.tsx +++ b/src/components/pages/uniformity/form/UniformityResultForm.tsx @@ -51,7 +51,7 @@ const UniformityResultForm = () => { }; const handleSubmit = async () => { - if (!uniformityFormData || !uniformityFormData.file) { + if (!uniformityFormData || !uniformityFormData.documents) { toast.error('Form data is missing. Please try again.'); return; } @@ -63,7 +63,7 @@ const UniformityResultForm = () => { date: uniformityFormData.date, week: uniformityFormData.week, project_flock_kandang_id: uniformityFormData.project_flock_kandang_id, - file: uniformityFormData.file, + documents: uniformityFormData.documents, }; const res = await UniformityApi.createUniformity(payload); @@ -236,7 +236,7 @@ const UniformityResultForm = () => { {/* Header */} diff --git a/src/services/api/uniformity.ts b/src/services/api/uniformity.ts index 73d979ca..0f1b8ad3 100644 --- a/src/services/api/uniformity.ts +++ b/src/services/api/uniformity.ts @@ -47,8 +47,8 @@ export class UniformityApiService extends BaseApiService< payload.project_flock_kandang_id.toString() ); - if (payload.file) { - formData.append('file', payload.file); + if (payload.documents) { + formData.append('documents', payload.documents); } return await this.create(formData as unknown as CreateUniformityPayload); @@ -58,15 +58,8 @@ export class UniformityApiService extends BaseApiService< payload: VerifyUniformityPayload ): Promise | undefined> { const formData = new FormData(); - formData.append('date', payload.date); - formData.append('week', payload.week.toString()); - formData.append( - 'project_flock_kandang_id', - payload.project_flock_kandang_id.toString() - ); - - if (payload.file) { - formData.append('file', payload.file); + if (payload.documents) { + formData.append('documents', payload.documents); } return await this.customRequest>( diff --git a/src/types/api/uniformity/uniformity.d.ts b/src/types/api/uniformity/uniformity.d.ts index 3af424ef..45291a28 100644 --- a/src/types/api/uniformity/uniformity.d.ts +++ b/src/types/api/uniformity/uniformity.d.ts @@ -34,7 +34,7 @@ export type UniformityInfoUmum = { lokasi_farm: string; project_flock: string; kandang: string; - file_name: string; + documents_name: string; }; export type UniformitySampling = { @@ -77,15 +77,12 @@ export type VerifyUniformityResponse = { export type CreateUniformityPayload = { date: string; project_flock_kandang_id: number; - file: File; + documents: File; week: number; }; export type VerifyUniformityPayload = { - date: string; - project_flock_kandang_id: number; - file: File; - week: number; + documents: File; }; // ==================== OTHER TYPES ==================== From 4e5f9c710c1fb46c2627f01ca374a48aa329ad76 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 30 Dec 2025 09:50:09 +0700 Subject: [PATCH 106/124] refactor(FE-316): Replace bulk delete with single-item delete --- .../pages/uniformity/UniformityTable.tsx | 60 ++++--------------- src/services/api/uniformity.ts | 16 ++--- 2 files changed, 14 insertions(+), 62 deletions(-) diff --git a/src/components/pages/uniformity/UniformityTable.tsx b/src/components/pages/uniformity/UniformityTable.tsx index 8b826a50..52b98c91 100644 --- a/src/components/pages/uniformity/UniformityTable.tsx +++ b/src/components/pages/uniformity/UniformityTable.tsx @@ -165,7 +165,6 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { const [isBulkActionLoading, setIsBulkActionLoading] = useState(false); const singleDeleteModal = useModal(); - const bulkDeleteModal = useModal(); const successModal = useModal(); const singleApproveModal = useModal(); const singleRejectModal = useModal(); @@ -275,34 +274,18 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { } }, [selectedUniformity?.id, refreshUniformities, singleRejectModal]); - const handleBulkDelete = useCallback(() => { - bulkDeleteModal.openModal(); - }, [bulkDeleteModal]); - - const bulkDeleteHandler = useCallback(async () => { - setIsBulkActionLoading(true); - - try { - await UniformityApi.bulkDelete(selectedRowIds); - - setRowSelection({}); - refreshUniformities(); - - bulkDeleteModal.closeModal(); - toast.success( - `Successfully deleted ${selectedRowIds.length} Uniformity data!` - ); - } catch { - toast.error('Failed to delete Uniformity data'); - } finally { - setIsBulkActionLoading(false); - } - }, [selectedRowIds, refreshUniformities, bulkDeleteModal]); - const handleCloseFab = useCallback(() => { setRowSelection({}); }, []); + const handleDelete = useCallback(() => { + if (selectedRowIds.length === 1) { + const uniformity = selectedUniformities[0]; + setSelectedUniformity(uniformity); + singleDeleteModal.openModal(); + } + }, [selectedRowIds, selectedUniformities, singleDeleteModal]); + const handleBulkApprove = useCallback(() => { bulkApproveModal.openModal(); }, [bulkApproveModal]); @@ -605,30 +588,6 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { - -
- -
-
- void }) => { action: 'DELETE', icon: 'mdi:delete-outline', label: 'Delete', - onClick: handleBulkDelete, + hidden: selectedRowIds.length !== 1, + onClick: handleDelete, permissions: 'lti.production.uniformity.delete', }, ]} diff --git a/src/services/api/uniformity.ts b/src/services/api/uniformity.ts index 0f1b8ad3..acbdb5e8 100644 --- a/src/services/api/uniformity.ts +++ b/src/services/api/uniformity.ts @@ -107,18 +107,10 @@ export class UniformityApiService extends BaseApiService< ); } - async bulkDelete( - ids: number[] - ): Promise | undefined> { - return await this.customRequest>( - 'bulk-delete', - { - method: 'POST', - payload: { - ids, - }, - } - ); + async delete(id: number): Promise | undefined> { + return await this.customRequest>(`/${id}`, { + method: 'DELETE', + }); } } From 02dc624036a736d7a9469de41c3827db1d708878 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 30 Dec 2025 10:11:51 +0700 Subject: [PATCH 107/124] feat(FE-316): Add filter modal and query params for Uniformity --- .../pages/uniformity/UniformityTable.tsx | 265 +++++++++++++++++- src/services/api/uniformity.ts | 21 +- 2 files changed, 280 insertions(+), 6 deletions(-) diff --git a/src/components/pages/uniformity/UniformityTable.tsx b/src/components/pages/uniformity/UniformityTable.tsx index 52b98c91..2b23c009 100644 --- a/src/components/pages/uniformity/UniformityTable.tsx +++ b/src/components/pages/uniformity/UniformityTable.tsx @@ -23,6 +23,18 @@ import UniformityTableSkeleton from '@/components/pages/uniformity/skeleton/Unif import RequirePermission from '@/components/helper/RequirePermission'; import { useUniformityStore } from '@/stores/uniformity/uniformity.store'; import FloatingActionsButton from '@/components/FloatingActionsButton'; +import Modal from '@/components/Modal'; +import SelectInput, { + OptionType, + useSelect, +} from '@/components/input/SelectInput'; +import DateInput from '@/components/input/DateInput'; +import { LocationApi } from '@/services/api/master-data'; +import { + ProjectFlockApi, + ProjectFlockKandangApi, +} from '@/services/api/production'; +import { Kandang } from '@/types/api/master-data/kandang'; import { getStatusColor, getStatusIndicatorColor, @@ -170,16 +182,170 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { const singleRejectModal = useModal(); const bulkApproveModal = useModal(); const bulkRejectModal = useModal(); + const filterModal = useModal(); + + // ===== FILTER STATE ===== + const [filterLocation, setFilterLocation] = useState(null); + const [filterProjectFlock, setFilterProjectFlock] = + useState(null); + const [filterKandang, setFilterKandang] = useState(null); + const [filterStartDate, setFilterStartDate] = useState(''); + const [filterEndDate, setFilterEndDate] = useState(''); + const [projectFlockSearchValue, setProjectFlockSearchValue] = useState(''); + + const { + setInputValue: setFilterLocationInputValue, + options: filterLocationOptions, + isLoadingOptions: isLoadingFilterLocations, + } = useSelect(LocationApi.basePath, 'id', 'name', 'search'); + + // ===== FETCH PROJECT FLOCKS DATA FOR FILTER ===== + const filterProjectFlocksUrl = useMemo(() => { + const params = new URLSearchParams({ + search: projectFlockSearchValue || '', + limit: '100', + }); + if (filterLocation) { + params.append('location_id', filterLocation.value.toString()); + } + return `${ProjectFlockApi.basePath}?${params.toString()}`; + }, [projectFlockSearchValue, filterLocation]); + + const { + data: filterProjectFlocksData, + isLoading: isLoadingFilterProjectFlocks, + } = useSWR(filterProjectFlocksUrl, ProjectFlockApi.getAllFetcher); + + const filterProjectFlocksDataList = useMemo( + () => + isResponseSuccess(filterProjectFlocksData) + ? filterProjectFlocksData.data + : undefined, + [filterProjectFlocksData] + ); + + const filterProjectFlockOptions = useMemo(() => { + let options: OptionType[] = []; + + if (isResponseSuccess(filterProjectFlocksData)) { + const flockOptions = + filterProjectFlocksData?.data.map((projectFlock) => ({ + value: projectFlock.id, + label: projectFlock.flock_name || '', + })) || []; + options = options.concat(flockOptions); + } + + return options; + }, [filterProjectFlocksData]); + + // ===== KANDANG OPTIONS FOR FILTER ===== + const filterKandangOptions = useMemo(() => { + let options: OptionType[] = []; + + if (filterProjectFlock && filterProjectFlocksDataList) { + const selectedProjectFlockData = filterProjectFlocksDataList.find( + (pf) => pf.id === filterProjectFlock.value + ); + + if (selectedProjectFlockData?.kandangs) { + const kandangOpts = selectedProjectFlockData.kandangs.map( + (kandang: Kandang) => ({ + value: kandang.id, + label: kandang.name || '', + }) + ); + options = options.concat(kandangOpts); + } + } + + return options; + }, [filterProjectFlock, filterProjectFlocksDataList]); + + // ===== BUILD SWR KEY WITH FILTERS ===== + const uniformitySwrKey = useMemo(() => { + const basePath = UniformityApi.basePath; + const queryParams = new URLSearchParams(); + + if (filterLocation) { + queryParams.append('location_id', filterLocation.value.toString()); + } + if (filterProjectFlock) { + queryParams.append( + 'project_flock_id', + filterProjectFlock.value.toString() + ); + } + if (filterKandang) { + queryParams.append('kandang_id', filterKandang.value.toString()); + } + if (filterStartDate) { + queryParams.append('start_date', filterStartDate); + } + if (filterEndDate) { + queryParams.append('end_date', filterEndDate); + } + + const tableQueryString = getTableFilterQueryString(); + const tableParams = new URLSearchParams( + tableQueryString.split('?')[1] || '' + ); + + tableParams.forEach((value, key) => { + queryParams.append(key, value); + }); + + const queryString = queryParams.toString(); + return queryString ? `${basePath}?${queryString}` : basePath; + }, [ + filterLocation, + filterProjectFlock, + filterKandang, + filterStartDate, + filterEndDate, + getTableFilterQueryString, + ]); const { data: uniformities, isLoading, mutate: refreshUniformities, - } = useSWR( - `${UniformityApi.basePath}${getTableFilterQueryString()}`, - UniformityApi.getAllFetcher + } = useSWR(uniformitySwrKey, UniformityApi.getAllFetcher); + + // ===== FILTER HANDLERS ===== + const handleFilterLocationChange = useCallback( + (val: OptionType | OptionType[] | null) => { + setFilterLocation(val as OptionType | null); + }, + [] ); + const handleFilterProjectFlockChange = useCallback( + (val: OptionType | OptionType[] | null) => { + setFilterProjectFlock(val as OptionType | null); + }, + [] + ); + + const handleFilterKandangChange = useCallback( + (val: OptionType | OptionType[] | null) => { + setFilterKandang(val as OptionType | null); + }, + [] + ); + + const handleResetFilters = useCallback(() => { + setFilterLocation(null); + setFilterProjectFlock(null); + setFilterKandang(null); + setFilterStartDate(''); + setFilterEndDate(''); + }, []); + + const handleApplyFilters = useCallback(() => { + filterModal.closeModal(); + }, [filterModal]); + const selectedRowIds = useMemo(() => { return Object.keys(rowSelection) .filter((key) => rowSelection[key]) @@ -485,7 +651,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
- @@ -683,6 +849,97 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
+ {/* Filter Modal */} + +
+
+

Filter Data

+ +
+ +
+
+ setFilterStartDate(e.target.value)} + className={{ wrapper: 'w-full' }} + /> + + setFilterEndDate(e.target.value)} + className={{ wrapper: 'w-full' }} + /> +
+ + + + + + +
+ +
+ + +
+
+
+ {/* Floating Actions Button */} | undefined> { - return await this.customRequest>(''); + async getUniformity( + location_id?: number, + project_flock_id?: number, + kandang_id?: number, + project_flock_kandang_id?: number, + start_date?: string, + end_date?: string + ): Promise | undefined> { + return await this.customRequest>('', { + method: 'GET', + params: { + location_id: location_id, + project_flock_id: project_flock_id, + kandang_id: kandang_id, + project_flock_kandang_id: project_flock_kandang_id, + start_date: start_date, + end_date: end_date, + }, + }); } async getUniformityDetail( From c385c42c8f17d71dc0e1e411e0e1c86607394e71 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 30 Dec 2025 10:17:46 +0700 Subject: [PATCH 108/124] feat(FE-316): Support multi-select filters in UniformityTable --- .../pages/uniformity/UniformityTable.tsx | 77 +++++++++++-------- src/services/api/uniformity.ts | 14 ++-- 2 files changed, 52 insertions(+), 39 deletions(-) diff --git a/src/components/pages/uniformity/UniformityTable.tsx b/src/components/pages/uniformity/UniformityTable.tsx index 2b23c009..205d6966 100644 --- a/src/components/pages/uniformity/UniformityTable.tsx +++ b/src/components/pages/uniformity/UniformityTable.tsx @@ -185,10 +185,11 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { const filterModal = useModal(); // ===== FILTER STATE ===== - const [filterLocation, setFilterLocation] = useState(null); - const [filterProjectFlock, setFilterProjectFlock] = - useState(null); - const [filterKandang, setFilterKandang] = useState(null); + const [filterLocation, setFilterLocation] = useState([]); + const [filterProjectFlock, setFilterProjectFlock] = useState( + [] + ); + const [filterKandang, setFilterKandang] = useState([]); const [filterStartDate, setFilterStartDate] = useState(''); const [filterEndDate, setFilterEndDate] = useState(''); const [projectFlockSearchValue, setProjectFlockSearchValue] = useState(''); @@ -205,8 +206,9 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { search: projectFlockSearchValue || '', limit: '100', }); - if (filterLocation) { - params.append('location_id', filterLocation.value.toString()); + if (filterLocation.length > 0) { + const locationIds = filterLocation.map((loc) => loc.value).join(','); + params.append('location_id', locationIds); } return `${ProjectFlockApi.basePath}?${params.toString()}`; }, [projectFlockSearchValue, filterLocation]); @@ -243,20 +245,21 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { const filterKandangOptions = useMemo(() => { let options: OptionType[] = []; - if (filterProjectFlock && filterProjectFlocksDataList) { - const selectedProjectFlockData = filterProjectFlocksDataList.find( - (pf) => pf.id === filterProjectFlock.value - ); + if (filterProjectFlock.length > 0 && filterProjectFlocksDataList) { + const selectedProjectFlockIds = filterProjectFlock.map((pf) => pf.value); - if (selectedProjectFlockData?.kandangs) { - const kandangOpts = selectedProjectFlockData.kandangs.map( - (kandang: Kandang) => ({ + filterProjectFlocksDataList.forEach((projectFlock) => { + if ( + selectedProjectFlockIds.includes(projectFlock.id) && + projectFlock.kandangs + ) { + const kandangOpts = projectFlock.kandangs.map((kandang: Kandang) => ({ value: kandang.id, label: kandang.name || '', - }) - ); - options = options.concat(kandangOpts); - } + })); + options = options.concat(kandangOpts); + } + }); } return options; @@ -267,17 +270,17 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { const basePath = UniformityApi.basePath; const queryParams = new URLSearchParams(); - if (filterLocation) { - queryParams.append('location_id', filterLocation.value.toString()); + if (filterLocation.length > 0) { + const locationIds = filterLocation.map((loc) => loc.value).join(','); + queryParams.append('location_id', locationIds); } - if (filterProjectFlock) { - queryParams.append( - 'project_flock_id', - filterProjectFlock.value.toString() - ); + if (filterProjectFlock.length > 0) { + const flockIds = filterProjectFlock.map((pf) => pf.value).join(','); + queryParams.append('project_flock_id', flockIds); } - if (filterKandang) { - queryParams.append('kandang_id', filterKandang.value.toString()); + if (filterKandang.length > 0) { + const kandangIds = filterKandang.map((k) => k.value).join(','); + queryParams.append('kandang_id', kandangIds); } if (filterStartDate) { queryParams.append('start_date', filterStartDate); @@ -315,29 +318,32 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { // ===== FILTER HANDLERS ===== const handleFilterLocationChange = useCallback( (val: OptionType | OptionType[] | null) => { - setFilterLocation(val as OptionType | null); + const arr = Array.isArray(val) ? val : val ? [val] : []; + setFilterLocation(arr); }, [] ); const handleFilterProjectFlockChange = useCallback( (val: OptionType | OptionType[] | null) => { - setFilterProjectFlock(val as OptionType | null); + const arr = Array.isArray(val) ? val : val ? [val] : []; + setFilterProjectFlock(arr); }, [] ); const handleFilterKandangChange = useCallback( (val: OptionType | OptionType[] | null) => { - setFilterKandang(val as OptionType | null); + const arr = Array.isArray(val) ? val : val ? [val] : []; + setFilterKandang(arr); }, [] ); const handleResetFilters = useCallback(() => { - setFilterLocation(null); - setFilterProjectFlock(null); - setFilterKandang(null); + setFilterLocation([]); + setFilterProjectFlock([]); + setFilterKandang([]); setFilterStartDate(''); setFilterEndDate(''); }, []); @@ -893,6 +899,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { onInputChange={setFilterLocationInputValue} isLoading={isLoadingFilterLocations} isClearable + isMulti className={{ wrapper: 'w-full' }} /> @@ -904,8 +911,9 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { options={filterProjectFlockOptions} onInputChange={setProjectFlockSearchValue} isLoading={isLoadingFilterProjectFlocks} - isDisabled={!filterLocation} + isDisabled={filterLocation.length === 0} isClearable + isMulti className={{ wrapper: 'w-full' }} /> @@ -915,8 +923,9 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { value={filterKandang} onChange={handleFilterKandangChange} options={filterKandangOptions} - isDisabled={!filterProjectFlock} + isDisabled={filterProjectFlock.length === 0} isClearable + isMulti className={{ wrapper: 'w-full' }} /> diff --git a/src/services/api/uniformity.ts b/src/services/api/uniformity.ts index b12ea5d7..59161734 100644 --- a/src/services/api/uniformity.ts +++ b/src/services/api/uniformity.ts @@ -18,12 +18,14 @@ export class UniformityApiService extends BaseApiService< } async getUniformity( - location_id?: number, - project_flock_id?: number, - kandang_id?: number, - project_flock_kandang_id?: number, + location_id?: string, + project_flock_id?: string, + kandang_id?: string, + project_flock_kandang_id?: string, start_date?: string, - end_date?: string + end_date?: string, + page?: number, + limit?: number ): Promise | undefined> { return await this.customRequest>('', { method: 'GET', @@ -34,6 +36,8 @@ export class UniformityApiService extends BaseApiService< project_flock_kandang_id: project_flock_kandang_id, start_date: start_date, end_date: end_date, + page: page, + limit: limit, }, }); } From f51236fcfcc5deca97d3ee2cc06f9426eff3c7dc Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 30 Dec 2025 10:23:01 +0700 Subject: [PATCH 109/124] refactor(FE-316): Use single-select filters, add PF-Kandang lookup --- .../pages/uniformity/UniformityTable.tsx | 127 +++++++++++------- src/services/api/uniformity.ts | 8 +- 2 files changed, 76 insertions(+), 59 deletions(-) diff --git a/src/components/pages/uniformity/UniformityTable.tsx b/src/components/pages/uniformity/UniformityTable.tsx index 205d6966..8e557983 100644 --- a/src/components/pages/uniformity/UniformityTable.tsx +++ b/src/components/pages/uniformity/UniformityTable.tsx @@ -12,6 +12,7 @@ import { useTableFilter } from '@/services/hooks/useTableFilter'; import { UniformityApi } from '@/services/api/uniformity'; import { type Uniformity } from '@/types/api/uniformity/uniformity'; import { isResponseSuccess } from '@/lib/api-helper'; +import { type BaseApiResponse } from '@/types/api/api-general'; import Table from '@/components/Table'; import Badge from '@/components/Badge'; import CheckboxInput from '@/components/input/CheckboxInput'; @@ -30,11 +31,9 @@ import SelectInput, { } from '@/components/input/SelectInput'; import DateInput from '@/components/input/DateInput'; import { LocationApi } from '@/services/api/master-data'; -import { - ProjectFlockApi, - ProjectFlockKandangApi, -} from '@/services/api/production'; +import { ProjectFlockApi } from '@/services/api/production'; import { Kandang } from '@/types/api/master-data/kandang'; +import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock'; import { getStatusColor, getStatusIndicatorColor, @@ -185,11 +184,12 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { const filterModal = useModal(); // ===== FILTER STATE ===== - const [filterLocation, setFilterLocation] = useState([]); - const [filterProjectFlock, setFilterProjectFlock] = useState( - [] - ); - const [filterKandang, setFilterKandang] = useState([]); + const [filterLocation, setFilterLocation] = useState(null); + const [filterProjectFlock, setFilterProjectFlock] = + useState(null); + const [filterKandang, setFilterKandang] = useState(null); + const [filterProjectFlockKandangId, setFilterProjectFlockKandangId] = + useState(undefined); const [filterStartDate, setFilterStartDate] = useState(''); const [filterEndDate, setFilterEndDate] = useState(''); const [projectFlockSearchValue, setProjectFlockSearchValue] = useState(''); @@ -206,9 +206,8 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { search: projectFlockSearchValue || '', limit: '100', }); - if (filterLocation.length > 0) { - const locationIds = filterLocation.map((loc) => loc.value).join(','); - params.append('location_id', locationIds); + if (filterLocation) { + params.append('location_id', filterLocation.value.toString()); } return `${ProjectFlockApi.basePath}?${params.toString()}`; }, [projectFlockSearchValue, filterLocation]); @@ -245,42 +244,70 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { const filterKandangOptions = useMemo(() => { let options: OptionType[] = []; - if (filterProjectFlock.length > 0 && filterProjectFlocksDataList) { - const selectedProjectFlockIds = filterProjectFlock.map((pf) => pf.value); + if (filterProjectFlock && filterProjectFlocksDataList) { + const selectedProjectFlockData = filterProjectFlocksDataList.find( + (pf) => pf.id === filterProjectFlock.value + ); - filterProjectFlocksDataList.forEach((projectFlock) => { - if ( - selectedProjectFlockIds.includes(projectFlock.id) && - projectFlock.kandangs - ) { - const kandangOpts = projectFlock.kandangs.map((kandang: Kandang) => ({ + if (selectedProjectFlockData?.kandangs) { + const kandangOpts = selectedProjectFlockData.kandangs.map( + (kandang: Kandang) => ({ value: kandang.id, label: kandang.name || '', - })); - options = options.concat(kandangOpts); - } - }); + }) + ); + options = options.concat(kandangOpts); + } } return options; }, [filterProjectFlock, filterProjectFlocksDataList]); + // ===== PROJECT FLOCK KANDANG LOOKUP ===== + const projectFlockKandangLookupUrl = useMemo(() => { + if (!filterProjectFlock || !filterKandang) return null; + const params = new URLSearchParams({ + project_flock_id: filterProjectFlock.value.toString(), + kandang_id: filterKandang.value.toString(), + withpopulation: Boolean(true).toString(), + }); + return `${ProjectFlockApi.basePath}/kandangs/lookup?${params.toString()}`; + }, [filterProjectFlock, filterKandang]); + + const { data: projectFlockKandangLookupData } = useSWR( + projectFlockKandangLookupUrl, + projectFlockKandangLookupUrl + ? () => + ProjectFlockApi.getAllFetcher( + projectFlockKandangLookupUrl + ) as Promise> + : null + ); + + const projectFlockKandangLookup = + projectFlockKandangLookupData?.status === 'success' + ? projectFlockKandangLookupData.data + : undefined; + + // Update filterProjectFlockKandangId when lookup changes + useEffect(() => { + if (projectFlockKandangLookup?.id) { + setFilterProjectFlockKandangId(projectFlockKandangLookup.id); + } else { + setFilterProjectFlockKandangId(undefined); + } + }, [projectFlockKandangLookup]); + // ===== BUILD SWR KEY WITH FILTERS ===== const uniformitySwrKey = useMemo(() => { const basePath = UniformityApi.basePath; const queryParams = new URLSearchParams(); - if (filterLocation.length > 0) { - const locationIds = filterLocation.map((loc) => loc.value).join(','); - queryParams.append('location_id', locationIds); - } - if (filterProjectFlock.length > 0) { - const flockIds = filterProjectFlock.map((pf) => pf.value).join(','); - queryParams.append('project_flock_id', flockIds); - } - if (filterKandang.length > 0) { - const kandangIds = filterKandang.map((k) => k.value).join(','); - queryParams.append('kandang_id', kandangIds); + if (filterProjectFlockKandangId) { + queryParams.append( + 'project_flock_kandang_id', + filterProjectFlockKandangId.toString() + ); } if (filterStartDate) { queryParams.append('start_date', filterStartDate); @@ -301,9 +328,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { const queryString = queryParams.toString(); return queryString ? `${basePath}?${queryString}` : basePath; }, [ - filterLocation, - filterProjectFlock, - filterKandang, + filterProjectFlockKandangId, filterStartDate, filterEndDate, getTableFilterQueryString, @@ -318,32 +343,33 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { // ===== FILTER HANDLERS ===== const handleFilterLocationChange = useCallback( (val: OptionType | OptionType[] | null) => { - const arr = Array.isArray(val) ? val : val ? [val] : []; - setFilterLocation(arr); + setFilterLocation(val as OptionType | null); + setFilterProjectFlock(null); + setFilterKandang(null); }, [] ); const handleFilterProjectFlockChange = useCallback( (val: OptionType | OptionType[] | null) => { - const arr = Array.isArray(val) ? val : val ? [val] : []; - setFilterProjectFlock(arr); + setFilterProjectFlock(val as OptionType | null); + setFilterKandang(null); }, [] ); const handleFilterKandangChange = useCallback( (val: OptionType | OptionType[] | null) => { - const arr = Array.isArray(val) ? val : val ? [val] : []; - setFilterKandang(arr); + setFilterKandang(val as OptionType | null); }, [] ); const handleResetFilters = useCallback(() => { - setFilterLocation([]); - setFilterProjectFlock([]); - setFilterKandang([]); + setFilterLocation(null); + setFilterProjectFlock(null); + setFilterKandang(null); + setFilterProjectFlockKandangId(undefined); setFilterStartDate(''); setFilterEndDate(''); }, []); @@ -899,7 +925,6 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { onInputChange={setFilterLocationInputValue} isLoading={isLoadingFilterLocations} isClearable - isMulti className={{ wrapper: 'w-full' }} /> @@ -911,9 +936,8 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { options={filterProjectFlockOptions} onInputChange={setProjectFlockSearchValue} isLoading={isLoadingFilterProjectFlocks} - isDisabled={filterLocation.length === 0} + isDisabled={!filterLocation} isClearable - isMulti className={{ wrapper: 'w-full' }} /> @@ -923,9 +947,8 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { value={filterKandang} onChange={handleFilterKandangChange} options={filterKandangOptions} - isDisabled={filterProjectFlock.length === 0} + isDisabled={!filterProjectFlock} isClearable - isMulti className={{ wrapper: 'w-full' }} /> diff --git a/src/services/api/uniformity.ts b/src/services/api/uniformity.ts index 59161734..1f048dc6 100644 --- a/src/services/api/uniformity.ts +++ b/src/services/api/uniformity.ts @@ -18,10 +18,7 @@ export class UniformityApiService extends BaseApiService< } async getUniformity( - location_id?: string, - project_flock_id?: string, - kandang_id?: string, - project_flock_kandang_id?: string, + project_flock_kandang_id?: number, start_date?: string, end_date?: string, page?: number, @@ -30,9 +27,6 @@ export class UniformityApiService extends BaseApiService< return await this.customRequest>('', { method: 'GET', params: { - location_id: location_id, - project_flock_id: project_flock_id, - kandang_id: kandang_id, project_flock_kandang_id: project_flock_kandang_id, start_date: start_date, end_date: end_date, From 32088b916f9f354b08569d7d08b68671ef77638d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 30 Dec 2025 10:39:37 +0700 Subject: [PATCH 110/124] refactor(FE-438): Refine Filter modal UI and controls --- .../pages/uniformity/UniformityTable.tsx | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/src/components/pages/uniformity/UniformityTable.tsx b/src/components/pages/uniformity/UniformityTable.tsx index 8e557983..78615b29 100644 --- a/src/components/pages/uniformity/UniformityTable.tsx +++ b/src/components/pages/uniformity/UniformityTable.tsx @@ -884,23 +884,33 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { {/* Filter Modal */} -
+
-

Filter Data

+

+ + Filter Data +

+
+
-
+
setFilterStartDate(e.target.value)} @@ -908,7 +918,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { /> setFilterEndDate(e.target.value)} @@ -953,20 +963,22 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { />
-
+
+ +
From 9be09ae2810fd71cccd36c74497a0222a4da187c Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 30 Dec 2025 10:55:43 +0700 Subject: [PATCH 111/124] refactor(FE-438): Make filter modal and controls responsive --- src/components/pages/uniformity/UniformityTable.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/pages/uniformity/UniformityTable.tsx b/src/components/pages/uniformity/UniformityTable.tsx index 78615b29..80ee34f4 100644 --- a/src/components/pages/uniformity/UniformityTable.tsx +++ b/src/components/pages/uniformity/UniformityTable.tsx @@ -884,7 +884,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { {/* Filter Modal */}
@@ -908,7 +908,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
-
+
void }) => {
-
+
- + + + Export + + } + align='end' + > + + + + +
diff --git a/src/components/pages/uniformity/export/UniformityExportExcel.tsx b/src/components/pages/uniformity/export/UniformityExportExcel.tsx new file mode 100644 index 00000000..cc9a2248 --- /dev/null +++ b/src/components/pages/uniformity/export/UniformityExportExcel.tsx @@ -0,0 +1,86 @@ +'use client'; + +import * as XLSX from 'xlsx'; +import type { Uniformity } from '@/types/api/uniformity/uniformity'; +import { formatDate, formatNumber } from '@/lib/helper'; + +interface UniformityExportExcelParams { + data: Uniformity[]; + params: { + location_name?: string; + project_flock_name?: string; + kandang_name?: string; + start_date?: string; + end_date?: string; + }; +} + +const getStatusText = (status: string) => { + switch (status) { + case 'APPROVED': + return 'Disetujui'; + case 'REJECTED': + return 'Ditolak'; + case 'CREATED': + return 'Pengajuan'; + default: + return status; + } +}; + +export const generateUniformityExcel = ( + data: UniformityExportExcelParams['data'], + params: UniformityExportExcelParams['params'] +): void => { + if (!data || data.length === 0) { + return; + } + + const excelData: { [key: string]: string | number }[] = data.map( + (item: Uniformity, index: number) => ({ + No: index + 1, + Lokasi: item.location_name || '', + 'Project Flock': item.flock_name || '', + Kandang: item.kandang_name || '', + Tanggal: formatDate(item.applied_at, 'DD MMM YYYY'), + Minggu: item.week || 0, + Status: getStatusText(item.status), + 'Uniformity (%)': formatNumber(item.uniformity), + 'CV (%)': formatNumber(item.cv), + 'Chick Qty': formatNumber(item.chick_qty_of_weight), + 'Uniform Qty': formatNumber(item.uniform_qty), + 'Mean Up': formatNumber(item.mean_up), + 'Mean Down': formatNumber(item.mean_down), + }) + ); + + const worksheet = XLSX.utils.json_to_sheet(excelData); + + const colWidths = [ + { wch: 6 }, // No + { wch: 25 }, // Lokasi + { wch: 20 }, // Project Flock + { wch: 15 }, // Kandang + { wch: 15 }, // Tanggal + { wch: 10 }, // Minggu + { wch: 12 }, // Status + { wch: 15 }, // Uniformity (%) + { wch: 10 }, // CV (%) + { wch: 12 }, // Chick Qty + { wch: 12 }, // Uniform Qty + { wch: 12 }, // Mean Up + { wch: 12 }, // Mean Down + ]; + worksheet['!cols'] = colWidths; + + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, worksheet, 'Uniformity'); + + const period = + params.start_date && params.end_date + ? `${params.start_date}-${params.end_date}` + : formatDate(new Date(), 'YYYY-MM-DD'); + const filename = `laporan-uniformity-${period}.xlsx`; + + XLSX.writeFile(workbook, filename); +}; diff --git a/src/components/pages/uniformity/export/UniformityExportPDF.tsx b/src/components/pages/uniformity/export/UniformityExportPDF.tsx new file mode 100644 index 00000000..b22efcfd --- /dev/null +++ b/src/components/pages/uniformity/export/UniformityExportPDF.tsx @@ -0,0 +1,339 @@ +'use client'; + +import { + Page, + Text, + View, + Document, + StyleSheet, + Font, + pdf, +} from '@react-pdf/renderer'; + +import { formatDate, formatNumber } from '@/lib/helper'; +import type { Uniformity } from '@/types/api/uniformity/uniformity'; + +Font.register({ + family: 'Helvetica', + src: 'helvetica', +}); + +const pdfStyles = StyleSheet.create({ + page: { + fontSize: 10, + fontFamily: 'Helvetica', + padding: 20, + backgroundColor: '#FFFFFF', + }, + titleSection: { + marginBottom: 10, + }, + mainTitle: { + fontSize: 14, + fontWeight: 'bold', + marginBottom: 5, + color: '#1f74bf', + }, + table: { + borderWidth: 1, + borderColor: '#000000', + marginBottom: 15, + }, + tableRow: { + flexDirection: 'row', + }, + tableHeader: { + backgroundColor: '#F5F5F5', + }, + tableCell: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 8, + textAlign: 'left', + }, + tableCellHeader: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 8, + fontWeight: 'bold', + backgroundColor: '#F5F5F5', + borderBottomWidth: 1, + borderBottomColor: '#000000', + borderBottomStyle: 'solid', + paddingVertical: 12, + textAlign: 'center', + }, + tableCellHeaderRight: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 8, + fontWeight: 'bold', + backgroundColor: '#F5F5F5', + textAlign: 'right', + borderBottomWidth: 1, + borderBottomColor: '#000000', + borderBottomStyle: 'solid', + paddingVertical: 12, + }, + tableCellRight: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 8, + textAlign: 'right', + }, + tableCellCenter: { + flex: 1, + borderRightWidth: 1, + borderRightColor: '#000000', + borderRightStyle: 'solid', + padding: 4, + fontSize: 8, + textAlign: 'center', + }, + tableBorderBottom: { + borderBottomWidth: 1, + borderBottomColor: '#000000', + borderBottomStyle: 'solid', + }, + badge: { + backgroundColor: '#1f74bf', + color: '#FFFFFF', + padding: 2, + borderRadius: 2, + fontSize: 7, + fontWeight: 'bold', + alignSelf: 'center', + }, + parameterBadge: { + backgroundColor: '#F5F5F5', + color: '#333333', + padding: 4, + borderRadius: 4, + fontSize: 8, + marginRight: 8, + marginBottom: 4, + }, + parameterContainer: { + flexDirection: 'row', + flexWrap: 'wrap', + marginBottom: 8, + }, +}); + +interface UniformityExportPDFParams { + data: Uniformity[]; + params: { + location_name?: string; + project_flock_name?: string; + kandang_name?: string; + start_date?: string; + end_date?: string; + }; +} + +const getParameterText = (params: UniformityExportPDFParams['params']) => { + const paramsText = []; + + if (params.location_name && params.location_name !== 'Semua Lokasi') { + paramsText.push(`Lokasi: ${params.location_name}`); + } + + if ( + params.project_flock_name && + params.project_flock_name !== 'Semua Project Flock' + ) { + paramsText.push(`Project Flock: ${params.project_flock_name}`); + } + + if (params.kandang_name && params.kandang_name !== 'Semua Kandang') { + paramsText.push(`Kandang: ${params.kandang_name}`); + } + + if (params.start_date && params.end_date) { + const formattedStartDate = formatDate(params.start_date, 'DD MMM YYYY'); + const formattedEndDate = formatDate(params.end_date, 'DD MMM YYYY'); + paramsText.push(`Periode: ${formattedStartDate} - ${formattedEndDate}`); + } else if (params.start_date) { + const formattedStartDate = formatDate(params.start_date, 'DD MMM YYYY'); + paramsText.push(`Tanggal Mulai: ${formattedStartDate}`); + } else if (params.end_date) { + const formattedEndDate = formatDate(params.end_date, 'DD MMM YYYY'); + paramsText.push(`Tanggal Akhir: ${formattedEndDate}`); + } + + const currentDate = formatDate(new Date().toISOString(), 'DD MMM YYYY HH:mm'); + paramsText.push(`Dicetak: ${currentDate}`); + + return paramsText; +}; + +const getStatusText = (status: string) => { + switch (status) { + case 'APPROVED': + return 'Disetujui'; + case 'REJECTED': + return 'Ditolak'; + case 'CREATED': + return 'Pengajuan'; + default: + return status; + } +}; + +const createPDFDocument = ( + data: UniformityExportPDFParams['data'], + params: UniformityExportPDFParams['params'] +) => { + return ( + + + {/* Title and Parameters */} + + Production > Uniformity + + {getParameterText(params).map((param, index) => ( + + {param} + + ))} + + + + {/* Table */} + + {/* Table Header */} + + + No + + + Lokasi + + + Project Flock + + + Kandang + + + Tanggal (Week) + + + Status + + + Uniformity (%) + + + CV (%) + + + Chick Qty + + + Uniform Qty + + + Mean Up + + + Mean Down + + + + {/* Table Body */} + {data.map((item: Uniformity, index: number) => ( + + + {index + 1} + + + {item.location_name || '-'} + + + {item.flock_name || '-'} + + + {item.kandang_name || '-'} + + + + {formatDate(item.applied_at, 'DD MMM YYYY')} (Week {item.week} + ) + + + + + {getStatusText(item.status)} + + + + {formatNumber(item.uniformity)} + + + {formatNumber(item.cv)} + + + {formatNumber(item.chick_qty_of_weight)} + + + {formatNumber(item.uniform_qty)} + + + {formatNumber(item.mean_up)} + + + {formatNumber(item.mean_down)} + + + ))} + + + + ); +}; + +export const generateUniformityPDF = async ( + data: UniformityExportPDFParams['data'], + params: UniformityExportPDFParams['params'] +): Promise => { + const PDFDocument = createPDFDocument(data, params); + + try { + const blob = await pdf(PDFDocument).toBlob(); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + + const period = + params.start_date && params.end_date + ? `${params.start_date}-${params.end_date}` + : formatDate(new Date(), 'YYYY-MM-DD'); + link.download = `laporan-uniformity-${period}.pdf`; + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + } catch (error) { + throw error; + } +}; From 7c64870fed6038fed0f07fd0356769e96ffaa708 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 30 Dec 2025 11:46:43 +0700 Subject: [PATCH 113/124] refactor(FE-316): Add submission state to apply filters --- .../pages/uniformity/UniformityTable.tsx | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/components/pages/uniformity/UniformityTable.tsx b/src/components/pages/uniformity/UniformityTable.tsx index ff7fbb15..37b04785 100644 --- a/src/components/pages/uniformity/UniformityTable.tsx +++ b/src/components/pages/uniformity/UniformityTable.tsx @@ -191,6 +191,9 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { const bulkRejectModal = useModal(); const filterModal = useModal(); + // ===== SUBMISSION STATE ===== + const [isSubmitted, setIsSubmitted] = useState(false); + // ===== FILTER STATE ===== const [filterLocation, setFilterLocation] = useState(null); const [filterProjectFlock, setFilterProjectFlock] = @@ -311,17 +314,19 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { const basePath = UniformityApi.basePath; const queryParams = new URLSearchParams(); - if (filterProjectFlockKandangId) { - queryParams.append( - 'project_flock_kandang_id', - filterProjectFlockKandangId.toString() - ); - } - if (filterStartDate) { - queryParams.append('start_date', filterStartDate); - } - if (filterEndDate) { - queryParams.append('end_date', filterEndDate); + if (isSubmitted) { + if (filterProjectFlockKandangId) { + queryParams.append( + 'project_flock_kandang_id', + filterProjectFlockKandangId.toString() + ); + } + if (filterStartDate) { + queryParams.append('start_date', filterStartDate); + } + if (filterEndDate) { + queryParams.append('end_date', filterEndDate); + } } const tableQueryString = getTableFilterQueryString(); @@ -336,6 +341,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { const queryString = queryParams.toString(); return queryString ? `${basePath}?${queryString}` : basePath; }, [ + isSubmitted, filterProjectFlockKandangId, filterStartDate, filterEndDate, @@ -383,6 +389,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { }, []); const handleApplyFilters = useCallback(() => { + setIsSubmitted(true); filterModal.closeModal(); }, [filterModal]); From 52cb440cb36efcbf0cf22a26db2beb8a92103062 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 30 Dec 2025 13:35:24 +0700 Subject: [PATCH 114/124] refactor(FE-316): Rename documents to document in uniformity --- .../uniformity/detail/UniformityDetail.tsx | 8 ++-- .../detail/UniformityDetailsPreview.tsx | 2 +- .../uniformity/form/UniformityForm.schema.ts | 14 +++--- .../pages/uniformity/form/UniformityForm.tsx | 46 +++++++++---------- .../uniformity/form/UniformityResultForm.tsx | 6 +-- src/services/api/uniformity.ts | 10 ++-- src/types/api/uniformity/uniformity.d.ts | 6 +-- 7 files changed, 46 insertions(+), 46 deletions(-) diff --git a/src/components/pages/uniformity/detail/UniformityDetail.tsx b/src/components/pages/uniformity/detail/UniformityDetail.tsx index 8195fa71..2528d2ab 100644 --- a/src/components/pages/uniformity/detail/UniformityDetail.tsx +++ b/src/components/pages/uniformity/detail/UniformityDetail.tsx @@ -90,8 +90,8 @@ const UniformityDetail: React.FC = ({ label: 'Kandang', }, { - id: 'documents-name', - value: 'documents-name', + id: 'document-name', + value: 'document-name', label: 'File Uniformity', }, { @@ -123,7 +123,7 @@ const UniformityDetail: React.FC = ({ 'lokasi-farm': info_umum.lokasi_farm, 'project-flock': info_umum.project_flock, kandang: info_umum.kandang, - 'documents-name': info_umum.documents_name, + 'document-name': info_umum.document_name, 'approval-status': statusValue, }; @@ -148,7 +148,7 @@ const UniformityDetail: React.FC = ({ return -; } - if (id === 'documents-name') { + if (id === 'document-name') { return (
{valueMap[id]} diff --git a/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx b/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx index 2c83ba00..86ba731c 100644 --- a/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx +++ b/src/components/pages/uniformity/detail/UniformityDetailsPreview.tsx @@ -214,7 +214,7 @@ const UniformityDetailsPreview = ({ {/* Header */} diff --git a/src/components/pages/uniformity/form/UniformityForm.schema.ts b/src/components/pages/uniformity/form/UniformityForm.schema.ts index 5463557a..0ff88188 100644 --- a/src/components/pages/uniformity/form/UniformityForm.schema.ts +++ b/src/components/pages/uniformity/form/UniformityForm.schema.ts @@ -20,16 +20,16 @@ type UniformityFormSchemaType = { label: string; } | null; kandang_id: number; - documents: File | undefined; + document: File | undefined; }; const FileSchema = Yup.mixed() - .test('documentsSize', 'Ukuran file maksimal 2 MB', (value): boolean => { + .test('documentSize', 'Ukuran file maksimal 2 MB', (value): boolean => { if (!value) return true; if (value instanceof File) return value.size <= 2 * 1024 * 1024; return false; }) - .test('documentsType', 'Format file harus Excel', (value): boolean => { + .test('documentType', 'Format file harus Excel', (value): boolean => { if (!value) return true; if (value instanceof File) { const allowedTypes = [ @@ -74,7 +74,7 @@ export const UniformityFormSchema: Yup.ObjectSchema = .min(1, 'Kandang wajib diisi!') .required('Kandang wajib diisi!') .typeError('Kandang wajib diisi!'), - documents: FileSchema.required('File wajib diisi!'), + document: FileSchema.required('File wajib diisi!'), }); export type UniformityFormValues = Yup.InferType; @@ -83,8 +83,8 @@ export type UniformityFormData = { date: string; week: number; project_flock_kandang_id: number; - documents: File | null; - documents_name: string; + document: File | null; + document_name: string; }; export const getUniformityFormInitialValues = ( @@ -115,6 +115,6 @@ export const getUniformityFormInitialValues = ( } : null, kandang_id: initialValues?.kandang?.id ?? 0, - documents: undefined, + document: undefined, }; }; diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx index 48a84c70..f68b8f85 100644 --- a/src/components/pages/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -246,12 +246,12 @@ const UniformityForm = ({ date: values.date, week: values.week, project_flock_kandang_id: projectFlockKandangId, - documents: values.documents as File, - documents_name: (values.documents as File).name, + document: values.document as File, + document_name: (values.document as File).name, }); const payload: VerifyUniformityPayload = { - documents: values.documents as File, + document: values.document as File, }; const res = await UniformityApi.verifyUniformity(payload); @@ -325,17 +325,17 @@ const UniformityForm = ({ const handleFileChange = useCallback( (e: React.ChangeEvent) => { - const documents = e.target.files?.[0]; + const document = e.target.files?.[0]; - formik.setFieldTouched('documents', true); + formik.setFieldTouched('document', true); - if (!documents) { - formik.setFieldValue('documents', undefined); + if (!document) { + formik.setFieldValue('document', undefined); return; } - if (documents.size > 2 * 1024 * 1024) { - toast.error(`Ukuran file ${documents.name} maksimal 2 MB!`); + if (document.size > 2 * 1024 * 1024) { + toast.error(`Ukuran file ${document.name} maksimal 2 MB!`); return; } @@ -345,12 +345,12 @@ const UniformityForm = ({ 'text/csv', ]; - if (!allowedTypes.includes(documents.type)) { - toast.error(`Format file ${documents.name} harus Excel atau CSV!`); + if (!allowedTypes.includes(document.type)) { + toast.error(`Format file ${document.name} harus Excel atau CSV!`); return; } - formik.setFieldValue('documents', documents); + formik.setFieldValue('document', document); }, [] ); @@ -363,7 +363,7 @@ const UniformityForm = ({ ); const handleRemoveFile = useCallback(() => { - formik.setFieldValue('documents', undefined); + formik.setFieldValue('document', undefined); if (fileInputRef.current) { fileInputRef.current.value = ''; } @@ -528,14 +528,14 @@ const UniformityForm = ({ htmlFor='file-upload-input' className={cn( "w-full text-sm font-normal leading-5 after:content-['*'] after:ml-0.5 after:text-red-500", - formik.touched.documents && - formik.errors.documents && + formik.touched.document && + formik.errors.document && 'text-red-500' )} > Upload File - {formik.values.documents && !isNextStep ? ( + {formik.values.document && !isNextStep ? ( - ) : !formik.values.documents && !isNextStep ? ( + ) : !formik.values.document && !isNextStep ? (
- {formik.values.documents.name} + {formik.values.document.name}
) : ( @@ -667,15 +667,15 @@ const UniformityForm = ({ ref={fileInputRef} type='file' id='file-upload-input' - name='documents' + name='document' accept='application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/csv' onChange={handleFileChange} className='hidden' /> - {formik.touched.documents && formik.errors.documents && ( + {formik.touched.document && formik.errors.document && (

- {formik.errors.documents as string} + {formik.errors.document as string}

)}
diff --git a/src/components/pages/uniformity/form/UniformityResultForm.tsx b/src/components/pages/uniformity/form/UniformityResultForm.tsx index 776e8dbc..ad2402ea 100644 --- a/src/components/pages/uniformity/form/UniformityResultForm.tsx +++ b/src/components/pages/uniformity/form/UniformityResultForm.tsx @@ -51,7 +51,7 @@ const UniformityResultForm = () => { }; const handleSubmit = async () => { - if (!uniformityFormData || !uniformityFormData.documents) { + if (!uniformityFormData || !uniformityFormData.document) { toast.error('Form data is missing. Please try again.'); return; } @@ -63,7 +63,7 @@ const UniformityResultForm = () => { date: uniformityFormData.date, week: uniformityFormData.week, project_flock_kandang_id: uniformityFormData.project_flock_kandang_id, - documents: uniformityFormData.documents, + document: uniformityFormData.document, }; const res = await UniformityApi.createUniformity(payload); @@ -236,7 +236,7 @@ const UniformityResultForm = () => { {/* Header */} diff --git a/src/services/api/uniformity.ts b/src/services/api/uniformity.ts index 1f048dc6..1ba9771f 100644 --- a/src/services/api/uniformity.ts +++ b/src/services/api/uniformity.ts @@ -62,8 +62,8 @@ export class UniformityApiService extends BaseApiService< payload.project_flock_kandang_id.toString() ); - if (payload.documents) { - formData.append('documents', payload.documents); + if (payload.document) { + formData.append('document', payload.document); } return await this.create(formData as unknown as CreateUniformityPayload); @@ -73,8 +73,8 @@ export class UniformityApiService extends BaseApiService< payload: VerifyUniformityPayload ): Promise | undefined> { const formData = new FormData(); - if (payload.documents) { - formData.append('documents', payload.documents); + if (payload.document) { + formData.append('document', payload.document); } return await this.customRequest>( @@ -130,5 +130,5 @@ export class UniformityApiService extends BaseApiService< } export const UniformityApi = new UniformityApiService( - 'http://localhost:4010/api/production/uniformities' + 'production/uniformities' ); diff --git a/src/types/api/uniformity/uniformity.d.ts b/src/types/api/uniformity/uniformity.d.ts index 45291a28..c63e90ff 100644 --- a/src/types/api/uniformity/uniformity.d.ts +++ b/src/types/api/uniformity/uniformity.d.ts @@ -34,7 +34,7 @@ export type UniformityInfoUmum = { lokasi_farm: string; project_flock: string; kandang: string; - documents_name: string; + document_name: string; }; export type UniformitySampling = { @@ -77,12 +77,12 @@ export type VerifyUniformityResponse = { export type CreateUniformityPayload = { date: string; project_flock_kandang_id: number; - documents: File; + document: File; week: number; }; export type VerifyUniformityPayload = { - documents: File; + document: File; }; // ==================== OTHER TYPES ==================== From 7c0581728edbb5f11a7d15e55cea0d0c0dd12f4d Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 30 Dec 2025 14:41:06 +0700 Subject: [PATCH 115/124] feat(FE-316): Use population instead of available_quantity --- .../pages/uniformity/export/UniformityTemplate.tsx | 8 ++++---- .../pages/uniformity/form/UniformityForm.tsx | 12 ++++++------ src/types/api/production/project-flock.d.ts | 1 + 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/components/pages/uniformity/export/UniformityTemplate.tsx b/src/components/pages/uniformity/export/UniformityTemplate.tsx index 820452b0..b3cab9a9 100644 --- a/src/components/pages/uniformity/export/UniformityTemplate.tsx +++ b/src/components/pages/uniformity/export/UniformityTemplate.tsx @@ -4,11 +4,11 @@ import * as XLSX from 'xlsx'; import { ProjectFlockKandangLookup } from '@/types/api/production/project-flock'; export const generateUniformityTemplate = ( - availableQuantity: number, + population: number, projectFlockKandangLookup: ProjectFlockKandangLookup ) => { try { - const sampleSize = Math.round(availableQuantity * 0.02); + const sampleSize = Math.round(population * 0.02); const kandangName = projectFlockKandangLookup.kandang?.name || 'Kandang'; const flockName = projectFlockKandangLookup.project_flock?.flock_name || ''; const flockPeriod = projectFlockKandangLookup.project_flock?.period || 1; @@ -23,7 +23,7 @@ export const generateUniformityTemplate = ( ['Nama Flock', flockName], ['Periode', flockPeriod], ['Kandang', kandangName], - ['Total Populasi', formatNumber(availableQuantity)], + ['Total Populasi', formatNumber(population)], ['Jumlah Sampel (2%)', formatNumber(sampleSize)], [''], ['CARA PENGISIAN:'], @@ -95,7 +95,7 @@ export const generateUniformityTemplate = ( XLSX.writeFile(workbook, filename); toast.success( - `Template berhasil dibuat dengan ${formatNumber(sampleSize)} baris data (2% dari ${formatNumber(availableQuantity)} populasi).` + `Template berhasil dibuat dengan ${formatNumber(sampleSize)} baris data (2% dari ${formatNumber(population)} populasi).` ); } catch (error) { console.error('Error generating uniformity template:', error); diff --git a/src/components/pages/uniformity/form/UniformityForm.tsx b/src/components/pages/uniformity/form/UniformityForm.tsx index f68b8f85..25c56a0f 100644 --- a/src/components/pages/uniformity/form/UniformityForm.tsx +++ b/src/components/pages/uniformity/form/UniformityForm.tsx @@ -370,14 +370,14 @@ const UniformityForm = ({ }, [formik]); const handleDownloadTemplate = useCallback(() => { - const availableQuantity = projectFlockKandangLookup?.available_quantity; + const population = projectFlockKandangLookup?.population; - if (!availableQuantity || !projectFlockKandangLookup) { + if (!population || !projectFlockKandangLookup) { toast.error('Silakan pilih Project Flock dan Kandang terlebih dahulu.'); return; } - generateUniformityTemplate(availableQuantity, projectFlockKandangLookup); + generateUniformityTemplate(population, projectFlockKandangLookup); }, [projectFlockKandangLookup]); // ===== SIDE EFFECTS ===== @@ -623,13 +623,13 @@ const UniformityForm = ({ Choose file to upload - {projectFlockKandangLookup?.available_quantity - ? `Jumlah data yang dibutuhkan: ${formatNumber(Math.round(projectFlockKandangLookup.available_quantity * 0.02))} (2% dari ${formatNumber(projectFlockKandangLookup.available_quantity)} populasi).` + {projectFlockKandangLookup?.population + ? `Jumlah data yang dibutuhkan: ${formatNumber(Math.round(projectFlockKandangLookup.population * 0.02))} (2% dari ${formatNumber(projectFlockKandangLookup.population)} populasi).` : 'Upload data file (*.xlsx)'}
- {projectFlockKandangLookup?.available_quantity && ( + {projectFlockKandangLookup?.population && ( <>
diff --git a/src/types/api/production/project-flock.d.ts b/src/types/api/production/project-flock.d.ts index 9f128093..43465a3f 100644 --- a/src/types/api/production/project-flock.d.ts +++ b/src/types/api/production/project-flock.d.ts @@ -69,6 +69,7 @@ export type ProjectFlockKandangLookup = { project_flock: ProjectFlock; quantity: number; available_quantity?: number; + population: number; }; export type ProjectFlockAvailableQuantity = { From 4e7b91a7b47dd190db7222a8096c0b704c842f39 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 30 Dec 2025 16:07:35 +0700 Subject: [PATCH 116/124] refactor(FE-316): Add commented local API URL to UniformityApi --- src/services/api/uniformity.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/api/uniformity.ts b/src/services/api/uniformity.ts index 1ba9771f..57c19436 100644 --- a/src/services/api/uniformity.ts +++ b/src/services/api/uniformity.ts @@ -131,4 +131,5 @@ export class UniformityApiService extends BaseApiService< export const UniformityApi = new UniformityApiService( 'production/uniformities' + // 'http://localhost:4010/api/production/uniformities' ); From ed7ee1a268b9f55a3d9427231d8d4c1e1a3d8a34 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Tue, 30 Dec 2025 16:42:19 +0700 Subject: [PATCH 117/124] refactor(FE-316): Refine Filter Modal layout and styles --- .../pages/uniformity/UniformityTable.tsx | 49 +++++++++---------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/src/components/pages/uniformity/UniformityTable.tsx b/src/components/pages/uniformity/UniformityTable.tsx index 37b04785..d7c8d83a 100644 --- a/src/components/pages/uniformity/UniformityTable.tsx +++ b/src/components/pages/uniformity/UniformityTable.tsx @@ -1018,30 +1018,27 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { {/* Filter Modal */} -
-
-

- - Filter Data -

- + +
- -
- -
+
void }) => { />
-
- -
+ {/* Action Buttons */} +
@@ -478,7 +495,7 @@ const PurchaseOrderAcceptApprovalForm = ({ }} /> -
+ Dokumen Surat Jalan * +
-
+
+
+ { + const files = Array.from(e.target.files || []); + formik.setFieldValue('travel_documents', files); + }} + onBlur={formik.handleBlur} + bottomLabel={ + formik.values.travel_documents && + formik.values.travel_documents.length > 0 + ? `${formik.values.travel_documents.length} file(s) dipilih` + : undefined + } + isError={ + formik.touched.travel_documents && + Boolean(formik.errors.travel_documents) + } + errorMessage={formik.errors.travel_documents as string} + /> +
+ {/* Action buttons */}
diff --git a/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts b/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts index c7da956d..bb70053f 100644 --- a/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts +++ b/src/components/pages/purchase/form/order/PurchaseOrderForm.schema.ts @@ -48,6 +48,7 @@ type PurchaseRequestAcceptApprovalFormSchemaType = { received_qty: number | string; transport_per_item: number | string; }[]; + travel_documents: File[]; }; export type PurchaseStaffApprovalItemSchema = { @@ -379,6 +380,11 @@ export const PurchaseRequestAcceptApprovalFormSchema: Yup.ObjectSchema().required()) + .required('Dokumen surat jalan wajib diupload!') + .min(1, 'Minimal upload 1 dokumen surat jalan!') + .typeError('Dokumen surat jalan wajib diupload!'), }); export const PurchaseRequestAcceptApprovalFormInitialValues: PurchaseRequestAcceptApprovalFormSchemaType = @@ -397,6 +403,7 @@ export const PurchaseRequestAcceptApprovalFormInitialValues: PurchaseRequestAcce transport_per_item: '', }, ], + travel_documents: [], }; export const PurchaseRequestAcceptApprovalFormDefaultValues = ( @@ -428,6 +435,7 @@ export const PurchaseRequestAcceptApprovalFormDefaultValues = ( transport_per_item: '', }, ], + travel_documents: [], }; }; diff --git a/src/services/api/purchase.ts b/src/services/api/purchase.ts index d0438e88..38ace6be 100644 --- a/src/services/api/purchase.ts +++ b/src/services/api/purchase.ts @@ -72,11 +72,29 @@ export const PurchaseApi = { purchaseRequestId: number, payload: CreateAcceptApprovalRequestPayload ): Promise | undefined> => { + const formData = new FormData(); + + formData.append('action', payload.action); + + if (payload.notes) { + formData.append('notes', payload.notes); + } + + if (payload.items) { + formData.append('items', JSON.stringify(payload.items)); + } + + if (payload.travel_documents) { + payload.travel_documents.forEach((file) => { + formData.append('travel_documents', file); + }); + } + return await basePurchaseApi.customRequest< BaseApiResponse<{ message: string }> >(`${purchaseRequestId}/receipts`, { method: 'POST', - payload, + payload: formData as unknown as Record, }); }, }, diff --git a/src/types/api/purchase/purchase.d.ts b/src/types/api/purchase/purchase.d.ts index e4de565b..7b698b74 100644 --- a/src/types/api/purchase/purchase.d.ts +++ b/src/types/api/purchase/purchase.d.ts @@ -118,6 +118,7 @@ export type CreateAcceptApprovalRequestPayload = { received_qty: number; transport_per_item: number; }[]; + travel_documents?: File[]; }; export type DeletePurchaseRequestItemPayload = { From a1e8f582badfb4432dfa10d6fa66e7d7b73f5219 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 31 Dec 2025 11:17:26 +0700 Subject: [PATCH 123/124] refactor(FE-316,317,438): Move Uniformity feature under production namespace --- .../{ => production}/uniformity/add/page.tsx | 2 +- .../uniformity/detail/page.tsx | 2 +- .../{ => production}/uniformity/layout.tsx | 2 +- src/app/{ => production}/uniformity/page.tsx | 2 +- .../uniformity/UniformityChart.tsx | 8 ++-- .../uniformity/UniformityPageWrapper.tsx | 8 ++-- .../uniformity/UniformityTable.tsx | 44 ++++++++----------- .../uniformity/chart/UniformityBarChart.tsx | 0 .../uniformity/chart/UniformityGaugeChart.tsx | 0 .../uniformity/chart/UniformityStat.tsx | 2 +- .../uniformity/detail/UniformityDetail.tsx | 14 +++--- .../detail/UniformityDetailsPreview.tsx | 8 ++-- .../export/UniformityExportExcel.tsx | 2 +- .../uniformity/export/UniformityExportPDF.tsx | 2 +- .../uniformity/export/UniformityTemplate.tsx | 0 .../uniformity/form/UniformityForm.schema.ts | 2 +- .../uniformity/form/UniformityForm.tsx | 16 ++++--- .../uniformity/form/UniformityPreviewForm.tsx | 2 +- .../uniformity/form/UniformityResultForm.tsx | 8 ++-- .../skeleton/UniformityBarChartSkeleton.tsx | 0 .../skeleton/UniformityGaugeChartSkeleton.tsx | 0 .../skeleton/UniformityTableSkeleton.tsx | 0 .../uniformity/uniformity-utils.ts | 0 src/config/constant.ts | 11 +++-- src/config/route-permission.ts | 8 ++-- src/services/api/uniformity.ts | 2 +- .../uniformity.d.ts | 0 src/types/stores.d.ts | 2 +- 28 files changed, 71 insertions(+), 76 deletions(-) rename src/app/{ => production}/uniformity/add/page.tsx (54%) rename src/app/{ => production}/uniformity/detail/page.tsx (93%) rename src/app/{ => production}/uniformity/layout.tsx (65%) rename src/app/{ => production}/uniformity/page.tsx (50%) rename src/components/pages/{ => production}/uniformity/UniformityChart.tsx (86%) rename src/components/pages/{ => production}/uniformity/UniformityPageWrapper.tsx (86%) rename src/components/pages/{ => production}/uniformity/UniformityTable.tsx (96%) rename src/components/pages/{ => production}/uniformity/chart/UniformityBarChart.tsx (100%) rename src/components/pages/{ => production}/uniformity/chart/UniformityGaugeChart.tsx (100%) rename src/components/pages/{ => production}/uniformity/chart/UniformityStat.tsx (98%) rename src/components/pages/{ => production}/uniformity/detail/UniformityDetail.tsx (93%) rename src/components/pages/{ => production}/uniformity/detail/UniformityDetailsPreview.tsx (97%) rename src/components/pages/{ => production}/uniformity/export/UniformityExportExcel.tsx (97%) rename src/components/pages/{ => production}/uniformity/export/UniformityExportPDF.tsx (99%) rename src/components/pages/{ => production}/uniformity/export/UniformityTemplate.tsx (100%) rename src/components/pages/{ => production}/uniformity/form/UniformityForm.schema.ts (97%) rename src/components/pages/{ => production}/uniformity/form/UniformityForm.tsx (97%) rename src/components/pages/{ => production}/uniformity/form/UniformityPreviewForm.tsx (98%) rename src/components/pages/{ => production}/uniformity/form/UniformityResultForm.tsx (97%) rename src/components/pages/{ => production}/uniformity/skeleton/UniformityBarChartSkeleton.tsx (100%) rename src/components/pages/{ => production}/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx (100%) rename src/components/pages/{ => production}/uniformity/skeleton/UniformityTableSkeleton.tsx (100%) rename src/components/pages/{ => production}/uniformity/uniformity-utils.ts (100%) rename src/types/api/{uniformity => production}/uniformity.d.ts (100%) diff --git a/src/app/uniformity/add/page.tsx b/src/app/production/uniformity/add/page.tsx similarity index 54% rename from src/app/uniformity/add/page.tsx rename to src/app/production/uniformity/add/page.tsx index 7c12cc72..136aab5d 100644 --- a/src/app/uniformity/add/page.tsx +++ b/src/app/production/uniformity/add/page.tsx @@ -1,4 +1,4 @@ -import UniformityForm from '@/components/pages/uniformity/form/UniformityForm'; +import UniformityForm from '@/components/pages/production/uniformity/form/UniformityForm'; const AddUniformity = () => { return ; diff --git a/src/app/uniformity/detail/page.tsx b/src/app/production/uniformity/detail/page.tsx similarity index 93% rename from src/app/uniformity/detail/page.tsx rename to src/app/production/uniformity/detail/page.tsx index e66d0a40..bf1458ef 100644 --- a/src/app/uniformity/detail/page.tsx +++ b/src/app/production/uniformity/detail/page.tsx @@ -1,6 +1,6 @@ 'use client'; -import UniformityDetail from '@/components/pages/uniformity/detail/UniformityDetail'; +import UniformityDetail from '@/components/pages/production/uniformity/detail/UniformityDetail'; import { isResponseError, isResponseSuccess } from '@/lib/api-helper'; import { UniformityApi } from '@/services/api/uniformity'; import { useRouter, useSearchParams } from 'next/navigation'; diff --git a/src/app/uniformity/layout.tsx b/src/app/production/uniformity/layout.tsx similarity index 65% rename from src/app/uniformity/layout.tsx rename to src/app/production/uniformity/layout.tsx index dd239577..511aa0a1 100644 --- a/src/app/uniformity/layout.tsx +++ b/src/app/production/uniformity/layout.tsx @@ -1,5 +1,5 @@ import { ReactNode } from 'react'; -import UniformityPageWrapper from '@/components/pages/uniformity/UniformityPageWrapper'; +import UniformityPageWrapper from '@/components/pages/production/uniformity/UniformityPageWrapper'; export default function UniformityLayout({ children, diff --git a/src/app/uniformity/page.tsx b/src/app/production/uniformity/page.tsx similarity index 50% rename from src/app/uniformity/page.tsx rename to src/app/production/uniformity/page.tsx index 24a31482..841a7507 100644 --- a/src/app/uniformity/page.tsx +++ b/src/app/production/uniformity/page.tsx @@ -1,4 +1,4 @@ -import UniformityTable from '@/components/pages/uniformity/UniformityTable'; +import UniformityTable from '@/components/pages/production/uniformity/UniformityTable'; const Uniformity = () => { return ; diff --git a/src/components/pages/uniformity/UniformityChart.tsx b/src/components/pages/production/uniformity/UniformityChart.tsx similarity index 86% rename from src/components/pages/uniformity/UniformityChart.tsx rename to src/components/pages/production/uniformity/UniformityChart.tsx index 12587202..1b58b16c 100644 --- a/src/components/pages/uniformity/UniformityChart.tsx +++ b/src/components/pages/production/uniformity/UniformityChart.tsx @@ -1,9 +1,9 @@ import React from 'react'; import Card from '@/components/Card'; -import UniformityBarChart from '@/components/pages/uniformity/chart/UniformityBarChart'; -import UniformityGaugeChart from '@/components/pages/uniformity/chart/UniformityGaugeChart'; -import UniformityBarChartSkeleton from '@/components/pages/uniformity/skeleton/UniformityBarChartSkeleton'; -import UniformityGaugeChartSkeleton from '@/components/pages/uniformity/skeleton/UniformityGaugeChartSkeleton'; +import UniformityBarChart from '@/components/pages/production/uniformity/chart/UniformityBarChart'; +import UniformityGaugeChart from '@/components/pages/production/uniformity/chart/UniformityGaugeChart'; +import UniformityBarChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityBarChartSkeleton'; +import UniformityGaugeChartSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton'; interface BarChartData { name: string; diff --git a/src/components/pages/uniformity/UniformityPageWrapper.tsx b/src/components/pages/production/uniformity/UniformityPageWrapper.tsx similarity index 86% rename from src/components/pages/uniformity/UniformityPageWrapper.tsx rename to src/components/pages/production/uniformity/UniformityPageWrapper.tsx index cbdc656b..bbc82665 100644 --- a/src/components/pages/uniformity/UniformityPageWrapper.tsx +++ b/src/components/pages/production/uniformity/UniformityPageWrapper.tsx @@ -3,7 +3,7 @@ import { usePathname, useRouter } from 'next/navigation'; import Drawer from '@/components/Drawer'; import React, { ReactNode } from 'react'; -import UniformityTable from '@/components/pages/uniformity/UniformityTable'; +import UniformityTable from '@/components/pages/production/uniformity/UniformityTable'; import { useUiStore } from '@/stores/ui/ui.store'; export default function UniformityPageWrapper({ @@ -27,7 +27,7 @@ export default function UniformityPageWrapper({ const handleBackdropClick = () => { const unsub = useUiStore.getState().subscribeIsValid((isValid) => { if (isValid) { - router.push('/uniformity'); + router.push('/production/uniformity'); unsub?.(); setExpandedDrawerOpen(false); } else { @@ -42,7 +42,7 @@ export default function UniformityPageWrapper({ <>
!isOpen && router.push('/uniformity')} + refresh={() => !isOpen && router.push('/production/uniformity')} />
@@ -50,7 +50,7 @@ export default function UniformityPageWrapper({ open={isOpen} setOpen={(v) => { if (!v) { - router.push('/uniformity'); + router.push('/production/uniformity'); setExpandedDrawerOpen(false); } }} diff --git a/src/components/pages/uniformity/UniformityTable.tsx b/src/components/pages/production/uniformity/UniformityTable.tsx similarity index 96% rename from src/components/pages/uniformity/UniformityTable.tsx rename to src/components/pages/production/uniformity/UniformityTable.tsx index be9fa1b0..cf981732 100644 --- a/src/components/pages/uniformity/UniformityTable.tsx +++ b/src/components/pages/production/uniformity/UniformityTable.tsx @@ -7,10 +7,10 @@ import { Icon } from '@iconify/react'; import { ColumnDef, SortingState } from '@tanstack/react-table'; import { cn, formatDate } from '@/lib/helper'; import Button from '@/components/Button'; -import UniformityChart from '@/components/pages/uniformity/UniformityChart'; +import UniformityChart from '@/components/pages/production/uniformity/UniformityChart'; import { useTableFilter } from '@/services/hooks/useTableFilter'; import { UniformityApi } from '@/services/api/uniformity'; -import { type Uniformity } from '@/types/api/uniformity/uniformity'; +import { type Uniformity } from '@/types/api/production/uniformity'; import { isResponseSuccess } from '@/lib/api-helper'; import { type BaseApiResponse } from '@/types/api/api-general'; import Table from '@/components/Table'; @@ -20,7 +20,7 @@ import { useModal } from '@/components/Modal'; import ConfirmationModal from '@/components/modal/ConfirmationModal'; import toast from 'react-hot-toast'; import Card from '@/components/Card'; -import UniformityTableSkeleton from '@/components/pages/uniformity/skeleton/UniformityTableSkeleton'; +import UniformityTableSkeleton from '@/components/pages/production/uniformity/skeleton/UniformityTableSkeleton'; import RequirePermission from '@/components/helper/RequirePermission'; import { useUniformityStore } from '@/stores/uniformity/uniformity.store'; import FloatingActionsButton from '@/components/FloatingActionsButton'; @@ -38,20 +38,20 @@ import { getStatusColor, getStatusIndicatorColor, getStatusText, -} from '@/components/pages/uniformity/uniformity-utils'; -import { generateUniformityPDF } from '@/components/pages/uniformity/export/UniformityExportPDF'; -import { generateUniformityExcel } from '@/components/pages/uniformity/export/UniformityExportExcel'; +} from '@/components/pages/production/uniformity/uniformity-utils'; +import { generateUniformityPDF } from '@/components/pages/production/uniformity/export/UniformityExportPDF'; +import { generateUniformityExcel } from '@/components/pages/production/uniformity/export/UniformityExportExcel'; import Dropdown from '@/components/Dropdown'; import Menu from '@/components/menu/Menu'; import MenuItem from '@/components/menu/MenuItem'; const isUniformityLocked = (uniformity: Uniformity): boolean => { - return ( - uniformity.status === 'Disetujui' || - uniformity.status === 'Ditolak' || - uniformity.status === 'APPROVED' || - uniformity.status === 'REJECTED' - ); + // Uniformity data is never locked - checkbox is always enabled + return false; +}; + +const canApproveRejectUniformity = (uniformity: Uniformity): boolean => { + return uniformity.status === 'CREATED' || uniformity.status === 'Pengajuan'; }; interface UniformityPreviewData { @@ -430,7 +430,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { singleRejectModal.openModal(); } - router.replace('/uniformity', { scroll: false }); + router.replace('/production/uniformity', { scroll: false }); } } } @@ -671,7 +671,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { const uniformity = uniformities.data.find( (r) => r.id === parseInt(rowId) ); - if (uniformity && !isUniformityLocked(uniformity)) { + if (uniformity) { newSelection[rowId] = true; } } @@ -692,10 +692,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { id: 'select', header: ({ table }) => { const allRows = table.getRowModel().rows; - const selectableRows = allRows.filter((row) => { - const uniformity = row.original; - return !isUniformityLocked(uniformity); - }); + const selectableRows = allRows; const hasNoSelectableRows = selectableRows.length === 0; @@ -730,17 +727,14 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { ); }, cell: ({ row }) => { - const uniformity = row.original; - const isDisabled = isUniformityLocked(uniformity); - return ( -
+
); @@ -805,7 +799,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => {
- @@ -1196,7 +1190,7 @@ const UniformityTable = ({ refresh }: { refresh?: () => void }) => { hidden: selectedRowIds.length !== 1, onClick() { router.push( - `/uniformity/detail?uniformityId=${selectedRowIds[0]}` + `/production/uniformity/detail?uniformityId=${selectedRowIds[0]}` ); setRowSelection({}); }, diff --git a/src/components/pages/uniformity/chart/UniformityBarChart.tsx b/src/components/pages/production/uniformity/chart/UniformityBarChart.tsx similarity index 100% rename from src/components/pages/uniformity/chart/UniformityBarChart.tsx rename to src/components/pages/production/uniformity/chart/UniformityBarChart.tsx diff --git a/src/components/pages/uniformity/chart/UniformityGaugeChart.tsx b/src/components/pages/production/uniformity/chart/UniformityGaugeChart.tsx similarity index 100% rename from src/components/pages/uniformity/chart/UniformityGaugeChart.tsx rename to src/components/pages/production/uniformity/chart/UniformityGaugeChart.tsx diff --git a/src/components/pages/uniformity/chart/UniformityStat.tsx b/src/components/pages/production/uniformity/chart/UniformityStat.tsx similarity index 98% rename from src/components/pages/uniformity/chart/UniformityStat.tsx rename to src/components/pages/production/uniformity/chart/UniformityStat.tsx index e7603e16..ea8a5c0e 100644 --- a/src/components/pages/uniformity/chart/UniformityStat.tsx +++ b/src/components/pages/production/uniformity/chart/UniformityStat.tsx @@ -1,4 +1,4 @@ -import Badge from '@/components/Badge'; +import Badge from '../../../../Badge'; import Card from '@/components/Card'; import { Icon } from '@iconify/react'; import { formatNumber } from '@/lib/helper'; diff --git a/src/components/pages/uniformity/detail/UniformityDetail.tsx b/src/components/pages/production/uniformity/detail/UniformityDetail.tsx similarity index 93% rename from src/components/pages/uniformity/detail/UniformityDetail.tsx rename to src/components/pages/production/uniformity/detail/UniformityDetail.tsx index fa1f6a5b..0cc39d9a 100644 --- a/src/components/pages/uniformity/detail/UniformityDetail.tsx +++ b/src/components/pages/production/uniformity/detail/UniformityDetail.tsx @@ -10,16 +10,16 @@ import Table from '@/components/Table'; import Badge from '@/components/Badge'; import Tooltip from '@/components/Tooltip'; import RequirePermission from '@/components/helper/RequirePermission'; -import { UniformityDetail as UniformityDetailType } from '@/types/api/uniformity/uniformity'; +import { UniformityDetail as UniformityDetailType } from '@/types/api/production/uniformity'; import { formatDate } from '@/lib/helper'; import { useUiStore } from '@/stores/ui/ui.store'; -import UniformityDetailsPreview from '@/components/pages/uniformity/detail/UniformityDetailsPreview'; +import UniformityDetailsPreview from '@/components/pages/production/uniformity/detail/UniformityDetailsPreview'; import { getStatusColor, getStatusIndicatorColor, getStatusText, -} from '@/components/pages/uniformity/uniformity-utils'; -import { DetailOptionType } from '@/types/api/uniformity/uniformity'; +} from '@/components/pages/production/uniformity/uniformity-utils'; +import { DetailOptionType } from '@/types/api/production/uniformity'; interface UniformityDetailProps { initialValues: UniformityDetailType; @@ -35,11 +35,11 @@ const UniformityDetail: React.FC = ({ ); const handleApprove = () => { - router.push(`/uniformity?action=approve&id=${initialValues.id}`); + router.push(`/production/uniformity?action=approve&id=${initialValues.id}`); }; const handleReject = () => { - router.push(`/uniformity?action=reject&id=${initialValues.id}`); + router.push(`/production/uniformity?action=reject&id=${initialValues.id}`); }; const handleViewUniformityDetails = () => { @@ -175,7 +175,7 @@ const UniformityDetail: React.FC = ({
{/* Header */} { const setExpandedDrawerOpen = useUiStore((s) => s.setExpandedDrawerOpen); diff --git a/src/components/pages/uniformity/form/UniformityResultForm.tsx b/src/components/pages/production/uniformity/form/UniformityResultForm.tsx similarity index 97% rename from src/components/pages/uniformity/form/UniformityResultForm.tsx rename to src/components/pages/production/uniformity/form/UniformityResultForm.tsx index 446e0a91..df144c64 100644 --- a/src/components/pages/uniformity/form/UniformityResultForm.tsx +++ b/src/components/pages/production/uniformity/form/UniformityResultForm.tsx @@ -20,12 +20,12 @@ import { getWeightStatusColor, getWeightStatusIndicatorColor, getWeightStatusText, -} from '@/components/pages/uniformity/uniformity-utils'; -import { DetailOptionType } from '@/types/api/uniformity/uniformity'; +} from '@/components/pages/production/uniformity/uniformity-utils'; +import { DetailOptionType } from '@/types/api/production/uniformity'; import { BodyWeightData, UniformityDetailItem, -} from '@/types/api/uniformity/uniformity'; +} from '@/types/api/production/uniformity'; const UniformityResultForm = () => { const router = useRouter(); @@ -82,7 +82,7 @@ const UniformityResultForm = () => { setIsNextStep(false); setUniformityStep('preview'); setVerifyUniformityResult(null); - router.push('/uniformity'); + router.push('/production/uniformity'); } finally { setIsSubmitting(false); } diff --git a/src/components/pages/uniformity/skeleton/UniformityBarChartSkeleton.tsx b/src/components/pages/production/uniformity/skeleton/UniformityBarChartSkeleton.tsx similarity index 100% rename from src/components/pages/uniformity/skeleton/UniformityBarChartSkeleton.tsx rename to src/components/pages/production/uniformity/skeleton/UniformityBarChartSkeleton.tsx diff --git a/src/components/pages/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx b/src/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx similarity index 100% rename from src/components/pages/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx rename to src/components/pages/production/uniformity/skeleton/UniformityGaugeChartSkeleton.tsx diff --git a/src/components/pages/uniformity/skeleton/UniformityTableSkeleton.tsx b/src/components/pages/production/uniformity/skeleton/UniformityTableSkeleton.tsx similarity index 100% rename from src/components/pages/uniformity/skeleton/UniformityTableSkeleton.tsx rename to src/components/pages/production/uniformity/skeleton/UniformityTableSkeleton.tsx diff --git a/src/components/pages/uniformity/uniformity-utils.ts b/src/components/pages/production/uniformity/uniformity-utils.ts similarity index 100% rename from src/components/pages/uniformity/uniformity-utils.ts rename to src/components/pages/production/uniformity/uniformity-utils.ts diff --git a/src/config/constant.ts b/src/config/constant.ts index f177b394..839bcd83 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -29,6 +29,11 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ text: 'Transfer to Laying', link: '/production/transfer-to-laying', }, + { + text: 'Uniformity', + link: '/production/uniformity', + permission: ['lti.production.uniformity.list'], + }, ], }, { @@ -42,12 +47,6 @@ export const MAIN_DRAWER_LINKS: SidebarMenuItem[] = [ link: '/marketing', icon: 'heroicons-outline:currency-dollar', }, - { - text: 'Uniformity', - link: '/uniformity', - icon: 'heroicons-outline:scale', - permission: ['lti.production.uniformity.list'], - }, { text: 'Keuangan', link: '/finance', diff --git a/src/config/route-permission.ts b/src/config/route-permission.ts index 47013aba..0a17175d 100644 --- a/src/config/route-permission.ts +++ b/src/config/route-permission.ts @@ -46,10 +46,10 @@ export const ROUTE_PERMISSIONS: Record = { ], // Production - Uniformity - '/uniformity/': ['lti.production.uniformity.list'], - '/uniformity/add/': ['lti.production.uniformity.create'], - '/uniformity/detail/': ['lti.production.uniformity.detail'], - '/uniformity/detail/edit/': ['lti.production.uniformity.update'], + '/production/uniformity/': ['lti.production.uniformity.list'], + '/production/uniformity/add/': ['lti.production.uniformity.create'], + '/production/uniformity/detail/': ['lti.production.uniformity.detail'], + '/production/uniformity/detail/edit/': ['lti.production.uniformity.update'], // Purchase '/purchase/': ['lti.purchase.list'], diff --git a/src/services/api/uniformity.ts b/src/services/api/uniformity.ts index 2ee1ee15..7ed8098e 100644 --- a/src/services/api/uniformity.ts +++ b/src/services/api/uniformity.ts @@ -6,7 +6,7 @@ import { VerifyUniformityPayload, VerifyUniformityResponse, CreateUniformityPayload, -} from '@/types/api/uniformity/uniformity'; +} from '@/types/api/production/uniformity'; export class UniformityApiService extends BaseApiService< Uniformity, diff --git a/src/types/api/uniformity/uniformity.d.ts b/src/types/api/production/uniformity.d.ts similarity index 100% rename from src/types/api/uniformity/uniformity.d.ts rename to src/types/api/production/uniformity.d.ts diff --git a/src/types/stores.d.ts b/src/types/stores.d.ts index 96a3c48b..48873805 100644 --- a/src/types/stores.d.ts +++ b/src/types/stores.d.ts @@ -3,7 +3,7 @@ import type { UniformityFormData, UniformityDetail, VerifyUniformityResponse, -} from '@/types/api/uniformity/uniformity'; +} from '@/types/api/production/uniformity'; type MainUiSlice = { mainDrawerOpen: boolean; From 28c94e3e1df2c6e8050f5ca7cb27650a0bebf518 Mon Sep 17 00:00:00 2001 From: rstubryan Date: Wed, 31 Dec 2025 11:21:35 +0700 Subject: [PATCH 124/124] refactor(FE): Show expedition vendor name in order detail --- src/components/pages/purchase/order/PurchaseOrderDetail.tsx | 2 +- src/types/api/purchase/purchase.d.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/pages/purchase/order/PurchaseOrderDetail.tsx b/src/components/pages/purchase/order/PurchaseOrderDetail.tsx index de07ee52..a5d6474b 100644 --- a/src/components/pages/purchase/order/PurchaseOrderDetail.tsx +++ b/src/components/pages/purchase/order/PurchaseOrderDetail.tsx @@ -583,7 +583,7 @@ const PurchaseOrderDetail = ({ { header: 'Ekspedisi', accessorKey: 'expedition_name', - cell: (props) => '-', + cell: (props) => props.row.original.expedition_vendor.name || '-', }, { header: 'Transport /Item', diff --git a/src/types/api/purchase/purchase.d.ts b/src/types/api/purchase/purchase.d.ts index 7b698b74..7f493d08 100644 --- a/src/types/api/purchase/purchase.d.ts +++ b/src/types/api/purchase/purchase.d.ts @@ -42,6 +42,12 @@ export type PurchaseItem = { expedition_vendor_name?: string | null; received_qty?: number | null; transport_per_item?: number | null; + expedition_vendor: { + id: number; + name: string; + alias: string; + category: string; + }; }; export type BasePurchase = {