From 30057d56c05aff56d0991da689e72de0dfa0a8e8 Mon Sep 17 00:00:00 2001 From: Malini Date: Thu, 4 Dec 2025 23:39:35 +0530 Subject: [PATCH] fixed security issues bugs and graph added for products --- ipi-survey-platform/package-lock.json | 527 +++++++-- ipi-survey-platform/package.json | 5 +- ipi-survey-platform/src/App.jsx | 23 +- .../src/components/admin/AdminHeader.jsx | 2 +- .../src/components/admin/SubmissionTable.jsx | 11 +- .../components/dashboard/WelcomeSection.jsx | 2 +- .../src/components/layout/HeaderBar.jsx | 4 +- .../EstablishmentInfo/EstablishmentInfo.jsx | 14 +- .../survey/ProductData/ProductData.jsx | 8 +- ipi-survey-platform/src/constants/assets.js | 8 + .../src/constants/validationMessages.js | 16 + .../src/pages/Admin/AdminDashboard.jsx | 2 +- .../src/pages/Admin/ValidationReview.jsx | 1037 ++++++++++++++--- .../src/pages/Admin/Validations.jsx | 5 +- .../AdminUsers/AddAdminUsers.jsx | 17 +- .../AdminUsers/EditUserModal.jsx | 2 +- .../AdminUsers/ListAdminUsers.jsx | 28 +- .../AdminUsers/ResetPasswordModal.jsx | 19 +- .../Admin/configuration/CompanyProfile.jsx | 14 +- .../configuration/EditCompanyProfile.jsx | 4 +- .../pages/Admin/configuration/IsicHsCodes.jsx | 10 +- .../pages/Admin/configuration/UnitMaster.jsx | 2 +- .../pages/ChangePassword/ChangePassword.jsx | 26 +- .../pages/ForgotPassword/ForgotPassword.jsx | 8 +- ipi-survey-platform/src/pages/Login/Login.jsx | 209 ++-- .../src/pages/Overview/Overview.jsx | 2 +- .../src/services/api/apiClient.js | 29 +- .../src/services/auth/authService.js | 2 +- .../services/configuration/adminService.js | 12 +- .../services/submissions/submissionService.js | 23 +- .../src/services/utils/establishment.js | 2 +- 31 files changed, 1558 insertions(+), 515 deletions(-) create mode 100644 ipi-survey-platform/src/constants/assets.js create mode 100644 ipi-survey-platform/src/constants/validationMessages.js diff --git a/ipi-survey-platform/package-lock.json b/ipi-survey-platform/package-lock.json index 84c0e49..6649592 100644 --- a/ipi-survey-platform/package-lock.json +++ b/ipi-survey-platform/package-lock.json @@ -9,13 +9,16 @@ "version": "0.0.0", "dependencies": { "axios": "^1.12.2", + "chart.js": "^4.5.1", "lucide-react": "^0.548.0", "react": "^19.1.1", + "react-chartjs-2": "^5.3.1", "react-dom": "^19.1.1", "react-google-recaptcha": "^3.1.0", "react-icons": "^5.5.0", "react-router-dom": "^7.9.4", - "react-toastify": "^11.0.5" + "react-toastify": "^11.0.5", + "recharts": "^3.5.0" }, "devDependencies": { "@eslint/js": "^9.36.0", @@ -774,7 +777,6 @@ "version": "4.9.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", - "dev": true, "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -793,7 +795,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -806,7 +807,6 @@ "version": "4.12.1", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -816,7 +816,6 @@ "version": "0.21.0", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.6", @@ -831,7 +830,6 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz", "integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/core": "^0.16.0" @@ -844,7 +842,6 @@ "version": "0.16.0", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" @@ -857,7 +854,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, "license": "MIT", "dependencies": { "ajv": "^6.12.4", @@ -881,7 +877,6 @@ "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -894,7 +889,6 @@ "version": "9.37.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz", "integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==", - "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -907,7 +901,6 @@ "version": "2.1.6", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -917,7 +910,6 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/core": "^0.16.0", @@ -931,7 +923,6 @@ "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18.18.0" @@ -941,7 +932,6 @@ "version": "0.16.7", "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@humanfs/core": "^0.19.1", @@ -955,7 +945,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=12.22" @@ -969,7 +958,6 @@ "version": "0.4.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=18.18" @@ -1042,6 +1030,48 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.0.tgz", + "integrity": "sha512-hBjYg0aaRL1O2Z0IqWhnTLytnjDIxekmRxm1snsHjHaKVmIF1HiImWqsq+PuEbn6zdMlkIj9WofK1vR8jjx+Xw==", + "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.0.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.0.0.tgz", + "integrity": "sha512-XtRG4SINt4dpqlnJvs70O2j6hH7H0X8fUzFsjMn1rwnETaxwp83HLNimXBjZ78MrKl3/d3/pkzDH0o0Lkxm37Q==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.38", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz", @@ -1357,6 +1387,18 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "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/@tailwindcss/node": { "version": "4.1.14", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.14.tgz", @@ -1678,25 +1720,86 @@ "@babel/types": "^7.28.2" } }, + "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", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "19.2.2", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -1712,6 +1815,12 @@ "@types/react": "^19.2.0" } }, + "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/@vitejs/plugin-react": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.4.tgz", @@ -1737,7 +1846,6 @@ "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -1750,7 +1858,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -1760,7 +1867,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", @@ -1777,7 +1883,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -1793,7 +1898,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/asynckit": { @@ -1855,7 +1959,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/baseline-browser-mapping": { @@ -1872,7 +1975,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -1930,7 +2032,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -1961,7 +2062,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -1974,6 +2074,18 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/chownr": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", @@ -1997,7 +2109,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2010,7 +2121,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -2029,7 +2139,6 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/convert-source-map": { @@ -2052,7 +2161,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -2067,14 +2175,134 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2088,11 +2316,16 @@ } } }, + "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", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, "license": "MIT" }, "node_modules/delayed-stream": { @@ -2194,6 +2427,16 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.42.0.tgz", + "integrity": "sha512-SLHIyY7VfDJBM8clz4+T2oquwTQxEzu263AyhVK4jREOAwJ+8eebaa4wM3nlvnAqhDrMm2EsA6hWHaQsMPQ1nA==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esbuild": { "version": "0.25.11", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", @@ -2250,7 +2493,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -2263,7 +2505,6 @@ "version": "9.37.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", - "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", @@ -2333,6 +2574,18 @@ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, + "node_modules/eslint-plugin-react-perf": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-perf/-/eslint-plugin-react-perf-3.3.3.tgz", + "integrity": "sha512-EzPdxsRJg5IllCAH9ny/3nK7sv9251tvKmi/d3Ouv5KzI8TB3zNhzScxL9wnh9Hvv8GYC5LEtzTauynfOEYiAw==", + "license": "MIT", + "engines": { + "node": ">=6.9.1" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, "node_modules/eslint-plugin-react-refresh": { "version": "0.4.23", "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.23.tgz", @@ -2347,7 +2600,6 @@ "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", @@ -2364,7 +2616,6 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2377,7 +2628,6 @@ "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.15.0", @@ -2395,7 +2645,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" @@ -2408,7 +2657,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" @@ -2421,7 +2669,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -2431,31 +2678,33 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, "license": "BSD-2-Clause", "engines": { "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/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, "license": "MIT" }, "node_modules/fdir": { @@ -2480,7 +2729,6 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, "license": "MIT", "dependencies": { "flat-cache": "^4.0.0" @@ -2493,7 +2741,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, "license": "MIT", "dependencies": { "locate-path": "^6.0.0", @@ -2510,7 +2757,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, "license": "MIT", "dependencies": { "flatted": "^3.2.9", @@ -2524,7 +2770,6 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, "license": "ISC" }, "node_modules/follow-redirects": { @@ -2652,7 +2897,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -2697,7 +2941,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2755,17 +2998,25 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, "license": "MIT", "engines": { "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", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -2782,17 +3033,24 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" } }, + "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/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -2802,7 +3060,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -2815,14 +3072,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -2838,7 +3094,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -2864,21 +3119,18 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, "license": "MIT" }, "node_modules/json5": { @@ -2898,7 +3150,6 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, "license": "MIT", "dependencies": { "json-buffer": "3.0.1" @@ -2908,7 +3159,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", @@ -3161,7 +3411,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, "license": "MIT", "dependencies": { "p-locate": "^5.0.0" @@ -3177,7 +3426,6 @@ "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, "license": "MIT" }, "node_modules/loose-envify": { @@ -3255,7 +3503,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -3291,7 +3538,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -3317,7 +3563,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, "license": "MIT" }, "node_modules/node-releases": { @@ -3350,7 +3595,6 @@ "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, "license": "MIT", "dependencies": { "deep-is": "^0.1.3", @@ -3368,7 +3612,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" @@ -3384,7 +3627,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, "license": "MIT", "dependencies": { "p-limit": "^3.0.2" @@ -3400,7 +3642,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -3413,7 +3654,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3423,7 +3663,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3489,7 +3728,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8.0" @@ -3516,7 +3754,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3544,6 +3781,16 @@ "react": ">=16.4.1" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz", + "integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-dom": { "version": "19.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", @@ -3584,6 +3831,29 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -3645,11 +3915,62 @@ "react-dom": "^18 || ^19" } }, + "node_modules/recharts": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.5.0.tgz", + "integrity": "sha512-jWqBtu8L3VICXWa3g/y+bKjL8DDHSRme7DHD/70LQ/Tk0di1h11Y0kKC0nPh6YJ2oaa0k6anIFNhg6SfzHWdEA==", + "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", + "eslint-plugin-react-perf": "^3.3.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/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-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -3723,7 +4044,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -3736,7 +4056,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3756,7 +4075,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3769,7 +4087,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -3826,6 +4143,12 @@ "node": ">=18" } }, + "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/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -3847,7 +4170,6 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" @@ -3891,12 +4213,42 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "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": { "version": "7.1.10", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.10.tgz", @@ -3976,7 +4328,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -3992,7 +4343,6 @@ "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4009,7 +4359,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" diff --git a/ipi-survey-platform/package.json b/ipi-survey-platform/package.json index 62e3856..a9327d2 100644 --- a/ipi-survey-platform/package.json +++ b/ipi-survey-platform/package.json @@ -11,13 +11,16 @@ }, "dependencies": { "axios": "^1.12.2", + "chart.js": "^4.5.1", "lucide-react": "^0.548.0", "react": "^19.1.1", + "react-chartjs-2": "^5.3.1", "react-dom": "^19.1.1", "react-google-recaptcha": "^3.1.0", "react-icons": "^5.5.0", "react-router-dom": "^7.9.4", - "react-toastify": "^11.0.5" + "react-toastify": "^11.0.5", + "recharts": "^3.5.0" }, "devDependencies": { "@eslint/js": "^9.36.0", diff --git a/ipi-survey-platform/src/App.jsx b/ipi-survey-platform/src/App.jsx index 508fd59..8ff7aed 100644 --- a/ipi-survey-platform/src/App.jsx +++ b/ipi-survey-platform/src/App.jsx @@ -17,30 +17,27 @@ import EditCompanyProfile from '@/pages/Admin/configuration/EditCompanyProfile'; import Navbar from './pages/LandingPage/component/Navbar'; const RequireRole = ({ allowedRoles = [], children }) => { let role; - let token; + try { - role = sessionStorage.getItem('user_role'); - token = sessionStorage.getItem('auth_token'); - } catch (error) { + role = localStorage.getItem("user_role"); + } catch { role = null; - token = null; } - const normalizedRole = String(role || '').toLowerCase(); - const isAllowed = - Boolean(token) && - allowedRoles.some( - (allowed) => String(allowed || '').toLowerCase() === normalizedRole - ); + const normalizedRole = String(role || "").toLowerCase(); + + const isAllowed = allowedRoles.some( + (allowed) => String(allowed || "").toLowerCase() === normalizedRole + ); if (isAllowed) { return children; } - // return ; - return + return ; }; + const SessionWarningModal = ({ show, remainingTime, onExtend, onLogout }) => { if (!show) return null; diff --git a/ipi-survey-platform/src/components/admin/AdminHeader.jsx b/ipi-survey-platform/src/components/admin/AdminHeader.jsx index 41ac81e..97248ce 100644 --- a/ipi-survey-platform/src/components/admin/AdminHeader.jsx +++ b/ipi-survey-platform/src/components/admin/AdminHeader.jsx @@ -72,7 +72,7 @@ const AdminHeader = () => { React.useEffect(() => { try { - const stored = sessionStorage.getItem('user_profile'); + const stored = localStorage.getItem('user_profile'); if (stored) { const parsed = JSON.parse(stored); if (parsed?.name || parsed?.username) { diff --git a/ipi-survey-platform/src/components/admin/SubmissionTable.jsx b/ipi-survey-platform/src/components/admin/SubmissionTable.jsx index 8f7da4b..0de6052 100644 --- a/ipi-survey-platform/src/components/admin/SubmissionTable.jsx +++ b/ipi-survey-platform/src/components/admin/SubmissionTable.jsx @@ -33,7 +33,8 @@ const SubmissionTable = ({ selectedQuarter, selectedYear }) => { params.append('year', year); } - const response = await apiClient.get(`/admin_dashboard?${params.toString()}`); + // const response = await apiClient.get(`/admin_dashboard?${params.toString()}`); + const response = await apiClient.get(`/admin_dashboard?${params.toString()}`, { withCredentials: true }); const result = response?.data; if (result?.status === 'success') { // Map the data to include the updated_at as reviewerOn @@ -139,7 +140,7 @@ const SubmissionTable = ({ selectedQuarter, selectedYear }) => { } }; - const userProfile = JSON.parse(sessionStorage.getItem("user_profile")); + const userProfile = JSON.parse(localStorage.getItem("user_profile")); const handleActionClick = (submission, type) => { setSelectedSubmission(submission); @@ -181,7 +182,7 @@ const handleApproveConfirm = async () => { if (ok) { toast.success('Submission approved successfully'); - const currentUser = JSON.parse(sessionStorage.getItem("user_profile"))?.name || 'Admin'; + const currentUser = JSON.parse(localStorage.getItem("user_profile"))?.name || 'Admin'; const updatedAt = response?.data?.updated_at || new Date().toISOString(); // Update the status of the current item in the data array @@ -235,7 +236,7 @@ const handleRejectConfirm = async () => { if (ok) { toast.error('Submission has been rejected'); - const currentUser = JSON.parse(sessionStorage.getItem("user_profile"))?.name || 'Admin'; + const currentUser = JSON.parse(localStorage.getItem("user_profile"))?.name || 'Admin'; const updatedAt = response?.data?.updated_at || new Date().toISOString(); // Update the status of the current item in the data array @@ -309,7 +310,7 @@ const tableRows = filtered.map((item) => [ renderStatusBadge(item?.status), // Reviewer - show reviewer_name after approval/rejection, otherwise show '-' item?.status?.toLowerCase() === 'approved' || item?.status?.toLowerCase() === 'rejected' - ? (item?.reviewer_name || JSON.parse(sessionStorage.getItem("user_profile"))?.name || 'Admin') + ? (item?.reviewer_name || JSON.parse(localStorage.getItem("user_profile"))?.name || 'Admin') : '-', // Reviewed On - show updated_at after approval/rejection, otherwise show '-' item?.status?.toLowerCase() === 'approved' || item?.status?.toLowerCase() === 'rejected' diff --git a/ipi-survey-platform/src/components/dashboard/WelcomeSection.jsx b/ipi-survey-platform/src/components/dashboard/WelcomeSection.jsx index 527e706..e5ee52c 100644 --- a/ipi-survey-platform/src/components/dashboard/WelcomeSection.jsx +++ b/ipi-survey-platform/src/components/dashboard/WelcomeSection.jsx @@ -36,7 +36,7 @@ export const WelcomeSection = ({ data = null, loading = false, error = '' }) => Welcome{' '} {(() => { try { - const userProfile = JSON.parse(sessionStorage.getItem('user_profile')); + const userProfile = JSON.parse(localStorage.getItem('user_profile')); return userProfile?.name ? userProfile.name : ''; } catch (error) { console.error('Error reading user_profile from sessionStorage:', error); diff --git a/ipi-survey-platform/src/components/layout/HeaderBar.jsx b/ipi-survey-platform/src/components/layout/HeaderBar.jsx index 3c01e31..b912c4c 100644 --- a/ipi-survey-platform/src/components/layout/HeaderBar.jsx +++ b/ipi-survey-platform/src/components/layout/HeaderBar.jsx @@ -18,7 +18,7 @@ const HeaderBar = () => { const [userOpen, setUserOpen] = React.useState(false); const profileRef = React.useRef(null); const navigate = useNavigate(); - const establishmentId = sessionStorage.getItem("establishment_id") || ""; + const establishmentId = localStorage.getItem("establishment_id") || ""; // Close dropdown when clicking outside React.useEffect(() => { @@ -37,7 +37,7 @@ const HeaderBar = () => { React.useEffect(() => { let parsed; try { - const stored = sessionStorage.getItem('user_profile'); + const stored = localStorage.getItem('user_profile'); if (stored) { parsed = JSON.parse(stored); } diff --git a/ipi-survey-platform/src/components/survey/EstablishmentInfo/EstablishmentInfo.jsx b/ipi-survey-platform/src/components/survey/EstablishmentInfo/EstablishmentInfo.jsx index 5451ece..2c97a38 100644 --- a/ipi-survey-platform/src/components/survey/EstablishmentInfo/EstablishmentInfo.jsx +++ b/ipi-survey-platform/src/components/survey/EstablishmentInfo/EstablishmentInfo.jsx @@ -1,4 +1,10 @@ import React, { useEffect, useState, useMemo } from 'react'; + +// Get and encode establishment ID for URL +const getSafeEstablishmentId = () => { + const establishmentId = localStorage.getItem('establishment_id') || ''; + return encodeURIComponent(establishmentId); +}; import { useNavigate, useLocation } from 'react-router-dom'; import { getEstablishmentProducts } from '../../../services/submissions/submissionService'; import Table from '@/components/common/Table'; @@ -109,7 +115,7 @@ const EstablishmentInfo = ({ // Fetch establishment products React.useEffect(() => { const fetchProducts = async () => { - const establishmentId = sessionStorage.getItem('establishment_id'); + const establishmentId = localStorage.getItem('establishment_id'); if (!establishmentId) return; setLoadingProducts(true); @@ -280,14 +286,14 @@ const EstablishmentInfo = ({ const handleEditProfile = (e) => { e.preventDefault(); - const establishmentId = sessionStorage.getItem('establishment_id'); + const establishmentId = localStorage.getItem('establishment_id'); if (!establishmentId) { alert('No establishment ID found in session'); return; } localStorage.setItem('returnTo', window.location.pathname); - sessionStorage.setItem('edit_profile_from', 'EstablishmentUser'); + localStorage.setItem('edit_profile_from', 'EstablishmentUser'); navigate(`/edit-profile/${establishmentId}`); }; @@ -687,7 +693,7 @@ const EstablishmentInfo = ({

Need to update details? Go to Profile →{' '} diff --git a/ipi-survey-platform/src/components/survey/ProductData/ProductData.jsx b/ipi-survey-platform/src/components/survey/ProductData/ProductData.jsx index 93942fa..2ead842 100644 --- a/ipi-survey-platform/src/components/survey/ProductData/ProductData.jsx +++ b/ipi-survey-platform/src/components/survey/ProductData/ProductData.jsx @@ -503,7 +503,7 @@ const ProductData = ({ useEffect(() => { const fetchEstablishmentProducts = async () => { try { - const establishmentId = sessionStorage.getItem('establishment_id'); + const establishmentId = localStorage.getItem('establishment_id'); if (!establishmentId) return; const fetchProducts = async () => { @@ -902,7 +902,7 @@ const location = useLocation(); // Fetch previous forecast data when product is selected if (selectedProduct?.value) { - const establishmentId = sessionStorage.getItem('establishment_id') || + const establishmentId = localStorage.getItem('establishment_id') || localStorage.getItem('establishmentId') || localStorage.getItem('establishment_id'); @@ -915,7 +915,7 @@ const location = useLocation(); establishmentId, quarter, year, - storageSource: sessionStorage.getItem('establishment_id') ? 'sessionStorage' : 'localStorage' + storageSource: localStorage.getItem('establishment_id') ? 'sessionStorage' : 'localStorage' }); // Call handleProductSelect with the product ID @@ -1354,7 +1354,7 @@ const location = useLocation(); if (selectedProduct?.value) { try { // Get establishment ID from sessionStorage or localStorage - const establishmentId = sessionStorage.getItem('establishment_id') || + const establishmentId = localStorage.getItem('establishment_id') || localStorage.getItem('establishmentId') || localStorage.getItem('establishment_id'); diff --git a/ipi-survey-platform/src/constants/assets.js b/ipi-survey-platform/src/constants/assets.js new file mode 100644 index 0000000..3555a21 --- /dev/null +++ b/ipi-survey-platform/src/constants/assets.js @@ -0,0 +1,8 @@ +export const ASSETS = { + IMAGES: { + LOGO: '/assets/images/FCSCLogo.svg' + }, + CLASSES: { + AUTH_CONTAINER: 'min-h-screen bg-[#F7F7F7] flex items-center justify-center px-4' + } +}; diff --git a/ipi-survey-platform/src/constants/validationMessages.js b/ipi-survey-platform/src/constants/validationMessages.js new file mode 100644 index 0000000..01fabff --- /dev/null +++ b/ipi-survey-platform/src/constants/validationMessages.js @@ -0,0 +1,16 @@ +export const VALIDATION_MESSAGES = { + OLD_PASSWORD_REQUIRED: 'Old password is required', + PASSWORD_REQUIRED: 'Password is required', + PASSWORD_MIN_LENGTH: 'Password must be at least 8 characters', + PASSWORD_UPPERCASE: 'Must contain at least one uppercase letter', + PASSWORD_LOWERCASE: 'Must contain at least one lowercase letter', + PASSWORD_NUMBER: 'Must contain at least one number', + PASSWORD_SPECIAL_CHAR: 'Must contain at least one special character (!@#$%^&*)', + CONFIRM_PASSWORD_REQUIRED: 'Please confirm your password', + PASSWORDS_DONT_MATCH: 'Passwords do not match', + INCORRECT_CURRENT_PASSWORD: 'The current password is incorrect', + PASSWORD_SAME_AS_CURRENT: 'New password cannot be the same as current password', + PASSWORD_TOO_WEAK: 'Password is too weak. Use a stronger password.', + PASSWORD_COMPLEXITY_ERROR: 'Password must meet complexity requirements.' + +}; diff --git a/ipi-survey-platform/src/pages/Admin/AdminDashboard.jsx b/ipi-survey-platform/src/pages/Admin/AdminDashboard.jsx index 6e5fc40..b7a038e 100644 --- a/ipi-survey-platform/src/pages/Admin/AdminDashboard.jsx +++ b/ipi-survey-platform/src/pages/Admin/AdminDashboard.jsx @@ -55,7 +55,7 @@ const AdminDashboard = () => { params.append('quarter', quarter || 'All'); params.append('year', year || 'All'); - const response = await apiClient.get(`/admin_dashboard?${params.toString()}`); + const response = await apiClient.get(`/admin_dashboard?${params.toString()}`, { withCredentials: true }); if (!isMounted.current) return; diff --git a/ipi-survey-platform/src/pages/Admin/ValidationReview.jsx b/ipi-survey-platform/src/pages/Admin/ValidationReview.jsx index d09a3af..d085c4a 100644 --- a/ipi-survey-platform/src/pages/Admin/ValidationReview.jsx +++ b/ipi-survey-platform/src/pages/Admin/ValidationReview.jsx @@ -2,7 +2,656 @@ import React from 'react'; import { Link, useNavigate, useParams, useLocation } from 'react-router-dom'; import AdminHeader from '@/components/admin/AdminHeader'; import { getSubmissionById } from '@/services/admin/submission'; +import { getProductSubmissionHistory } from '@/services/submissions/submissionService'; import apiClient from '@/services/api/apiClient'; +import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, LineElement, PointElement, Title, Tooltip, Legend } from 'chart.js'; +import { Line, Bar } from 'react-chartjs-2'; + +// Register ChartJS components +ChartJS.register(CategoryScale, LinearScale, BarElement, LineElement, PointElement, Title, Tooltip, Legend); + +// Static Quarterly Data Component +const StaticQuarterlyChart = () => { + const staticChartData = { + labels: ['Q1 2023', 'Q2 2023', 'Q3 2023', 'Q4 2023', 'Q1 2024', 'Q2 2024', 'Q3 2024', 'Q4 2024', 'Q1 2025', 'Q2 2025', 'Q3 2025', 'Q4 2025'], + datasets: [ + { + label: 'Details', + data: [1200, 1300, 1250, 1400, 1350, 1450, 1500, 1550, 1600, 1650, 1700, 1750], + backgroundColor: 'rgba(75, 192, 192, 0.6)', + borderColor: 'rgba(75, 192, 192, 1)', + borderWidth: 1 + }, + { + label: 'Capacity', + data: [1500, 1500, 1500, 1500, 1600, 1600, 1600, 1600, 1700, 1700, 1700, 1700], + backgroundColor: 'rgba(54, 162, 235, 0.6)', + borderColor: 'rgba(54, 162, 235, 1)', + borderWidth: 1 + }, + { + label: 'Difference', + data: [300, 200, 250, 100, 250, 150, 100, 50, 100, 50, 0, -50], + backgroundColor: 'rgba(255, 99, 132, 0.6)', + borderColor: 'rgba(255, 99, 132, 1)', + borderWidth: 1 + } + ] + }; + + const options = { + responsive: true, + maintainAspectRatio: false, + scales: { + y: { + beginAtZero: true, + title: { + display: true, + text: 'Quantity', + font: { + weight: 'bold' + } + } + } + }, + plugins: { + legend: { + position: 'top', + }, + tooltip: { + callbacks: { + label: function(context) { + return `${context.dataset.label}: ${context.parsed.y.toLocaleString()}`; + } + } + } + } + }; + + return ( +

+

Quarterly Overview (2023-2025)

+
+ +
+
+ ); +}; + +// Wrapper component to render multiple charts for products +const CombinedProductsChart = ({ products, establishmentId }) => { + if (!products?.length) return null; + + return ( +
+ {products.map((product, index) => ( +
+

+ {product.product_name || `Product ${index + 1}`} Metrics +

+ +
+ ))} +
+ ); +}; + +const ProductChart = ({ product, establishmentId }) => { + const [productHistory, setProductHistory] = React.useState(null); + const [loading, setLoading] = React.useState(true); + const [error, setError] = React.useState(null); + + React.useEffect(() => { + const fetchProductHistory = async () => { + if (!establishmentId || !product?.product_id) return; + + try { + setLoading(true); + const result = await getProductSubmissionHistory(establishmentId, product.product_id); + setProductHistory(result); + } catch (err) { + console.error(`Error fetching history for product ${product.product_id}:`, err); + setError(`Failed to load history for product ${product.product_name || product.product_id}`); + } finally { + setLoading(false); + } + }; + + fetchProductHistory(); + }, [establishmentId, product]); + + // Process the data for the chart + const processChartData = () => { + if (loading || error || !productHistory?.data?.length) { + return { + labels: [], + datasets: [] + }; + } + + // Create an array to hold all quarter entries + const allQuarters = []; + + // Extract all quarters from the data + productHistory.data.forEach(item => { + if (!item.quarter_periods) return; + + // Add previous quarter + allQuarters.push({ + type: 'previous', + quarter: item.quarter_periods.previous_quarter, + year: item.quarter_periods.previous_year, + quantity: item.previous_quantity, + cost: item.previous_cost, + capacity: item.annual_installed_capacity + }); + + // Add current quarter + allQuarters.push({ + type: 'current', + quarter: item.quarter_periods.current_quarter, + year: item.quarter_periods.current_year, + quantity: item.current_quantity, + cost: item.current_cost, + capacity: item.annual_installed_capacity + }); + + // Add forecast quarter + allQuarters.push({ + type: 'forecast', + quarter: item.quarter_periods.forecast_quarter, + year: item.quarter_periods.forecast_year, + quantity: item.forecast_quantity, + cost: item.forecast_cost, + capacity: item.annual_installed_capacity + }); + }); + + // Sort quarters chronologically + allQuarters.sort((a, b) => { + const yearDiff = parseInt(a.year) - parseInt(b.year); + if (yearDiff !== 0) return yearDiff; + + const qOrder = { 'Q1': 1, 'Q2': 2, 'Q3': 3, 'Q4': 4 }; + const typeOrder = { 'previous': 1, 'current': 2, 'forecast': 3 }; + + const qA = qOrder[a.quarter] || 0; + const qB = qOrder[b.quarter] || 0; + if (qA !== qB) return qA - qB; + + return typeOrder[a.type] - typeOrder[b.type]; + }); + + // Prepare labels and data points + const labels = []; + const quantityData = []; + const capacityData = []; + const costData = []; + const backgroundColors = []; + + allQuarters.forEach(q => { + const typeLabel = q.type.charAt(0).toUpperCase() + q.type.slice(1); + labels.push(`${typeLabel} (${q.quarter} ${q.year})`); + quantityData.push(parseInt(q.quantity) || 0); + capacityData.push(parseInt(q.capacity) || 0); + costData.push(parseFloat(q.cost) || 0); + + // Set different background colors for different types + if (q.type === 'previous') { + backgroundColors.push('rgba(199, 177, 133, 0.8)'); + } else if (q.type === 'current') { + backgroundColors.push('rgba(106, 84, 48, 0.8)'); + } else { + backgroundColors.push('rgba(245, 238, 207, 0.8)'); + } + }); + + // Prepare datasets for the chart + const datasets = [ + // Quantity line + { + type: 'line', + label: 'Quantity', + data: quantityData, + borderColor: '#3B82F6', + backgroundColor: 'transparent', + borderWidth: 2, + yAxisID: 'y1', + tension: 0.3, + pointRadius: 3, + pointHoverRadius: 5, + }, + // Capacity line (dashed) + { + type: 'line', + label: 'Annual Capacity', + data: capacityData, + borderColor: '#10B981', + backgroundColor: 'transparent', + borderWidth: 1.5, + borderDash: [5, 5], + pointRadius: 0, + borderCapStyle: 'round', + yAxisID: 'y1', + }, + // Cost bars + { + type: 'bar', + label: 'Cost (AED)', + data: costData, + backgroundColor: backgroundColors, + borderColor: backgroundColors.map(c => c.replace('0.8', '1')), + borderWidth: 1, + yAxisID: 'y', + barPercentage: 0.6, + categoryPercentage: 0.8, + } + ]; + + return { + labels: labels, + datasets: datasets + }; + }; + + const chartData = processChartData(); + + const options = { + responsive: true, + maintainAspectRatio: false, + interaction: { + mode: 'index', + intersect: false, + }, + scales: { + y: { + type: 'linear', + display: true, + position: 'left', + title: { + display: true, + text: 'Total Cost (AED)', + font: { + weight: 'bold' + } + }, + grid: { + drawOnChartArea: false + }, + ticks: { + beginAtZero: true, + callback: function(value) { + // Format large numbers with commas + return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); + } + } + }, + y1: { + type: 'linear', + display: true, + position: 'right', + title: { + display: true, + text: 'Quantity', + font: { + weight: 'bold' + } + }, + grid: { + drawOnChartArea: false + }, + ticks: { + beginAtZero: true + } + }, + x: { + grid: { + display: false + } + } + }, + plugins: { + legend: { + position: 'top', + align: 'center', + labels: { + usePointStyle: true, + boxWidth: 12, + boxHeight: 12, + padding: 12, + font: { + size: 12, + lineHeight: '16px' + }, + generateLabels: function(chart) { + const data = chart.data; + if (data.labels.length && data.datasets.length) { + return chart.data.datasets.map((dataset, i) => { + const meta = chart.getDatasetMeta(i); + let label = dataset.label || ''; + label = ' ' + label.trim(); + + // For Annual Capacity (dashed line) + if (label.includes('Annual Capacity')) { + return { + text: label, + fillStyle: 'transparent', + hidden: !meta.visible, + lineDash: [3, 3], + lineWidth: 2, + strokeStyle: dataset.borderColor, + pointStyle: 'line', + rotation: 0, + datasetIndex: i + }; + } + + // For Quantity (solid line) + if (label.includes('Quantity')) { + return { + text: label, + fillStyle: 'transparent', + hidden: !meta.visible, + lineDash: [], + lineWidth: 2, + strokeStyle: dataset.borderColor, + pointStyle: 'line', + rotation: 0, + datasetIndex: i + }; + } + + // For bar items (Previous, Current, Forecast) - square boxes + return { + text: label, + fillStyle: dataset.backgroundColor, + hidden: !meta.visible, + lineWidth: 0, + strokeStyle: 'transparent', + pointStyle: 'rect', + rotation: 0, + datasetIndex: i + }; + }); + } + return []; + } + } + }, + tooltip: { + callbacks: { + label: function(context) { + let label = context.dataset.label || ''; + if (label) { + label += ': '; + } + if (context.parsed.y !== null) { + label += context.parsed.y.toLocaleString(); + // Add unit to the tooltip + if (context.dataset.yAxisID === 'y') { + label += ' (Cost)'; + } else { + label += ' (Qty)'; + } + } + return label; + } + } + } + } + }; + + if (loading) { + return ( +
+
+ + + + + Loading product history... +
+
+ ); + } + + if (error) { + return ( +
+
{error}
+
+ ); + } + + if (!chartData.labels.length) { + return ( +
+
No historical data available for this product
+
+ ); + } + + // Color legend component + const ColorLegend = () => ( +
+
+
+ Previous Quarter +
+
+
+ Current Quarter +
+
+
+ Forecast Quarter +
+
+
+ Quantity +
+
+
+ Annual Capacity +
+
+ ); + + return ( +
+
+

Products Overview

+
+ +
+
+
+ +
+
+
+
+ ); +}; + +const BarChart = ({ product, quarterPeriods }) => { + if (!product || !quarterPeriods) return null; + + // Get month names for x-axis labels + const getMonthName = (monthIndex) => { + const date = new Date(2000, monthIndex - 1, 1); + return date.toLocaleString('default', { month: 'short' }); + }; + + // Generate x-axis labels based on the quarters + const getMonthLabels = () => { + const months = []; + if (quarterPeriods?.previous_month) { + months.push( + getMonthName(quarterPeriods.previous_month.previous_period_one), + getMonthName(quarterPeriods.previous_month.previous_period_two), + getMonthName(quarterPeriods.previous_month.previous_period_three) + ); + } + if (quarterPeriods?.current_month) { + months.push( + getMonthName(quarterPeriods.current_month.current_period_one), + getMonthName(quarterPeriods.current_month.current_period_two), + getMonthName(quarterPeriods.current_month.current_period_three) + ); + } + if (quarterPeriods?.forecast_month) { + months.push( + getMonthName(quarterPeriods.forecast_month.forecast_period_one), + getMonthName(quarterPeriods.forecast_month.forecast_period_two), + getMonthName(quarterPeriods.forecast_month.forecast_period_three) + ); + } + return months; + }; + + // Prepare data for the chart + const chartData = { + labels: getMonthLabels(), + datasets: [ + // Previous Quarter + { + label: `Previous (${quarterPeriods.previous_quarter} ${quarterPeriods.previous_year})`, + data: [ + product.previous_quantity_period_one, + product.previous_quantity_period_two, + product.previous_quantity_period_three, + null, null, null, // Empty for current quarter + null, null, null // Empty for forecast quarter + ], + borderColor: '#92722A', // Gray color for previous + backgroundColor: 'transparent', + borderWidth: 2, + tension: 0.3, + pointRadius: 3, + pointHoverRadius: 5 + }, + // Current Quarter + { + label: `Current (${quarterPeriods.current_quarter} ${quarterPeriods.current_year})`, + data: [ + null, null, null, // Empty for previous quarter + product.current_quantity_period_one, + product.current_quantity_period_two, + product.current_quantity_period_three, + null, null, null // Empty for forecast quarter + ], + borderColor: '#10B981', // Green color for current + backgroundColor: 'transparent', + borderWidth: 2, + tension: 0.3, + pointRadius: 3, + pointHoverRadius: 5 + }, + // Forecast Quarter + { + label: `Forecast (${quarterPeriods.forecast_quarter} ${quarterPeriods.forecast_year})`, + data: [ + null, null, null, // Empty for previous quarter + null, null, null, // Empty for current quarter + product.forecast_quantity_period_one, + product.forecast_quantity_period_two, + product.forecast_quantity_period_three + ], + borderColor: '#F59E0B', // Yellow color for forecast + backgroundColor: 'transparent', + borderWidth: 2, + tension: 0.3, + pointRadius: 3, + pointHoverRadius: 5 + }, + // Annual Capacity Line + { + label: 'Annual Capacity', + data: new Array(9).fill(product.annual_installed_capacity), + borderColor: '#3B82F6', // Blue color for capacity line + backgroundColor: 'transparent', + borderWidth: 1, + borderDash: [5, 5], + pointRadius: 0, + borderCapStyle: 'round' + } + ] + }; + + const options = { + responsive: true, + maintainAspectRatio: false, + interaction: { + mode: 'index', + intersect: false, + }, + scales: { + y: { + beginAtZero: true, + title: { + display: true, + text: 'Quantity' + }, + grid: { + drawBorder: false + } + }, + x: { + grid: { + display: false + } + } + }, + plugins: { + legend: { + position: 'top', + align: 'end', + labels: { + usePointStyle: true, + boxWidth: 10, + padding: 20 + } + }, + tooltip: { + callbacks: { + label: function(context) { + let label = context.dataset.label || ''; + if (label) { + label += ': '; + } + if (context.parsed.y !== null) { + label += context.parsed.y; + } + return label; + } + } + } + } + }; + + return ( +
+ +
+ ); +}; + const caretUpSrc = '/assets/images/caret-up.svg'; const establishmentIconSrc = '/assets/images/Establishment.svg'; const establishmentIdIconSrc = '/assets/images/Establishmentid.svg'; @@ -36,6 +685,8 @@ const ValidationReview = () => { const [submissionData, setSubmissionData] = React.useState(null); const [quarterPeriods, setQuarterPeriods] = React.useState(null); const [isApproveOpen, setIsApproveOpen] = React.useState(false); + const [establishmentId, setEstablishmentId] = React.useState(null); + const [products, setProducts] = React.useState([]); React.useEffect(() => { const fetchSubmission = async () => { @@ -71,6 +722,12 @@ const ValidationReview = () => { } setSubmissionData(data); + if (data.establishment?.id) { + setEstablishmentId(data.establishment.id); + } + if (data.products) { + setProducts(data.products); + } } catch (err) { console.error('Failed to load submission details:', err); setError(err.message || 'Unable to load submission details. Please try again later.'); @@ -255,8 +912,8 @@ const ValidationReview = () => { const forecastQuarter = quarterPeriods?.forecast_quarter; const year = submissionData?.year || new Date().getFullYear(); - const products = submissionData?.products || []; - const productDetails = Array.isArray(products) ? products.map((product) => { + const submissionProducts = submissionData?.products || []; + const productDetails = Array.isArray(submissionProducts) ? submissionProducts.map((product) => { const quarters = [ { label: `${previousQuarter}-${year}`, color: '#0C64F6', cellColor: '#F4F8FF', type: 'previous' }, { label: `${currentQuarter}-${year}`, color: '#1E7C34', cellColor: '#F3FAF4', type: 'current' }, @@ -366,7 +1023,11 @@ const ValidationReview = () => { {employmentDetails.map((item) => (
{employmentIcons[item.label] ? ( - {item.label} + {item.label} ) : ( )} @@ -382,178 +1043,185 @@ const ValidationReview = () => {
-{submissionData?.products?.length > 0 ? ( -
- {submissionData.products.map((product, idx) => ( -
- {/* Header Section */} -
-
- Product:{' '} - - {product?.product?.hs_code} - {product?.product?.product_name} - -
-
- - Submitted By: System - - - Unit: {product?.unit?.uom || 'units'} - - - Capacity: {product?.annual_installed_capacity} - -
-
+ {/* Products will be rendered below with their respective charts */} - {/* Table Section */} -
- - - - - - - - + {submissionData?.products?.length > 0 ? ( +
+ {submissionData.products.map((product, idx) => ( +
+ {/* Header Section */} +
+
+ Product:{' '} + + {product?.product?.hs_code} - {product?.product?.product_name} + +
+
+ + Submitted By: System + + + Unit: {product?.unit?.uom || 'units'} + + + Capacity: {product?.annual_installed_capacity} + +
+
-
- - - + {/* Product Metrics with Chart */} +
+

Product Metrics

+
+ +
+
- - - + {/* Table Section */} +
+
- Metric - - {quarterPeriods?.previous_quarter}-{quarterPeriods?.previous_year}{' '} - (Previous Quarter) - - {quarterPeriods?.current_quarter}-{quarterPeriods?.current_year}{' '} - (Current Quarter) - - {quarterPeriods?.forecast_quarter}-{quarterPeriods?.forecast_year}{' '} - (Next Quarter) -
- {quarterPeriods?.previous_month?.previous_period_one} - - {quarterPeriods?.previous_month?.previous_period_two} - - {quarterPeriods?.previous_month?.previous_period_three} - - {quarterPeriods?.current_month?.current_period_one} - - {quarterPeriods?.current_month?.current_period_two} - - {quarterPeriods?.current_month?.current_period_three} -
+ + + + + + + - - - - - + + + + - - {/* Quantity Row */} - - - - - + + + - - - + + + + + - - - - + + {/* Quantity Row */} + + + + + - {/* Cost Row */} - - - - - + + + - - - + + + + - - - - + {/* Cost Row */} + + + + + - {/* Reason (Current) */} - - - - + + + + + + + + + {/* Reason (Current) */} + + + + - - + + + - {/* Reason (Forecast) */} - - - - + {/* Reason (Forecast) */} + + + + - - -
+ Metric + + {quarterPeriods?.previous_quarter}-{quarterPeriods?.previous_year}{' '} + (Previous Quarter) + + {quarterPeriods?.current_quarter}-{quarterPeriods?.current_year}{' '} + (Current Quarter) + + {quarterPeriods?.forecast_quarter}-{quarterPeriods?.forecast_year}{' '} + (Next Quarter) +
- {quarterPeriods?.forecast_month?.forecast_period_one} - - {quarterPeriods?.forecast_month?.forecast_period_two} - - {quarterPeriods?.forecast_month?.forecast_period_three} -
+ {quarterPeriods?.previous_month?.previous_period_one} + + {quarterPeriods?.previous_month?.previous_period_two} + + {quarterPeriods?.previous_month?.previous_period_three} +
- Quantity ({product?.unit?.uom || 'units'}) - {product.previous_quantity_period_one || '0'}{product.previous_quantity_period_two || '0'}{product.previous_quantity_period_three || '0'} + {quarterPeriods?.current_month?.current_period_one} + + {quarterPeriods?.current_month?.current_period_two} + + {quarterPeriods?.current_month?.current_period_three} + {product.current_quantity_period_one || '0'}{product.current_quantity_period_two || '0'}{product.current_quantity_period_three || '0'} + {quarterPeriods?.forecast_month?.forecast_period_one} + + {quarterPeriods?.forecast_month?.forecast_period_two} + + {quarterPeriods?.forecast_month?.forecast_period_three} +
{product.forecast_quantity_period_one || '0'}{product.forecast_quantity_period_two || '0'}{product.forecast_quantity_period_three || '0'}
+ Quantity ({product?.unit?.uom || 'units'}) + {product.previous_quantity_period_one || '0'}{product.previous_quantity_period_two || '0'}{product.previous_quantity_period_three || '0'}
- Cost (AED) - {product.previous_cost_period_one || '0'}{product.previous_cost_period_two || '0'}{product.previous_cost_period_three || '0'}{product.current_quantity_period_one || '0'}{product.current_quantity_period_two || '0'}{product.current_quantity_period_three || '0'}{product.current_cost_period_one || '0'}{product.current_cost_period_two || '0'}{product.current_cost_period_three || '0'}{product.forecast_quantity_period_one || '0'}{product.forecast_quantity_period_two || '0'}{product.forecast_quantity_period_three || '0'}
{product.forecast_cost_period_one || '0'}{product.forecast_cost_period_two || '0'}{product.forecast_cost_period_three || '0'}
+ Cost (AED) + {product.previous_cost_period_one || '0'}{product.previous_cost_period_two || '0'}{product.previous_cost_period_three || '0'}
- Reason (Current) - - -- - - {product.other_variation_reason || + {product.current_cost_period_one || '0'}{product.current_cost_period_two || '0'}{product.current_cost_period_three || '0'}{product.forecast_cost_period_one || '0'}{product.forecast_cost_period_two || '0'}{product.forecast_cost_period_three || '0'}
+ Reason (Current) + + -- + + {product.other_variation_reason || (product?.variation_reason?.reason ? (product.variation_reason.reason.startsWith('Other (specify)') ? '' : product.variation_reason.reason) : 'No reason provided') } - - -- -
+ -- +
- Reason (Forecast) - - -- - - -- -
+ Reason (Forecast) + + -- + + -- + { : 'No reason provided') }
+ + + -
- Remarks:{' '} - {product.remarks || 'NA'} +
+ Remarks:{' '} + {product.remarks || 'NA'} +
+ +
+ + +
+
+
+ ))}
- -
- - + ) : ( +
+

No products found for this submission.

-
- - ))} - -) : ( -
-

No products found for this submission.

-
-)} - + )} {actionType !== 'view' && (
diff --git a/ipi-survey-platform/src/pages/Admin/Validations.jsx b/ipi-survey-platform/src/pages/Admin/Validations.jsx index 35da305..55152fe 100644 --- a/ipi-survey-platform/src/pages/Admin/Validations.jsx +++ b/ipi-survey-platform/src/pages/Admin/Validations.jsx @@ -145,7 +145,7 @@ const ManageSubmissions = () => { showToast('success', 'Submission approved successfully'); // Refresh the submissions list const data = await fetchDashboardData(selectedQuarter, selectedYear); - const userProfile = JSON.parse(sessionStorage.getItem("user_profile")); + const userProfile = JSON.parse(localStorage.getItem("user_profile")); const mapped = (data || []).map((item) => ({ id: item.id, establishment: item?.establishment?.factory_name || '-', @@ -305,7 +305,8 @@ React.useEffect(() => { setLoading(true); // First, get the current quarter and year - const response = await apiClient.get('/admin_dashboard'); + // const response = await apiClient.get('/admin_dashboard'); + const response = await apiClient.get(`/admin_dashboard?${params.toString()}`, { withCredentials: true }); const currentQuarter = response?.data?.selected_quarter || 'Q1'; // const currentQuarter = 'All'; const currentYear = response?.data?.selected_year || new Date().getFullYear().toString(); diff --git a/ipi-survey-platform/src/pages/Admin/configuration/AdminUsers/AddAdminUsers.jsx b/ipi-survey-platform/src/pages/Admin/configuration/AdminUsers/AddAdminUsers.jsx index 2bca05f..70026ac 100644 --- a/ipi-survey-platform/src/pages/Admin/configuration/AdminUsers/AddAdminUsers.jsx +++ b/ipi-survey-platform/src/pages/Admin/configuration/AdminUsers/AddAdminUsers.jsx @@ -3,6 +3,7 @@ import { X } from 'lucide-react'; import { TextField, SelectField } from '@/components/common/FormControls'; import { createadminusers } from '@/services/configuration/adminService'; import CustomToast from '@/components/common/CustomToast'; +import { VALIDATION_MESSAGES } from '../../../../constants/validationMessages'; const AddAdminUsers = ({ isOpen, onClose, onSave }) => { const [formData, setFormData] = useState({ @@ -49,7 +50,7 @@ const AddAdminUsers = ({ isOpen, onClose, onSave }) => { } if (formData.password !== formData.confirmPassword) { - newErrors.confirmPassword = 'Passwords do not match'; + newErrors.confirmPassword = VALIDATION_MESSAGES.PASSWORDS_DONT_MATCH; } return newErrors; @@ -71,12 +72,12 @@ const AddAdminUsers = ({ isOpen, onClose, onSave }) => { }, [isOpen]); const validatePassword = (password) => { - if (!password) return 'Password is required'; - if (password.length < 8) return 'Password must be at least 8 characters'; - if (!/[A-Z]/.test(password)) return 'Must contain at least one uppercase letter'; - if (!/[a-z]/.test(password)) return 'Must contain at least one lowercase letter'; - if (!/\d/.test(password)) return 'Must contain at least one number'; - if (!/[!@#$%^&*]/.test(password)) return 'Must contain at least one special character (!@#$%^&*)'; + if (!password) return VALIDATION_MESSAGES.PASSWORD_REQUIRED; + if (password.length < 8) return VALIDATION_MESSAGES.PASSWORD_MIN_LENGTH; + if (!/[A-Z]/.test(password)) return VALIDATION_MESSAGES.PASSWORD_UPPERCASE; + if (!/[a-z]/.test(password)) return VALIDATION_MESSAGES.PASSWORD_LOWERCASE; + if (!/\d/.test(password)) return VALIDATION_MESSAGES.PASSWORD_NUMBER; + if (!/[!@#$%^&*]/.test(password)) return VALIDATION_MESSAGES.PASSWORD_SPECIAL_CHAR; return ''; // Return empty string if password is valid }; @@ -269,7 +270,7 @@ const AddAdminUsers = ({ isOpen, onClose, onSave }) => { - {/* ✅ Custom Toast Display */} + {/*Custom Toast Display */} {toastData && (
{ // Old password validation if (!oldPassword?.trim()) { - newErrors.oldPassword = 'Old password is required'; + newErrors.oldPassword = VALIDATION_MESSAGES.OLD_PASSWORD_REQUIRED; } // New password validation if (!newPassword?.trim()) { - newErrors.newPassword = 'New password is required'; + newErrors.newPassword = VALIDATION_MESSAGES.PASSWORD_REQUIRED; } else if (newPassword.length < 8) { - newErrors.newPassword = 'Password must be at least 8 characters long'; + newErrors.newPassword = VALIDATION_MESSAGES.PASSWORD_MIN_LENGTH; } // Confirm password validation if (!confirmPassword?.trim()) { - newErrors.confirmPassword = 'Please confirm your password'; + newErrors.confirmPassword = VALIDATION_MESSAGES.CONFIRM_PASSWORD_REQUIRED; } else if (newPassword !== confirmPassword) { - newErrors.confirmPassword = 'Passwords do not match'; + newErrors.confirmPassword = VALIDATION_MESSAGES.PASSWORDS_DONT_MATCH; } return newErrors; @@ -555,7 +556,7 @@ const AdminUsers = () => { handleCloseResetModal(); } catch (error) { - console.error('❌ Error resetting admin password:', error); + console.error('Error resetting admin password:', error); // Handle specific error cases let errorMessage = 'Failed to reset password'; @@ -570,27 +571,28 @@ const AdminUsers = () => { errorMessage.toLowerCase().includes('incorrect password')) { setResetErrors(prev => ({ ...prev, - oldPassword: 'The current password is incorrect' + oldPassword: VALIDATION_MESSAGES.INCORRECT_CURRENT_PASSWORD })); - showToast('The current password you entered is incorrect', 'error'); + showToast(VALIDATION_MESSAGES.INCORRECT_CURRENT_PASSWORD, 'error'); } else if (errorMessage.toLowerCase().includes('match')) { setResetErrors(prev => ({ ...prev, - confirmPassword: 'New passwords do not match' + confirmPassword: VALIDATION_MESSAGES.PASSWORDS_DONT_MATCH })); + showToast(VALIDATION_MESSAGES.PASSWORDS_DONT_MATCH, 'error'); showToast('New passwords do not match', 'error'); } else if (errorMessage.toLowerCase().includes('same') || errorMessage.toLowerCase().includes('previous')) { setResetErrors(prev => ({ ...prev, - newPassword: 'New password cannot be the same as current password' + newPassword: VALIDATION_MESSAGES.PASSWORD_SAME_AS_CURRENT })); - showToast('New password cannot be the same as current password', 'error'); + showToast(VALIDATION_MESSAGES.PASSWORD_SAME_AS_CURRENT, 'error'); } else if (errorMessage.toLowerCase().includes('weak') || errorMessage.toLowerCase().includes('strength')) { setResetErrors(prev => ({ ...prev, - newPassword: 'Password is too weak. Use a stronger password.' + newPassword: VALIDATION_MESSAGES.PASSWORD_TOO_WEAK })); showToast('Password is too weak. Please use a stronger password.', 'error'); } else if (error.status === 404) { diff --git a/ipi-survey-platform/src/pages/Admin/configuration/AdminUsers/ResetPasswordModal.jsx b/ipi-survey-platform/src/pages/Admin/configuration/AdminUsers/ResetPasswordModal.jsx index a50cec7..bde7941 100644 --- a/ipi-survey-platform/src/pages/Admin/configuration/AdminUsers/ResetPasswordModal.jsx +++ b/ipi-survey-platform/src/pages/Admin/configuration/AdminUsers/ResetPasswordModal.jsx @@ -1,5 +1,6 @@ import { Eye, EyeOff } from 'lucide-react'; import React, { useState } from 'react'; +import { VALIDATION_MESSAGES } from '../../../../constants/validationMessages'; const ResetPasswordModal = ({ isOpen, @@ -22,12 +23,12 @@ const ResetPasswordModal = ({ const [confirmPasswordTouched, setConfirmPasswordTouched] = useState(false); const validatePassword = (password) => { - if (!password) return 'Password is required'; - if (password.length < 8) return 'Password must be at least 8 characters'; - if (!/[A-Z]/.test(password)) return 'Must contain at least one uppercase letter'; - if (!/[a-z]/.test(password)) return 'Must contain at least one lowercase letter'; - if (!/\d/.test(password)) return 'Must contain at least one number'; - if (!/[!@#$%^&*]/.test(password)) return 'Must contain at least one special character (!@#$%^&*)'; + if (!password) return VALIDATION_MESSAGES.PASSWORD_REQUIRED; + if (password.length < 8) return VALIDATION_MESSAGES.PASSWORD_MIN_LENGTH; + if (!/[A-Z]/.test(password)) return VALIDATION_MESSAGES.PASSWORD_UPPERCASE; + if (!/[a-z]/.test(password)) return VALIDATION_MESSAGES.PASSWORD_LOWERCASE; + if (!/\d/.test(password)) return VALIDATION_MESSAGES.PASSWORD_NUMBER; + if (!/[!@#$%^&*]/.test(password)) return VALIDATION_MESSAGES.PASSWORD_SPECIAL_CHAR; return ''; }; @@ -37,7 +38,7 @@ const ResetPasswordModal = ({ const newErrors = {}; if (!oldPassword.trim()) { - newErrors.oldPassword = 'Old password is required'; + newErrors.oldPassword = VALIDATION_MESSAGES.OLD_PASSWORD_REQUIRED; } if (passwordTouched && passwordError) { @@ -45,9 +46,9 @@ const ResetPasswordModal = ({ } if (!confirmPassword.trim()) { - newErrors.confirmPassword = 'Please confirm your password'; + newErrors.confirmPassword = VALIDATION_MESSAGES.CONFIRM_PASSWORD_REQUIRED; } else if (newPassword !== confirmPassword) { - newErrors.confirmPassword = 'Passwords do not match'; + newErrors.confirmPassword = VALIDATION_MESSAGES.PASSWORDS_DONT_MATCH; } return newErrors; diff --git a/ipi-survey-platform/src/pages/Admin/configuration/CompanyProfile.jsx b/ipi-survey-platform/src/pages/Admin/configuration/CompanyProfile.jsx index 230f7cd..3d2ba25 100644 --- a/ipi-survey-platform/src/pages/Admin/configuration/CompanyProfile.jsx +++ b/ipi-survey-platform/src/pages/Admin/configuration/CompanyProfile.jsx @@ -174,7 +174,7 @@ const createEmptyProfile = () => ({ contactMakaniNumber: '', contactPersonName: '', contactPersonDesignation: '', - contactCountryCode: '+971', + contactCountryCode: '', contactMobileNumber: '', contactEmail: '', contactWebsite: '', @@ -191,7 +191,7 @@ const createEmptyProfile = () => ({ corporateMakaniNumber: '', corporateContactPersonName: '', corporateContactPersonDesignation: '', - corporateCountryCode: '+971', + corporateCountryCode: '', corporateMobileNumber: '', corporateEmail: '', corporateWebsite: '', @@ -606,7 +606,7 @@ const CompanyProfile = () => { // Load current user from session storage React.useEffect(() => { try { - const profile = sessionStorage.getItem('user_profile'); + const profile = localStorage.getItem('user_profile'); if (profile) { const userData = JSON.parse(profile); setCurrentUser(userData); @@ -807,7 +807,7 @@ const CompanyProfile = () => { const currentUserId = React.useMemo(() => { try { - const profile = sessionStorage.getItem('user_profile'); + const profile = localStorage.getItem('user_profile'); if (!profile) return null; const parsed = JSON.parse(profile); return parsed; @@ -817,10 +817,10 @@ const CompanyProfile = () => { } }, []); - const profile = sessionStorage.getItem('user_profile'); + const profile = localStorage.getItem('user_profile'); const userProfile = React.useMemo(() => { try { - const profile = sessionStorage.getItem('user_profile'); + const profile = localStorage.getItem('user_profile'); return profile ? JSON.parse(profile) : null; } catch (error) { console.error('Error parsing user profile:', error); @@ -1935,7 +1935,7 @@ const displayedRows = sortedProfiles.map((item) => { const mapApiEstablishmentToProfile = React.useCallback((item) => { let currentUser = null; try { - const profile = sessionStorage.getItem('user_profile'); + const profile = localStorage.getItem('user_profile'); if (profile) { currentUser = JSON.parse(profile); } diff --git a/ipi-survey-platform/src/pages/Admin/configuration/EditCompanyProfile.jsx b/ipi-survey-platform/src/pages/Admin/configuration/EditCompanyProfile.jsx index 33cabcd..80fe005 100644 --- a/ipi-survey-platform/src/pages/Admin/configuration/EditCompanyProfile.jsx +++ b/ipi-survey-platform/src/pages/Admin/configuration/EditCompanyProfile.jsx @@ -657,7 +657,7 @@ const EditCompanyProfile = () => { required error={fieldErrors.userProfileEmail} /> -
+ {/*
{ /> {passwordError &&

{passwordError}

} {passwordReuseError &&

{passwordReuseError}

} -
+
*/}
diff --git a/ipi-survey-platform/src/pages/Admin/configuration/IsicHsCodes.jsx b/ipi-survey-platform/src/pages/Admin/configuration/IsicHsCodes.jsx index 707ede4..f7d7909 100644 --- a/ipi-survey-platform/src/pages/Admin/configuration/IsicHsCodes.jsx +++ b/ipi-survey-platform/src/pages/Admin/configuration/IsicHsCodes.jsx @@ -405,7 +405,7 @@ const IsicHsCodes = () => { // Get user profile from session const userProfile = React.useMemo(() => { try { - const profile = sessionStorage.getItem('user_profile'); + const profile = localStorage.getItem('user_profile'); return profile ? JSON.parse(profile) : null; } catch (error) { console.error('Error parsing user profile:', error); @@ -464,7 +464,7 @@ const IsicHsCodes = () => { const createdById = product.created_by; try { - const userProfileStr = sessionStorage.getItem('user_profile'); + const userProfileStr = localStorage.getItem('user_profile'); if (userProfileStr) { const userProfile = JSON.parse(userProfileStr); if (createdById && createdById === userProfile.id) { @@ -643,7 +643,7 @@ const IsicHsCodes = () => { let displayName = item.createdBy; if (displayName === '-' && item.createdById) { try { - const userProfileStr = sessionStorage.getItem('user_profile'); + const userProfileStr = localStorage.getItem('user_profile'); if (userProfileStr) { const userProfile = JSON.parse(userProfileStr); if (item.createdById === userProfile.id) { @@ -962,7 +962,7 @@ const IsicHsCodes = () => { // Get user profile from session storage for created_by let userProfile = null; try { - const userProfileStr = sessionStorage.getItem('user_profile'); + const userProfileStr = localStorage.getItem('user_profile'); if (userProfileStr) { userProfile = JSON.parse(userProfileStr); } @@ -1129,7 +1129,7 @@ const IsicHsCodes = () => { // Get user profile from session storage for created_by let createdByName = '-'; try { - const userProfileStr = sessionStorage.getItem('user_profile'); + const userProfileStr = localStorage.getItem('user_profile'); if (userProfileStr) { const userProfile = JSON.parse(userProfileStr); createdByName = userProfile.name || createdByName; diff --git a/ipi-survey-platform/src/pages/Admin/configuration/UnitMaster.jsx b/ipi-survey-platform/src/pages/Admin/configuration/UnitMaster.jsx index cfdcb1e..638ad38 100644 --- a/ipi-survey-platform/src/pages/Admin/configuration/UnitMaster.jsx +++ b/ipi-survey-platform/src/pages/Admin/configuration/UnitMaster.jsx @@ -196,7 +196,7 @@ const UnitMaster = () => { }, [sortedUnits, searchTerm]); useEffect(() => { - const userProfile = sessionStorage.getItem('user_profile'); + const userProfile = localStorage.getItem('user_profile'); if (userProfile) { try { setCurrentUser(JSON.parse(userProfile)); diff --git a/ipi-survey-platform/src/pages/ChangePassword/ChangePassword.jsx b/ipi-survey-platform/src/pages/ChangePassword/ChangePassword.jsx index 8252717..82fbe32 100644 --- a/ipi-survey-platform/src/pages/ChangePassword/ChangePassword.jsx +++ b/ipi-survey-platform/src/pages/ChangePassword/ChangePassword.jsx @@ -1,8 +1,10 @@ import React from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import apiClient from '@/services/api/apiClient'; +import { VALIDATION_MESSAGES } from '@/constants/validationMessages'; +import { ASSETS } from '@/constants/assets'; -const logoSrc = '/assets/images/FCSCLogo.svg'; +// const logoSrc = ASSETS.IMAGES.LOGO; const ToggleableInput = ({ label, @@ -81,15 +83,15 @@ const ChangePassword = () => { const validatePassword = (password) => { const rules = [ - { regex: /.{8,}/, msg: "At least 8 characters" }, - { regex: /[A-Z]/, msg: "At least one uppercase letter" }, - { regex: /[a-z]/, msg: "At least one lowercase letter" }, - { regex: /[0-9]/, msg: "At least one number" }, - { regex: /[!@#$%^&*(),.?\":{}|<>]/, msg: "At least one special character" }, + { regex: /.{8,}/, msg: VALIDATION_MESSAGES.PASSWORD_MIN_LENGTH }, + { regex: /[A-Z]/, msg: VALIDATION_MESSAGES.PASSWORD_UPPERCASE }, + { regex: /[a-z]/, msg: VALIDATION_MESSAGES.PASSWORD_LOWERCASE }, + { regex: /[0-9]/, msg: VALIDATION_MESSAGES.PASSWORD_NUMBER }, + { regex: /[!@#$%^&*(),.?\":{}|<>]/, msg: VALIDATION_MESSAGES.PASSWORD_SPECIAL_CHAR }, ]; const failedRule = rules.find(r => !r.regex.test(password)); - return failedRule ? `Password must meet complexity requirements.` : ""; + return failedRule ? VALIDATION_MESSAGES.PASSWORD_COMPLEXITY_ERROR : ""; }; const handleSubmit = async (e) => { @@ -105,11 +107,11 @@ const handleSubmit = async (e) => { let hasError = false; if (!otp.trim()) { - setOtpError("Verification code is required."); + setOtpError("Verification code is required"); hasError = true; } if (!newPassword.trim()) { - setNewPasswordError("New password is required."); + setNewPasswordError(VALIDATION_MESSAGES.PASSWORD_REQUIRED); hasError = true; } if (!confirmPassword.trim()) { @@ -126,7 +128,7 @@ if (passwordError) { } if (newPassword !== confirmPassword) { - setConfirmPasswordError("Passwords do not match."); + setConfirmPasswordError(VALIDATION_MESSAGES.PASSWORDS_DONT_MATCH); return; } @@ -168,11 +170,11 @@ if (newPassword !== confirmPassword) { }; return ( -
+
- FCSC + FCSC

Reset Password

Enter the verification code sent to your registered email and set a new password. diff --git a/ipi-survey-platform/src/pages/ForgotPassword/ForgotPassword.jsx b/ipi-survey-platform/src/pages/ForgotPassword/ForgotPassword.jsx index 2a977f0..9e5b590 100644 --- a/ipi-survey-platform/src/pages/ForgotPassword/ForgotPassword.jsx +++ b/ipi-survey-platform/src/pages/ForgotPassword/ForgotPassword.jsx @@ -1,8 +1,8 @@ import React from 'react'; import { useNavigate } from 'react-router-dom'; import { requestPasswordReset } from '@/services/auth/authService.js'; - -const logoSrc = '/assets/images/FCSCLogo.svg'; +import { ASSETS } from '@/constants/assets'; +const CONTAINER_CLASS = "min-h-screen bg-[#F7F7F7] flex items-center justify-center px-4"; const ForgotPassword = () => { const navigate = useNavigate(); @@ -38,11 +38,11 @@ const ForgotPassword = () => { }; return ( -

+
- FCSC + FCSC

Reset Password

Enter your email and we will send you instructions to reset your password.

diff --git a/ipi-survey-platform/src/pages/Login/Login.jsx b/ipi-survey-platform/src/pages/Login/Login.jsx index 6a7d49b..3d13c87 100644 --- a/ipi-survey-platform/src/pages/Login/Login.jsx +++ b/ipi-survey-platform/src/pages/Login/Login.jsx @@ -152,138 +152,111 @@ const Login = () => { return valid; }; - const submit = async (e) => { - e.preventDefault(); - - if (isLocked) { - const minutes = Math.floor(remainingTime / 60); - const seconds = remainingTime % 60; - showToast('error', `Account locked. Please wait ${minutes}m ${seconds}s or contact support.`); - return; - } - - if (!validateForm()) return; - - setLoading(true); - - try { - const response = await login({ email, password }); - - if (response?.status !== 'success' || !response?.data) { - throw new Error('Invalid credentials'); - } - - const token = response.data; - sessionStorage.setItem('auth_token', token); - - let payload; - try { - const [, base64Payload] = token.split('.'); - payload = JSON.parse(atob(base64Payload)); - } catch { - throw new Error('Invalid token format.'); - } - - const role = payload?.role ?? (payload?.is_admin ? 'Admin' : undefined); - if (role) sessionStorage.setItem('user_role', role); - - const profile = { - id: payload?.id || payload?.user_id || '', - name: payload?.name || payload?.username || '', - email: payload?.email || '', - }; - sessionStorage.setItem('user_profile', JSON.stringify(profile)); - - const establishmentId = payload?.establishment_id ?? payload?.establishmentId; - const isAdmin = - String(role || '').toLowerCase() === 'admin' || payload?.is_admin === true; - const successMessage = response?.message || 'Login successful.'; - - // Clear login attempts for this email - const storedAttempts = JSON.parse(localStorage.getItem('login_attempts') || '{}'); +const submit = async (e) => { + e.preventDefault(); + + if (isLocked) { + const minutes = Math.floor(remainingTime / 60); + const seconds = remainingTime % 60; + showToast('error', `Account locked. Please wait ${minutes}m ${seconds}s or contact support.`); + return; + } + + if (!validateForm()) return; + + setLoading(true); + + try { + const response = await login({ email, password }); + console.log("Login response:", response); + + // Check if login was successful + if (response?.status === 'success' && response?.data) { + const userData = response.data; + console.log("User data:", userData); + + // Clear any existing failed attempts + const storedAttempts = JSON.parse(localStorage.getItem("login_attempts") || "{}"); if (storedAttempts[email]) { delete storedAttempts[email]; - localStorage.setItem('login_attempts', JSON.stringify(storedAttempts)); + localStorage.setItem("login_attempts", JSON.stringify(storedAttempts)); } - + + // Store user data in localStorage + localStorage.setItem('user_role', userData.role); + + // Store establishment_id if user is an EstablishmentUser + if (userData.role === 'EstablishmentUser' && userData.establishment_id) { + localStorage.setItem('establishment_id', userData.establishment_id); + } + + // Store user profile + const userProfile = { + id: userData.id, + name: userData.name, + email: userData.email + }; + localStorage.setItem('user_profile', JSON.stringify(userProfile)); + + // Reset login attempt counters setFailedAttempts(0); setIsLocked(false); setRemainingTime(0); + if (countdownIntervalRef.current) { clearInterval(countdownIntervalRef.current); countdownIntervalRef.current = null; } - - if (isAdmin) { - sessionStorage.removeItem('establishment_id'); - showToast('success', successMessage); - scheduleNavigate('/admin/dashboard'); - return; - } - - if (rememberMe) { - sessionStorage.setItem('remember_me', 'true'); - sessionStorage.setItem('remembered_email', email); - sessionStorage.setItem('remembered_password', password); - } else { - sessionStorage.removeItem('remember_me'); - sessionStorage.removeItem('remembered_email'); - sessionStorage.removeItem('remembered_password'); - } - - if (establishmentId) { - sessionStorage.setItem('establishment_id', String(establishmentId)); - showToast('success', successMessage); - scheduleNavigate('/dashboard'); - } else { - showToast('error', 'Email or password is incorrect.'); - } - } catch (err) { - console.error('Login failed', err); + + // Show success message + showToast('success', response.message || 'Login successful'); - // Get the error message from the API response - const errorMessage = err.response?.data?.message || - err.message || - 'An error occurred during login. Please try again.'; - - // Show the exact error message for inactive users - if (errorMessage.toLowerCase().includes('inactive')) { - showToast('error', errorMessage); - return; - } - - // For other errors, show the generic message and update login attempts - const storedAttempts = JSON.parse(localStorage.getItem('login_attempts') || '{}'); - const attempts = (storedAttempts[email]?.attempts || 0) + 1; - - // Update attempts for this email - const updatedAttempts = { - ...storedAttempts, - [email]: { - attempts, - lockUntil: attempts >= MAX_FAILED_ATTEMPTS - ? Date.now() + LOCK_DURATION_MINUTES * 60 * 1000 - : null - } - }; - - localStorage.setItem('login_attempts', JSON.stringify(updatedAttempts)); - setFailedAttempts(attempts); - - if (attempts >= MAX_FAILED_ATTEMPTS) { - const lockUntil = Date.now() + LOCK_DURATION_MINUTES * 60 * 1000; - setIsLocked(true); - setLockExpiry(lockUntil); - startCountdown(lockUntil); - showToast('error', `Too many failed attempts. Account locked for ${LOCK_DURATION_MINUTES} minutes.`); + // Redirect based on role + if (userData.role === 'Admin') { + navigate('/admin/dashboard', { replace: true }); } else { - const remainingAttempts = MAX_FAILED_ATTEMPTS - attempts; - showToast('error', `Email or password is incorrect. ${remainingAttempts} attempt${remainingAttempts !== 1 ? 's' : ''} remaining.`); + navigate('/dashboard'); } - } finally { - setLoading(false); + } else { + throw new Error(response?.message || 'Invalid credentials'); } - }; + } catch (err) { + console.error('Login failed', err); + const errorMessage = err.response?.data?.message || + err.message || + 'An error occurred during login. Please try again.'; + + // Handle failed login attempts + const storedAttempts = JSON.parse(localStorage.getItem("login_attempts") || "{}"); + const attempts = (storedAttempts[email]?.attempts || 0) + 1; + + const updatedAttempts = { + ...storedAttempts, + [email]: { + attempts, + lockUntil: attempts >= MAX_FAILED_ATTEMPTS + ? Date.now() + LOCK_DURATION_MINUTES * 60 * 1000 + : null + } + }; + + localStorage.setItem("login_attempts", JSON.stringify(updatedAttempts)); + setFailedAttempts(attempts); + + if (attempts >= MAX_FAILED_ATTEMPTS) { + const lockUntil = Date.now() + LOCK_DURATION_MINUTES * 60 * 1000; + setIsLocked(true); + setLockExpiry(lockUntil); + startCountdown(lockUntil); + showToast('error', `Too many failed attempts. Account locked for ${LOCK_DURATION_MINUTES} minutes.`); + } else { + const remainingAttempts = MAX_FAILED_ATTEMPTS - attempts; + showToast('error', `Email or password is incorrect. ${remainingAttempts} attempt${remainingAttempts !== 1 ? 's' : ''} remaining.`); + } + } finally { + setLoading(false); + } +}; const formatTime = (seconds) => { const mins = Math.floor(seconds / 60); diff --git a/ipi-survey-platform/src/pages/Overview/Overview.jsx b/ipi-survey-platform/src/pages/Overview/Overview.jsx index 4bb4097..96b75c4 100644 --- a/ipi-survey-platform/src/pages/Overview/Overview.jsx +++ b/ipi-survey-platform/src/pages/Overview/Overview.jsx @@ -32,7 +32,7 @@ const Overview = () => { const [submissions, setSubmissions] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const establishmentId = sessionStorage.getItem('establishment_id'); + const establishmentId = localStorage.getItem('establishment_id'); const [pagination, setPagination] = useState({ currentPage: 1, pageSize: 10, diff --git a/ipi-survey-platform/src/services/api/apiClient.js b/ipi-survey-platform/src/services/api/apiClient.js index 891ba3c..0d34ef7 100644 --- a/ipi-survey-platform/src/services/api/apiClient.js +++ b/ipi-survey-platform/src/services/api/apiClient.js @@ -6,6 +6,7 @@ const fallbackToken = import.meta.env.VITE_API_TOKEN; const apiClient = axios.create({ baseURL, + withCredentials: true, headers: { 'Content-Type': 'application/json', Accept: 'application/json', @@ -19,20 +20,20 @@ apiClient.interceptors.request.use((config) => { return config; } - if (!config.headers.Authorization) { - let token; - try { - token = sessionStorage.getItem('auth_token'); - } catch (error) { - token = undefined; - } - if (!token && fallbackToken) { - token = fallbackToken; - } - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } - } + // if (!config.headers.Authorization) { + // let token; + // try { + // token = sessionStorage.getItem('auth_token'); + // } catch (error) { + // token = undefined; + // } + // if (!token && fallbackToken) { + // token = fallbackToken; + // } + // if (token) { + // config.headers.Authorization = `Bearer ${token}`; + // } + // } return config; }); diff --git a/ipi-survey-platform/src/services/auth/authService.js b/ipi-survey-platform/src/services/auth/authService.js index 04d1544..7b39491 100644 --- a/ipi-survey-platform/src/services/auth/authService.js +++ b/ipi-survey-platform/src/services/auth/authService.js @@ -1,7 +1,7 @@ import apiClient from '@/services/api/apiClient'; export const login = async ({ email, password }) => { - const response = await apiClient.post('/auth/login', { email, password }, { skipAuth: true }); + const response = await apiClient.post('/auth/login', { email, password }, { skipAuth: true , withCredentials: true}); return response.data; }; diff --git a/ipi-survey-platform/src/services/configuration/adminService.js b/ipi-survey-platform/src/services/configuration/adminService.js index cb3e937..e7492ce 100644 --- a/ipi-survey-platform/src/services/configuration/adminService.js +++ b/ipi-survey-platform/src/services/configuration/adminService.js @@ -4,7 +4,8 @@ const admin = '/admin_users'; export const getAdminUser = async () => { try { - const response = await getRequest(admin); + // const response = await getRequest(admin); + const response = await getRequest(admin, { withCredentials: true }); return response.data || []; } catch (error) { console.error('Error fetching units:', error); @@ -44,13 +45,6 @@ export const getAdminUserById = async (id) => { export const changeAdminUserPassword = async (id, passwordData) => { try { if (!id) throw new Error("Invalid user ID"); - - console.log('Debug: Password data received:', { - hasOldPassword: !!passwordData.old_password, - hasNewPassword: !!passwordData.new_password, - hasConfirmPassword: !!passwordData.confirm_password - }); - // Prepare the request data according to API specification const requestData = { old_password: passwordData.old_password, @@ -69,7 +63,7 @@ export const changeAdminUserPassword = async (id, passwordData) => { console.error("Error changing admin user password:", error); if (error.response) { - console.error("🔍 Debug: API error response:", { + console.error("Debug: API error response:", { status: error.response.status, data: error.response.data, headers: error.response.headers diff --git a/ipi-survey-platform/src/services/submissions/submissionService.js b/ipi-survey-platform/src/services/submissions/submissionService.js index 7f2a1a8..022741c 100644 --- a/ipi-survey-platform/src/services/submissions/submissionService.js +++ b/ipi-survey-platform/src/services/submissions/submissionService.js @@ -161,6 +161,26 @@ export const getEstablishmentProducts = async (establishmentId, config = {}) => } }; +export const getProductSubmissionHistory = async (establishmentId, productId, config = {}) => { + try { + if (!establishmentId || !productId) { + throw new Error('Establishment ID and Product ID are required to fetch product submission history'); + } + const response = await getRequest('/submissions/getProductSubmissionHistory', { + ...config, + params: { + establishment_id: establishmentId, + product_id: productId, + ...(config.params || {}) + } + }); + return response.data; + } catch (error) { + console.error('Error fetching product submission history:', error); + throw error; + } +}; + export default { getSubmissions, submitSurvey, @@ -171,5 +191,6 @@ export default { getPreviousForecastData, getSubmissionHistoryByEstablishment, getSubmissionAuditHistory, - getEstablishmentProducts + getEstablishmentProducts, + getProductSubmissionHistory }; \ No newline at end of file diff --git a/ipi-survey-platform/src/services/utils/establishment.js b/ipi-survey-platform/src/services/utils/establishment.js index 01b4a0f..7fc379c 100644 --- a/ipi-survey-platform/src/services/utils/establishment.js +++ b/ipi-survey-platform/src/services/utils/establishment.js @@ -15,7 +15,7 @@ export const resolveEstablishmentId = (inputId) => { } let storedId; try { - storedId = sessionStorage.getItem('establishment_id'); + storedId = localStorage.getItem('establishment_id'); } catch (error) { storedId = undefined; }