Merge pull request 'dev' (#8) from dev into furqan
Reviewed-on: https://gitea.maskantech.in/gitea_admin/saas_frontend/pulls/8
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
# Development environment
|
||||||
|
VITE_API_BASE_URL=https://dev-api.yourdomain.com
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Local environment
|
||||||
|
VITE_API_BASE_URL=http://localhost:8000
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Production environment
|
||||||
|
VITE_API_BASE_URL=https://api.yourdomain.com
|
||||||
Generated
+96
-17
@@ -20,6 +20,7 @@
|
|||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-hook-form": "^7.50.0",
|
"react-hook-form": "^7.50.0",
|
||||||
"react-i18next": "^16.5.3",
|
"react-i18next": "^16.5.3",
|
||||||
|
"react-phone-number-input": "^3.4.14",
|
||||||
"react-router-dom": "^7.12.0",
|
"react-router-dom": "^7.12.0",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
"tailwindcss": "^4.1.18"
|
"tailwindcss": "^4.1.18"
|
||||||
@@ -70,7 +71,6 @@
|
|||||||
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
|
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.28.6",
|
"@babel/code-frame": "^7.28.6",
|
||||||
"@babel/generator": "^7.28.6",
|
"@babel/generator": "^7.28.6",
|
||||||
@@ -1793,7 +1793,6 @@
|
|||||||
"integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==",
|
"integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
@@ -1804,7 +1803,6 @@
|
|||||||
"integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==",
|
"integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
@@ -1864,7 +1862,6 @@
|
|||||||
"integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==",
|
"integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.53.0",
|
"@typescript-eslint/scope-manager": "8.53.0",
|
||||||
"@typescript-eslint/types": "8.53.0",
|
"@typescript-eslint/types": "8.53.0",
|
||||||
@@ -2112,7 +2109,6 @@
|
|||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -2235,7 +2231,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -2311,6 +2306,12 @@
|
|||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/classnames": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/clsx": {
|
"node_modules/clsx": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
@@ -2379,6 +2380,12 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/country-flag-icons": {
|
||||||
|
"version": "1.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/country-flag-icons/-/country-flag-icons-1.6.4.tgz",
|
||||||
|
"integrity": "sha512-Z3Zi419FI889tlElMsVhCIS5eRkiLDWixr576J5DPiTe5RGxpbRi+enMpHdYVp5iK5WFjr8P/RgyIFAGhFsiFg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -2593,7 +2600,6 @@
|
|||||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -3105,7 +3111,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.28.4"
|
"@babel/runtime": "^7.28.4"
|
||||||
},
|
},
|
||||||
@@ -3164,6 +3169,27 @@
|
|||||||
"node": ">=0.8.19"
|
"node": ">=0.8.19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/input-format": {
|
||||||
|
"version": "0.3.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/input-format/-/input-format-0.3.14.tgz",
|
||||||
|
"integrity": "sha512-gHMrgrbCgmT4uK5Um5eVDUohuV9lcs95ZUUN9Px2Y0VIfjTzT2wF8Q3Z4fwLFm7c5Z2OXCm53FHoovj6SlOKdg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18.1.0",
|
||||||
|
"react-dom": ">=18.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-extglob": {
|
"node_modules/is-extglob": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
@@ -3207,7 +3233,6 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
@@ -3284,8 +3309,7 @@
|
|||||||
"version": "1.9.4",
|
"version": "1.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/leaflet-control-geocoder": {
|
"node_modules/leaflet-control-geocoder": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
@@ -3322,6 +3346,12 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/libphonenumber-js": {
|
||||||
|
"version": "1.12.34",
|
||||||
|
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.34.tgz",
|
||||||
|
"integrity": "sha512-v/Ip8k8eYdp7bINpzqDh46V/PaQ8sK+qi97nMQgjZzFlb166YFqlR/HVI+MzsI9JqcyyVWCOipmmretiaSyQyw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lightningcss": {
|
"node_modules/lightningcss": {
|
||||||
"version": "1.30.2",
|
"version": "1.30.2",
|
||||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
||||||
@@ -3594,6 +3624,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/loose-envify": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"loose-envify": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
@@ -3704,6 +3746,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/object-assign": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/open-location-code": {
|
"node_modules/open-location-code": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/open-location-code/-/open-location-code-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/open-location-code/-/open-location-code-1.0.3.tgz",
|
||||||
@@ -3805,7 +3856,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -3851,6 +3901,17 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prop-types": {
|
||||||
|
"version": "15.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
|
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"react-is": "^16.13.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proxy-from-env": {
|
"node_modules/proxy-from-env": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
@@ -3872,7 +3933,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||||
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -3882,7 +3942,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||||
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
@@ -3933,6 +3992,29 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-is": {
|
||||||
|
"version": "16.13.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/react-phone-number-input": {
|
||||||
|
"version": "3.4.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-phone-number-input/-/react-phone-number-input-3.4.14.tgz",
|
||||||
|
"integrity": "sha512-T9MziNuvthzv6+JAhKD71ab/jVXW5U20nQZRBJd6+q+ujmkC+/ISOf2GYo8pIi4VGjdIYRIHDftMAYn3WKZT3w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"classnames": "^2.5.1",
|
||||||
|
"country-flag-icons": "^1.5.17",
|
||||||
|
"input-format": "^0.3.14",
|
||||||
|
"libphonenumber-js": "^1.12.27",
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8",
|
||||||
|
"react-dom": ">=16.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-router": {
|
"node_modules/react-router": {
|
||||||
"version": "7.12.0",
|
"version": "7.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz",
|
||||||
@@ -4185,7 +4267,6 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -4280,7 +4361,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
@@ -4411,7 +4491,6 @@
|
|||||||
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
|
"integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -4,7 +4,9 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"local": "vite --mode localhost",
|
||||||
|
"dev": "vite --mode development",
|
||||||
|
"prod": "vite --mode production",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
@@ -22,6 +24,7 @@
|
|||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-hook-form": "^7.50.0",
|
"react-hook-form": "^7.50.0",
|
||||||
"react-i18next": "^16.5.3",
|
"react-i18next": "^16.5.3",
|
||||||
|
"react-phone-number-input": "^3.4.14",
|
||||||
"react-router-dom": "^7.12.0",
|
"react-router-dom": "^7.12.0",
|
||||||
"react-toastify": "^11.0.5",
|
"react-toastify": "^11.0.5",
|
||||||
"tailwindcss": "^4.1.18"
|
"tailwindcss": "^4.1.18"
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { apiClient } from "../../lib/apiClient";
|
||||||
|
import type {
|
||||||
|
ApiMessage,
|
||||||
|
User,
|
||||||
|
UserApiResponse,
|
||||||
|
UserCreateRequest,
|
||||||
|
UserStatus,
|
||||||
|
UserUpdateRequest,
|
||||||
|
UserPaginatedResponse,
|
||||||
|
} from "./UserTypes";
|
||||||
|
|
||||||
|
const buildQueryString = (params: Record<string, any>): string => {
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined && value !== null && value !== "") {
|
||||||
|
searchParams.append(key, String(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const query = searchParams.toString();
|
||||||
|
return query ? `?${query}` : "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeStatus = (status?: string): UserStatus =>
|
||||||
|
status?.toLowerCase() === "inactive" ? "inactive" : "active";
|
||||||
|
|
||||||
|
const normalizeId = (value?: string | null) => {
|
||||||
|
if (value === undefined) return undefined;
|
||||||
|
if (value === null) return null;
|
||||||
|
const trimmed = value.trim();
|
||||||
|
return trimmed ? trimmed : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toUser = (user: UserApiResponse): User => ({
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
password: "",
|
||||||
|
first_name: user.first_name,
|
||||||
|
last_name: user.last_name ?? null,
|
||||||
|
phone_number: user.phone_number ?? null,
|
||||||
|
status: normalizeStatus(user.status),
|
||||||
|
tenant_id: user.tenant_id ?? null,
|
||||||
|
tenant_name: null,
|
||||||
|
role_id: user.role_id ?? null,
|
||||||
|
role_name: null,
|
||||||
|
created_at: user.created_at,
|
||||||
|
updated_at: user.updated_at,
|
||||||
|
password_updated_at: user.password_updated_at ?? user.updated_at,
|
||||||
|
});
|
||||||
|
|
||||||
|
const toCreatePayload = (payload: UserCreateRequest) => ({
|
||||||
|
email: payload.email,
|
||||||
|
password: payload.password,
|
||||||
|
first_name: payload.first_name,
|
||||||
|
last_name: payload.last_name || undefined,
|
||||||
|
phone_number: payload.phone_number || undefined,
|
||||||
|
status: normalizeStatus(payload.status),
|
||||||
|
tenant_id: normalizeId(payload.tenant_id),
|
||||||
|
role_id: normalizeId(payload.role_id),
|
||||||
|
});
|
||||||
|
|
||||||
|
const toUpdatePayload = (payload: UserUpdateRequest) => ({
|
||||||
|
email: payload.email?.trim() || undefined,
|
||||||
|
password: payload.password,
|
||||||
|
first_name: payload.first_name?.trim() || undefined,
|
||||||
|
last_name:
|
||||||
|
payload.last_name !== undefined ? payload.last_name?.trim() || null : undefined,
|
||||||
|
phone_number:
|
||||||
|
payload.phone_number !== undefined
|
||||||
|
? payload.phone_number?.trim() || null
|
||||||
|
: undefined,
|
||||||
|
status: payload.status ? normalizeStatus(payload.status) : undefined,
|
||||||
|
tenant_id:
|
||||||
|
payload.tenant_id !== undefined ? normalizeId(payload.tenant_id) : undefined,
|
||||||
|
role_id:
|
||||||
|
payload.role_id !== undefined ? normalizeId(payload.role_id) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const usersApi = {
|
||||||
|
getAll: async () => {
|
||||||
|
const data = await apiClient.get<UserApiResponse[]>("/api/user/get");
|
||||||
|
return data.map(toUser);
|
||||||
|
},
|
||||||
|
|
||||||
|
getById: async (userId: string) => {
|
||||||
|
const data = await apiClient.get<UserApiResponse>(`/api/user/get/${userId}`);
|
||||||
|
return toUser(data);
|
||||||
|
},
|
||||||
|
|
||||||
|
create: async (payload: UserCreateRequest) => {
|
||||||
|
const data = await apiClient.post<UserApiResponse>(
|
||||||
|
"/api/user/create",
|
||||||
|
toCreatePayload(payload),
|
||||||
|
{ successMessage: "User created successfully", errorMessage: "Failed to create user" }
|
||||||
|
);
|
||||||
|
return toUser(data);
|
||||||
|
},
|
||||||
|
|
||||||
|
update: async (userId: string, payload: UserUpdateRequest) => {
|
||||||
|
const data = await apiClient.put<UserApiResponse>(
|
||||||
|
`/api/user/update/${userId}`,
|
||||||
|
toUpdatePayload(payload),
|
||||||
|
{ successMessage: "User updated successfully", errorMessage: "Failed to update user" }
|
||||||
|
);
|
||||||
|
return toUser(data);
|
||||||
|
},
|
||||||
|
|
||||||
|
remove: async (userId: string): Promise<ApiMessage> =>
|
||||||
|
apiClient.delete<ApiMessage>(`/api/user/delete/${userId}`, { successMessage: "User deleted", errorMessage: "Failed to delete user" }),
|
||||||
|
|
||||||
|
getPaginated: (params: {
|
||||||
|
page?: number;
|
||||||
|
page_size?: number;
|
||||||
|
search?: string;
|
||||||
|
status?: UserStatus | null;
|
||||||
|
}) => {
|
||||||
|
const queryString = buildQueryString({
|
||||||
|
page: params.page,
|
||||||
|
page_size: params.page_size,
|
||||||
|
search: params.search,
|
||||||
|
status: params.status ?? undefined,
|
||||||
|
});
|
||||||
|
return apiClient.get<UserPaginatedResponse>(`/api/user/list${queryString}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
export type UserStatus = "active" | "inactive";
|
||||||
|
|
||||||
|
export type User = {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
first_name: string;
|
||||||
|
last_name?: string | null;
|
||||||
|
phone_number?: string | null;
|
||||||
|
status: UserStatus;
|
||||||
|
tenant_id?: string | null;
|
||||||
|
tenant_name?: string | null;
|
||||||
|
role_id?: string | null;
|
||||||
|
role_name?: string | null;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
password_updated_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserCreateRequest = {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
first_name: string;
|
||||||
|
last_name?: string;
|
||||||
|
phone_number?: string;
|
||||||
|
status?: UserStatus;
|
||||||
|
tenant_id?: string;
|
||||||
|
role_id?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserUpdateRequest = {
|
||||||
|
email?: string;
|
||||||
|
password?: string;
|
||||||
|
first_name?: string;
|
||||||
|
last_name?: string | null;
|
||||||
|
phone_number?: string | null;
|
||||||
|
status?: UserStatus;
|
||||||
|
tenant_id?: string | null;
|
||||||
|
role_id?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiMessage = {
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserApiResponse = {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
first_name: string;
|
||||||
|
last_name?: string | null;
|
||||||
|
phone_number?: string | null;
|
||||||
|
status?: string;
|
||||||
|
tenant_id?: string | null;
|
||||||
|
role_id?: string | null;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
password_updated_at?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserPaginatedResponse = {
|
||||||
|
items: User[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
page_size: number;
|
||||||
|
total_pages: number;
|
||||||
|
};
|
||||||
@@ -0,0 +1,416 @@
|
|||||||
|
import { useEffect, useMemo, useState, useCallback } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
CustomButton,
|
||||||
|
CustomBackButton,
|
||||||
|
CustomCheckBox,
|
||||||
|
CustomDropdown,
|
||||||
|
CustomInput,
|
||||||
|
} from "../../../components/custom";
|
||||||
|
import { CustomPhoneInput } from "../../../components/custom/CustomPhoneInput";
|
||||||
|
import { usersApi } from "../UserApi";
|
||||||
|
import type { UserCreateRequest, UserStatus } from "../UserTypes";
|
||||||
|
import { tenantsApi } from "../../tenants/TenantsApi";
|
||||||
|
import type { Tenant } from "../../tenants/TenantsTypes";
|
||||||
|
import { rolesApi } from "../../roles/RolesApi";
|
||||||
|
import type { Role } from "../../roles/RolesTypes";
|
||||||
|
import { useAuth } from "../../../context/AuthContext";
|
||||||
|
|
||||||
|
const normalizeStatus = (value: string): UserStatus =>
|
||||||
|
value.toLowerCase() === "inactive" ? "inactive" : "active";
|
||||||
|
|
||||||
|
const AddUsers = () => {
|
||||||
|
const { t } = useTranslation(['users', 'common']);
|
||||||
|
const { user, hasAccess, isLoading: isAuthLoading } = useAuth();
|
||||||
|
const canReadAllTenants = hasAccess("superadmin.tenant.read");
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [formData, setFormData] = useState<UserCreateRequest>({
|
||||||
|
first_name: "",
|
||||||
|
last_name: "",
|
||||||
|
email: "",
|
||||||
|
password: "",
|
||||||
|
phone_number: "",
|
||||||
|
tenant_id: "",
|
||||||
|
role_id: "",
|
||||||
|
status: "active",
|
||||||
|
});
|
||||||
|
const [isSuperAdminUser, setIsSuperAdminUser] = useState(false);
|
||||||
|
const [lastTenantId, setLastTenantId] = useState("");
|
||||||
|
const [tenants, setTenants] = useState<Tenant[]>([]);
|
||||||
|
const [currentTenantName, setCurrentTenantName] = useState("");
|
||||||
|
const [roles, setRoles] = useState<Role[]>([]);
|
||||||
|
const [isTenantLoading, setIsTenantLoading] = useState(false);
|
||||||
|
const [isRoleLoading, setIsRoleLoading] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
|
const [loadError, setLoadError] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAuthLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isMounted = true;
|
||||||
|
|
||||||
|
const loadTenants = async () => {
|
||||||
|
if (!canReadAllTenants) {
|
||||||
|
setIsTenantLoading(true);
|
||||||
|
try {
|
||||||
|
if (user?.tenant_id && user?.tenant_name) {
|
||||||
|
if (isMounted) {
|
||||||
|
setCurrentTenantName(user.tenant_name);
|
||||||
|
setTenants([]);
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
tenant_id: user.tenant_id ?? "",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await tenantsApi.getMine();
|
||||||
|
if (isMounted) {
|
||||||
|
setCurrentTenantName(data.tenant_name);
|
||||||
|
setTenants([]);
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
tenant_id: data.id,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (isMounted) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Unable to load tenant.";
|
||||||
|
setLoadError((prev) => prev || message);
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
tenant_id: user?.tenant_id ?? "",
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (isMounted) {
|
||||||
|
setIsTenantLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsTenantLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await tenantsApi.getAll();
|
||||||
|
if (isMounted) {
|
||||||
|
setTenants(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (isMounted) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: "Unable to load tenants.";
|
||||||
|
setLoadError((prev) => prev || message);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (isMounted) {
|
||||||
|
setIsTenantLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadRoles = async () => {
|
||||||
|
setIsRoleLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await rolesApi.getAll();
|
||||||
|
if (isMounted) {
|
||||||
|
setRoles(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (isMounted) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : "Unable to load roles.";
|
||||||
|
setLoadError((prev) => prev || message);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (isMounted) {
|
||||||
|
setIsRoleLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadTenants();
|
||||||
|
loadRoles();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
|
}, [canReadAllTenants, isAuthLoading, user?.tenant_id, user?.tenant_name]);
|
||||||
|
|
||||||
|
const isRoleAllowedForTenant = useCallback(
|
||||||
|
(role: Role, tenantId?: string, forceSuperAdmin?: boolean) => {
|
||||||
|
const isSuperAdminMode = forceSuperAdmin ?? isSuperAdminUser;
|
||||||
|
if (isSuperAdminMode) {
|
||||||
|
return !role.tenant_id;
|
||||||
|
}
|
||||||
|
if (canReadAllTenants) {
|
||||||
|
if (!tenantId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return role.tenant_id === tenantId;
|
||||||
|
}
|
||||||
|
if (tenantId) {
|
||||||
|
return !role.tenant_id || role.tenant_id === tenantId;
|
||||||
|
}
|
||||||
|
return !role.tenant_id;
|
||||||
|
},
|
||||||
|
[canReadAllTenants, isSuperAdminUser]
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredRoles = useMemo(
|
||||||
|
() =>
|
||||||
|
roles.filter((role) =>
|
||||||
|
isRoleAllowedForTenant(role, formData.tenant_id)
|
||||||
|
),
|
||||||
|
[isRoleAllowedForTenant, roles, formData.tenant_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
|
||||||
|
) => {
|
||||||
|
const { name, value } = event.target;
|
||||||
|
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePhoneChange = (value: string | undefined) => {
|
||||||
|
setFormData((prev) => ({ ...prev, phone_number: value || "" }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRoleChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const { value } = event.target;
|
||||||
|
setFormData((prev) => ({
|
||||||
|
...prev,
|
||||||
|
role_id: value || "",
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTenantChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const tenantId = event.target.value;
|
||||||
|
setLastTenantId(tenantId);
|
||||||
|
setFormData((prev) => {
|
||||||
|
const roleStillValid = roles.some(
|
||||||
|
(role) =>
|
||||||
|
role.id === prev.role_id && isRoleAllowedForTenant(role, tenantId)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
tenant_id: tenantId || "",
|
||||||
|
role_id: roleStillValid ? prev.role_id : "",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSuperAdminToggle = (
|
||||||
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
|
const checked = event.target.checked;
|
||||||
|
setIsSuperAdminUser(checked);
|
||||||
|
if (checked) {
|
||||||
|
setLastTenantId(formData.tenant_id || "");
|
||||||
|
}
|
||||||
|
setFormData((prev) => {
|
||||||
|
const nextTenantId = checked ? "" : lastTenantId || "";
|
||||||
|
const roleStillValid = roles.some(
|
||||||
|
(role) =>
|
||||||
|
role.id === prev.role_id &&
|
||||||
|
isRoleAllowedForTenant(role, nextTenantId, checked)
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
tenant_id: nextTenantId,
|
||||||
|
role_id: roleStillValid ? prev.role_id : "",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (event: React.FormEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setErrorMessage("");
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const statusValue = formData.status?.trim() || "active";
|
||||||
|
const payload: UserCreateRequest = {
|
||||||
|
first_name: formData.first_name.trim(),
|
||||||
|
last_name: formData.last_name?.trim() || undefined,
|
||||||
|
email: formData.email.trim(),
|
||||||
|
password: formData.password,
|
||||||
|
phone_number: formData.phone_number?.trim() || undefined,
|
||||||
|
tenant_id: formData.tenant_id?.trim() || undefined,
|
||||||
|
role_id: formData.role_id?.trim() || undefined,
|
||||||
|
status: normalizeStatus(statusValue),
|
||||||
|
};
|
||||||
|
|
||||||
|
await usersApi.create(payload);
|
||||||
|
navigate("/users");
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : "Unable to add user.";
|
||||||
|
setErrorMessage(message);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<CustomBackButton to="/users" tooltip={t('backToUsers')} />
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-semibold text-(--text-primary)">{t('add')}</h1>
|
||||||
|
<p className="text-sm text-(--text-secondary)">
|
||||||
|
{t('subTitle')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="space-y-6 rounded-lg border border-(--card-border) bg-(--card-bg) p-6 shadow-sm"
|
||||||
|
>
|
||||||
|
{canReadAllTenants && (
|
||||||
|
<div className="flex items-center justify-end gap-3">
|
||||||
|
<CustomCheckBox
|
||||||
|
label={t('fields.superAdminUser')}
|
||||||
|
checked={isSuperAdminUser}
|
||||||
|
onChange={handleSuperAdminToggle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
|
<CustomInput
|
||||||
|
label={t('fields.firstName')}
|
||||||
|
name="first_name"
|
||||||
|
placeholder={t('fields.firstNamePlaceholder')}
|
||||||
|
value={formData.first_name}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<CustomInput
|
||||||
|
label={t('fields.lastName')}
|
||||||
|
name="last_name"
|
||||||
|
placeholder={t('fields.lastNamePlaceholder')}
|
||||||
|
value={formData.last_name}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
<CustomInput
|
||||||
|
label={t('fields.email')}
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
placeholder={t('fields.emailPlaceholder')}
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CustomPhoneInput
|
||||||
|
label={t('fields.phone')}
|
||||||
|
name="phone_number"
|
||||||
|
placeholder={t('fields.phonePlaceholder')}
|
||||||
|
value={formData.phone_number}
|
||||||
|
onChange={handlePhoneChange}
|
||||||
|
required
|
||||||
|
defaultCountry="IN"
|
||||||
|
/>
|
||||||
|
{canReadAllTenants ? (
|
||||||
|
isSuperAdminUser ? (
|
||||||
|
<CustomInput
|
||||||
|
label={t('fields.tenant')}
|
||||||
|
name="tenant_name"
|
||||||
|
value={t('fields.superAdminNoTenant')}
|
||||||
|
disabled
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CustomDropdown
|
||||||
|
label={t('fields.tenant')}
|
||||||
|
name="tenant_id"
|
||||||
|
placeholder={
|
||||||
|
isTenantLoading ? t('fields.tenantLoading') : t('fields.tenantPlaceholder')
|
||||||
|
}
|
||||||
|
value={formData.tenant_id ?? ""}
|
||||||
|
onChange={handleTenantChange}
|
||||||
|
options={tenants.map((tenant) => ({
|
||||||
|
label: tenant.tenant_name,
|
||||||
|
value: tenant.id,
|
||||||
|
}))}
|
||||||
|
disabled={isTenantLoading}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<CustomInput
|
||||||
|
label={t('fields.tenant')}
|
||||||
|
name="tenant_name"
|
||||||
|
value={currentTenantName || user?.tenant_name || user?.tenant_id || ""}
|
||||||
|
placeholder={
|
||||||
|
isTenantLoading ? t('fields.tenantLoading') : t('fields.assignedTenant')
|
||||||
|
}
|
||||||
|
disabled
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<CustomDropdown
|
||||||
|
label={t('fields.role')}
|
||||||
|
name="role_id"
|
||||||
|
placeholder={
|
||||||
|
isSuperAdminUser
|
||||||
|
? isRoleLoading
|
||||||
|
? t('fields.roleLoading')
|
||||||
|
: t('fields.roleSuperAdminPlaceholder')
|
||||||
|
: formData.tenant_id
|
||||||
|
? isRoleLoading
|
||||||
|
? t('fields.roleLoading')
|
||||||
|
: t('fields.rolePlaceholder')
|
||||||
|
: t('fields.roleFilterPlaceholder')
|
||||||
|
}
|
||||||
|
value={formData.role_id ?? ""}
|
||||||
|
onChange={handleRoleChange}
|
||||||
|
options={filteredRoles.map((role) => ({
|
||||||
|
label: role.role_name,
|
||||||
|
value: role.id,
|
||||||
|
}))}
|
||||||
|
disabled={isRoleLoading || (!isRoleLoading && filteredRoles.length === 0)}
|
||||||
|
/>
|
||||||
|
<CustomInput
|
||||||
|
label={t('fields.password')}
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
placeholder={t('fields.passwordPlaceholder')}
|
||||||
|
value={formData.password}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(errorMessage || loadError) && (
|
||||||
|
<div className="rounded-lg border border-red-200 bg-red-50 dark:bg-red-900/20 px-4 py-2 text-sm text-red-600 dark:text-red-400">
|
||||||
|
{errorMessage || loadError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<CustomButton type="submit" variant="primary" loading={isLoading}>
|
||||||
|
{t('actions.create')}
|
||||||
|
</CustomButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddUsers;
|
||||||
@@ -0,0 +1,561 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { Edit2, Eye, Trash2 } from "lucide-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import {
|
||||||
|
CustomButton,
|
||||||
|
CustomCheckBox,
|
||||||
|
CustomConfirmationModal,
|
||||||
|
CustomDropdown,
|
||||||
|
CustomInput,
|
||||||
|
CustomModal,
|
||||||
|
CustomTable,
|
||||||
|
CustomLoader,
|
||||||
|
CustomActionMenu,
|
||||||
|
CustomActionItem,
|
||||||
|
CustomStatus,
|
||||||
|
} from "../../../components/custom";
|
||||||
|
import type { ColumnDef } from "../../../components/custom/CustomTable";
|
||||||
|
import type {
|
||||||
|
User,
|
||||||
|
UserStatus,
|
||||||
|
UserUpdateRequest,
|
||||||
|
UserPaginatedResponse,
|
||||||
|
} from "../UserTypes";
|
||||||
|
import { usersApi } from "../UserApi";
|
||||||
|
import { rolesApi } from "../../roles/RolesApi";
|
||||||
|
import type { Role } from "../../roles/RolesTypes";
|
||||||
|
import { useAuth } from "../../../context/AuthContext";
|
||||||
|
import { tenantsApi } from "../../tenants/TenantsApi";
|
||||||
|
import type { Tenant } from "../../tenants/TenantsTypes";
|
||||||
|
import { useDebounce } from "../../../components/hooks/useDebounce";
|
||||||
|
import { ProtectedComponent } from "../../../components/auth/ProtectedComponent";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const formatName = (firstName: string, lastName?: string | null) =>
|
||||||
|
[firstName, lastName].filter(Boolean).join(" ");
|
||||||
|
|
||||||
|
const formatDate = (dateString?: string | null, language: string = 'en') => {
|
||||||
|
if (!dateString) return "";
|
||||||
|
try {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
if (isNaN(date.getTime())) return dateString;
|
||||||
|
|
||||||
|
const hasTime = dateString.includes("T") || dateString.includes(":");
|
||||||
|
|
||||||
|
const options: Intl.DateTimeFormatOptions = {
|
||||||
|
day: "numeric",
|
||||||
|
month: "short",
|
||||||
|
year: "numeric",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (hasTime) {
|
||||||
|
options.hour = "2-digit";
|
||||||
|
options.minute = "2-digit";
|
||||||
|
options.hour12 = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return date.toLocaleString(language === 'ar' ? 'ar-EG' : 'en-GB', options);
|
||||||
|
} catch {
|
||||||
|
return dateString;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
type EditFormState = {
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
email: string;
|
||||||
|
phone_number: string;
|
||||||
|
tenant_id: string;
|
||||||
|
role_id: string;
|
||||||
|
status: UserStatus;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const emptyEditForm: EditFormState = {
|
||||||
|
first_name: "",
|
||||||
|
last_name: "",
|
||||||
|
email: "",
|
||||||
|
phone_number: "",
|
||||||
|
tenant_id: "",
|
||||||
|
role_id: "",
|
||||||
|
status: "active",
|
||||||
|
password: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const AllUsers = () => {
|
||||||
|
const { t, i18n } = useTranslation(['users', 'common']);
|
||||||
|
const { user: currentUser, hasAccess, isLoading: isAuthLoading } = useAuth();
|
||||||
|
|
||||||
|
// State
|
||||||
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
|
|
||||||
|
|
||||||
|
// Pagination & filters
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [pageSize, setPageSize] = useState(10);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [statusFilter, setStatusFilter] = useState<UserStatus | null>(null);
|
||||||
|
const [totalRows, setTotalRows] = useState(0);
|
||||||
|
const [, setTotalPages] = useState(0);
|
||||||
|
|
||||||
|
const debouncedSearch = useDebounce(search, 500);
|
||||||
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const prevLoadingRef = useRef(isLoading);
|
||||||
|
|
||||||
|
// Modals
|
||||||
|
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||||||
|
const [isViewOpen, setIsViewOpen] = useState(false);
|
||||||
|
const [isEditOpen, setIsEditOpen] = useState(false);
|
||||||
|
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||||
|
const [editForm, setEditForm] = useState<EditFormState>(emptyEditForm);
|
||||||
|
const [editError, setEditError] = useState("");
|
||||||
|
const [deleteError, setDeleteError] = useState("");
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
|
||||||
|
// Supporting data
|
||||||
|
const [roles, setRoles] = useState<Role[]>([]);
|
||||||
|
const [tenants, setTenants] = useState<Tenant[]>([]);
|
||||||
|
const [currentTenant, setCurrentTenant] = useState<Tenant | null>(null);
|
||||||
|
const [isRoleLoading, setIsRoleLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isMounted = true;
|
||||||
|
|
||||||
|
const loadSupportingData = async () => {
|
||||||
|
setIsRoleLoading(true);
|
||||||
|
try {
|
||||||
|
const rolesData = await rolesApi.getAll();
|
||||||
|
if (isMounted) setRoles(rolesData);
|
||||||
|
|
||||||
|
if (hasAccess("superadmin.tenant.read")) {
|
||||||
|
const tenantsData = await tenantsApi.getAll();
|
||||||
|
if (isMounted) setTenants(tenantsData);
|
||||||
|
} else if (currentUser?.tenant_id) {
|
||||||
|
const tenant = await tenantsApi.getMine();
|
||||||
|
if (isMounted) setCurrentTenant(tenant);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load roles/tenants:", err);
|
||||||
|
} finally {
|
||||||
|
if (isMounted) setIsRoleLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadUsers = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setErrorMessage("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response: UserPaginatedResponse = await usersApi.getPaginated({
|
||||||
|
page,
|
||||||
|
page_size: pageSize,
|
||||||
|
search: debouncedSearch || undefined,
|
||||||
|
status: statusFilter,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isMounted) {
|
||||||
|
setUsers(response.items);
|
||||||
|
setTotalRows(response.total);
|
||||||
|
setTotalPages(response.total_pages);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (isMounted) {
|
||||||
|
const msg = err instanceof Error ? err.message : "Unable to load users.";
|
||||||
|
setErrorMessage(msg);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (isMounted) setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isAuthLoading) {
|
||||||
|
loadSupportingData();
|
||||||
|
loadUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
isMounted = false;
|
||||||
|
};
|
||||||
|
}, [
|
||||||
|
isAuthLoading,
|
||||||
|
hasAccess,
|
||||||
|
currentUser?.tenant_id,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
debouncedSearch,
|
||||||
|
statusFilter,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Reset page on search/filter change
|
||||||
|
useEffect(() => {
|
||||||
|
setPage(1);
|
||||||
|
}, [debouncedSearch, statusFilter]);
|
||||||
|
|
||||||
|
// Restore search focus
|
||||||
|
useEffect(() => {
|
||||||
|
if (prevLoadingRef.current && !isLoading && search.trim()) {
|
||||||
|
searchInputRef.current?.focus({ preventScroll: true });
|
||||||
|
}
|
||||||
|
prevLoadingRef.current = isLoading;
|
||||||
|
}, [isLoading, search]);
|
||||||
|
|
||||||
|
const getTenantName = useCallback(
|
||||||
|
(tenantId?: string | null) => {
|
||||||
|
if (!tenantId) return "";
|
||||||
|
const found = tenants.find((t) => t.id === tenantId);
|
||||||
|
if (found) return found.tenant_name;
|
||||||
|
if (currentTenant?.id === tenantId) return currentTenant.tenant_name;
|
||||||
|
return tenantId;
|
||||||
|
},
|
||||||
|
[tenants, currentTenant]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getRoleName = useCallback(
|
||||||
|
(roleId?: string | null) => {
|
||||||
|
if (!roleId) return "";
|
||||||
|
return roles.find((r) => r.id === roleId)?.role_name || roleId;
|
||||||
|
},
|
||||||
|
[roles]
|
||||||
|
);
|
||||||
|
|
||||||
|
const openView = useCallback((user: User) => {
|
||||||
|
setSelectedUser(user);
|
||||||
|
setIsViewOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const openEdit = useCallback(
|
||||||
|
(user: User) => {
|
||||||
|
const resolvedTenantId = currentUser?.tenant_id ?? user.tenant_id ?? "";
|
||||||
|
setSelectedUser(user);
|
||||||
|
setEditForm({
|
||||||
|
first_name: user.first_name,
|
||||||
|
last_name: user.last_name ?? "",
|
||||||
|
email: user.email,
|
||||||
|
phone_number: user.phone_number ?? "",
|
||||||
|
tenant_id: resolvedTenantId,
|
||||||
|
role_id: user.role_id ?? "",
|
||||||
|
status: user.status,
|
||||||
|
password: "",
|
||||||
|
});
|
||||||
|
setEditError("");
|
||||||
|
setIsEditOpen(true);
|
||||||
|
},
|
||||||
|
[currentUser?.tenant_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const openDelete = useCallback((user: User) => {
|
||||||
|
setSelectedUser(user);
|
||||||
|
setDeleteError("");
|
||||||
|
setIsDeleteOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeView = useCallback(() => {
|
||||||
|
setIsViewOpen(false);
|
||||||
|
setSelectedUser(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeEdit = useCallback(() => {
|
||||||
|
setIsEditOpen(false);
|
||||||
|
setSelectedUser(null);
|
||||||
|
setEditError("");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeDelete = useCallback(() => {
|
||||||
|
setIsDeleteOpen(false);
|
||||||
|
setSelectedUser(null);
|
||||||
|
setDeleteError("");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleEditChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setEditForm((prev) => ({ ...prev, [name]: value }));
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleStatusChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setEditForm((prev) => ({
|
||||||
|
...prev,
|
||||||
|
status: e.target.checked ? "active" : "inactive",
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredRoles = useMemo(
|
||||||
|
() =>
|
||||||
|
roles.filter((role) => {
|
||||||
|
if (editForm.tenant_id) {
|
||||||
|
return !role.tenant_id || role.tenant_id === editForm.tenant_id;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
|
[roles, editForm.tenant_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleUpdate = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!selectedUser) return;
|
||||||
|
|
||||||
|
setIsSaving(true);
|
||||||
|
setEditError("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload: UserUpdateRequest = {
|
||||||
|
first_name: editForm.first_name.trim(),
|
||||||
|
last_name: editForm.last_name.trim() || null,
|
||||||
|
email: editForm.email.trim(),
|
||||||
|
phone_number: editForm.phone_number.trim() || null,
|
||||||
|
status: editForm.status,
|
||||||
|
role_id: editForm.role_id || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (editForm.password.trim()) {
|
||||||
|
payload.password = editForm.password.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await usersApi.update(selectedUser.id, payload);
|
||||||
|
setUsers((prev) => prev.map((u) => (u.id === updated.id ? updated : u)));
|
||||||
|
closeEdit();
|
||||||
|
} catch (err) {
|
||||||
|
setEditError(err instanceof Error ? err.message : "Failed to update user");
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (!selectedUser) return;
|
||||||
|
setIsDeleting(true);
|
||||||
|
try {
|
||||||
|
await usersApi.remove(selectedUser.id);
|
||||||
|
setUsers((prev) => prev.filter((u) => u.id !== selectedUser.id));
|
||||||
|
closeDelete();
|
||||||
|
} catch (err) {
|
||||||
|
setDeleteError(err instanceof Error ? err.message : "Failed to delete user");
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = useMemo<Array<ColumnDef<User>>>(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
key: "first_name",
|
||||||
|
header: t('columns.name'),
|
||||||
|
render: (row) => formatName(row.first_name, row.last_name),
|
||||||
|
},
|
||||||
|
{ key: "email", header: t('columns.email') },
|
||||||
|
{
|
||||||
|
key: "phone_number",
|
||||||
|
header: t('columns.phone'),
|
||||||
|
render: (row) => row.phone_number || "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "status",
|
||||||
|
header: t('columns.status'),
|
||||||
|
searchable: false,
|
||||||
|
render: (row) => <CustomStatus status={row.status} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "tenant_id",
|
||||||
|
header: t('columns.tenant'),
|
||||||
|
render: (row) => getTenantName(row.tenant_id),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "role_id",
|
||||||
|
header: t('columns.role'),
|
||||||
|
render: (row) => getRoleName(row.role_id),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
key: "id",
|
||||||
|
header: t('columns.actions'),
|
||||||
|
searchable: false,
|
||||||
|
render: (row) => (
|
||||||
|
<CustomActionMenu>
|
||||||
|
<ProtectedComponent requiredAccess="admin.user.read">
|
||||||
|
<CustomActionItem onClick={() => openView(row)}>
|
||||||
|
<Eye size={16} className="mr-2" /> {t('actions.view')}
|
||||||
|
</CustomActionItem>
|
||||||
|
</ProtectedComponent>
|
||||||
|
|
||||||
|
<ProtectedComponent requiredAccess="admin.user.update">
|
||||||
|
<CustomActionItem onClick={() => openEdit(row)}>
|
||||||
|
<Edit2 size={16} className="mr-2" /> {t('actions.edit')}
|
||||||
|
</CustomActionItem>
|
||||||
|
</ProtectedComponent>
|
||||||
|
|
||||||
|
<ProtectedComponent requiredAccess="admin.user.delete">
|
||||||
|
<CustomActionItem onClick={() => openDelete(row)} className="text-red-600 hover:text-red-700 hover:bg-red-50">
|
||||||
|
<Trash2 size={16} className="mr-2" /> {t('actions.delete')}
|
||||||
|
</CustomActionItem>
|
||||||
|
</ProtectedComponent>
|
||||||
|
</CustomActionMenu>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[getTenantName, getRoleName, openView, openEdit, openDelete, t]
|
||||||
|
);
|
||||||
|
|
||||||
|
const filterControls = (
|
||||||
|
<select
|
||||||
|
value={statusFilter === null ? "all" : statusFilter}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value as "all" | "active" | "inactive";
|
||||||
|
setStatusFilter(val === "all" ? null : val);
|
||||||
|
}}
|
||||||
|
className="rounded-md border border-(--card-border) bg-(--card-bg) text-(--text-primary) text-sm py-1.5 px-2 focus:outline-none focus:ring focus:ring-blue-600"
|
||||||
|
>
|
||||||
|
<option value="all" className="bg-(--card-bg) text-(--text-primary)">
|
||||||
|
{t('common:common.allStatuses', { defaultValue: 'All Statuses' })}
|
||||||
|
</option>
|
||||||
|
<option value="active" className="bg-(--card-bg) text-(--text-primary)">
|
||||||
|
{t('common:status.active')}
|
||||||
|
</option>
|
||||||
|
<option value="inactive" className="bg-(--card-bg) text-(--text-primary)">
|
||||||
|
{t('common:status.inactive')}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-semibold text-[var(--text-primary)]">{t('title')}</h1>
|
||||||
|
</div>
|
||||||
|
<ProtectedComponent requiredAccess="admin.user.create">
|
||||||
|
<Link to="/users/add">
|
||||||
|
<CustomButton variant="primary">+ {t('add')}</CustomButton>
|
||||||
|
</Link>
|
||||||
|
</ProtectedComponent>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="rounded-lg border border-[var(--card-border)] bg-[var(--card-bg)] p-6">
|
||||||
|
<CustomLoader />
|
||||||
|
</div>
|
||||||
|
) : errorMessage ? (
|
||||||
|
<div className="rounded-lg border border-red-200 bg-red-50 p-6 text-sm text-red-600">
|
||||||
|
{errorMessage}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<CustomTable
|
||||||
|
data={users}
|
||||||
|
columns={columns}
|
||||||
|
getRowId={(row) => row.id}
|
||||||
|
manualPagination
|
||||||
|
manualFiltering
|
||||||
|
totalRows={totalRows}
|
||||||
|
page={page}
|
||||||
|
pageSize={pageSize}
|
||||||
|
search={search}
|
||||||
|
onPageChange={setPage}
|
||||||
|
onPageSizeChange={setPageSize}
|
||||||
|
onSearchChange={setSearch}
|
||||||
|
searchInputRef={searchInputRef}
|
||||||
|
filterControls={filterControls}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* View Modal */}
|
||||||
|
<CustomModal
|
||||||
|
isOpen={isViewOpen}
|
||||||
|
onClose={closeView}
|
||||||
|
title={t('details')}
|
||||||
|
size="lg"
|
||||||
|
footer={<CustomButton variant="outlined" onClick={closeView}>{t('actions.close')}</CustomButton>}
|
||||||
|
>
|
||||||
|
{selectedUser ? (
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
<div><p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">{t('columns.name')}</p><p className="text-sm font-medium text-[var(--text-primary)]">{formatName(selectedUser.first_name, selectedUser.last_name)}</p></div>
|
||||||
|
<div><p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">{t('columns.email')}</p><p className="text-sm font-medium text-[var(--text-primary)]">{selectedUser.email}</p></div>
|
||||||
|
<div><p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">{t('columns.phone')}</p><p className="text-sm font-medium text-[var(--text-primary)]">{selectedUser.phone_number || "--"}</p></div>
|
||||||
|
<div><p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">{t('columns.status')}</p><CustomStatus status={selectedUser.status} /></div>
|
||||||
|
<div><p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">{t('columns.tenant')}</p><p className="text-sm font-medium text-[var(--text-primary)]">{getTenantName(selectedUser.tenant_id)}</p></div>
|
||||||
|
<div><p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">{t('columns.role')}</p><p className="text-sm font-medium text-[var(--text-primary)]">{getRoleName(selectedUser.role_id)}</p></div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">{t('columns.created')}</p>
|
||||||
|
<p className="text-sm font-medium text-[var(--text-primary)]">
|
||||||
|
{formatDate(selectedUser.created_at, i18n.language)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs uppercase tracking-wide text-[var(--text-secondary)]">{t('columns.updated')}</p>
|
||||||
|
<p className="text-sm font-medium text-[var(--text-primary)]">
|
||||||
|
{formatDate(selectedUser.updated_at, i18n.language)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-[var(--text-secondary)]">{t('common:common.noData')}</p>
|
||||||
|
)}
|
||||||
|
</CustomModal>
|
||||||
|
|
||||||
|
{/* Edit Modal */}
|
||||||
|
<CustomModal
|
||||||
|
isOpen={isEditOpen}
|
||||||
|
onClose={closeEdit}
|
||||||
|
title={t('edit')}
|
||||||
|
size="lg"
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<CustomButton variant="outlined" onClick={closeEdit} disabled={isSaving}>{t('actions.cancel')}</CustomButton>
|
||||||
|
<CustomButton type="submit" form="edit-user-form" variant="primary" loading={isSaving}>{t('actions.update')}</CustomButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<form id="edit-user-form" onSubmit={handleUpdate} className="space-y-6">
|
||||||
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||||
|
<CustomInput label={t('fields.firstName')} name="first_name" value={editForm.first_name} onChange={handleEditChange} required />
|
||||||
|
<CustomInput label={t('fields.lastName')} name="last_name" value={editForm.last_name} onChange={handleEditChange} />
|
||||||
|
<CustomInput label={t('fields.email')} name="email" type="email" value={editForm.email} onChange={handleEditChange} required />
|
||||||
|
<CustomInput label={t('fields.phone')} name="phone_number" type="tel" maxLength={10} phonePrefix="+91" value={editForm.phone_number} onChange={handleEditChange} />
|
||||||
|
<CustomInput label={t('fields.tenant')} value={getTenantName(currentUser?.tenant_id ?? editForm.tenant_id) || "--"} disabled readOnly />
|
||||||
|
<CustomDropdown
|
||||||
|
label={t('fields.role')}
|
||||||
|
name="role_id"
|
||||||
|
value={editForm.role_id}
|
||||||
|
onChange={handleEditChange}
|
||||||
|
options={filteredRoles.map((r) => ({ label: r.role_name, value: r.id }))}
|
||||||
|
placeholder={isRoleLoading ? t('fields.roleLoading') : t('fields.rolePlaceholder')}
|
||||||
|
disabled={isRoleLoading || filteredRoles.length === 0}
|
||||||
|
/>
|
||||||
|
<CustomInput label={t('fields.password')} name="password" type="password" placeholder={t('fields.passwordEditPlaceholder')} value={editForm.password} onChange={handleEditChange} />
|
||||||
|
<div className="flex items-center">
|
||||||
|
<CustomCheckBox label={t('fields.active')} checked={editForm.status === "active"} onChange={handleStatusChange} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{editError && <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm text-red-600">{editError}</div>}
|
||||||
|
</form>
|
||||||
|
</CustomModal>
|
||||||
|
|
||||||
|
{/* Delete Modal */}
|
||||||
|
<CustomConfirmationModal
|
||||||
|
isOpen={isDeleteOpen}
|
||||||
|
onClose={closeDelete}
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
title={t('messages.deleteTitle')}
|
||||||
|
description={deleteError || t('messages.deleteConfirm')}
|
||||||
|
confirmText={t('actions.delete')}
|
||||||
|
variant="danger"
|
||||||
|
isLoading={isDeleting}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AllUsers;
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Route, Routes } from "react-router-dom";
|
||||||
|
import AllUsers from "./components/AllUsers";
|
||||||
|
import AddUsers from "./components/AddUsers";
|
||||||
|
|
||||||
|
const Users: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route index element={<AllUsers />} />
|
||||||
|
<Route path="add" element={<AddUsers />} />
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Users;
|
||||||
@@ -10,6 +10,7 @@ import Tenants from "../application/tenants";
|
|||||||
import ProtectedRoutes from "./ProtectedRoutes";
|
import ProtectedRoutes from "./ProtectedRoutes";
|
||||||
import Theme from "../application/theme";
|
import Theme from "../application/theme";
|
||||||
import SettingsPage from "../application/settings/SettingsPage";
|
import SettingsPage from "../application/settings/SettingsPage";
|
||||||
|
import Users from "../application/users";
|
||||||
const AppRoutes = () => {
|
const AppRoutes = () => {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
@@ -26,6 +27,7 @@ const AppRoutes = () => {
|
|||||||
<Route path="/roles/*" element={<Roles />} />
|
<Route path="/roles/*" element={<Roles />} />
|
||||||
<Route path="/theme/*" element={<Theme />} />
|
<Route path="/theme/*" element={<Theme />} />
|
||||||
<Route path="/settings" element={<SettingsPage />} />
|
<Route path="/settings" element={<SettingsPage />} />
|
||||||
|
<Route path="/users/*" element={<Users />} />
|
||||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
|||||||
+8
-3
@@ -1,8 +1,13 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig, loadEnv } from 'vite'
|
||||||
import react from '@vitejs/plugin-react-swc'
|
import react from '@vitejs/plugin-react-swc'
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig(({ mode }) => {
|
||||||
plugins: [react(),tailwindcss()],
|
const env = loadEnv(mode, process.cwd(), '');
|
||||||
|
console.log(`\n 🔌 Backend API URL: \x1b[36m${env.VITE_API_BASE_URL}\x1b[0m\n`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
plugins: [react(), tailwindcss()],
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user