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-toggle": "^1.0.3",
|
||||
"@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",
|
||||
"clsx": "^2.1.0",
|
||||
"cmdk": "^1.0.0",
|
||||
@ -46,6 +49,7 @@
|
||||
"react-day-picker": "^8.10.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.51.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-resizable-panels": "^2.0.12",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"recharts": "^2.12.2",
|
||||
@ -2912,6 +2916,32 @@
|
||||
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||
"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": {
|
||||
"version": "1.23.2",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
|
||||
@ -2956,6 +2986,18 @@
|
||||
"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": {
|
||||
"version": "0.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||
@ -3397,6 +3439,12 @@
|
||||
"@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": {
|
||||
"version": "8.53.1",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
@ -3749,6 +3806,12 @@
|
||||
"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": {
|
||||
"version": "10.4.23",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz",
|
||||
@ -3786,6 +3849,17 @@
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"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_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": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@ -3958,6 +4045,18 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@ -4155,6 +4254,15 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
@ -4181,6 +4289,20 @@
|
||||
"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": {
|
||||
"version": "1.5.267",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
|
||||
@ -4230,6 +4352,51 @@
|
||||
"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": {
|
||||
"version": "0.25.12",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||
@ -4566,6 +4733,42 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "5.3.4",
|
||||
"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": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
@ -4617,6 +4829,30 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
||||
@ -4626,6 +4862,19 @@
|
||||
"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": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
@ -4652,6 +4901,18 @@
|
||||
"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": {
|
||||
"version": "4.2.11",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||
@ -4669,6 +4930,45 @@
|
||||
"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": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
|
||||
@ -4696,6 +4996,16 @@
|
||||
"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": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||
@ -5203,6 +5513,36 @@
|
||||
"@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": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
@ -5525,6 +5865,12 @@
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"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": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@ -5596,6 +5942,29 @@
|
||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||
"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",
|
||||
@ -5780,6 +6149,27 @@
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"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-toggle": "^1.0.3",
|
||||
"@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",
|
||||
"clsx": "^2.1.0",
|
||||
"cmdk": "^1.0.0",
|
||||
@ -48,6 +51,7 @@
|
||||
"react-day-picker": "^8.10.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.51.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-resizable-panels": "^2.0.12",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"recharts": "^2.12.2",
|
||||
|
||||
79
src/App.tsx
79
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 { LoginPage } from './components/auth/LoginPage';
|
||||
import { Sidebar } from './components/layout/Sidebar';
|
||||
@ -22,6 +25,7 @@ import { FinanceFnFPage } from './components/applications/FinanceFnFPage';
|
||||
import { FinancePaymentDetailsPage } from './components/applications/FinancePaymentDetailsPage';
|
||||
import { FinanceFnFDetailsPage } from './components/applications/FinanceFnFDetailsPage';
|
||||
import { MasterPage } from './components/applications/MasterPage';
|
||||
import { UserManagementPage } from './components/admin/UserManagementPage';
|
||||
import { ConstitutionalChangePage } from './components/applications/ConstitutionalChangePage';
|
||||
import { ConstitutionalChangeDetails } from './components/applications/ConstitutionalChangeDetails';
|
||||
import { RelocationRequestPage } from './components/applications/RelocationRequestPage';
|
||||
@ -31,15 +35,16 @@ import { DealerResignationPage } from './components/dealer/DealerResignationPage
|
||||
import { DealerConstitutionalChangePage } from './components/dealer/DealerConstitutionalChangePage';
|
||||
import { DealerRelocationPage } from './components/dealer/DealerRelocationPage';
|
||||
import { Toaster } from './components/ui/sonner';
|
||||
import { mockUsers, User } from './lib/mock-data';
|
||||
import { User } from './lib/mock-data';
|
||||
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';
|
||||
|
||||
export default function App() {
|
||||
const dispatch = useDispatch<any>();
|
||||
const { user: currentUser, isAuthenticated, loading } = useSelector((state: RootState) => state.auth);
|
||||
const [showAdminLogin, setShowAdminLogin] = useState(false);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
||||
const [currentView, setCurrentView] = useState<View>('dashboard');
|
||||
const [selectedApplicationId, setSelectedApplicationId] = useState<string | null>(null);
|
||||
const [selectedResignationId, setSelectedResignationId] = useState<string | null>(null);
|
||||
@ -56,22 +61,46 @@ export default function App() {
|
||||
requestTitle: string;
|
||||
} | null>(null);
|
||||
|
||||
const handleLogin = (email: string, password: string) => {
|
||||
// Find user in mock data
|
||||
const user = mockUsers.find(u => u.email === email && u.password === password);
|
||||
useEffect(() => {
|
||||
dispatch(initializeAuth());
|
||||
}, [dispatch]);
|
||||
|
||||
if (user) {
|
||||
setCurrentUser(user);
|
||||
setIsAuthenticated(true);
|
||||
toast.success(`Welcome back, ${user.name}! (${user.role})`);
|
||||
const handleLogin = async (email: string, password: string) => {
|
||||
try {
|
||||
const response = await API.login({ email, password });
|
||||
|
||||
if (response.ok && response.data) {
|
||||
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 {
|
||||
toast.error('Invalid credentials');
|
||||
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 = () => {
|
||||
setIsAuthenticated(false);
|
||||
setCurrentUser(null);
|
||||
dispatch(logoutAction());
|
||||
setCurrentView('dashboard');
|
||||
setSelectedApplicationId(null);
|
||||
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
|
||||
if (!isAuthenticated && !showAdminLogin) {
|
||||
return (
|
||||
@ -272,13 +310,11 @@ export default function App() {
|
||||
activeView={currentView}
|
||||
onNavigate={handleNavigate}
|
||||
onLogout={handleLogout}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<Header
|
||||
title={getPageTitle()}
|
||||
currentUser={currentUser}
|
||||
onRefresh={() => window.location.reload()}
|
||||
/>
|
||||
|
||||
@ -442,16 +478,7 @@ export default function App() {
|
||||
)}
|
||||
|
||||
{currentView === 'users' && (
|
||||
<div className="bg-white rounded-lg border border-slate-200 p-8">
|
||||
<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>
|
||||
<UserManagementPage />
|
||||
)}
|
||||
|
||||
{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>
|
||||
);
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { masterService } from '../../services/master.service';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
||||
import { Badge } from '../ui/badge';
|
||||
import { Button } from '../ui/button';
|
||||
@ -238,6 +239,7 @@ interface UserAssignment {
|
||||
email: string;
|
||||
phone: string;
|
||||
status: 'Active' | 'Inactive';
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
export function MasterPage() {
|
||||
@ -268,6 +270,7 @@ export function MasterPage() {
|
||||
const [selectedUserZone, setSelectedUserZone] = useState<string>('');
|
||||
const [selectedUserRegion, setSelectedUserRegion] = useState<string>('');
|
||||
const [selectedUserDistricts, setSelectedUserDistricts] = useState<string[]>([]);
|
||||
const [selectedUserForEdit, setSelectedUserForEdit] = useState<any>(null);
|
||||
|
||||
// Permission management state
|
||||
const [userLookupValue, setUserLookupValue] = useState<string>('');
|
||||
@ -296,18 +299,110 @@ export function MasterPage() {
|
||||
const [zbhName, setZbhName] = useState<string>('');
|
||||
const [zbhEmail, setZbhEmail] = useState<string>('');
|
||||
const [zbhPhone, setZbhPhone] = useState<string>('');
|
||||
const [zonalManagersData, setZonalManagersData] = useState<{[key: number]: ZonalManager}>({});
|
||||
const [zonalManagersData, setZonalManagersData] = useState<{ [key: number]: ZonalManager }>({});
|
||||
|
||||
// Mock data for roles
|
||||
const [roles] = useState<Role[]>([
|
||||
{ id: '1', name: 'DD', permissions: ['view_applications', 'review_level1', 'schedule_interview'], userCount: 25 },
|
||||
{ id: '2', name: 'DD-ZM', permissions: ['view_applications', 'approve_ranking'], userCount: 12 },
|
||||
{ id: '3', name: 'RBM', permissions: ['view_applications', 'review_documents'], userCount: 8 },
|
||||
{ id: '4', name: 'ZBH', permissions: ['view_applications', 'final_approval'], userCount: 5 },
|
||||
{ id: '5', name: 'DD Lead', permissions: ['view_all', 'manage_opportunities', 'assign_users'], userCount: 3 },
|
||||
{ id: '6', name: 'Finance', permissions: ['view_payments', 'verify_payments', 'fnf_settlement'], userCount: 10 },
|
||||
// Real data state
|
||||
const [availablePermissions, setAvailablePermissions] = useState<any[]>([]);
|
||||
const [roles, setRoles] = useState<Role[]>([]);
|
||||
const [zones, setZones] = useState<Zone[]>([]);
|
||||
const [regionalOffices, setRegionalOffices] = useState<RegionalOffice[]>([]);
|
||||
const [asms, setAsms] = useState<ASM[]>([]);
|
||||
const [zonalManagerMappings, setZonalManagerMappings] = useState<ZonalManagerMapping[]>([]);
|
||||
const [userAssignedData, setUserAssignedData] = useState<UserAssignment[]>([]);
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetchInitialData();
|
||||
}, []);
|
||||
|
||||
const fetchInitialData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [rolesRes, zonesRes, permsRes, regionsRes, usersRes] = await Promise.all([
|
||||
masterService.getRoles() as Promise<any>,
|
||||
masterService.getZones() as Promise<any>,
|
||||
masterService.getPermissions() as Promise<any>,
|
||||
masterService.getRegions() as Promise<any>,
|
||||
masterService.getUsers() as Promise<any>
|
||||
]);
|
||||
|
||||
if (rolesRes.success) {
|
||||
setRoles(rolesRes.data.map((r: any) => ({
|
||||
id: r.id,
|
||||
name: r.roleName,
|
||||
permissions: r.permissions?.map((p: any) => p.permissionCode) || [],
|
||||
userCount: r.userCount || 0
|
||||
})));
|
||||
}
|
||||
|
||||
if (zonesRes.success) {
|
||||
setZones(zonesRes.data.map((z: any) => ({
|
||||
id: z.id,
|
||||
code: z.zoneCode,
|
||||
name: z.zoneName,
|
||||
description: z.description || '',
|
||||
states: z.states?.map((s: any) => s.stateName) || [],
|
||||
zmCount: z.managers?.length || 0,
|
||||
zbh: {
|
||||
name: z.zonalBusinessHead?.fullName || 'Not Assigned',
|
||||
email: z.zonalBusinessHead?.email || '',
|
||||
phone: z.zonalBusinessHead?.mobileNumber || ''
|
||||
},
|
||||
zonalManagers: z.managers?.map((m: any) => ({
|
||||
name: m.user?.fullName || 'Unknown',
|
||||
email: m.user?.email || '',
|
||||
phone: m.user?.mobileNumber || '',
|
||||
districts: []
|
||||
})) || []
|
||||
})));
|
||||
}
|
||||
|
||||
if (permsRes.success) {
|
||||
setAvailablePermissions(permsRes.data);
|
||||
}
|
||||
|
||||
if (regionsRes.success) {
|
||||
setRegionalOffices(regionsRes.data?.map((r: any) => ({
|
||||
id: r.id,
|
||||
code: r.regionCode,
|
||||
name: r.regionName,
|
||||
zoneId: r.zoneId,
|
||||
zoneName: r.zone?.zoneName || 'Unknown',
|
||||
states: r.states?.map((s: any) => s.stateName) || [],
|
||||
cities: [],
|
||||
regionalOfficerCount: 0,
|
||||
asmCount: 0,
|
||||
status: r.isActive ? 'Active' : 'Inactive'
|
||||
})));
|
||||
}
|
||||
|
||||
if (usersRes.success) {
|
||||
setUserAssignedData(usersRes.data.map((u: any) => ({
|
||||
id: u.id,
|
||||
name: u.fullName,
|
||||
role: u.role?.roleName || u.roleCode || 'User',
|
||||
roleCode: u.roleCode,
|
||||
region: u.region?.regionName || 'Not Assigned',
|
||||
regionId: u.regionId,
|
||||
zone: u.zone?.zoneName || 'Not Assigned',
|
||||
zoneId: u.zoneId,
|
||||
email: u.email,
|
||||
phone: u.mobileNumber || 'N/A',
|
||||
status: u.isActive ? 'Active' : 'Inactive',
|
||||
employeeId: u.employeeId,
|
||||
permissions: u.role?.permissions?.map((p: any) => p.permissionCode) || []
|
||||
})));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching master data:', error);
|
||||
toast.error('Failed to load configuration data');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Mock data for SLA
|
||||
const [slaConfigs] = useState<SLAConfig[]>([
|
||||
{
|
||||
@ -429,194 +524,9 @@ export function MasterPage() {
|
||||
]);
|
||||
|
||||
// Mock data for locations
|
||||
const [locations] = useState<Location[]>([
|
||||
{ id: '1', state: 'Karnataka', city: 'Bangalore', district: 'Bangalore Urban', activeFrom: '2024-01-01', activeTo: '2025-12-31', status: 'Active' },
|
||||
{ id: '2', state: 'Karnataka', city: 'Mysore', district: 'Mysuru', activeFrom: '2024-02-15', activeTo: '2025-06-30', status: 'Active' },
|
||||
{ id: '3', state: 'Tamil Nadu', city: 'Chennai', district: 'Chennai', activeFrom: '2024-01-01', activeTo: '2025-12-31', status: 'Active' },
|
||||
{ id: '4', state: 'Tamil Nadu', city: 'Coimbatore', district: 'Coimbatore', activeFrom: '2024-03-01', activeTo: '2025-09-30', status: 'Active' },
|
||||
{ id: '5', state: 'Maharashtra', city: 'Mumbai', district: 'Mumbai City', activeFrom: '2024-01-01', activeTo: '2026-12-31', status: 'Active' },
|
||||
{ id: '6', state: 'Maharashtra', city: 'Pune', district: 'Pune', status: 'Active', activeFrom: '2024-04-01', activeTo: '2025-03-31' },
|
||||
{ id: '7', state: 'Delhi', city: 'New Delhi', district: 'Central Delhi', activeFrom: '2024-01-15', activeTo: '2025-12-31', status: 'Active' },
|
||||
{ id: '8', state: 'Rajasthan', city: 'Jaipur', district: 'Jaipur', activeFrom: '2023-06-01', activeTo: '2024-05-31', status: 'Inactive' },
|
||||
]);
|
||||
const [locations] = useState<Location[]>([]);
|
||||
|
||||
// Mock data for zones - Top level geographical divisions
|
||||
const [zones, setZones] = useState<Zone[]>([
|
||||
{
|
||||
id: 'north',
|
||||
code: 'NZ',
|
||||
name: 'North Zone',
|
||||
states: ['Delhi', 'Haryana', 'Punjab', 'Himachal Pradesh', 'Jammu & Kashmir', 'Uttarakhand', 'Uttar Pradesh', 'Rajasthan', 'Chandigarh'],
|
||||
zmCount: 3,
|
||||
description: 'Covers northern states including NCR, UP, Punjab, and Rajasthan',
|
||||
zbh: { name: 'Rajesh Kumar', email: 'rajesh.k@royalenfield.com', phone: '+91 98100 00001' },
|
||||
zonalManagers: [
|
||||
{ name: 'Amit Sharma', email: 'amit.s@royalenfield.com', phone: '+91 98100 00011', districts: ['North Delhi', 'Central Delhi', 'Gurugram', 'Faridabad'] },
|
||||
{ name: 'Priya Gupta', email: 'priya.g@royalenfield.com', phone: '+91 98100 00012', districts: ['Amritsar', 'Ludhiana', 'Jalandhar', 'Patiala'] },
|
||||
{ name: 'Vikram Singh', email: 'vikram.s@royalenfield.com', phone: '+91 98100 00013', districts: ['Lucknow', 'Kanpur', 'Agra', 'Meerut'] }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'south',
|
||||
code: 'SZ',
|
||||
name: 'South Zone',
|
||||
states: ['Karnataka', 'Tamil Nadu', 'Kerala', 'Andhra Pradesh', 'Telangana', 'Puducherry'],
|
||||
zmCount: 4,
|
||||
description: 'Covers southern states including Karnataka, Tamil Nadu, AP, and Telangana',
|
||||
zbh: { name: 'Suresh Reddy', email: 'suresh.r@royalenfield.com', phone: '+91 98200 00001' },
|
||||
zonalManagers: [
|
||||
{ name: 'Lakshmi Iyer', email: 'lakshmi.i@royalenfield.com', phone: '+91 98200 00011', districts: ['Bengaluru Urban', 'Mysuru', 'Mangaluru'] },
|
||||
{ name: 'Karthik Nair', email: 'karthik.n@royalenfield.com', phone: '+91 98200 00012', districts: ['Chennai', 'Coimbatore', 'Madurai'] },
|
||||
{ name: 'Arjun Menon', email: 'arjun.m@royalenfield.com', phone: '+91 98200 00013', districts: ['Thiruvananthapuram', 'Kochi', 'Kozhikode'] },
|
||||
{ name: 'Deepika Rao', email: 'deepika.r@royalenfield.com', phone: '+91 98200 00014', districts: ['Hyderabad', 'Vijayawada', 'Visakhapatnam'] }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'east',
|
||||
code: 'EZ',
|
||||
name: 'East Zone',
|
||||
states: ['West Bengal', 'Odisha', 'Bihar', 'Jharkhand', 'Assam', 'Arunachal Pradesh', 'Manipur', 'Meghalaya', 'Mizoram', 'Nagaland', 'Sikkim', 'Tripura'],
|
||||
zmCount: 2,
|
||||
description: 'Covers eastern and north-eastern states',
|
||||
zbh: { name: 'Sourav Banerjee', email: 'sourav.b@royalenfield.com', phone: '+91 98300 00001' },
|
||||
zonalManagers: [
|
||||
{ name: 'Debjani Das', email: 'debjani.d@royalenfield.com', phone: '+91 98300 00011', districts: ['Kolkata', 'Howrah', 'Durgapur'] },
|
||||
{ name: 'Arijit Sen', email: 'arijit.s@royalenfield.com', phone: '+91 98300 00012', districts: ['Patna', 'Gaya', 'Ranchi', 'Jamshedpur'] }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'west',
|
||||
code: 'WZ',
|
||||
name: 'West Zone',
|
||||
states: ['Maharashtra', 'Gujarat', 'Goa', 'Daman & Diu', 'Dadra & Nagar Haveli'],
|
||||
zmCount: 3,
|
||||
description: 'Covers western states including Maharashtra and Gujarat',
|
||||
zbh: { name: 'Rohit Patel', email: 'rohit.p@royalenfield.com', phone: '+91 98400 00001' },
|
||||
zonalManagers: [
|
||||
{ name: 'Neha Shah', email: 'neha.s@royalenfield.com', phone: '+91 98400 00011', districts: ['Mumbai', 'Thane', 'Pune', 'Nagpur'] },
|
||||
{ name: 'Karan Mehta', email: 'karan.m@royalenfield.com', phone: '+91 98400 00012', districts: ['Ahmedabad', 'Surat', 'Vadodara', 'Rajkot'] },
|
||||
{ name: 'Sanjay Desai', email: 'sanjay.d@royalenfield.com', phone: '+91 98400 00013', districts: ['Jaipur', 'Jodhpur', 'Udaipur'] }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'central',
|
||||
code: 'CZ',
|
||||
name: 'Central Zone',
|
||||
states: ['Madhya Pradesh', 'Chhattisgarh'],
|
||||
zmCount: 2,
|
||||
description: 'Covers central Indian states',
|
||||
zbh: { name: 'Amit Jain', email: 'amit.j@royalenfield.com', phone: '+91 98500 00001' },
|
||||
zonalManagers: [
|
||||
{ name: 'Pooja Verma', email: 'pooja.v@royalenfield.com', phone: '+91 98500 00011', districts: ['Indore', 'Bhopal', 'Jabalpur', 'Gwalior'] },
|
||||
{ name: 'Rahul Tiwari', email: 'rahul.t@royalenfield.com', phone: '+91 98500 00012', districts: ['Raipur', 'Bhilai', 'Bilaspur'] }
|
||||
]
|
||||
},
|
||||
]);
|
||||
|
||||
// Mock data for regional offices
|
||||
const [regionalOffices] = useState<RegionalOffice[]>([
|
||||
// North Zone Regions
|
||||
{ id: 'n-r1', code: 'NZ-R1', name: 'Delhi NCR Region', zoneId: 'north', zoneName: 'North', states: ['Delhi', 'Haryana', 'Chandigarh'], cities: ['Delhi', 'Gurgaon', 'Noida', 'Faridabad', 'Chandigarh'], regionalOfficerCount: 2, asmCount: 8, status: 'Active' },
|
||||
{ id: 'n-r2', code: 'NZ-R2', name: 'Punjab Region', zoneId: 'north', zoneName: 'North', states: ['Punjab'], cities: ['Ludhiana', 'Amritsar', 'Jalandhar', 'Patiala'], regionalOfficerCount: 1, asmCount: 5, status: 'Active' },
|
||||
{ id: 'n-r3', code: 'NZ-R3', name: 'UP West Region', zoneId: 'north', zoneName: 'North', states: ['Uttar Pradesh'], cities: ['Meerut', 'Ghaziabad', 'Agra', 'Aligarh'], regionalOfficerCount: 2, asmCount: 10, status: 'Active' },
|
||||
{ id: 'n-r4', code: 'NZ-R4', name: 'UP East Region', zoneId: 'north', zoneName: 'North', states: ['Uttar Pradesh'], cities: ['Lucknow', 'Kanpur', 'Varanasi', 'Allahabad'], regionalOfficerCount: 2, asmCount: 9, status: 'Active' },
|
||||
{ id: 'n-r5', code: 'NZ-R5', name: 'Rajasthan Region', zoneId: 'north', zoneName: 'North', states: ['Rajasthan'], cities: ['Jaipur', 'Jodhpur', 'Udaipur', 'Kota'], regionalOfficerCount: 2, asmCount: 7, status: 'Active' },
|
||||
{ id: 'n-r6', code: 'NZ-R6', name: 'Hill States Region', zoneId: 'north', zoneName: 'North', states: ['Himachal Pradesh', 'Uttarakhand', 'Jammu & Kashmir'], cities: ['Shimla', 'Dehradun', 'Jammu', 'Srinagar'], regionalOfficerCount: 1, asmCount: 4, status: 'Active' },
|
||||
|
||||
// South Zone Regions
|
||||
{ id: 's-r1', code: 'SZ-R1', name: 'Karnataka North Region', zoneId: 'south', zoneName: 'South', states: ['Karnataka'], cities: ['Bangalore', 'Mysore', 'Hubli', 'Belgaum'], regionalOfficerCount: 2, asmCount: 9, status: 'Active' },
|
||||
{ id: 's-r2', code: 'SZ-R2', name: 'Karnataka South Region', zoneId: 'south', zoneName: 'South', states: ['Karnataka'], cities: ['Mangalore', 'Udupi'], regionalOfficerCount: 1, asmCount: 4, status: 'Active' },
|
||||
{ id: 's-r3', code: 'SZ-R3', name: 'Tamil Nadu North Region', zoneId: 'south', zoneName: 'South', states: ['Tamil Nadu'], cities: ['Chennai', 'Vellore', 'Tiruvallur'], regionalOfficerCount: 2, asmCount: 8, status: 'Active' },
|
||||
{ id: 's-r4', code: 'SZ-R4', name: 'Tamil Nadu West Region', zoneId: 'south', zoneName: 'South', states: ['Tamil Nadu'], cities: ['Coimbatore', 'Salem', 'Erode'], regionalOfficerCount: 2, asmCount: 7, status: 'Active' },
|
||||
{ id: 's-r5', code: 'SZ-R5', name: 'Kerala Region', zoneId: 'south', zoneName: 'South', states: ['Kerala'], cities: ['Kochi', 'Trivandrum', 'Kozhikode', 'Thrissur'], regionalOfficerCount: 2, asmCount: 6, status: 'Active' },
|
||||
{ id: 's-r6', code: 'SZ-R6', name: 'Andhra Pradesh Region', zoneId: 'south', zoneName: 'South', states: ['Andhra Pradesh'], cities: ['Visakhapatnam', 'Vijayawada', 'Guntur'], regionalOfficerCount: 1, asmCount: 5, status: 'Active' },
|
||||
{ id: 's-r7', code: 'SZ-R7', name: 'Telangana Region', zoneId: 'south', zoneName: 'South', states: ['Telangana'], cities: ['Hyderabad', 'Warangal', 'Nizamabad'], regionalOfficerCount: 2, asmCount: 7, status: 'Active' },
|
||||
|
||||
// East Zone Regions
|
||||
{ id: 'e-r1', code: 'EZ-R1', name: 'West Bengal Region', zoneId: 'east', zoneName: 'East', states: ['West Bengal'], cities: ['Kolkata', 'Howrah', 'Durgapur', 'Siliguri'], regionalOfficerCount: 2, asmCount: 7, status: 'Active' },
|
||||
{ id: 'e-r2', code: 'EZ-R2', name: 'Odisha Region', zoneId: 'east', zoneName: 'East', states: ['Odisha'], cities: ['Bhubaneswar', 'Cuttack', 'Rourkela'], regionalOfficerCount: 1, asmCount: 5, status: 'Active' },
|
||||
{ id: 'e-r3', code: 'EZ-R3', name: 'Bihar-Jharkhand Region', zoneId: 'east', zoneName: 'East', states: ['Bihar', 'Jharkhand'], cities: ['Patna', 'Ranchi', 'Jamshedpur', 'Dhanbad'], regionalOfficerCount: 2, asmCount: 6, status: 'Active' },
|
||||
{ id: 'e-r4', code: 'EZ-R4', name: 'North East Region', zoneId: 'east', zoneName: 'East', states: ['Assam', 'Meghalaya', 'Manipur', 'Nagaland', 'Tripura', 'Arunachal Pradesh', 'Mizoram', 'Sikkim'], cities: ['Guwahati', 'Shillong', 'Imphal', 'Agartala'], regionalOfficerCount: 1, asmCount: 4, status: 'Active' },
|
||||
|
||||
// West Zone Regions
|
||||
{ id: 'w-r1', code: 'WZ-R1', name: 'Mumbai Metropolitan Region', zoneId: 'west', zoneName: 'West', states: ['Maharashtra'], cities: ['Mumbai', 'Navi Mumbai', 'Thane', 'Kalyan'], regionalOfficerCount: 3, asmCount: 10, status: 'Active' },
|
||||
{ id: 'w-r2', code: 'WZ-R2', name: 'Pune Region', zoneId: 'west', zoneName: 'West', states: ['Maharashtra'], cities: ['Pune', 'Pimpri-Chinchwad', 'Solapur'], regionalOfficerCount: 2, asmCount: 7, status: 'Active' },
|
||||
{ id: 'w-r3', code: 'WZ-R3', name: 'Maharashtra Central Region', zoneId: 'west', zoneName: 'West', states: ['Maharashtra'], cities: ['Nagpur', 'Nashik', 'Aurangabad'], regionalOfficerCount: 2, asmCount: 6, status: 'Active' },
|
||||
{ id: 'w-r4', code: 'WZ-R4', name: 'Gujarat North Region', zoneId: 'west', zoneName: 'West', states: ['Gujarat'], cities: ['Ahmedabad', 'Gandhinagar', 'Mehsana'], regionalOfficerCount: 2, asmCount: 8, status: 'Active' },
|
||||
{ id: 'w-r5', code: 'WZ-R5', name: 'Gujarat South Region', zoneId: 'west', zoneName: 'West', states: ['Gujarat'], cities: ['Surat', 'Vadodara', 'Rajkot'], regionalOfficerCount: 2, asmCount: 7, status: 'Active' },
|
||||
{ id: 'w-r6', code: 'WZ-R6', name: 'Goa & UT Region', zoneId: 'west', zoneName: 'West', states: ['Goa', 'Daman & Diu', 'Dadra & Nagar Haveli'], cities: ['Panaji', 'Margao', 'Daman'], regionalOfficerCount: 1, asmCount: 3, status: 'Active' },
|
||||
|
||||
// Central Zone Regions
|
||||
{ id: 'c-r1', code: 'CZ-R1', name: 'MP North Region', zoneId: 'central', zoneName: 'Central', states: ['Madhya Pradesh'], cities: ['Bhopal', 'Indore', 'Gwalior'], regionalOfficerCount: 2, asmCount: 6, status: 'Active' },
|
||||
{ id: 'c-r2', code: 'CZ-R2', name: 'MP South Region', zoneId: 'central', zoneName: 'Central', states: ['Madhya Pradesh'], cities: ['Jabalpur', 'Ujjain'], regionalOfficerCount: 1, asmCount: 4, status: 'Active' },
|
||||
{ id: 'c-r3', code: 'CZ-R3', name: 'Chhattisgarh Region', zoneId: 'central', zoneName: 'Central', states: ['Chhattisgarh'], cities: ['Raipur', 'Bhilai', 'Bilaspur'], regionalOfficerCount: 1, asmCount: 4, status: 'Active' },
|
||||
]);
|
||||
|
||||
// Mock data for ASMs
|
||||
const [asms] = useState<ASM[]>([
|
||||
// North Zone ASMs
|
||||
{ id: 'asm-n1', name: 'Rajesh Kumar', code: 'ASM-NZ-001', email: 'rajesh.k@re.com', phone: '+91 98100 12345', zoneId: 'north', zoneName: 'North', regionId: 'n-r1', regionName: 'Delhi NCR Region', areasManaged: ['Central Delhi', 'South Delhi'], status: 'Active' },
|
||||
{ id: 'asm-n2', name: 'Priya Sharma', code: 'ASM-NZ-002', email: 'priya.s@re.com', phone: '+91 98100 12346', zoneId: 'north', zoneName: 'North', regionId: 'n-r1', regionName: 'Delhi NCR Region', areasManaged: ['Gurgaon', 'Noida'], status: 'Active' },
|
||||
{ id: 'asm-n3', name: 'Vikram Singh', code: 'ASM-NZ-003', email: 'vikram.s@re.com', phone: '+91 98100 12347', zoneId: 'north', zoneName: 'North', regionId: 'n-r2', regionName: 'Punjab Region', areasManaged: ['Ludhiana', 'Jalandhar'], status: 'Active' },
|
||||
{ id: 'asm-n4', name: 'Anjali Gupta', code: 'ASM-NZ-004', email: 'anjali.g@re.com', phone: '+91 98100 12348', zoneId: 'north', zoneName: 'North', regionId: 'n-r3', regionName: 'UP West Region', areasManaged: ['Meerut', 'Agra'], status: 'Active' },
|
||||
|
||||
// South Zone ASMs
|
||||
{ id: 'asm-s1', name: 'Arjun Reddy', code: 'ASM-SZ-001', email: 'arjun.r@re.com', phone: '+91 98200 12345', zoneId: 'south', zoneName: 'South', regionId: 's-r1', regionName: 'Karnataka North Region', areasManaged: ['Bangalore North', 'Bangalore East'], status: 'Active' },
|
||||
{ id: 'asm-s2', name: 'Lakshmi Iyer', code: 'ASM-SZ-002', email: 'lakshmi.i@re.com', phone: '+91 98200 12346', zoneId: 'south', zoneName: 'South', regionId: 's-r3', regionName: 'Tamil Nadu North Region', areasManaged: ['Chennai Central', 'Chennai South'], status: 'Active' },
|
||||
{ id: 'asm-s3', name: 'Karthik Nair', code: 'ASM-SZ-003', email: 'karthik.n@re.com', phone: '+91 98200 12347', zoneId: 'south', zoneName: 'South', regionId: 's-r5', regionName: 'Kerala Region', areasManaged: ['Kochi', 'Trivandrum'], status: 'Active' },
|
||||
|
||||
// East Zone ASMs
|
||||
{ id: 'asm-e1', name: 'Sourav Banerjee', code: 'ASM-EZ-001', email: 'sourav.b@re.com', phone: '+91 98300 12345', zoneId: 'east', zoneName: 'East', regionId: 'e-r1', regionName: 'West Bengal Region', areasManaged: ['Kolkata North', 'Howrah'], status: 'Active' },
|
||||
{ id: 'asm-e2', name: 'Debjani Das', code: 'ASM-EZ-002', email: 'debjani.d@re.com', phone: '+91 98300 12346', zoneId: 'east', zoneName: 'East', regionId: 'e-r2', regionName: 'Odisha Region', areasManaged: ['Bhubaneswar', 'Cuttack'], status: 'Active' },
|
||||
|
||||
// West Zone ASMs
|
||||
{ id: 'asm-w1', name: 'Rohit Patel', code: 'ASM-WZ-001', email: 'rohit.p@re.com', phone: '+91 98400 12345', zoneId: 'west', zoneName: 'West', regionId: 'w-r1', regionName: 'Mumbai Metropolitan Region', areasManaged: ['Bandra', 'Andheri'], status: 'Active' },
|
||||
{ id: 'asm-w2', name: 'Neha Shah', code: 'ASM-WZ-002', email: 'neha.s@re.com', phone: '+91 98400 12346', zoneId: 'west', zoneName: 'West', regionId: 'w-r2', regionName: 'Pune Region', areasManaged: ['Pune City', 'Pimpri'], status: 'Active' },
|
||||
{ id: 'asm-w3', name: 'Karan Mehta', code: 'ASM-WZ-003', email: 'karan.m@re.com', phone: '+91 98400 12347', zoneId: 'west', zoneName: 'West', regionId: 'w-r4', regionName: 'Gujarat North Region', areasManaged: ['Ahmedabad West', 'Gandhinagar'], status: 'Active' },
|
||||
|
||||
// Central Zone ASMs
|
||||
{ id: 'asm-c1', name: 'Amit Jain', code: 'ASM-CZ-001', email: 'amit.j@re.com', phone: '+91 98500 12345', zoneId: 'central', zoneName: 'Central', regionId: 'c-r1', regionName: 'MP North Region', areasManaged: ['Bhopal', 'Indore'], status: 'Active' },
|
||||
{ id: 'asm-c2', name: 'Pooja Verma', code: 'ASM-CZ-002', email: 'pooja.v@re.com', phone: '+91 98500 12346', zoneId: 'central', zoneName: 'Central', regionId: 'c-r3', regionName: 'Chhattisgarh Region', areasManaged: ['Raipur', 'Bhilai'], status: 'Active' },
|
||||
]);
|
||||
|
||||
// Mock data for Zonal Manager Mappings
|
||||
const [zonalManagerMappings] = useState<ZonalManagerMapping[]>([
|
||||
// North Zone ZMs
|
||||
{ id: 'zm-n1', name: 'Amit Sharma', code: 'ZM-NZ-001', email: 'amit.s@royalenfield.com', phone: '+91 98100 00011', zoneId: 'north', zoneName: 'North', regionId: 'n-r1', regionName: 'Delhi NCR Region', districts: ['North Delhi', 'Central Delhi', 'Gurugram', 'Faridabad'], status: 'Active' },
|
||||
{ id: 'zm-n2', name: 'Priya Gupta', code: 'ZM-NZ-002', email: 'priya.g@royalenfield.com', phone: '+91 98100 00012', zoneId: 'north', zoneName: 'North', regionId: 'n-r2', regionName: 'Punjab Region', districts: ['Amritsar', 'Ludhiana', 'Jalandhar', 'Patiala'], status: 'Active' },
|
||||
{ id: 'zm-n3', name: 'Vikram Singh', code: 'ZM-NZ-003', email: 'vikram.s@royalenfield.com', phone: '+91 98100 00013', zoneId: 'north', zoneName: 'North', regionId: 'n-r3', regionName: 'UP West Region', districts: ['Lucknow', 'Kanpur', 'Agra', 'Meerut'], status: 'Active' },
|
||||
|
||||
// South Zone ZMs
|
||||
{ id: 'zm-s1', name: 'Lakshmi Iyer', code: 'ZM-SZ-001', email: 'lakshmi.i@royalenfield.com', phone: '+91 98200 00011', zoneId: 'south', zoneName: 'South', regionId: 's-r1', regionName: 'Karnataka North Region', districts: ['Bengaluru Urban', 'Mysuru', 'Mangaluru'], status: 'Active' },
|
||||
{ id: 'zm-s2', name: 'Karthik Nair', code: 'ZM-SZ-002', email: 'karthik.n@royalenfield.com', phone: '+91 98200 00012', zoneId: 'south', zoneName: 'South', regionId: 's-r3', regionName: 'Tamil Nadu North Region', districts: ['Chennai', 'Coimbatore', 'Madurai'], status: 'Active' },
|
||||
{ id: 'zm-s3', name: 'Arjun Menon', code: 'ZM-SZ-003', email: 'arjun.m@royalenfield.com', phone: '+91 98200 00013', zoneId: 'south', zoneName: 'South', regionId: 's-r5', regionName: 'Kerala Region', districts: ['Thiruvananthapuram', 'Kochi', 'Kozhikode'], status: 'Active' },
|
||||
{ id: 'zm-s4', name: 'Deepika Rao', code: 'ZM-SZ-004', email: 'deepika.r@royalenfield.com', phone: '+91 98200 00014', zoneId: 'south', zoneName: 'South', regionId: 's-r6', regionName: 'AP & Telangana Region', districts: ['Hyderabad', 'Vijayawada', 'Visakhapatnam'], status: 'Active' },
|
||||
|
||||
// East Zone ZMs
|
||||
{ id: 'zm-e1', name: 'Debjani Das', code: 'ZM-EZ-001', email: 'debjani.d@royalenfield.com', phone: '+91 98300 00011', zoneId: 'east', zoneName: 'East', regionId: 'e-r1', regionName: 'West Bengal Region', districts: ['Kolkata', 'Howrah', 'Durgapur'], status: 'Active' },
|
||||
{ id: 'zm-e2', name: 'Arijit Sen', code: 'ZM-EZ-002', email: 'arijit.s@royalenfield.com', phone: '+91 98300 00012', zoneId: 'east', zoneName: 'East', regionId: 'e-r3', regionName: 'Bihar-Jharkhand Region', districts: ['Patna', 'Gaya', 'Ranchi', 'Jamshedpur'], status: 'Active' },
|
||||
|
||||
// West Zone ZMs
|
||||
{ id: 'zm-w1', name: 'Neha Shah', code: 'ZM-WZ-001', email: 'neha.s@royalenfield.com', phone: '+91 98400 00011', zoneId: 'west', zoneName: 'West', regionId: 'w-r1', regionName: 'Mumbai Metropolitan Region', districts: ['Mumbai', 'Thane', 'Pune', 'Nagpur'], status: 'Active' },
|
||||
{ id: 'zm-w2', name: 'Karan Mehta', code: 'ZM-WZ-002', email: 'karan.m@royalenfield.com', phone: '+91 98400 00012', zoneId: 'west', zoneName: 'West', regionId: 'w-r4', regionName: 'Gujarat North Region', districts: ['Ahmedabad', 'Surat', 'Vadodara', 'Rajkot'], status: 'Active' },
|
||||
{ id: 'zm-w3', name: 'Sanjay Desai', code: 'ZM-WZ-003', email: 'sanjay.d@royalenfield.com', phone: '+91 98400 00013', zoneId: 'west', zoneName: 'West', regionId: 'w-r6', regionName: 'Goa & UT Region', districts: ['Jaipur', 'Jodhpur', 'Udaipur'], status: 'Active' },
|
||||
|
||||
// Central Zone ZMs
|
||||
{ id: 'zm-c1', name: 'Pooja Verma', code: 'ZM-CZ-001', email: 'pooja.v@royalenfield.com', phone: '+91 98500 00011', zoneId: 'central', zoneName: 'Central', regionId: 'c-r1', regionName: 'MP North Region', districts: ['Indore', 'Bhopal', 'Jabalpur', 'Gwalior'], status: 'Active' },
|
||||
{ id: 'zm-c2', name: 'Rahul Tiwari', code: 'ZM-CZ-002', email: 'rahul.t@royalenfield.com', phone: '+91 98500 00012', zoneId: 'central', zoneName: 'Central', regionId: 'c-r3', regionName: 'Chhattisgarh Region', districts: ['Raipur', 'Bhilai', 'Bilaspur'], status: 'Active' },
|
||||
]);
|
||||
|
||||
// Mock data for user assignments
|
||||
const [userAssignments] = useState<UserAssignment[]>([
|
||||
{ id: '1', name: 'Rahul Verma', role: 'DD', region: 'North', zone: 'N-Z1', email: 'rahul.v@re.com', phone: '+91 98765 43210', status: 'Active' },
|
||||
{ id: '2', name: 'Priya Sharma', role: 'DD-ZM', region: 'North', zone: 'N-Z1', email: 'priya.s@re.com', phone: '+91 98765 43211', status: 'Active' },
|
||||
{ id: '3', name: 'Arjun Kapoor', role: 'RBM', region: 'North', zone: 'N-Z1', email: 'arjun.k@re.com', phone: '+91 98765 43212', status: 'Active' },
|
||||
{ id: '4', name: 'Sneha Reddy', role: 'DD', region: 'South', zone: 'S-Z1', email: 'sneha.r@re.com', phone: '+91 98765 43213', status: 'Active' },
|
||||
{ id: '5', name: 'Vikram Singh', role: 'DD-ZM', region: 'South', zone: 'S-Z1', email: 'vikram.s@re.com', phone: '+91 98765 43214', status: 'Active' },
|
||||
{ id: '6', name: 'Anjali Gupta', role: 'DD', region: 'East', zone: 'E-Z1', email: 'anjali.g@re.com', phone: '+91 98765 43215', status: 'Active' },
|
||||
{ id: '7', name: 'Karan Patel', role: 'DD', region: 'West', zone: 'W-Z1', email: 'karan.p@re.com', phone: '+91 98765 43216', status: 'Active' },
|
||||
{ id: '8', name: 'Neha Jain', role: 'RBM', region: 'Central', zone: 'C-Z1', email: 'neha.j@re.com', phone: '+91 98765 43217', status: 'Active' },
|
||||
]);
|
||||
|
||||
const handleSaveRole = () => {
|
||||
toast.success('Role saved successfully!');
|
||||
@ -766,28 +676,68 @@ export function MasterPage() {
|
||||
|
||||
const handleEditRole = (role: Role) => {
|
||||
setSelectedRoleForEdit(role);
|
||||
// Pre-populate with current permissions
|
||||
setRoleActionPermissions(role.permissions.filter(p =>
|
||||
['view_applications', 'review_level1', 'schedule_interview', 'approve_ranking', 'review_documents', 'final_approval'].includes(p)
|
||||
|
||||
// Map current permissions from backend-like codes to our category states
|
||||
// In our new system, role.permissions is string[] of permissionCodes
|
||||
setRoleActionPermissions(role.permissions.filter(code =>
|
||||
availablePermissions.find((p: any) => p.permissionCode === code && p.permissionCategory === 'ACTION')
|
||||
));
|
||||
setRoleViewPermissions(role.permissions.filter(p =>
|
||||
['view_all', 'view_payments', 'verify_payments'].includes(p)
|
||||
setRoleViewPermissions(role.permissions.filter(code =>
|
||||
availablePermissions.find((p: any) => p.permissionCode === code && p.permissionCategory === 'VIEW')
|
||||
));
|
||||
setRoleStageAccess(role.permissions.filter(p =>
|
||||
['manage_opportunities', 'assign_users', 'fnf_settlement'].includes(p)
|
||||
setRoleStageAccess(role.permissions.filter(code =>
|
||||
availablePermissions.find((p: any) => p.permissionCode === code && p.permissionCategory === 'STAGE')
|
||||
));
|
||||
|
||||
setShowEditRoleDialog(true);
|
||||
};
|
||||
|
||||
const handleSaveRolePermissions = () => {
|
||||
const handleSaveRolePermissions = async () => {
|
||||
if (!selectedRoleForEdit) return;
|
||||
|
||||
const allPermissionCodes = [
|
||||
...roleActionPermissions,
|
||||
...roleViewPermissions,
|
||||
...roleStageAccess
|
||||
];
|
||||
|
||||
// Find IDs for these codes
|
||||
const permissionIds = availablePermissions
|
||||
.filter((p: any) => allPermissionCodes.includes(p.permissionCode))
|
||||
.map((p: any) => p.id);
|
||||
|
||||
try {
|
||||
const response = await masterService.updateRole(selectedRoleForEdit.id, {
|
||||
roleName: selectedRoleForEdit.name,
|
||||
permissionIds
|
||||
}) as any;
|
||||
|
||||
if (response.success) {
|
||||
toast.success(`Permissions for ${selectedRoleForEdit.name} role updated successfully!`);
|
||||
setShowEditRoleDialog(false);
|
||||
setSelectedRoleForEdit(null);
|
||||
setRoleActionPermissions([]);
|
||||
setRoleViewPermissions([]);
|
||||
setRoleStageAccess([]);
|
||||
fetchInitialData(); // Refresh data
|
||||
} else {
|
||||
toast.error(response.message || 'Failed to update permissions');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Update role error:', error);
|
||||
toast.error('Error saving role permissions');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditUserAssignment = (user: any) => {
|
||||
setSelectedUserForEdit(user);
|
||||
setFetchedUser({
|
||||
...user,
|
||||
fullName: user.name, // Mapping for dialog
|
||||
});
|
||||
setSelectedUserZone(user.zoneId || '');
|
||||
setSelectedUserRegion(user.regionId || '');
|
||||
setShowUserAssignDialog(true);
|
||||
};
|
||||
|
||||
const handleFetchUser = () => {
|
||||
@ -815,22 +765,33 @@ export function MasterPage() {
|
||||
toast.success('User details fetched successfully!');
|
||||
};
|
||||
|
||||
const handleSaveUserAssignment = () => {
|
||||
const handleSaveUserAssignment = async () => {
|
||||
if (!fetchedUser) {
|
||||
toast.error('Please fetch user details first');
|
||||
return;
|
||||
}
|
||||
|
||||
toast.success('User permissions updated successfully!');
|
||||
try {
|
||||
const response = await masterService.updateUser(fetchedUser.id, {
|
||||
roleCode: fetchedUser.roleCode,
|
||||
zoneId: selectedUserZone,
|
||||
regionId: selectedUserRegion,
|
||||
// Add other fields if needed
|
||||
}) as any;
|
||||
|
||||
if (response.success) {
|
||||
toast.success('User updated successfully!');
|
||||
setShowUserAssignDialog(false);
|
||||
setUserLookupValue('');
|
||||
setSelectedUserForEdit(null);
|
||||
setFetchedUser(null);
|
||||
setActionPermissions([]);
|
||||
setViewPermissions([]);
|
||||
setStageAccess([]);
|
||||
setSelectedUserZone('');
|
||||
setSelectedUserRegion('');
|
||||
setSelectedUserDistricts([]);
|
||||
fetchInitialData(); // Refresh table
|
||||
} else {
|
||||
toast.error(response.message || 'Failed to update user');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Update user error:', error);
|
||||
toast.error('Error saving user data');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@ -845,6 +806,12 @@ export function MasterPage() {
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
{loading ? (
|
||||
<div className="flex flex-col items-center justify-center p-20 space-y-4">
|
||||
<div className="w-12 h-12 border-4 border-amber-600 border-t-transparent rounded-full animate-spin"></div>
|
||||
<p className="text-slate-600 font-medium">Loading configuration...</p>
|
||||
</div>
|
||||
) : (
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-5 h-auto">
|
||||
<TabsTrigger value="hierarchy" className="flex items-center gap-2 py-3">
|
||||
@ -881,8 +848,7 @@ export function MasterPage() {
|
||||
return (
|
||||
<Card
|
||||
key={zone.id}
|
||||
className={`border-2 transition-all cursor-pointer ${
|
||||
selectedZone === zone.id ? 'border-amber-600 shadow-lg' : 'hover:border-amber-400'
|
||||
className={`border-2 transition-all cursor-pointer ${selectedZone === zone.id ? 'border-amber-600 shadow-lg' : 'hover:border-amber-400'
|
||||
}`}
|
||||
onClick={() => setSelectedZone(selectedZone === zone.id ? 'all' : zone.id)}
|
||||
>
|
||||
@ -1355,7 +1321,7 @@ export function MasterPage() {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{userAssignments.map((user) => (
|
||||
{userAssignedData.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
@ -1375,13 +1341,13 @@ export function MasterPage() {
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<Badge variant="outline" className="text-xs bg-green-50 text-green-700 border-green-200">
|
||||
5 Actions
|
||||
{user.permissions?.filter(p => availablePermissions.find(ap => ap.permissionCode === p && ap.permissionCategory === 'ACTION'))?.length || 0} Actions
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-xs bg-blue-50 text-blue-700 border-blue-200">
|
||||
7 Views
|
||||
{user.permissions?.filter(p => availablePermissions.find(ap => ap.permissionCode === p && ap.permissionCategory === 'VIEW'))?.length || 0} Views
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-xs bg-amber-50 text-amber-700 border-amber-200">
|
||||
4 Stages
|
||||
{user.permissions?.filter(p => availablePermissions.find(ap => ap.permissionCode === p && ap.permissionCategory === 'STAGE'))?.length || 0} Stages
|
||||
</Badge>
|
||||
</div>
|
||||
</TableCell>
|
||||
@ -1400,7 +1366,7 @@ export function MasterPage() {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => setShowUserAssignDialog(true)}>
|
||||
<Button variant="outline" size="sm" onClick={() => handleEditUserAssignment(user)}>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="text-red-600 hover:bg-red-50">
|
||||
@ -1737,6 +1703,7 @@ export function MasterPage() {
|
||||
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
|
||||
{/* Add/Edit Role Dialog */}
|
||||
<Dialog open={showRoleDialog} onOpenChange={setShowRoleDialog}>
|
||||
@ -2897,8 +2864,10 @@ export function MasterPage() {
|
||||
<Dialog open={showUserAssignDialog} onOpenChange={setShowUserAssignDialog}>
|
||||
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Configure User Permissions</DialogTitle>
|
||||
<DialogDescription>Fetch user details and configure their action, view, and stage access permissions</DialogDescription>
|
||||
<DialogTitle>{selectedUserForEdit ? 'Edit User Assignments' : 'Add User & Configure Permissions'}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{selectedUserForEdit ? `Update role and region assignments for ${selectedUserForEdit.name}` : 'Fetch user details and configure their role and geographical assignments'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-5">
|
||||
{/* User Lookup Section */}
|
||||
@ -2936,7 +2905,7 @@ export function MasterPage() {
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label className="text-xs text-slate-600">Full Name</Label>
|
||||
<p className="text-sm text-slate-900 mt-1">{fetchedUser.name}</p>
|
||||
<p className="text-sm text-slate-900 mt-1 font-medium">{fetchedUser.name || fetchedUser.fullName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-slate-600">Employee ID</Label>
|
||||
@ -2947,36 +2916,58 @@ export function MasterPage() {
|
||||
<p className="text-sm text-slate-900 mt-1">{fetchedUser.email}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-slate-600">Phone</Label>
|
||||
<p className="text-sm text-slate-900 mt-1">{fetchedUser.phone}</p>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-slate-600">Role</Label>
|
||||
<Badge variant="outline" className="bg-purple-50 text-purple-700 border-purple-200 mt-1">
|
||||
{fetchedUser.role}
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-slate-600">Department</Label>
|
||||
<p className="text-sm text-slate-900 mt-1">{fetchedUser.department}</p>
|
||||
<Label className="text-xs text-slate-600">Role Assignment</Label>
|
||||
<Select
|
||||
value={fetchedUser.roleCode}
|
||||
onValueChange={(val) => setFetchedUser({ ...fetchedUser, roleCode: val })}
|
||||
>
|
||||
<SelectTrigger className="h-9 mt-1">
|
||||
<SelectValue placeholder="Select Role" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{roles.map(role => (
|
||||
<SelectItem key={role.id} value={role.id}>{role.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-slate-600">Zone</Label>
|
||||
<p className="text-sm text-slate-900 mt-1">{fetchedUser.zone}</p>
|
||||
<Select
|
||||
value={selectedUserZone}
|
||||
onValueChange={(val) => {
|
||||
setSelectedUserZone(val);
|
||||
setSelectedUserRegion(''); // Reset region on zone change
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-9 mt-1">
|
||||
<SelectValue placeholder="Select Zone" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{zones.map(zone => (
|
||||
<SelectItem key={zone.id} value={zone.id}>{zone.name}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs text-slate-600">Region</Label>
|
||||
<p className="text-sm text-slate-900 mt-1">{fetchedUser.region}</p>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label className="text-xs text-slate-600">Assigned Districts</Label>
|
||||
<div className="flex flex-wrap gap-1 mt-1">
|
||||
{fetchedUser.districts.map((district: string) => (
|
||||
<Badge key={district} variant="secondary" className="text-xs">
|
||||
{district}
|
||||
</Badge>
|
||||
<Label className="text-xs text-slate-600">Regional Office</Label>
|
||||
<Select
|
||||
value={selectedUserRegion}
|
||||
onValueChange={setSelectedUserRegion}
|
||||
disabled={!selectedUserZone}
|
||||
>
|
||||
<SelectTrigger className="h-9 mt-1">
|
||||
<SelectValue placeholder="Select Region" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{regionalOffices
|
||||
.filter(r => r.zoneId === selectedUserZone)
|
||||
.map(region => (
|
||||
<SelectItem key={region.id} value={region.id}>{region.name}</SelectItem>
|
||||
))}
|
||||
</div>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -3156,32 +3147,23 @@ export function MasterPage() {
|
||||
<div className="border rounded-lg p-4 bg-gradient-to-br from-green-50 to-emerald-50 border-green-200">
|
||||
<h5 className="text-sm text-green-900 mb-3">Action Permissions</h5>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{[
|
||||
{ id: 'approve', label: 'Approve Applications' },
|
||||
{ id: 'reject', label: 'Reject Applications' },
|
||||
{ id: 'upload_docs', label: 'Upload Documents' },
|
||||
{ id: 'request_changes', label: 'Request Changes' },
|
||||
{ id: 'forward', label: 'Forward to Others' },
|
||||
{ id: 'reassign', label: 'Reassign Applications' },
|
||||
{ id: 'schedule_interview', label: 'Schedule Interviews' },
|
||||
{ id: 'add_comments', label: 'Add Comments/Notes' },
|
||||
{ id: 'rank_applicants', label: 'Rank Applicants' },
|
||||
{ id: 'final_approval', label: 'Final Approval' }
|
||||
].map((perm) => (
|
||||
{availablePermissions
|
||||
.filter(p => p.permissionCategory === 'ACTION')
|
||||
.map((perm) => (
|
||||
<div key={perm.id} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={`role-action-${perm.id}`}
|
||||
checked={roleActionPermissions.includes(perm.id)}
|
||||
id={`role-action-${perm.permissionCode}`}
|
||||
checked={roleActionPermissions.includes(perm.permissionCode)}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
setRoleActionPermissions([...roleActionPermissions, perm.id]);
|
||||
setRoleActionPermissions([...roleActionPermissions, perm.permissionCode]);
|
||||
} else {
|
||||
setRoleActionPermissions(roleActionPermissions.filter(p => p !== perm.id));
|
||||
setRoleActionPermissions(roleActionPermissions.filter(p => p !== perm.permissionCode));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label htmlFor={`role-action-${perm.id}`} className="text-sm cursor-pointer text-slate-900">
|
||||
{perm.label}
|
||||
<label htmlFor={`role-action-${perm.permissionCode}`} className="text-sm cursor-pointer text-slate-900">
|
||||
{perm.permissionName}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
@ -3192,32 +3174,23 @@ export function MasterPage() {
|
||||
<div className="border rounded-lg p-4 bg-gradient-to-br from-blue-50 to-cyan-50 border-blue-200">
|
||||
<h5 className="text-sm text-blue-900 mb-3">View/Access Permissions</h5>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{[
|
||||
{ id: 'view_details', label: 'Application Details' },
|
||||
{ id: 'view_financial', label: 'Financial Information' },
|
||||
{ id: 'view_discussions', label: 'Discussion Notes' },
|
||||
{ id: 'view_progress', label: 'Progress Tracking' },
|
||||
{ id: 'view_audit', label: 'Audit Logs' },
|
||||
{ id: 'view_documents', label: 'All Documents' },
|
||||
{ id: 'view_personal', label: 'Personal Information' },
|
||||
{ id: 'view_business', label: 'Business Details' },
|
||||
{ id: 'view_reports', label: 'Reports & Analytics' },
|
||||
{ id: 'view_history', label: 'Application History' }
|
||||
].map((perm) => (
|
||||
{availablePermissions
|
||||
.filter(p => p.permissionCategory === 'VIEW')
|
||||
.map((perm) => (
|
||||
<div key={perm.id} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={`role-view-${perm.id}`}
|
||||
checked={roleViewPermissions.includes(perm.id)}
|
||||
id={`role-view-${perm.permissionCode}`}
|
||||
checked={roleViewPermissions.includes(perm.permissionCode)}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
setRoleViewPermissions([...roleViewPermissions, perm.id]);
|
||||
setRoleViewPermissions([...roleViewPermissions, perm.permissionCode]);
|
||||
} else {
|
||||
setRoleViewPermissions(roleViewPermissions.filter(p => p !== perm.id));
|
||||
setRoleViewPermissions(roleViewPermissions.filter(p => p !== perm.permissionCode));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label htmlFor={`role-view-${perm.id}`} className="text-sm cursor-pointer text-slate-900">
|
||||
{perm.label}
|
||||
<label htmlFor={`role-view-${perm.permissionCode}`} className="text-sm cursor-pointer text-slate-900">
|
||||
{perm.permissionName}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
@ -3228,32 +3201,23 @@ export function MasterPage() {
|
||||
<div className="border rounded-lg p-4 bg-gradient-to-br from-amber-50 to-orange-50 border-amber-200">
|
||||
<h5 className="text-sm text-amber-900 mb-3">Application Stage Access</h5>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{[
|
||||
{ id: 'initial_review', label: 'Initial Review' },
|
||||
{ id: 'field_verification', label: 'Field Verification' },
|
||||
{ id: 'level1_interview', label: 'Level 1 Interview' },
|
||||
{ id: 'level2_interview', label: 'Level 2 Interview' },
|
||||
{ id: 'ranking', label: 'Ranking & Selection' },
|
||||
{ id: 'legal_review', label: 'Legal Review' },
|
||||
{ id: 'financial_review', label: 'Financial Review' },
|
||||
{ id: 'final_approval', label: 'Final Approval' },
|
||||
{ id: 'payment', label: 'Payment Verification' },
|
||||
{ id: 'onboarding', label: 'Onboarding' }
|
||||
].map((stage) => (
|
||||
{availablePermissions
|
||||
.filter(p => p.permissionCategory === 'STAGE')
|
||||
.map((stage) => (
|
||||
<div key={stage.id} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={`role-stage-${stage.id}`}
|
||||
checked={roleStageAccess.includes(stage.id)}
|
||||
id={`role-stage-${stage.permissionCode}`}
|
||||
checked={roleStageAccess.includes(stage.permissionCode)}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
setRoleStageAccess([...roleStageAccess, stage.id]);
|
||||
setRoleStageAccess([...roleStageAccess, stage.permissionCode]);
|
||||
} else {
|
||||
setRoleStageAccess(roleStageAccess.filter(s => s !== stage.id));
|
||||
setRoleStageAccess(roleStageAccess.filter(s => s !== stage.permissionCode));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<label htmlFor={`role-stage-${stage.id}`} className="text-sm cursor-pointer text-slate-900">
|
||||
{stage.label}
|
||||
<label htmlFor={`role-stage-${stage.permissionCode}`} className="text-sm cursor-pointer text-slate-900">
|
||||
{stage.permissionName}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@ -16,6 +16,7 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
||||
const [rememberMe, setRememberMe] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [showForgotPassword, setShowForgotPassword] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
|
||||
|
||||
const copyToClipboard = async (text: string, index: number) => {
|
||||
@ -54,17 +55,24 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const quickLogin = (userEmail: string, userPassword: string) => {
|
||||
const quickLogin = async (userEmail: string, userPassword: string) => {
|
||||
setEmail(userEmail);
|
||||
setPassword(userPassword);
|
||||
// Auto-submit after a short delay
|
||||
setTimeout(() => {
|
||||
onLogin(userEmail, userPassword);
|
||||
}, 100);
|
||||
setError('');
|
||||
setIsLoading(true);
|
||||
try {
|
||||
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();
|
||||
if (isLoading) return;
|
||||
|
||||
setError('');
|
||||
|
||||
if (!email || !password) {
|
||||
@ -72,13 +80,14 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Simple validation for demo
|
||||
if (password.length < 6) {
|
||||
setError('Invalid credentials');
|
||||
return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await onLogin(email, password);
|
||||
} catch (err) {
|
||||
setError('An unexpected error occurred');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
onLogin(email, password);
|
||||
};
|
||||
|
||||
const handleForgotPassword = (e: React.FormEvent) => {
|
||||
@ -103,8 +112,8 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
||||
<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">
|
||||
<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"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<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" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-white mb-2">Royal Enfield</h1>
|
||||
@ -124,6 +133,7 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -136,6 +146,7 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -145,6 +156,7 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
||||
id="remember"
|
||||
checked={rememberMe}
|
||||
onCheckedChange={(checked) => setRememberMe(checked as boolean)}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<Label htmlFor="remember" className="cursor-pointer">
|
||||
Remember Me
|
||||
@ -153,7 +165,8 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
||||
<button
|
||||
type="button"
|
||||
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?
|
||||
</button>
|
||||
@ -162,12 +175,23 @@ export function LoginPage({ onLogin }: LoginPageProps) {
|
||||
{error && (
|
||||
<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" />
|
||||
<span className="text-red-600">{error}</span>
|
||||
<span className="text-red-600 font-medium text-sm">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button type="submit" className="w-full bg-amber-600 hover:bg-amber-700">
|
||||
Login
|
||||
<Button
|
||||
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>
|
||||
</form>
|
||||
) : (
|
||||
|
||||
@ -7,15 +7,17 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from '../ui/dropdown-menu';
|
||||
import { Badge } from '../ui/badge';
|
||||
import { User } from '../../lib/mock-data';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '../../store';
|
||||
|
||||
interface HeaderProps {
|
||||
title: string;
|
||||
currentUser?: User | null;
|
||||
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 = [
|
||||
{
|
||||
id: '1',
|
||||
@ -101,8 +103,7 @@ export function Header({ title, currentUser, onRefresh }: HeaderProps) {
|
||||
{notifications.map((notification) => (
|
||||
<DropdownMenuItem
|
||||
key={notification.id}
|
||||
className={`p-3 cursor-pointer ${
|
||||
notification.unread ? 'bg-amber-50' : ''
|
||||
className={`p-3 cursor-pointer ${notification.unread ? 'bg-amber-50' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex-1">
|
||||
|
||||
@ -16,18 +16,19 @@ import {
|
||||
MapPin
|
||||
} from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '../../store';
|
||||
import { Input } from '../ui/input';
|
||||
import { Button } from '../ui/button';
|
||||
import { User } from '../../lib/mock-data';
|
||||
|
||||
interface SidebarProps {
|
||||
activeView: string;
|
||||
onNavigate: (view: string) => 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 [searchQuery, setSearchQuery] = useState('');
|
||||
const [offboardingExpanded, setOffboardingExpanded] = useState(false);
|
||||
@ -102,8 +103,7 @@ export function Sidebar({ activeView, onNavigate, onLogout, currentUser }: Sideb
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`bg-slate-900 text-white h-screen flex flex-col transition-all duration-300 ${
|
||||
collapsed ? 'w-20' : 'w-64'
|
||||
className={`bg-slate-900 text-white h-screen flex flex-col transition-all duration-300 ${collapsed ? 'w-20' : 'w-64'
|
||||
}`}
|
||||
>
|
||||
{/* Header with Logo */}
|
||||
@ -113,8 +113,8 @@ export function Sidebar({ activeView, onNavigate, onLogout, currentUser }: Sideb
|
||||
<div className="flex items-center gap-2">
|
||||
<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">
|
||||
<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"/>
|
||||
<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" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-amber-600">RE Dealer</span>
|
||||
@ -176,8 +176,7 @@ export function Sidebar({ activeView, onNavigate, onLogout, currentUser }: Sideb
|
||||
onNavigate(item.id);
|
||||
}
|
||||
}}
|
||||
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${
|
||||
isActive || isSubmenuActive
|
||||
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${isActive || isSubmenuActive
|
||||
? 'bg-amber-600 text-white'
|
||||
: 'text-slate-300 hover:bg-slate-800 hover:text-white'
|
||||
}`}
|
||||
@ -207,8 +206,7 @@ export function Sidebar({ activeView, onNavigate, onLogout, currentUser }: Sideb
|
||||
<button
|
||||
key={subItem.id}
|
||||
onClick={() => onNavigate(subItem.id)}
|
||||
className={`w-full flex items-center gap-3 px-4 py-2 rounded-lg transition-colors text-sm ${
|
||||
isSubActive
|
||||
className={`w-full flex items-center gap-3 px-4 py-2 rounded-lg transition-colors text-sm ${isSubActive
|
||||
? 'bg-amber-600 text-white'
|
||||
: 'text-slate-400 hover:bg-slate-800 hover:text-white'
|
||||
}`}
|
||||
@ -244,8 +242,7 @@ export function Sidebar({ activeView, onNavigate, onLogout, currentUser }: Sideb
|
||||
<Button
|
||||
onClick={onLogout}
|
||||
variant="ghost"
|
||||
className={`w-full ${
|
||||
collapsed ? 'px-2' : 'justify-start'
|
||||
className={`w-full ${collapsed ? 'px-2' : 'justify-start'
|
||||
} text-slate-300 hover:bg-slate-800 hover:text-white`}
|
||||
title={collapsed ? 'Logout' : undefined}
|
||||
>
|
||||
|
||||
@ -172,6 +172,20 @@ export const mockUsers: User[] = [
|
||||
password: 'password',
|
||||
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)
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { Provider } from 'react-redux'
|
||||
import { store } from './store'
|
||||
import App from './App.tsx'
|
||||
import './styles/globals.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>
|
||||
</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