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:
laxmanhalaki 2026-01-27 19:09:54 +05:30
parent 978930d4eb
commit cb793354bf
16 changed files with 2523 additions and 1436 deletions

390
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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
View 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
View 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;

View 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>
);
}

View File

@ -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>('');
@ -298,16 +301,108 @@ export function MasterPage() {
const [zbhPhone, setZbhPhone] = useState<string>('');
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>
))}

View File

@ -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) => {
@ -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>
) : (

View File

@ -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">

View File

@ -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 */}
@ -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}
>

View File

@ -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)

View File

@ -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>,
)

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

View 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
View 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;

View 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;