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 (
+
+ );
+ }
+
+ if (!chartData.labels.length) {
+ return (
+
+
No historical data available for this product
+
+ );
+ }
+
+ // Color legend component
+ const ColorLegend = () => (
+
+ );
+
+ 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] ? (
-

+

) : (
)}
@@ -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 */}
-
-
-
-
- |
- 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)
- |
-
+ {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}
+
+
+
-
- |
- {quarterPeriods?.previous_month?.previous_period_one}
- |
-
- {quarterPeriods?.previous_month?.previous_period_two}
- |
-
- {quarterPeriods?.previous_month?.previous_period_three}
- |
+ {/* Product Metrics with Chart */}
+
-
- {quarterPeriods?.current_month?.current_period_one}
- |
-
- {quarterPeriods?.current_month?.current_period_two}
- |
-
- {quarterPeriods?.current_month?.current_period_three}
- |
+ {/* 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?.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 Row */}
-
- |
- 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 Row */}
+
+ |
+ Quantity ({product?.unit?.uom || 'units'})
+ |
+ {product.previous_quantity_period_one || '0'} |
+ {product.previous_quantity_period_two || '0'} |
+ {product.previous_quantity_period_three || '0'} |
- {/* Cost Row */}
-
- |
- 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 Row */}
+
+ |
+ Cost (AED)
+ |
+ {product.previous_cost_period_one || '0'} |
+ {product.previous_cost_period_two || '0'} |
+ {product.previous_cost_period_three || '0'} |
- {/* Reason (Current) */}
-
- |
- 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) */}
+
+ |
+ 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)
- |
-
- --
- |
-
- --
- |
+ {/* 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 (
-
+
-

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

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