login api with username/password temporary and api integrated for zone state anad user role and permision , user management page created for super admin
This commit is contained in:
parent
978930d4eb
commit
cb793354bf
390
package-lock.json
generated
390
package-lock.json
generated
@ -33,6 +33,9 @@
|
|||||||
"@radix-ui/react-tabs": "^1.0.4",
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
"@radix-ui/react-toggle": "^1.0.3",
|
"@radix-ui/react-toggle": "^1.0.3",
|
||||||
"@radix-ui/react-toggle-group": "^1.0.4",
|
"@radix-ui/react-toggle-group": "^1.0.4",
|
||||||
|
"@reduxjs/toolkit": "^2.11.2",
|
||||||
|
"apisauce": "^3.2.2",
|
||||||
|
"axios": "^1.13.3",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
@ -46,6 +49,7 @@
|
|||||||
"react-day-picker": "^8.10.0",
|
"react-day-picker": "^8.10.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.51.0",
|
"react-hook-form": "^7.51.0",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
"react-resizable-panels": "^2.0.12",
|
"react-resizable-panels": "^2.0.12",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.22.3",
|
||||||
"recharts": "^2.12.2",
|
"recharts": "^2.12.2",
|
||||||
@ -2912,6 +2916,32 @@
|
|||||||
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@reduxjs/toolkit": {
|
||||||
|
"version": "2.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
||||||
|
"integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/spec": "^1.0.0",
|
||||||
|
"@standard-schema/utils": "^0.3.0",
|
||||||
|
"immer": "^11.0.0",
|
||||||
|
"redux": "^5.0.1",
|
||||||
|
"redux-thunk": "^3.1.0",
|
||||||
|
"reselect": "^5.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
||||||
|
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@remix-run/router": {
|
"node_modules/@remix-run/router": {
|
||||||
"version": "1.23.2",
|
"version": "1.23.2",
|
||||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
|
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
|
||||||
@ -2956,6 +2986,18 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@standard-schema/spec": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@standard-schema/utils": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@swc/helpers": {
|
"node_modules/@swc/helpers": {
|
||||||
"version": "0.5.15",
|
"version": "0.5.15",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||||
@ -3397,6 +3439,12 @@
|
|||||||
"@types/react": "^18.0.0"
|
"@types/react": "^18.0.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/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.53.1",
|
"version": "8.53.1",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.1.tgz",
|
||||||
@ -3730,6 +3778,15 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/apisauce": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/apisauce/-/apisauce-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-YvZ6v7KgGWSjqHEDcGxE+U5bI8U8ckVqVFvfs0SKGtMrvbX0o3MDMlMzlL9lvWgg9hb//162vAHPconN/Ed1oA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/argparse": {
|
"node_modules/argparse": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
@ -3749,6 +3806,12 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/autoprefixer": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "10.4.23",
|
"version": "10.4.23",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
|
||||||
@ -3786,6 +3849,17 @@
|
|||||||
"postcss": "^8.1.0"
|
"postcss": "^8.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.13.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.3.tgz",
|
||||||
|
"integrity": "sha512-ERT8kdX7DZjtUm7IitEyV7InTHAF42iJuMArIiDIV5YtPanJkgw4hw5Dyg9fh0mihdWNn1GKaeIWErfe56UQ1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.6",
|
||||||
|
"form-data": "^4.0.4",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@ -3847,6 +3921,19 @@
|
|||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/callsites": {
|
"node_modules/callsites": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||||
@ -3958,6 +4045,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@ -4155,6 +4254,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
@ -4181,6 +4289,20 @@
|
|||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.267",
|
"version": "1.5.267",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
|
||||||
@ -4230,6 +4352,51 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-set-tostringtag": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.6",
|
||||||
|
"has-tostringtag": "^1.0.2",
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.12",
|
"version": "0.25.12",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||||
@ -4566,6 +4733,42 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||||
|
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||||
|
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fraction.js": {
|
"node_modules/fraction.js": {
|
||||||
"version": "5.3.4",
|
"version": "5.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
|
||||||
@ -4607,6 +4810,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/gensync": {
|
"node_modules/gensync": {
|
||||||
"version": "1.0.0-beta.2",
|
"version": "1.0.0-beta.2",
|
||||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||||
@ -4617,6 +4829,30 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-nonce": {
|
"node_modules/get-nonce": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
||||||
@ -4626,6 +4862,19 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/glob-parent": {
|
"node_modules/glob-parent": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||||
@ -4652,6 +4901,18 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/graceful-fs": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
@ -4669,6 +4930,45 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-tostringtag": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-symbols": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hermes-estree": {
|
"node_modules/hermes-estree": {
|
||||||
"version": "0.25.1",
|
"version": "0.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
|
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
|
||||||
@ -4696,6 +4996,16 @@
|
|||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/immer": {
|
||||||
|
"version": "11.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz",
|
||||||
|
"integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/import-fresh": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||||
@ -5203,6 +5513,36 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
@ -5525,6 +5865,12 @@
|
|||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
@ -5596,6 +5942,29 @@
|
|||||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.17.0",
|
"version": "0.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||||
@ -5780,6 +6149,27 @@
|
|||||||
"decimal.js-light": "^2.4.1"
|
"decimal.js-light": "^2.4.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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": {
|
"node_modules/resolve-from": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||||
|
|||||||
@ -35,6 +35,9 @@
|
|||||||
"@radix-ui/react-tabs": "^1.0.4",
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
"@radix-ui/react-toggle": "^1.0.3",
|
"@radix-ui/react-toggle": "^1.0.3",
|
||||||
"@radix-ui/react-toggle-group": "^1.0.4",
|
"@radix-ui/react-toggle-group": "^1.0.4",
|
||||||
|
"@reduxjs/toolkit": "^2.11.2",
|
||||||
|
"apisauce": "^3.2.2",
|
||||||
|
"axios": "^1.13.3",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
@ -48,6 +51,7 @@
|
|||||||
"react-day-picker": "^8.10.0",
|
"react-day-picker": "^8.10.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.51.0",
|
"react-hook-form": "^7.51.0",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
"react-resizable-panels": "^2.0.12",
|
"react-resizable-panels": "^2.0.12",
|
||||||
"react-router-dom": "^6.22.3",
|
"react-router-dom": "^6.22.3",
|
||||||
"recharts": "^2.12.2",
|
"recharts": "^2.12.2",
|
||||||
@ -74,4 +78,4 @@
|
|||||||
"typescript-eslint": "^8.53.1",
|
"typescript-eslint": "^8.53.1",
|
||||||
"vite": "^6.0.0"
|
"vite": "^6.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
81
src/App.tsx
81
src/App.tsx
@ -1,4 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { RootState } from './store';
|
||||||
|
import { setCredentials, logout as logoutAction, initializeAuth } from './store/slices/authSlice';
|
||||||
import { ApplicationFormPage } from './components/public/ApplicationFormPage';
|
import { ApplicationFormPage } from './components/public/ApplicationFormPage';
|
||||||
import { LoginPage } from './components/auth/LoginPage';
|
import { LoginPage } from './components/auth/LoginPage';
|
||||||
import { Sidebar } from './components/layout/Sidebar';
|
import { Sidebar } from './components/layout/Sidebar';
|
||||||
@ -22,6 +25,7 @@ import { FinanceFnFPage } from './components/applications/FinanceFnFPage';
|
|||||||
import { FinancePaymentDetailsPage } from './components/applications/FinancePaymentDetailsPage';
|
import { FinancePaymentDetailsPage } from './components/applications/FinancePaymentDetailsPage';
|
||||||
import { FinanceFnFDetailsPage } from './components/applications/FinanceFnFDetailsPage';
|
import { FinanceFnFDetailsPage } from './components/applications/FinanceFnFDetailsPage';
|
||||||
import { MasterPage } from './components/applications/MasterPage';
|
import { MasterPage } from './components/applications/MasterPage';
|
||||||
|
import { UserManagementPage } from './components/admin/UserManagementPage';
|
||||||
import { ConstitutionalChangePage } from './components/applications/ConstitutionalChangePage';
|
import { ConstitutionalChangePage } from './components/applications/ConstitutionalChangePage';
|
||||||
import { ConstitutionalChangeDetails } from './components/applications/ConstitutionalChangeDetails';
|
import { ConstitutionalChangeDetails } from './components/applications/ConstitutionalChangeDetails';
|
||||||
import { RelocationRequestPage } from './components/applications/RelocationRequestPage';
|
import { RelocationRequestPage } from './components/applications/RelocationRequestPage';
|
||||||
@ -31,15 +35,16 @@ import { DealerResignationPage } from './components/dealer/DealerResignationPage
|
|||||||
import { DealerConstitutionalChangePage } from './components/dealer/DealerConstitutionalChangePage';
|
import { DealerConstitutionalChangePage } from './components/dealer/DealerConstitutionalChangePage';
|
||||||
import { DealerRelocationPage } from './components/dealer/DealerRelocationPage';
|
import { DealerRelocationPage } from './components/dealer/DealerRelocationPage';
|
||||||
import { Toaster } from './components/ui/sonner';
|
import { Toaster } from './components/ui/sonner';
|
||||||
import { mockUsers, User } from './lib/mock-data';
|
import { User } from './lib/mock-data';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { API } from './api/API';
|
||||||
|
|
||||||
type View = 'dashboard' | 'applications' | 'all-applications' | 'opportunity-requests' | 'unopportunity-requests' | 'tasks' | 'reports' | 'settings' | 'users' | 'resignation' | 'termination' | 'fnf' | 'finance-onboarding' | 'finance-fnf' | 'master' | 'constitutional-change' | 'relocation-requests' | 'worknote' | 'dealer-resignation' | 'dealer-constitutional' | 'dealer-relocation';
|
type View = 'dashboard' | 'applications' | 'all-applications' | 'opportunity-requests' | 'unopportunity-requests' | 'tasks' | 'reports' | 'settings' | 'users' | 'resignation' | 'termination' | 'fnf' | 'finance-onboarding' | 'finance-fnf' | 'master' | 'constitutional-change' | 'relocation-requests' | 'worknote' | 'dealer-resignation' | 'dealer-constitutional' | 'dealer-relocation';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
const dispatch = useDispatch<any>();
|
||||||
|
const { user: currentUser, isAuthenticated, loading } = useSelector((state: RootState) => state.auth);
|
||||||
const [showAdminLogin, setShowAdminLogin] = useState(false);
|
const [showAdminLogin, setShowAdminLogin] = useState(false);
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
||||||
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
|
||||||
const [currentView, setCurrentView] = useState<View>('dashboard');
|
const [currentView, setCurrentView] = useState<View>('dashboard');
|
||||||
const [selectedApplicationId, setSelectedApplicationId] = useState<string | null>(null);
|
const [selectedApplicationId, setSelectedApplicationId] = useState<string | null>(null);
|
||||||
const [selectedResignationId, setSelectedResignationId] = useState<string | null>(null);
|
const [selectedResignationId, setSelectedResignationId] = useState<string | null>(null);
|
||||||
@ -56,22 +61,46 @@ export default function App() {
|
|||||||
requestTitle: string;
|
requestTitle: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
|
||||||
const handleLogin = (email: string, password: string) => {
|
useEffect(() => {
|
||||||
// Find user in mock data
|
dispatch(initializeAuth());
|
||||||
const user = mockUsers.find(u => u.email === email && u.password === password);
|
}, [dispatch]);
|
||||||
|
|
||||||
if (user) {
|
const handleLogin = async (email: string, password: string) => {
|
||||||
setCurrentUser(user);
|
try {
|
||||||
setIsAuthenticated(true);
|
const response = await API.login({ email, password });
|
||||||
toast.success(`Welcome back, ${user.name}! (${user.role})`);
|
|
||||||
} else {
|
if (response.ok && response.data) {
|
||||||
toast.error('Invalid credentials');
|
const { token, user } = response.data as any;
|
||||||
|
|
||||||
|
// Store token for persistence
|
||||||
|
localStorage.setItem('token', token);
|
||||||
|
|
||||||
|
// Use backend user data
|
||||||
|
const simplifiedUser: User = {
|
||||||
|
id: user.id,
|
||||||
|
name: user.fullName || email.split('@')[0],
|
||||||
|
email: user.email,
|
||||||
|
password: password,
|
||||||
|
role: typeof user.role === 'string' ? user.role : (user.roleCode || 'User')
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatch(setCredentials({
|
||||||
|
user: simplifiedUser,
|
||||||
|
token
|
||||||
|
}));
|
||||||
|
toast.success(`Welcome back, ${simplifiedUser.name}!`);
|
||||||
|
} else {
|
||||||
|
const errorMsg = (response.data as any)?.message || 'Invalid credentials';
|
||||||
|
toast.error(errorMsg);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
toast.error('Something went wrong. Please try again.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
setIsAuthenticated(false);
|
dispatch(logoutAction());
|
||||||
setCurrentUser(null);
|
|
||||||
setCurrentView('dashboard');
|
setCurrentView('dashboard');
|
||||||
setSelectedApplicationId(null);
|
setSelectedApplicationId(null);
|
||||||
setShowAdminLogin(false);
|
setShowAdminLogin(false);
|
||||||
@ -246,6 +275,15 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Show loading state while initializing auth
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center h-screen bg-slate-50">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-red-600"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Show public application form if not authenticated and not trying to log in as admin
|
// Show public application form if not authenticated and not trying to log in as admin
|
||||||
if (!isAuthenticated && !showAdminLogin) {
|
if (!isAuthenticated && !showAdminLogin) {
|
||||||
return (
|
return (
|
||||||
@ -272,13 +310,11 @@ export default function App() {
|
|||||||
activeView={currentView}
|
activeView={currentView}
|
||||||
onNavigate={handleNavigate}
|
onNavigate={handleNavigate}
|
||||||
onLogout={handleLogout}
|
onLogout={handleLogout}
|
||||||
currentUser={currentUser}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
<Header
|
<Header
|
||||||
title={getPageTitle()}
|
title={getPageTitle()}
|
||||||
currentUser={currentUser}
|
|
||||||
onRefresh={() => window.location.reload()}
|
onRefresh={() => window.location.reload()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -442,16 +478,7 @@ export default function App() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{currentView === 'users' && (
|
{currentView === 'users' && (
|
||||||
<div className="bg-white rounded-lg border border-slate-200 p-8">
|
<UserManagementPage />
|
||||||
<h2 className="text-slate-900 mb-4">User Management</h2>
|
|
||||||
<p className="text-slate-600 mb-4">Manage system users and their roles</p>
|
|
||||||
<div className="space-y-2 text-slate-600">
|
|
||||||
<p>• Add/Edit/Remove users</p>
|
|
||||||
<p>• Assign roles and permissions</p>
|
|
||||||
<p>• View user activity logs</p>
|
|
||||||
<p>• Manage access controls</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{currentView === 'resignation' && (
|
{currentView === 'resignation' && (
|
||||||
|
|||||||
26
src/api/API.ts
Normal file
26
src/api/API.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import client from './client';
|
||||||
|
|
||||||
|
export const API = {
|
||||||
|
// Auth routes
|
||||||
|
login: (data: any) => client.post('/auth/login', data),
|
||||||
|
logout: () => client.post('/auth/logout'),
|
||||||
|
getCurrentUser: () => client.get('/auth/me'),
|
||||||
|
|
||||||
|
// Master module routes
|
||||||
|
getRoles: () => client.get('/admin/roles'),
|
||||||
|
getPermissions: () => client.get('/admin/permissions'),
|
||||||
|
updateRole: (id: string, data: any) => client.put(`/admin/roles/${id}`, data),
|
||||||
|
|
||||||
|
getZones: () => client.get('/master/zones'),
|
||||||
|
getRegions: () => client.get('/master/regions'),
|
||||||
|
getStates: (zoneId?: string) => client.get('/master/states', { zoneId }),
|
||||||
|
getDistricts: (stateId?: string) => client.get('/master/districts', { stateId }),
|
||||||
|
|
||||||
|
// User management routes
|
||||||
|
getUsers: () => client.get('/admin/users'),
|
||||||
|
updateUser: (id: string, data: any) => client.put(`/admin/users/${id}`, data),
|
||||||
|
updateUserStatus: (id: string, data: any) => client.patch(`/admin/users/${id}/status`, data),
|
||||||
|
deleteUser: (id: string) => client.delete(`/admin/users/${id}`),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default API;
|
||||||
32
src/api/client.ts
Normal file
32
src/api/client.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { create } from 'apisauce';
|
||||||
|
|
||||||
|
const API_BASE_URL = (import.meta as any).env?.VITE_API_URL || 'http://localhost:5000/api';
|
||||||
|
|
||||||
|
const client = create({
|
||||||
|
baseURL: API_BASE_URL,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Interceptor for Auth Token
|
||||||
|
client.addRequestTransform((request) => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token && request.headers) {
|
||||||
|
request.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Response transform for global error handling
|
||||||
|
client.addResponseTransform((response) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 401) {
|
||||||
|
console.error('Unauthorized access - potential token expiration');
|
||||||
|
// Potential logic to logout user or refresh token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default client;
|
||||||
406
src/components/admin/UserManagementPage.tsx
Normal file
406
src/components/admin/UserManagementPage.tsx
Normal file
@ -0,0 +1,406 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { adminService } from '../../services/admin.service';
|
||||||
|
import { masterService } from '../../services/master.service';
|
||||||
|
import { Card, CardContent } from '../ui/card';
|
||||||
|
import { Badge } from '../ui/badge';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
import { Input } from '../ui/input';
|
||||||
|
import { Label } from '../ui/label';
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
|
||||||
|
import { Switch } from '../ui/switch';
|
||||||
|
import {
|
||||||
|
Users,
|
||||||
|
UserPlus,
|
||||||
|
Search,
|
||||||
|
Edit2,
|
||||||
|
Trash2,
|
||||||
|
Shield,
|
||||||
|
Mail,
|
||||||
|
Phone,
|
||||||
|
Filter,
|
||||||
|
RefreshCw,
|
||||||
|
CheckCircle,
|
||||||
|
XCircle
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '../ui/dialog';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
||||||
|
|
||||||
|
export function UserManagementPage() {
|
||||||
|
const [users, setUsers] = useState<any[]>([]);
|
||||||
|
const [roles, setRoles] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
const [roleFilter, setRoleFilter] = useState('all');
|
||||||
|
const [statusFilter, setStatusFilter] = useState('all');
|
||||||
|
|
||||||
|
// Edit/Add Modal State
|
||||||
|
const [showUserModal, setShowUserModal] = useState(false);
|
||||||
|
const [editingUser, setEditingUser] = useState<any>(null);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
fullName: '',
|
||||||
|
email: '',
|
||||||
|
roleCode: '',
|
||||||
|
status: 'active',
|
||||||
|
isActive: true,
|
||||||
|
mobileNumber: '',
|
||||||
|
department: '',
|
||||||
|
designation: '',
|
||||||
|
employeeId: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const usersRes = await adminService.getAllUsers() as any;
|
||||||
|
const rolesRes = await masterService.getRoles() as any;
|
||||||
|
|
||||||
|
if (usersRes.success) setUsers(usersRes.data);
|
||||||
|
if (rolesRes.success) setRoles(rolesRes.data);
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to load user management data');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditUser = (user: any) => {
|
||||||
|
setEditingUser(user);
|
||||||
|
setFormData({
|
||||||
|
fullName: user.fullName || '',
|
||||||
|
email: user.email || '',
|
||||||
|
roleCode: user.roleCode || '',
|
||||||
|
status: user.status || 'active',
|
||||||
|
isActive: user.isActive ?? true,
|
||||||
|
mobileNumber: user.mobileNumber || '',
|
||||||
|
department: user.department || '',
|
||||||
|
designation: user.designation || '',
|
||||||
|
employeeId: user.employeeId || ''
|
||||||
|
});
|
||||||
|
setShowUserModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formData.fullName || !formData.email || !formData.roleCode) {
|
||||||
|
toast.error('Please fill in all required fields');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (editingUser) {
|
||||||
|
const res = await adminService.updateUser(editingUser.id, formData);
|
||||||
|
if (res.success) {
|
||||||
|
setShowUserModal(false);
|
||||||
|
fetchData();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Implementation for Create User can be added here
|
||||||
|
toast.info('Create user functionality coming soon');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Operation failed');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleUserStatus = async (user: any) => {
|
||||||
|
const newStatus = user.status === 'active' ? 'inactive' : 'active';
|
||||||
|
const newActive = !user.isActive;
|
||||||
|
|
||||||
|
const res = await adminService.updateUserStatus(user.id, newStatus, newActive);
|
||||||
|
if (res.success) {
|
||||||
|
fetchData();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredUsers = users.filter(user => {
|
||||||
|
const matchesSearch =
|
||||||
|
user.fullName?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
user.email?.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
user.employeeId?.toLowerCase().includes(searchQuery.toLowerCase());
|
||||||
|
|
||||||
|
const matchesRole = roleFilter === 'all' || user.roleCode === roleFilter;
|
||||||
|
const matchesStatus = statusFilter === 'all' || user.status === statusFilter;
|
||||||
|
|
||||||
|
return matchesSearch && matchesRole && matchesStatus;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6 max-w-7xl mx-auto py-6 px-4">
|
||||||
|
{/* Header section */}
|
||||||
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-slate-900 flex items-center gap-2">
|
||||||
|
<Users className="w-6 h-6 text-amber-600" />
|
||||||
|
User Management
|
||||||
|
</h1>
|
||||||
|
<p className="text-slate-500">Manage system users, roles, and access permissions.</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setEditingUser(null); setFormData({
|
||||||
|
fullName: '', email: '', roleCode: '', status: 'active', isActive: true,
|
||||||
|
mobileNumber: '', department: '', designation: '', employeeId: ''
|
||||||
|
}); setShowUserModal(true);
|
||||||
|
}}
|
||||||
|
className="bg-amber-600 hover:bg-amber-700 text-white shrink-0"
|
||||||
|
>
|
||||||
|
<UserPlus className="w-4 h-4 mr-2" />
|
||||||
|
Add New User
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filters section */}
|
||||||
|
<Card className="border-slate-200">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search by name, email, ID..."
|
||||||
|
className="pl-10"
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Select value={roleFilter} onValueChange={setRoleFilter}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<Filter className="w-4 h-4 mr-2 text-slate-400" />
|
||||||
|
<SelectValue placeholder="Filter by Role" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All Roles</SelectItem>
|
||||||
|
{roles.map(role => (
|
||||||
|
<SelectItem key={role.id} value={role.roleCode}>{role.roleName}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Filter by Status" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">All Status</SelectItem>
|
||||||
|
<SelectItem value="active">Active</SelectItem>
|
||||||
|
<SelectItem value="inactive">Inactive</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Button variant="outline" onClick={fetchData} disabled={loading}>
|
||||||
|
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Users Table */}
|
||||||
|
<Card className="border-slate-200 overflow-hidden">
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<Table>
|
||||||
|
<TableHeader className="bg-slate-50">
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>User Information</TableHead>
|
||||||
|
<TableHead>Account Details</TableHead>
|
||||||
|
<TableHead>Role & Department</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{loading ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} className="h-40 text-center">
|
||||||
|
<div className="flex flex-col items-center justify-center space-y-2 text-slate-500">
|
||||||
|
<RefreshCw className="w-8 h-8 animate-spin" />
|
||||||
|
<p>Loading users...</p>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : filteredUsers.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={5} className="h-40 text-center text-slate-500">
|
||||||
|
<Users className="w-12 h-12 mx-auto mb-2 opacity-20" />
|
||||||
|
No users found matching your criteria.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
filteredUsers.map((user) => (
|
||||||
|
<TableRow key={user.id} className="hover:bg-slate-50/50">
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-full bg-amber-100 text-amber-700 flex items-center justify-center font-bold">
|
||||||
|
{user.fullName?.charAt(0) || user.email?.charAt(0)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-slate-900">{user.fullName}</div>
|
||||||
|
<div className="text-xs text-slate-500 flex items-center gap-1">
|
||||||
|
<Mail className="w-3 h-3" />
|
||||||
|
{user.email}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="text-sm font-medium">ID: {user.employeeId || 'N/A'}</div>
|
||||||
|
<div className="text-xs text-slate-500 flex items-center gap-1">
|
||||||
|
<Phone className="w-3 h-3" />
|
||||||
|
{user.mobileNumber || 'No phone'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Badge variant="outline" className="bg-purple-50 text-purple-700 border-purple-200">
|
||||||
|
<Shield className="w-3 h-3 mr-1" />
|
||||||
|
{user.roleCode}
|
||||||
|
</Badge>
|
||||||
|
<div className="text-xs text-slate-500">{user.department || 'No department'}</div>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex flex-col items-start gap-1">
|
||||||
|
<Badge
|
||||||
|
variant={user.status === 'active' ? 'default' : 'destructive'}
|
||||||
|
className={`text-xs ${user.status === 'active' ? 'bg-green-600 hover:bg-green-700' : ''}`}
|
||||||
|
>
|
||||||
|
{user.status === 'active' ? (
|
||||||
|
<CheckCircle className="w-3 h-3 mr-1" />
|
||||||
|
) : (
|
||||||
|
<XCircle className="w-3 h-3 mr-1" />
|
||||||
|
)}
|
||||||
|
{user.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={user.isActive}
|
||||||
|
onCheckedChange={() => toggleUserStatus(user)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button variant="ghost" size="icon" onClick={() => handleEditUser(user)}>
|
||||||
|
<Edit2 className="w-4 h-4 text-slate-400 hover:text-amber-600" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon">
|
||||||
|
<Trash2 className="w-4 h-4 text-slate-400 hover:text-red-600" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* User Edit/Add Modal */}
|
||||||
|
<Dialog open={showUserModal} onOpenChange={setShowUserModal}>
|
||||||
|
<DialogContent className="max-w-2xl bg-white">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{editingUser ? 'Edit User' : 'Add New User'}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Modify user profiles and system access settings.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4 py-4">
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="fullName">Full Name *</Label>
|
||||||
|
<Input
|
||||||
|
id="fullName"
|
||||||
|
value={formData.fullName}
|
||||||
|
onChange={(e) => setFormData({ ...formData, fullName: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="email">Email Address *</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="employeeId">Employee ID</Label>
|
||||||
|
<Input
|
||||||
|
id="employeeId"
|
||||||
|
value={formData.employeeId}
|
||||||
|
onChange={(e) => setFormData({ ...formData, employeeId: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="roleCode">Role *</Label>
|
||||||
|
<Select
|
||||||
|
value={formData.roleCode}
|
||||||
|
onValueChange={(val) => setFormData({ ...formData, roleCode: val })}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="roleCode">
|
||||||
|
<SelectValue placeholder="Select a role" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{roles.map(role => (
|
||||||
|
<SelectItem key={role.id} value={role.roleCode}>{role.roleName}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="department">Department</Label>
|
||||||
|
<Input
|
||||||
|
id="department"
|
||||||
|
value={formData.department}
|
||||||
|
onChange={(e) => setFormData({ ...formData, department: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="designation">Designation</Label>
|
||||||
|
<Input
|
||||||
|
id="designation"
|
||||||
|
value={formData.designation}
|
||||||
|
onChange={(e) => setFormData({ ...formData, designation: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="mobileNumber">Mobile Number</Label>
|
||||||
|
<Input
|
||||||
|
id="mobileNumber"
|
||||||
|
value={formData.mobileNumber}
|
||||||
|
onChange={(e) => setFormData({ ...formData, mobileNumber: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label htmlFor="status">Account Status</Label>
|
||||||
|
<Select
|
||||||
|
value={formData.status}
|
||||||
|
onValueChange={(val) => setFormData({ ...formData, status: val })}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="status">
|
||||||
|
<SelectValue placeholder="Select status" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="active">Active</SelectItem>
|
||||||
|
<SelectItem value="inactive">Inactive</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="bg-slate-50 -mx-6 -mb-6 p-4 rounded-b-lg">
|
||||||
|
<Button variant="outline" onClick={() => setShowUserModal(false)}>Cancel</Button>
|
||||||
|
<Button className="bg-amber-600 hover:bg-amber-700 text-white" onClick={handleSubmit}>
|
||||||
|
{editingUser ? 'Save Changes' : 'Create User'}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -16,6 +16,7 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
|||||||
const [rememberMe, setRememberMe] = useState(false);
|
const [rememberMe, setRememberMe] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [showForgotPassword, setShowForgotPassword] = useState(false);
|
const [showForgotPassword, setShowForgotPassword] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
|
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
const copyToClipboard = async (text: string, index: number) => {
|
const copyToClipboard = async (text: string, index: number) => {
|
||||||
@ -30,7 +31,7 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Clipboard API blocked, try fallback method
|
// Clipboard API blocked, try fallback method
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback method for older browsers or blocked clipboard
|
// Fallback method for older browsers or blocked clipboard
|
||||||
try {
|
try {
|
||||||
const textArea = document.createElement('textarea');
|
const textArea = document.createElement('textarea');
|
||||||
@ -41,10 +42,10 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
|||||||
document.body.appendChild(textArea);
|
document.body.appendChild(textArea);
|
||||||
textArea.focus();
|
textArea.focus();
|
||||||
textArea.select();
|
textArea.select();
|
||||||
|
|
||||||
const successful = document.execCommand('copy');
|
const successful = document.execCommand('copy');
|
||||||
document.body.removeChild(textArea);
|
document.body.removeChild(textArea);
|
||||||
|
|
||||||
if (successful) {
|
if (successful) {
|
||||||
setCopiedIndex(index);
|
setCopiedIndex(index);
|
||||||
setTimeout(() => setCopiedIndex(null), 2000);
|
setTimeout(() => setCopiedIndex(null), 2000);
|
||||||
@ -54,17 +55,24 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const quickLogin = (userEmail: string, userPassword: string) => {
|
const quickLogin = async (userEmail: string, userPassword: string) => {
|
||||||
setEmail(userEmail);
|
setEmail(userEmail);
|
||||||
setPassword(userPassword);
|
setPassword(userPassword);
|
||||||
// Auto-submit after a short delay
|
setError('');
|
||||||
setTimeout(() => {
|
setIsLoading(true);
|
||||||
onLogin(userEmail, userPassword);
|
try {
|
||||||
}, 100);
|
await onLogin(userEmail, userPassword);
|
||||||
|
} catch (err) {
|
||||||
|
setError('Auto-login failed');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (isLoading) return;
|
||||||
|
|
||||||
setError('');
|
setError('');
|
||||||
|
|
||||||
if (!email || !password) {
|
if (!email || !password) {
|
||||||
@ -72,13 +80,14 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple validation for demo
|
setIsLoading(true);
|
||||||
if (password.length < 6) {
|
try {
|
||||||
setError('Invalid credentials');
|
await onLogin(email, password);
|
||||||
return;
|
} catch (err) {
|
||||||
|
setError('An unexpected error occurred');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
onLogin(email, password);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleForgotPassword = (e: React.FormEvent) => {
|
const handleForgotPassword = (e: React.FormEvent) => {
|
||||||
@ -103,8 +112,8 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
|||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<div className="inline-flex items-center justify-center w-20 h-20 bg-amber-600 rounded-full mb-4">
|
<div className="inline-flex items-center justify-center w-20 h-20 bg-amber-600 rounded-full mb-4">
|
||||||
<svg viewBox="0 0 24 24" className="w-12 h-12 text-white" fill="currentColor">
|
<svg viewBox="0 0 24 24" className="w-12 h-12 text-white" fill="currentColor">
|
||||||
<path d="M12 2L4 6v6c0 5.5 3.8 10.7 8 12 4.2-1.3 8-6.5 8-12V6l-8-4zm0 2.2l6 3v4.8c0 4.5-3.1 8.7-6 10-2.9-1.3-6-5.5-6-10V7.2l6-3z"/>
|
<path d="M12 2L4 6v6c0 5.5 3.8 10.7 8 12 4.2-1.3 8-6.5 8-12V6l-8-4zm0 2.2l6 3v4.8c0 4.5-3.1 8.7-6 10-2.9-1.3-6-5.5-6-10V7.2l6-3z" />
|
||||||
<circle cx="12" cy="12" r="3"/>
|
<circle cx="12" cy="12" r="3" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-white mb-2">Royal Enfield</h1>
|
<h1 className="text-white mb-2">Royal Enfield</h1>
|
||||||
@ -124,6 +133,7 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
|||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -136,6 +146,7 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
|||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -145,6 +156,7 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
|||||||
id="remember"
|
id="remember"
|
||||||
checked={rememberMe}
|
checked={rememberMe}
|
||||||
onCheckedChange={(checked) => setRememberMe(checked as boolean)}
|
onCheckedChange={(checked) => setRememberMe(checked as boolean)}
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="remember" className="cursor-pointer">
|
<Label htmlFor="remember" className="cursor-pointer">
|
||||||
Remember Me
|
Remember Me
|
||||||
@ -153,7 +165,8 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowForgotPassword(true)}
|
onClick={() => setShowForgotPassword(true)}
|
||||||
className="text-amber-600 hover:text-amber-700"
|
className="text-amber-600 hover:text-amber-700 disabled:opacity-50"
|
||||||
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
Forgot Password?
|
Forgot Password?
|
||||||
</button>
|
</button>
|
||||||
@ -162,12 +175,23 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
|||||||
{error && (
|
{error && (
|
||||||
<div className="flex items-center gap-2 p-3 bg-red-50 border border-red-200 rounded-md">
|
<div className="flex items-center gap-2 p-3 bg-red-50 border border-red-200 rounded-md">
|
||||||
<AlertCircle className="w-4 h-4 text-red-600" />
|
<AlertCircle className="w-4 h-4 text-red-600" />
|
||||||
<span className="text-red-600">{error}</span>
|
<span className="text-red-600 font-medium text-sm">{error}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button type="submit" className="w-full bg-amber-600 hover:bg-amber-700">
|
<Button
|
||||||
Login
|
type="submit"
|
||||||
|
className="w-full bg-amber-600 hover:bg-amber-700 h-11"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center gap-2">
|
||||||
|
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||||
|
<span>Logging in...</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
'Login'
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -7,15 +7,17 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '../ui/dropdown-menu';
|
} from '../ui/dropdown-menu';
|
||||||
import { Badge } from '../ui/badge';
|
import { Badge } from '../ui/badge';
|
||||||
import { User } from '../../lib/mock-data';
|
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { RootState } from '../../store';
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
title: string;
|
title: string;
|
||||||
currentUser?: User | null;
|
|
||||||
onRefresh?: () => void;
|
onRefresh?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Header({ title, currentUser, onRefresh }: HeaderProps) {
|
export function Header({ title, onRefresh }: HeaderProps) {
|
||||||
|
const { user: currentUser } = useSelector((state: RootState) => state.auth);
|
||||||
const notifications = [
|
const notifications = [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
@ -84,8 +86,8 @@ export function Header({ title, currentUser, onRefresh }: HeaderProps) {
|
|||||||
<Button variant="outline" size="icon" className="relative">
|
<Button variant="outline" size="icon" className="relative">
|
||||||
<Bell className="w-4 h-4" />
|
<Bell className="w-4 h-4" />
|
||||||
{unreadCount > 0 && (
|
{unreadCount > 0 && (
|
||||||
<Badge
|
<Badge
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
className="absolute -top-1 -right-1 w-5 h-5 p-0 flex items-center justify-center text-xs"
|
className="absolute -top-1 -right-1 w-5 h-5 p-0 flex items-center justify-center text-xs"
|
||||||
>
|
>
|
||||||
{unreadCount}
|
{unreadCount}
|
||||||
@ -99,11 +101,10 @@ export function Header({ title, currentUser, onRefresh }: HeaderProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="max-h-96 overflow-y-auto">
|
<div className="max-h-96 overflow-y-auto">
|
||||||
{notifications.map((notification) => (
|
{notifications.map((notification) => (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
key={notification.id}
|
key={notification.id}
|
||||||
className={`p-3 cursor-pointer ${
|
className={`p-3 cursor-pointer ${notification.unread ? 'bg-amber-50' : ''
|
||||||
notification.unread ? 'bg-amber-50' : ''
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="text-slate-900">{notification.message}</p>
|
<p className="text-slate-900">{notification.message}</p>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
FileText,
|
FileText,
|
||||||
LogOut,
|
LogOut,
|
||||||
Users,
|
Users,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
@ -16,18 +16,19 @@ import {
|
|||||||
MapPin
|
MapPin
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { RootState } from '../../store';
|
||||||
import { Input } from '../ui/input';
|
import { Input } from '../ui/input';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { User } from '../../lib/mock-data';
|
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
activeView: string;
|
activeView: string;
|
||||||
onNavigate: (view: string) => void;
|
onNavigate: (view: string) => void;
|
||||||
onLogout: () => void;
|
onLogout: () => void;
|
||||||
currentUser: User | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Sidebar({ activeView, onNavigate, onLogout, currentUser }: SidebarProps) {
|
export function Sidebar({ activeView, onNavigate, onLogout }: SidebarProps) {
|
||||||
|
const { user: currentUser } = useSelector((state: RootState) => state.auth);
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [offboardingExpanded, setOffboardingExpanded] = useState(false);
|
const [offboardingExpanded, setOffboardingExpanded] = useState(false);
|
||||||
@ -46,9 +47,9 @@ export function Sidebar({ activeView, onNavigate, onLogout, currentUser }: Sideb
|
|||||||
] : [
|
] : [
|
||||||
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
||||||
{ id: 'applications', label: 'Dealership Requests', icon: FileText },
|
{ id: 'applications', label: 'Dealership Requests', icon: FileText },
|
||||||
{
|
{
|
||||||
id: 'offboarding',
|
id: 'offboarding',
|
||||||
label: 'Offboarding',
|
label: 'Offboarding',
|
||||||
icon: UserMinus,
|
icon: UserMinus,
|
||||||
hasSubmenu: true,
|
hasSubmenu: true,
|
||||||
submenuKey: 'offboarding',
|
submenuKey: 'offboarding',
|
||||||
@ -69,9 +70,9 @@ export function Sidebar({ activeView, onNavigate, onLogout, currentUser }: Sideb
|
|||||||
|
|
||||||
// Add All Requests for DD Lead role (before Dealership Requests)
|
// Add All Requests for DD Lead role (before Dealership Requests)
|
||||||
if (currentUser?.role === 'DD Lead') {
|
if (currentUser?.role === 'DD Lead') {
|
||||||
menuItems.splice(1, 0, {
|
menuItems.splice(1, 0, {
|
||||||
id: 'all-requests',
|
id: 'all-requests',
|
||||||
label: 'All Requests',
|
label: 'All Requests',
|
||||||
icon: FolderOpen,
|
icon: FolderOpen,
|
||||||
hasSubmenu: true,
|
hasSubmenu: true,
|
||||||
submenuKey: 'allRequests',
|
submenuKey: 'allRequests',
|
||||||
@ -86,7 +87,7 @@ export function Sidebar({ activeView, onNavigate, onLogout, currentUser }: Sideb
|
|||||||
if (currentUser?.role === 'Super Admin' || currentUser?.role === 'DD Admin' || currentUser?.role === 'DD Lead') {
|
if (currentUser?.role === 'Super Admin' || currentUser?.role === 'DD Admin' || currentUser?.role === 'DD Lead') {
|
||||||
menuItems.push({ id: 'master', label: 'Master', icon: Settings });
|
menuItems.push({ id: 'master', label: 'Master', icon: Settings });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentUser?.role === 'Super Admin') {
|
if (currentUser?.role === 'Super Admin') {
|
||||||
menuItems.push({ id: 'users', label: 'User Management', icon: Users });
|
menuItems.push({ id: 'users', label: 'User Management', icon: Users });
|
||||||
}
|
}
|
||||||
@ -101,10 +102,9 @@ export function Sidebar({ activeView, onNavigate, onLogout, currentUser }: Sideb
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`bg-slate-900 text-white h-screen flex flex-col transition-all duration-300 ${
|
className={`bg-slate-900 text-white h-screen flex flex-col transition-all duration-300 ${collapsed ? 'w-20' : 'w-64'
|
||||||
collapsed ? 'w-20' : 'w-64'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{/* Header with Logo */}
|
{/* Header with Logo */}
|
||||||
<div className="p-4 border-b border-slate-800">
|
<div className="p-4 border-b border-slate-800">
|
||||||
@ -113,8 +113,8 @@ export function Sidebar({ activeView, onNavigate, onLogout, currentUser }: Sideb
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-10 h-10 bg-amber-600 rounded-lg flex items-center justify-center">
|
<div className="w-10 h-10 bg-amber-600 rounded-lg flex items-center justify-center">
|
||||||
<svg viewBox="0 0 24 24" className="w-6 h-6 text-white" fill="currentColor">
|
<svg viewBox="0 0 24 24" className="w-6 h-6 text-white" fill="currentColor">
|
||||||
<path d="M12 2L4 6v6c0 5.5 3.8 10.7 8 12 4.2-1.3 8-6.5 8-12V6l-8-4zm0 2.2l6 3v4.8c0 4.5-3.1 8.7-6 10-2.9-1.3-6-5.5-6-10V7.2l6-3z"/>
|
<path d="M12 2L4 6v6c0 5.5 3.8 10.7 8 12 4.2-1.3 8-6.5 8-12V6l-8-4zm0 2.2l6 3v4.8c0 4.5-3.1 8.7-6 10-2.9-1.3-6-5.5-6-10V7.2l6-3z" />
|
||||||
<circle cx="12" cy="12" r="3"/>
|
<circle cx="12" cy="12" r="3" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-amber-600">RE Dealer</span>
|
<span className="text-amber-600">RE Dealer</span>
|
||||||
@ -156,12 +156,12 @@ export function Sidebar({ activeView, onNavigate, onLogout, currentUser }: Sideb
|
|||||||
const isActive = activeView === item.id;
|
const isActive = activeView === item.id;
|
||||||
const hasSubmenu = item.hasSubmenu;
|
const hasSubmenu = item.hasSubmenu;
|
||||||
const isSubmenuActive = hasSubmenu && item.submenu?.some(sub => activeView === sub.id);
|
const isSubmenuActive = hasSubmenu && item.submenu?.some(sub => activeView === sub.id);
|
||||||
|
|
||||||
// Determine which submenu is expanded based on submenuKey
|
// Determine which submenu is expanded based on submenuKey
|
||||||
const submenuKey = (item as any).submenuKey;
|
const submenuKey = (item as any).submenuKey;
|
||||||
const isExpanded = submenuKey === 'offboarding' ? offboardingExpanded :
|
const isExpanded = submenuKey === 'offboarding' ? offboardingExpanded :
|
||||||
submenuKey === 'allRequests' ? allRequestsExpanded : false;
|
submenuKey === 'allRequests' ? allRequestsExpanded : false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={item.id}>
|
<div key={item.id}>
|
||||||
<button
|
<button
|
||||||
@ -176,11 +176,10 @@ export function Sidebar({ activeView, onNavigate, onLogout, currentUser }: Sideb
|
|||||||
onNavigate(item.id);
|
onNavigate(item.id);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${
|
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive || isSubmenuActive
|
||||||
isActive || isSubmenuActive
|
? 'bg-amber-600 text-white'
|
||||||
? 'bg-amber-600 text-white'
|
: 'text-slate-300 hover:bg-slate-800 hover:text-white'
|
||||||
: 'text-slate-300 hover:bg-slate-800 hover:text-white'
|
}`}
|
||||||
}`}
|
|
||||||
title={collapsed ? item.label : undefined}
|
title={collapsed ? item.label : undefined}
|
||||||
>
|
>
|
||||||
<Icon className="w-5 h-5 flex-shrink-0" />
|
<Icon className="w-5 h-5 flex-shrink-0" />
|
||||||
@ -197,7 +196,7 @@ export function Sidebar({ activeView, onNavigate, onLogout, currentUser }: Sideb
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Submenu */}
|
{/* Submenu */}
|
||||||
{hasSubmenu && isExpanded && !collapsed && (
|
{hasSubmenu && isExpanded && !collapsed && (
|
||||||
<div className="ml-4 mt-2 space-y-1">
|
<div className="ml-4 mt-2 space-y-1">
|
||||||
@ -207,11 +206,10 @@ export function Sidebar({ activeView, onNavigate, onLogout, currentUser }: Sideb
|
|||||||
<button
|
<button
|
||||||
key={subItem.id}
|
key={subItem.id}
|
||||||
onClick={() => onNavigate(subItem.id)}
|
onClick={() => onNavigate(subItem.id)}
|
||||||
className={`w-full flex items-center gap-3 px-4 py-2 rounded-lg transition-colors text-sm ${
|
className={`w-full flex items-center gap-3 px-4 py-2 rounded-lg transition-colors text-sm ${isSubActive
|
||||||
isSubActive
|
? 'bg-amber-600 text-white'
|
||||||
? 'bg-amber-600 text-white'
|
: 'text-slate-400 hover:bg-slate-800 hover:text-white'
|
||||||
: 'text-slate-400 hover:bg-slate-800 hover:text-white'
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<span className="w-1 h-1 rounded-full bg-current flex-shrink-0" />
|
<span className="w-1 h-1 rounded-full bg-current flex-shrink-0" />
|
||||||
<span>{subItem.label}</span>
|
<span>{subItem.label}</span>
|
||||||
@ -240,13 +238,12 @@ export function Sidebar({ activeView, onNavigate, onLogout, currentUser }: Sideb
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={onLogout}
|
onClick={onLogout}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className={`w-full ${
|
className={`w-full ${collapsed ? 'px-2' : 'justify-start'
|
||||||
collapsed ? 'px-2' : 'justify-start'
|
} text-slate-300 hover:bg-slate-800 hover:text-white`}
|
||||||
} text-slate-300 hover:bg-slate-800 hover:text-white`}
|
|
||||||
title={collapsed ? 'Logout' : undefined}
|
title={collapsed ? 'Logout' : undefined}
|
||||||
>
|
>
|
||||||
<LogOut className="w-5 h-5 flex-shrink-0" />
|
<LogOut className="w-5 h-5 flex-shrink-0" />
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
// Mock data for the dealership onboarding system
|
// Mock data for the dealership onboarding system
|
||||||
|
|
||||||
export type UserRole =
|
export type UserRole =
|
||||||
| 'DD-ZM'
|
| 'DD-ZM'
|
||||||
| 'RBM'
|
| 'RBM'
|
||||||
| 'DD'
|
| 'DD'
|
||||||
| 'ZBH'
|
| 'ZBH'
|
||||||
| 'DD Lead'
|
| 'DD Lead'
|
||||||
| 'DD Head'
|
| 'DD Head'
|
||||||
| 'NBH'
|
| 'NBH'
|
||||||
| 'DD Admin'
|
| 'DD Admin'
|
||||||
| 'Legal Admin'
|
| 'Legal Admin'
|
||||||
| 'Super Admin'
|
| 'Super Admin'
|
||||||
| 'DD AM'
|
| 'DD AM'
|
||||||
| 'FDD'
|
| 'FDD'
|
||||||
@ -18,7 +18,7 @@ export type UserRole =
|
|||||||
| 'Finance Admin'
|
| 'Finance Admin'
|
||||||
| 'Dealer';
|
| 'Dealer';
|
||||||
|
|
||||||
export type ApplicationStatus =
|
export type ApplicationStatus =
|
||||||
| 'Submitted'
|
| 'Submitted'
|
||||||
| 'Questionnaire Pending'
|
| 'Questionnaire Pending'
|
||||||
| 'Questionnaire Completed'
|
| 'Questionnaire Completed'
|
||||||
@ -172,6 +172,20 @@ export const mockUsers: User[] = [
|
|||||||
password: 'password',
|
password: 'password',
|
||||||
role: 'Dealer',
|
role: 'Dealer',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: '15',
|
||||||
|
name: 'Laxman H',
|
||||||
|
email: 'admin@royalenfield.com',
|
||||||
|
password: 'Admin@123',
|
||||||
|
role: 'DD Lead',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '16',
|
||||||
|
name: 'Yashwin',
|
||||||
|
email: 'yashwin@royalenfield.com',
|
||||||
|
password: 'password',
|
||||||
|
role: 'ZBH',
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
// Mock current user (default)
|
// Mock current user (default)
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import { Provider } from 'react-redux'
|
||||||
|
import { store } from './store'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
import './styles/globals.css'
|
import './styles/globals.css'
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<Provider store={store}>
|
||||||
|
<App />
|
||||||
|
</Provider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
)
|
)
|
||||||
|
|||||||
43
src/services/admin.service.ts
Normal file
43
src/services/admin.service.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import API from '../api/API';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export const adminService = {
|
||||||
|
async getAllUsers() {
|
||||||
|
try {
|
||||||
|
const response = await API.getUsers() as any;
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error fetching users:', error);
|
||||||
|
toast.error(error.response?.data?.message || 'Failed to fetch users');
|
||||||
|
return { success: false, data: [] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateUser(id: string, userData: any) {
|
||||||
|
try {
|
||||||
|
const response = await API.updateUser(id, userData) as any;
|
||||||
|
if (response.success) {
|
||||||
|
toast.success(response.message || 'User updated successfully');
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error updating user:', error);
|
||||||
|
toast.error(error.response?.data?.message || 'Failed to update user');
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateUserStatus(id: string, status: string, isActive: boolean) {
|
||||||
|
try {
|
||||||
|
const response = await API.updateUserStatus(id, { status, isActive }) as any;
|
||||||
|
if (response.success) {
|
||||||
|
toast.success(response.message || 'User status updated');
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error updating status:', error);
|
||||||
|
toast.error(error.response?.data?.message || 'Failed to update status');
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
45
src/services/master.service.ts
Normal file
45
src/services/master.service.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { API } from '../api/API';
|
||||||
|
|
||||||
|
export const masterService = {
|
||||||
|
// Roles & Permissions
|
||||||
|
getRoles: async () => {
|
||||||
|
const response = await API.getRoles();
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
getPermissions: async () => {
|
||||||
|
const response = await API.getPermissions();
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
updateRole: async (id: string, data: any) => {
|
||||||
|
const response = await API.updateRole(id, data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Zones & Regions
|
||||||
|
getZones: async () => {
|
||||||
|
const response = await API.getZones();
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
getRegions: async () => {
|
||||||
|
const response = await API.getRegions();
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
getStates: async (zoneId?: string) => {
|
||||||
|
const response = await API.getStates(zoneId);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
getDistricts: async (stateId?: string) => {
|
||||||
|
const response = await API.getDistricts(stateId);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
// User Management
|
||||||
|
getUsers: async () => {
|
||||||
|
const response = await API.getUsers();
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
updateUser: async (id: string, data: any) => {
|
||||||
|
const response = await API.updateUser(id, data);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
};
|
||||||
11
src/store/index.ts
Normal file
11
src/store/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
|
import authReducer from './slices/authSlice';
|
||||||
|
|
||||||
|
export const store = configureStore({
|
||||||
|
reducer: {
|
||||||
|
auth: authReducer,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export type RootState = ReturnType<typeof store.getState>;
|
||||||
|
export type AppDispatch = typeof store.dispatch;
|
||||||
99
src/store/slices/authSlice.ts
Normal file
99
src/store/slices/authSlice.ts
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import { User } from '../../lib/mock-data';
|
||||||
|
import { API } from '../../api/API';
|
||||||
|
|
||||||
|
interface AuthState {
|
||||||
|
user: User | null;
|
||||||
|
token: string | null;
|
||||||
|
isAuthenticated: boolean;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: AuthState = {
|
||||||
|
user: null,
|
||||||
|
token: localStorage.getItem('token'),
|
||||||
|
isAuthenticated: false,
|
||||||
|
loading: true,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const initializeAuth = createAsyncThunk(
|
||||||
|
'auth/initializeAuth',
|
||||||
|
async (_, { rejectWithValue }) => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (!token) {
|
||||||
|
return rejectWithValue('No token found');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await API.getCurrentUser();
|
||||||
|
if (response.ok && response.data) {
|
||||||
|
const { user } = response.data as any;
|
||||||
|
return {
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
name: user.fullName || user.email.split('@')[0],
|
||||||
|
email: user.email,
|
||||||
|
password: '',
|
||||||
|
role: typeof user.role === 'string' ? user.role : (user.roleCode || 'User')
|
||||||
|
} as User,
|
||||||
|
token
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return rejectWithValue('Failed to fetch user');
|
||||||
|
} catch (error: any) {
|
||||||
|
return rejectWithValue(error.message || 'Session restoration failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const authSlice = createSlice({
|
||||||
|
name: 'auth',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setCredentials: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<{ user: User; token: string }>
|
||||||
|
) => {
|
||||||
|
state.user = action.payload.user;
|
||||||
|
state.token = action.payload.token;
|
||||||
|
state.isAuthenticated = true;
|
||||||
|
state.loading = false;
|
||||||
|
localStorage.setItem('token', action.payload.token);
|
||||||
|
},
|
||||||
|
logout: (state) => {
|
||||||
|
state.user = null;
|
||||||
|
state.token = null;
|
||||||
|
state.isAuthenticated = false;
|
||||||
|
state.loading = false;
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
},
|
||||||
|
setLoading: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.loading = action.payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
extraReducers: (builder) => {
|
||||||
|
builder
|
||||||
|
.addCase(initializeAuth.pending, (state) => {
|
||||||
|
state.loading = true;
|
||||||
|
})
|
||||||
|
.addCase(initializeAuth.fulfilled, (state, action) => {
|
||||||
|
state.user = action.payload.user;
|
||||||
|
state.token = action.payload.token;
|
||||||
|
state.isAuthenticated = true;
|
||||||
|
state.loading = false;
|
||||||
|
})
|
||||||
|
.addCase(initializeAuth.rejected, (state) => {
|
||||||
|
state.user = null;
|
||||||
|
state.token = null;
|
||||||
|
state.isAuthenticated = false;
|
||||||
|
state.loading = false;
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setCredentials, logout, setLoading } = authSlice.actions;
|
||||||
|
|
||||||
|
export default authSlice.reducer;
|
||||||
Loading…
Reference in New Issue
Block a user