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",
@ -74,4 +78,4 @@
"typescript-eslint": "^8.53.1",
"vite": "^6.0.0"
}
}
}

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})`);
} else {
toast.error('Invalid credentials');
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 {
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>
);
}

File diff suppressed because it is too large Load Diff

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) => {
@ -30,7 +31,7 @@ export function LoginPage({ onLogin }: LoginPageProps) {
} catch (err) {
// Clipboard API blocked, try fallback method
}
// Fallback method for older browsers or blocked clipboard
try {
const textArea = document.createElement('textarea');
@ -41,10 +42,10 @@ export function LoginPage({ onLogin }: LoginPageProps) {
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
const successful = document.execCommand('copy');
document.body.removeChild(textArea);
if (successful) {
setCopiedIndex(index);
setTimeout(() => setCopiedIndex(null), 2000);
@ -54,17 +55,24 @@ export function LoginPage({ onLogin }: LoginPageProps) {
}
};
const quickLogin = (userEmail: string, userPassword: string) => {
const quickLogin = async (userEmail: string, userPassword: string) => {
setEmail(userEmail);
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>
) : (

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',
@ -84,8 +86,8 @@ export function Header({ title, currentUser, onRefresh }: HeaderProps) {
<Button variant="outline" size="icon" className="relative">
<Bell className="w-4 h-4" />
{unreadCount > 0 && (
<Badge
variant="destructive"
<Badge
variant="destructive"
className="absolute -top-1 -right-1 w-5 h-5 p-0 flex items-center justify-center text-xs"
>
{unreadCount}
@ -99,11 +101,10 @@ export function Header({ title, currentUser, onRefresh }: HeaderProps) {
</div>
<div className="max-h-96 overflow-y-auto">
{notifications.map((notification) => (
<DropdownMenuItem
<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">
<p className="text-slate-900">{notification.message}</p>

View File

@ -1,6 +1,6 @@
import {
LayoutDashboard,
FileText,
import {
LayoutDashboard,
FileText,
LogOut,
Users,
ChevronLeft,
@ -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);
@ -46,9 +47,9 @@ export function Sidebar({ activeView, onNavigate, onLogout, currentUser }: Sideb
] : [
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
{ id: 'applications', label: 'Dealership Requests', icon: FileText },
{
id: 'offboarding',
label: 'Offboarding',
{
id: 'offboarding',
label: 'Offboarding',
icon: UserMinus,
hasSubmenu: true,
submenuKey: 'offboarding',
@ -69,9 +70,9 @@ export function Sidebar({ activeView, onNavigate, onLogout, currentUser }: Sideb
// Add All Requests for DD Lead role (before Dealership Requests)
if (currentUser?.role === 'DD Lead') {
menuItems.splice(1, 0, {
id: 'all-requests',
label: 'All Requests',
menuItems.splice(1, 0, {
id: 'all-requests',
label: 'All Requests',
icon: FolderOpen,
hasSubmenu: true,
submenuKey: 'allRequests',
@ -86,7 +87,7 @@ export function Sidebar({ activeView, onNavigate, onLogout, currentUser }: Sideb
if (currentUser?.role === 'Super Admin' || currentUser?.role === 'DD Admin' || currentUser?.role === 'DD Lead') {
menuItems.push({ id: 'master', label: 'Master', icon: Settings });
}
if (currentUser?.role === 'Super Admin') {
menuItems.push({ id: 'users', label: 'User Management', icon: Users });
}
@ -101,10 +102,9 @@ 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'
}`}
<div
className={`bg-slate-900 text-white h-screen flex flex-col transition-all duration-300 ${collapsed ? 'w-20' : 'w-64'
}`}
>
{/* Header with Logo */}
<div className="p-4 border-b border-slate-800">
@ -113,8 +113,8 @@ export function Sidebar({ activeView, onNavigate, onLogout, currentUser }: Sideb
<div className="flex items-center gap-2">
<div className="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>
@ -156,12 +156,12 @@ export function Sidebar({ activeView, onNavigate, onLogout, currentUser }: Sideb
const isActive = activeView === item.id;
const hasSubmenu = item.hasSubmenu;
const isSubmenuActive = hasSubmenu && item.submenu?.some(sub => activeView === sub.id);
// Determine which submenu is expanded based on submenuKey
const submenuKey = (item as any).submenuKey;
const isExpanded = submenuKey === 'offboarding' ? offboardingExpanded :
submenuKey === 'allRequests' ? allRequestsExpanded : false;
const isExpanded = submenuKey === 'offboarding' ? offboardingExpanded :
submenuKey === 'allRequests' ? allRequestsExpanded : false;
return (
<div key={item.id}>
<button
@ -176,11 +176,10 @@ 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
? 'bg-amber-600 text-white'
: 'text-slate-300 hover:bg-slate-800 hover:text-white'
}`}
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'
}`}
title={collapsed ? item.label : undefined}
>
<Icon className="w-5 h-5 flex-shrink-0" />
@ -197,7 +196,7 @@ export function Sidebar({ activeView, onNavigate, onLogout, currentUser }: Sideb
</>
)}
</button>
{/* Submenu */}
{hasSubmenu && isExpanded && !collapsed && (
<div className="ml-4 mt-2 space-y-1">
@ -207,11 +206,10 @@ 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
? 'bg-amber-600 text-white'
: 'text-slate-400 hover:bg-slate-800 hover:text-white'
}`}
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'
}`}
>
<span className="w-1 h-1 rounded-full bg-current flex-shrink-0" />
<span>{subItem.label}</span>
@ -240,13 +238,12 @@ export function Sidebar({ activeView, onNavigate, onLogout, currentUser }: Sideb
</div>
</div>
)}
<Button
onClick={onLogout}
variant="ghost"
className={`w-full ${
collapsed ? 'px-2' : 'justify-start'
} text-slate-300 hover:bg-slate-800 hover:text-white`}
className={`w-full ${collapsed ? 'px-2' : 'justify-start'
} text-slate-300 hover:bg-slate-800 hover:text-white`}
title={collapsed ? 'Logout' : undefined}
>
<LogOut className="w-5 h-5 flex-shrink-0" />

View File

@ -1,15 +1,15 @@
// Mock data for the dealership onboarding system
export type UserRole =
| 'DD-ZM'
| 'RBM'
| 'DD'
| 'ZBH'
| 'DD Lead'
| 'DD Head'
| 'NBH'
| 'DD Admin'
| 'Legal Admin'
export type UserRole =
| 'DD-ZM'
| 'RBM'
| 'DD'
| 'ZBH'
| 'DD Lead'
| 'DD Head'
| 'NBH'
| 'DD Admin'
| 'Legal Admin'
| 'Super Admin'
| 'DD AM'
| 'FDD'
@ -18,7 +18,7 @@ export type UserRole =
| 'Finance Admin'
| 'Dealer';
export type ApplicationStatus =
export type ApplicationStatus =
| 'Submitted'
| 'Questionnaire Pending'
| 'Questionnaire Completed'
@ -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>
<App />
<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;