Get Api and Create Api for tenant.
This commit is contained in:
parent
c672e0cea0
commit
cb27f8c259
261
package-lock.json
generated
261
package-lock.json
generated
@ -18,6 +18,7 @@
|
||||
"apexcharts": "^4.5.0",
|
||||
"axios": "^1.7.9",
|
||||
"axios-retry": "^4.5.0",
|
||||
"bootstrap": "^5.3.7",
|
||||
"dotenv": "^16.4.7",
|
||||
"dotenv-webpack": "^8.1.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
@ -29,6 +30,7 @@
|
||||
"perfect-scrollbar": "^1.5.5",
|
||||
"react": "^18.2.0",
|
||||
"react-apexcharts": "^1.7.0",
|
||||
"react-bootstrap": "^2.10.10",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-quill": "^2.0.0",
|
||||
@ -897,6 +899,31 @@
|
||||
"pako": "^1.0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-aria/ssr": {
|
||||
"version": "3.9.10",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz",
|
||||
"integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.0.tgz",
|
||||
@ -928,6 +955,60 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@restart/hooks": {
|
||||
"version": "0.4.16",
|
||||
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz",
|
||||
"integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@restart/ui": {
|
||||
"version": "1.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.9.4.tgz",
|
||||
"integrity": "sha512-N4C7haUc3vn4LTwVUPlkJN8Ach/+yIMvRuTVIhjilNHqegY60SGLrzud6errOMNJwSnmYFnt1J0H/k8FE3A4KA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@react-aria/ssr": "^3.5.0",
|
||||
"@restart/hooks": "^0.5.0",
|
||||
"@types/warning": "^3.0.3",
|
||||
"dequal": "^2.0.3",
|
||||
"dom-helpers": "^5.2.0",
|
||||
"uncontrollable": "^8.0.4",
|
||||
"warning": "^4.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.14.0",
|
||||
"react-dom": ">=16.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@restart/ui/node_modules/@restart/hooks": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.5.1.tgz",
|
||||
"integrity": "sha512-EMoH04NHS1pbn07iLTjIjgttuqb7qu4+/EyhAx27MHpoENcB2ZdSsLTNxmKD+WEPnZigo62Qc8zjGnNxoSE/5Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dequal": "^2.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@restart/ui/node_modules/uncontrollable": {
|
||||
"version": "8.0.4",
|
||||
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz",
|
||||
"integrity": "sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz",
|
||||
@ -1416,6 +1497,21 @@
|
||||
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.17",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
|
||||
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/helpers/node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/@swc/types": {
|
||||
"version": "0.1.17",
|
||||
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz",
|
||||
@ -1562,8 +1658,7 @@
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
|
||||
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
|
||||
"devOptional": true
|
||||
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="
|
||||
},
|
||||
"node_modules/@types/quill": {
|
||||
"version": "1.3.10",
|
||||
@ -1578,7 +1673,6 @@
|
||||
"version": "18.3.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.16.tgz",
|
||||
"integrity": "sha512-oh8AMIC4Y2ciKufU8hnKgs+ufgbA/dhPTACaZPM86AbwX9QwnFtSoPWEeRUj8fge+v6kFt78BXcDhAU1SrrAsw==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.0.2"
|
||||
@ -1593,11 +1687,26 @@
|
||||
"@types/react": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-transition-group": {
|
||||
"version": "4.4.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
|
||||
"integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"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=="
|
||||
},
|
||||
"node_modules/@types/warning": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz",
|
||||
"integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/web": {
|
||||
"version": "0.0.216",
|
||||
"resolved": "https://registry.npmjs.org/@types/web/-/web-0.0.216.tgz",
|
||||
@ -2140,6 +2249,25 @@
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/bootstrap": {
|
||||
"version": "5.3.7",
|
||||
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.7.tgz",
|
||||
"integrity": "sha512-7KgiD8UHjfcPBHEpDNg+zGz8L3LqR3GVwqZiBRFX04a1BCArZOz1r2kjly2HQ0WokqTO0v1nF+QAt8dsW4lKlw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/twbs"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/bootstrap"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@popperjs/core": "^2.11.8"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
@ -2299,6 +2427,12 @@
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"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/clone": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||
@ -2401,8 +2535,7 @@
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"devOptional": true
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
||||
},
|
||||
"node_modules/data-view-buffer": {
|
||||
"version": "1.0.1",
|
||||
@ -2537,6 +2670,15 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dequal": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/doctrine": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||
@ -2549,6 +2691,16 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.4.7",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||
@ -3611,6 +3763,15 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/invariant": {
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-arguments": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
|
||||
@ -4646,6 +4807,19 @@
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/prop-types-extra": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz",
|
||||
"integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-is": "^16.3.2",
|
||||
"warning": "^4.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=0.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
@ -4765,6 +4939,37 @@
|
||||
"react": ">=0.13"
|
||||
}
|
||||
},
|
||||
"node_modules/react-bootstrap": {
|
||||
"version": "2.10.10",
|
||||
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.10.tgz",
|
||||
"integrity": "sha512-gMckKUqn8aK/vCnfwoBpBVFUGT9SVQxwsYrp9yDHt0arXMamxALerliKBxr1TPbntirK/HGrUAHYbAeQTa9GHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.24.7",
|
||||
"@restart/hooks": "^0.4.9",
|
||||
"@restart/ui": "^1.9.4",
|
||||
"@types/prop-types": "^15.7.12",
|
||||
"@types/react-transition-group": "^4.4.6",
|
||||
"classnames": "^2.3.2",
|
||||
"dom-helpers": "^5.2.1",
|
||||
"invariant": "^2.2.4",
|
||||
"prop-types": "^15.8.1",
|
||||
"prop-types-extra": "^1.1.0",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"uncontrollable": "^7.2.1",
|
||||
"warning": "^4.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.14.8",
|
||||
"react": ">=16.14.0",
|
||||
"react-dom": ">=16.14.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
@ -4798,6 +5003,12 @@
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"node_modules/react-lifecycles-compat": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-quill": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0.tgz",
|
||||
@ -4885,6 +5096,22 @@
|
||||
"react-dom": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/react-transition-group": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dom-helpers": "^5.0.1",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.6.0",
|
||||
"react-dom": ">=16.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/redux": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
@ -5746,6 +5973,21 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/uncontrollable": {
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz",
|
||||
"integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.6.3",
|
||||
"@types/react": ">=16.9.11",
|
||||
"invariant": "^2.2.4",
|
||||
"react-lifecycles-compat": "^3.0.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.20.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
||||
@ -5876,6 +6118,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/warning": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
||||
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
"apexcharts": "^4.5.0",
|
||||
"axios": "^1.7.9",
|
||||
"axios-retry": "^4.5.0",
|
||||
"bootstrap": "^5.3.7",
|
||||
"dotenv": "^16.4.7",
|
||||
"dotenv-webpack": "^8.1.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
@ -32,6 +33,7 @@
|
||||
"perfect-scrollbar": "^1.5.5",
|
||||
"react": "^18.2.0",
|
||||
"react-apexcharts": "^1.7.0",
|
||||
"react-bootstrap": "^2.10.10",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.54.2",
|
||||
"react-quill": "^2.0.0",
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import Breadcrumb from "../common/Breadcrumb"; // ✅ Adjust the path if needed
|
||||
import { Modal } from "react-bootstrap"; // Ensure you have react-bootstrap installed
|
||||
import Breadcrumb from "../common/Breadcrumb";
|
||||
import { Modal } from "react-bootstrap";
|
||||
import { apiTenant } from "./apiTenant";
|
||||
import { useCreateTenant } from "./useTenants";
|
||||
|
||||
const defaultAvatar = "https://via.placeholder.com/100x100.png?text=Avatar";
|
||||
|
||||
@ -10,14 +12,16 @@ const initialData = {
|
||||
lastName: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
domain: "",
|
||||
organization: "",
|
||||
mobile: "",
|
||||
domainName: "",
|
||||
organizationName: "",
|
||||
description: "",
|
||||
size: "",
|
||||
industry: "",
|
||||
organizationSize: "",
|
||||
industryId: "",
|
||||
reference: "",
|
||||
taxId: "",
|
||||
billingAddress: "",
|
||||
onBoardingDate: "",
|
||||
};
|
||||
|
||||
const CreateTenant = () => {
|
||||
@ -25,22 +29,69 @@ const CreateTenant = () => {
|
||||
const location = useLocation();
|
||||
const formData = location.state?.formData || null;
|
||||
|
||||
// Assume useCreateTenant also handles updates, so we destructure `updateTenant` as well.
|
||||
const { createTenant, updateTenant, loading, error, success } = useCreateTenant();
|
||||
|
||||
const [form, setForm] = useState(initialData);
|
||||
const [imagePreview, setImagePreview] = useState(defaultAvatar);
|
||||
const [imageFile, setImageFile] = useState(null);
|
||||
const [showImageModal, setShowImageModal] = useState(false);
|
||||
const [showImageSizeModal, setShowImageSizeModal] = useState(false);
|
||||
const [industryOptions, setIndustryOptions] = useState([]);
|
||||
|
||||
// Load form data if it's passed via location state
|
||||
useEffect(() => {
|
||||
if (formData) {
|
||||
setForm({ ...initialData, ...formData });
|
||||
const { contactName, contactNumber, logoImage, ...rest } = formData;
|
||||
|
||||
// Load profileImage preview if available
|
||||
if (formData.profileImage) {
|
||||
setImagePreview(formData.profileImage);
|
||||
// A more robust way to split the name from the backend data
|
||||
let firstName = "";
|
||||
let lastName = "";
|
||||
if (contactName) {
|
||||
const nameParts = contactName.trim().split(" ");
|
||||
firstName = nameParts.shift() || ""; // Take the first word
|
||||
lastName = nameParts.join(" ") || ""; // Join the rest
|
||||
}
|
||||
|
||||
setForm({
|
||||
...initialData,
|
||||
...rest,
|
||||
firstName,
|
||||
lastName,
|
||||
phone: contactNumber || "",
|
||||
});
|
||||
|
||||
if (logoImage) {
|
||||
setImagePreview(logoImage);
|
||||
}
|
||||
}
|
||||
}, [formData]);
|
||||
|
||||
// Load industry options from the API when the component mounts
|
||||
useEffect(() => {
|
||||
const fetchIndustries = async () => {
|
||||
try {
|
||||
const res = await apiTenant.getIndustries();
|
||||
if (Array.isArray(res.data)) {
|
||||
setIndustryOptions(res.data);
|
||||
if (formData?.industry) {
|
||||
const matchedIndustry = res.data.find(
|
||||
(industry) => industry.name === formData.industry.name
|
||||
);
|
||||
if (matchedIndustry) {
|
||||
setForm((prev) => ({ ...prev, industryId: matchedIndustry.id }));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.error("Unexpected response format for industries", res);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load industries:", err);
|
||||
}
|
||||
};
|
||||
fetchIndustries();
|
||||
}, [formData]);
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setForm((prev) => ({ ...prev, [name]: value }));
|
||||
@ -48,15 +99,17 @@ const CreateTenant = () => {
|
||||
|
||||
const handleImageChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file && file.size <= 800 * 1024) {
|
||||
setImageFile(file);
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setImagePreview(reader.result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
alert("File must be JPG/PNG/GIF and less than 800KB");
|
||||
if (file) {
|
||||
if (file.size <= 200 * 1024) {
|
||||
setImageFile(file);
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setImagePreview(reader.result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
} else {
|
||||
setShowImageSizeModal(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -65,28 +118,45 @@ const CreateTenant = () => {
|
||||
setImagePreview(defaultAvatar);
|
||||
};
|
||||
|
||||
// const handleSubmit = (e) => {
|
||||
// e.preventDefault();
|
||||
// const submissionData = {
|
||||
// ...form,
|
||||
// profileImage: imagePreview, // Save base64/URL of image
|
||||
// };
|
||||
// console.log("Form submitted:", submissionData);
|
||||
// navigate("/tenant/profile/subscription", { state: { formData: submissionData } });
|
||||
// };
|
||||
const handleSubmit = useCallback(
|
||||
async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const submissionData = {
|
||||
...form,
|
||||
profileImage: imagePreview, // Save base64/URL of image
|
||||
};
|
||||
if (formData?.id) {
|
||||
navigate("/tenant/profile");
|
||||
} else {
|
||||
navigate("/tenant/profile/subscription", { state: { formData: submissionData } });
|
||||
}
|
||||
};
|
||||
// Prepare the data, ensuring firstName and lastName are included
|
||||
const submissionData = {
|
||||
...form, // This spreads all fields from your form state, including firstName and lastName
|
||||
logoImage: imagePreview,
|
||||
contactNumber: form.phone,
|
||||
contactName: `${form.firstName} ${form.lastName}`.trim(),
|
||||
};
|
||||
|
||||
let result;
|
||||
if (formData?.id) {
|
||||
// This is the update path. Call the update function from the hook.
|
||||
result = await updateTenant(formData.id, submissionData);
|
||||
|
||||
if (result) {
|
||||
alert("Tenant updated successfully!");
|
||||
// Navigate back to the profile page with the updated tenant data
|
||||
navigate("/tenant/profile", { state: { newTenant: result } });
|
||||
} else {
|
||||
alert("Failed to update tenant. Please check the form and try again.");
|
||||
}
|
||||
} else {
|
||||
// This is the creation path. Call the create function from the hook.
|
||||
result = await createTenant(submissionData);
|
||||
|
||||
if (result) {
|
||||
alert("Tenant created successfully!");
|
||||
// Navigate to the subscription page with the new tenant's data
|
||||
navigate("/tenant/profile/subscription", { state: { formData: result } });
|
||||
} else {
|
||||
alert("Failed to create tenant. Please check the form and try again.");
|
||||
}
|
||||
}
|
||||
},
|
||||
[form, imagePreview, formData, navigate, createTenant, updateTenant]
|
||||
);
|
||||
|
||||
const RequiredLabel = ({ label }) => (
|
||||
<label className="form-label small mb-1">
|
||||
@ -96,7 +166,6 @@ const handleSubmit = (e) => {
|
||||
|
||||
return (
|
||||
<div className="container py-3">
|
||||
{/* ✅ Breadcrumb */}
|
||||
<Breadcrumb
|
||||
data={[
|
||||
{ label: "Home", link: "/" },
|
||||
@ -112,47 +181,8 @@ const handleSubmit = (e) => {
|
||||
</h5>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
{/* 👤 Image Upload */}
|
||||
<div className="mb-4 text-start d-flex align-items-start gap-3">
|
||||
<img
|
||||
src={imagePreview}
|
||||
alt="Profile Preview"
|
||||
onClick={() => setShowImageModal(true)}
|
||||
style={{
|
||||
width: "100px",
|
||||
height: "100px",
|
||||
objectFit: "cover",
|
||||
borderRadius: "8px",
|
||||
border: "1px solid #ccc",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<div className="mb-2">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/png, image/jpeg, image/gif"
|
||||
onChange={handleImageChange}
|
||||
style={{ display: "none" }}
|
||||
id="upload-photo"
|
||||
/>
|
||||
<label htmlFor="upload-photo" className="btn btn-sm btn-primary me-2">
|
||||
Upload New Photo
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-secondary"
|
||||
onClick={handleImageReset}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
<small className="text-muted">Allowed JPG, GIF or PNG. Max size of 800K</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 🧾 Form Fields */}
|
||||
<div className="row g-4 text-start">
|
||||
{/* Form fields */}
|
||||
<div className="col-md-6">
|
||||
<RequiredLabel label="First Name" />
|
||||
<input
|
||||
@ -186,7 +216,7 @@ const handleSubmit = (e) => {
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<div className="col-md-3">
|
||||
<RequiredLabel label="Phone" />
|
||||
<input
|
||||
type="text"
|
||||
@ -197,6 +227,16 @@ const handleSubmit = (e) => {
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-3">
|
||||
<label className="form-label small mb-1">Landline Number</label>
|
||||
<input
|
||||
type="text"
|
||||
name="mobile"
|
||||
className="form-control form-control-sm"
|
||||
value={form.mobile}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<RequiredLabel label="Billing Address" />
|
||||
<input
|
||||
@ -209,22 +249,22 @@ const handleSubmit = (e) => {
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<RequiredLabel label="Organization" />
|
||||
<RequiredLabel label="Organization Name" />
|
||||
<input
|
||||
type="text"
|
||||
name="organization"
|
||||
name="organizationName"
|
||||
className="form-control form-control-sm"
|
||||
value={form.organization}
|
||||
value={form.organizationName}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
<RequiredLabel label="Size" />
|
||||
<RequiredLabel label="Organization Size" />
|
||||
<select
|
||||
name="size"
|
||||
name="organizationSize"
|
||||
className="form-select form-select-sm"
|
||||
value={form.size}
|
||||
value={form.organizationSize}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
@ -238,16 +278,18 @@ const handleSubmit = (e) => {
|
||||
<div className="col-md-4">
|
||||
<RequiredLabel label="Industry" />
|
||||
<select
|
||||
name="industry"
|
||||
name="industryId"
|
||||
className="form-select form-select-sm"
|
||||
value={form.industry}
|
||||
value={form.industryId}
|
||||
onChange={handleChange}
|
||||
required
|
||||
>
|
||||
<option value="">Select</option>
|
||||
<option>Technology</option>
|
||||
<option>Finance</option>
|
||||
<option>Healthcare</option>
|
||||
{industryOptions.map((industry) => (
|
||||
<option key={industry.id} value={industry.id}>
|
||||
{industry.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
@ -263,6 +305,7 @@ const handleSubmit = (e) => {
|
||||
<option>Google</option>
|
||||
<option>Friend</option>
|
||||
<option>Advertisement</option>
|
||||
<option>Root Tenant</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
@ -273,19 +316,29 @@ const handleSubmit = (e) => {
|
||||
className="form-control form-control-sm"
|
||||
value={form.taxId}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<label className="form-label small mb-1">Domain Name</label>
|
||||
<input
|
||||
type="text"
|
||||
name="domain"
|
||||
name="domainName"
|
||||
className="form-control form-control-sm"
|
||||
value={form.domain}
|
||||
value={form.domainName}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<RequiredLabel label="Onboarding Date" />
|
||||
<input
|
||||
type="date"
|
||||
name="onBoardingDate"
|
||||
className="form-control form-control-sm"
|
||||
value={form.onBoardingDate}
|
||||
onChange={handleChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="col-12">
|
||||
<label className="form-label small mb-1">Description</label>
|
||||
<textarea
|
||||
@ -296,12 +349,62 @@ const handleSubmit = (e) => {
|
||||
onChange={handleChange}
|
||||
></textarea>
|
||||
</div>
|
||||
<div className="mb-0 text-start d-flex align-items-start gap-3 position-relative">
|
||||
<div style={{ position: "relative", width: "100px", height: "100px" }}>
|
||||
<img
|
||||
src={imagePreview}
|
||||
alt="Profile Preview"
|
||||
onClick={() => setShowImageModal(true)}
|
||||
style={{
|
||||
width: "100px",
|
||||
height: "100px",
|
||||
objectFit: "cover",
|
||||
borderRadius: "8px",
|
||||
border: "1px solid #ccc",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-light position-absolute"
|
||||
onClick={handleImageReset}
|
||||
style={{
|
||||
top: "-10px",
|
||||
right: "-10px",
|
||||
padding: "0.25rem 0.5rem",
|
||||
borderRadius: "50%",
|
||||
boxShadow: "0 0 3px rgba(0,0,0,0.3)",
|
||||
}}
|
||||
title="Delete Photo"
|
||||
>
|
||||
<i className="bx bx-trash text-danger"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-2">
|
||||
<input
|
||||
type="file"
|
||||
accept="image/png, image/jpeg, image/gif"
|
||||
onChange={handleImageChange}
|
||||
style={{ display: "none" }}
|
||||
id="upload-photo"
|
||||
/>
|
||||
<label htmlFor="upload-photo" className="btn btn-sm btn-primary me-2">
|
||||
Upload New Photo
|
||||
</label>
|
||||
</div>
|
||||
<small className="text-muted">Allowed JPG, GIF or PNG. Max size of 200KB</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 🔘 Buttons */}
|
||||
<div className="mt-4 text-center">
|
||||
<button type="submit" className="btn btn-sm btn-primary px-4">
|
||||
{formData?.id ? "Update" : "Save & Continue"}
|
||||
<button
|
||||
type="submit"
|
||||
className="btn btn-sm btn-primary px-4"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Saving..." : formData?.id ? "Update" : "Save & Continue"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@ -315,19 +418,31 @@ const handleSubmit = (e) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 🔍 Image Preview Modal */}
|
||||
<Modal show={showImageModal} onHide={() => setShowImageModal(false)} centered size="lg">
|
||||
<Modal.Body className="text-center">
|
||||
<img
|
||||
src={imagePreview}
|
||||
alt="Preview"
|
||||
style={{ width: "100%", height: "auto", borderRadius: "8px" }}
|
||||
|
||||
/>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
|
||||
<Modal show={showImageSizeModal} onHide={() => setShowImageSizeModal(false)} centered>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Image Size Warning</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
The selected image file must be less than 200KB. Please choose a smaller file.
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button className="btn btn-primary" onClick={() => setShowImageSizeModal(false)}>
|
||||
Close
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateTenant;
|
||||
export default CreateTenant;
|
||||
@ -1,147 +1,104 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import Breadcrumb from "../common/Breadcrumb";
|
||||
import { useTenants } from "./useTenants";
|
||||
import Avatar from "../common/Avatar";
|
||||
|
||||
const Tenant = () => {
|
||||
const [tenants, setTenants] = useState([
|
||||
{
|
||||
id: 1,
|
||||
firstName: "Albert",
|
||||
lastName: "Cook",
|
||||
email: "albert.cook@example.com",
|
||||
phone: "+1 (555) 123-4567",
|
||||
organization: "Innovate Corp",
|
||||
size: "101-500",
|
||||
industry: "Technology",
|
||||
domain: "innovate.com",
|
||||
description: "Innovative solutions for businesses",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
firstName: "Barry",
|
||||
lastName: "Hunter",
|
||||
email: "barry.hunter@example.com",
|
||||
phone: "+1 (555) 987-6543",
|
||||
organization: "Creative Solutions",
|
||||
size: "51-100",
|
||||
industry: "Marketing",
|
||||
domain: "creatives.com",
|
||||
description: "Creative marketing strategies",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
firstName: "Sophia",
|
||||
lastName: "Johnson",
|
||||
email: "sophia.johnson@example.com",
|
||||
phone: "+1 (555) 678-9012",
|
||||
organization: "TechWorld Inc",
|
||||
size: "501-1000",
|
||||
industry: "IT Services",
|
||||
domain: "techworld.com",
|
||||
description: "Cutting-edge tech services",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
firstName: "Daniel",
|
||||
lastName: "Lee",
|
||||
email: "daniel.lee@example.com",
|
||||
phone: "+1 (555) 345-6789",
|
||||
organization: "EduPro",
|
||||
size: "101-500",
|
||||
industry: "Education",
|
||||
domain: "edupro.org",
|
||||
description: "Smart learning platforms",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
firstName: "Emily",
|
||||
lastName: "Davis",
|
||||
email: "emily.davis@example.com",
|
||||
phone: "+1 (555) 765-4321",
|
||||
organization: "GreenEarth Solutions",
|
||||
size: "51-100",
|
||||
industry: "Environmental",
|
||||
domain: "greenearth.com",
|
||||
description: "Eco-friendly innovations",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
firstName: "Michael",
|
||||
lastName: "Brown",
|
||||
email: "michael.brown@example.com",
|
||||
phone: "+1 (555) 888-1212",
|
||||
organization: "FinanceLink",
|
||||
size: "1000+",
|
||||
industry: "Finance",
|
||||
domain: "financelink.net",
|
||||
description: "Reliable financial services",
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
firstName: "Olivia",
|
||||
lastName: "Taylor",
|
||||
email: "olivia.taylor@example.com",
|
||||
phone: "+1 (555) 222-3344",
|
||||
organization: "HealthPlus",
|
||||
size: "201-500",
|
||||
industry: "Healthcare",
|
||||
domain: "healthplus.com",
|
||||
description: "Comprehensive health care solutions",
|
||||
},
|
||||
]);
|
||||
|
||||
const { tenants, loading, error } = useTenants();
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [localTenants, setLocalTenants] = useState([]);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
// Handle form submission result
|
||||
// This useEffect syncs the fetched tenants with the local state.
|
||||
useEffect(() => {
|
||||
if (tenants) {
|
||||
setLocalTenants(tenants);
|
||||
}
|
||||
}, [tenants]);
|
||||
|
||||
// This useEffect handles adding or updating a tenant from the location state.
|
||||
useEffect(() => {
|
||||
const newTenant = location.state?.newTenant;
|
||||
if (newTenant) {
|
||||
if (newTenant.id) {
|
||||
// Update existing tenant
|
||||
setTenants((prev) =>
|
||||
prev.map((t) => (t.id === newTenant.id ? newTenant : t))
|
||||
);
|
||||
} else {
|
||||
// Add new tenant
|
||||
setTenants((prev) => [...prev, { ...newTenant, id: Date.now() }]);
|
||||
}
|
||||
setLocalTenants((prev) => {
|
||||
// Find if the tenant already exists by ID
|
||||
const tenantIndex = prev.findIndex((t) => t.id === newTenant.id);
|
||||
if (tenantIndex > -1) {
|
||||
// If exists, update the tenant
|
||||
return prev.map((t) => (t.id === newTenant.id ? newTenant : t));
|
||||
} else {
|
||||
// If not, add a new tenant with a generated ID
|
||||
return [...prev, { ...newTenant, id: newTenant.id || Date.now() }];
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [location.state]);
|
||||
|
||||
const handleCreate = () => {
|
||||
navigate("/tenant/profile/create");
|
||||
navigate("/tenant/profile/manage");
|
||||
};
|
||||
|
||||
const handleEdit = (tenant) => {
|
||||
navigate("/tenant/profile/create", { state: { formData: tenant } });
|
||||
navigate("/tenant/profile/manage", { state: { formData: tenant } });
|
||||
};
|
||||
|
||||
const handleView = (tenant) => {
|
||||
alert(
|
||||
`Tenant Info:\n\nName: ${tenant.firstName} ${tenant.lastName}\nEmail: ${tenant.email}\nPhone: ${tenant.phone}\nOrganization: ${tenant.organization}`
|
||||
);
|
||||
navigate("/tenant/profile/viewtenant", { state: { formData: tenant } });
|
||||
};
|
||||
|
||||
// This function handles tenant deletion from the local state.
|
||||
const handleDelete = (id) => {
|
||||
if (window.confirm("Are you sure to delete this tenant?")) {
|
||||
setTenants((prev) => prev.filter((t) => t.id !== id));
|
||||
if (window.confirm("Are you sure you want to delete this tenant?")) {
|
||||
setLocalTenants((prev) => prev.filter((t) => t.id !== id));
|
||||
}
|
||||
};
|
||||
|
||||
// Utility function for case-insensitive and whitespace-insensitive search
|
||||
const normalize = (str) => (str?.toLowerCase().replace(/\s+/g, "") || "");
|
||||
|
||||
// Filters the tenants based on the search term
|
||||
const filteredTenants = localTenants.filter((tenant) => {
|
||||
const term = normalize(searchTerm);
|
||||
return (
|
||||
normalize(tenant.contactName).includes(term) ||
|
||||
normalize(tenant.email).includes(term) ||
|
||||
normalize(tenant.contactNumber).includes(term) ||
|
||||
normalize(tenant.domainName).includes(term) ||
|
||||
normalize(tenant.name).includes(term) ||
|
||||
normalize(tenant.oragnizationSize).includes(term) ||
|
||||
normalize(tenant.industry?.name).includes(term)
|
||||
);
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return <div className="container mt-4">Loading tenants...</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className="container mt-4">Error fetching tenants: {error.message}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
{/* ✅ Breadcrumb added */}
|
||||
<div className="container">
|
||||
<Breadcrumb data={[{ label: "Home", link: "/" }, { label: "Tenant" }]} />
|
||||
|
||||
<div className="card mt-3">
|
||||
<div className="d-flex justify-content-end p-3 pb-0">
|
||||
<div className="d-flex justify-content-between align-items-center p-3 pb-0">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search Tenant..."
|
||||
className="form-control w-25"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
<button className="btn btn-sm btn-primary" onClick={handleCreate}>
|
||||
<i className="bx bx-plus-circle me-2"></i> Create
|
||||
<i className="bx bx-plus-circle me-2"></i> Create Tenant
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="table-responsive p-3">
|
||||
<div className="table-responsive p-5 pt-3">
|
||||
<table className="table text-start align-middle">
|
||||
<thead>
|
||||
<tr className="fs-6">
|
||||
@ -156,46 +113,70 @@ const Tenant = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="table-border-bottom-0">
|
||||
{tenants.map((tenant) => (
|
||||
<tr key={tenant.id} style={{ height: "50px" }} className="align-middle">
|
||||
<td>{tenant.firstName} {tenant.lastName}</td>
|
||||
<td><i className="bx bx-envelope text-primary me-2"></i>{tenant.email}</td>
|
||||
<td><i className="bx bx-phone text-success me-2"></i>{tenant.phone}</td>
|
||||
<td><i className="bx bx-globe text-info me-2"></i>{tenant.domain}</td>
|
||||
<td><i className="bx bx-building text-secondary me-2"></i>{tenant.organization}</td>
|
||||
<td><i className="bx bx-group text-warning me-2"></i>{tenant.size}</td>
|
||||
<td><i className="bx bx-briefcase-alt text-dark me-2"></i>{tenant.industry}</td>
|
||||
<td>
|
||||
<div className="d-flex gap-2">
|
||||
<button
|
||||
className="btn btn-sm text-secondary p-0"
|
||||
title="View"
|
||||
onClick={() => handleView(tenant)}
|
||||
>
|
||||
<i className="bx bx-show fs-5"></i>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm text-primary p-0"
|
||||
title="Edit"
|
||||
onClick={() => handleEdit(tenant)}
|
||||
>
|
||||
<i className="bx bx-edit-alt fs-5"></i>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm text-danger p-0"
|
||||
title="Delete"
|
||||
onClick={() => handleDelete(tenant.id)}
|
||||
>
|
||||
<i className="bx bx-trash fs-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{tenants.length === 0 && (
|
||||
{filteredTenants.map((tenant) => {
|
||||
const names = tenant.contactName?.split(" ") || ["", ""];
|
||||
const firstName = names[0];
|
||||
const lastName = names[1];
|
||||
|
||||
return (
|
||||
<tr key={tenant.id} style={{ height: "50px" }} className="align-middle">
|
||||
<td>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
{tenant.logoImage ? (
|
||||
<img
|
||||
src={tenant.logoImage}
|
||||
alt={`${tenant.name} Logo`}
|
||||
style={{ width: "40px", height: "40px", borderRadius: "8px", objectFit: "cover" }}
|
||||
/>
|
||||
) : (
|
||||
<Avatar firstName={firstName} lastName={lastName} />
|
||||
)}
|
||||
<span>{tenant.contactName}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><i className="bx bx-envelope text-primary me-2"></i>{tenant.email}</td>
|
||||
<td><i className="bx bx-phone text-success me-2"></i>{tenant.contactNumber}</td>
|
||||
<td><i className="bx bx-globe text-info me-2"></i>{tenant.domainName}</td>
|
||||
<td><i className="bx bx-building text-secondary me-2"></i>{tenant.name}</td>
|
||||
<td><i className="bx bx-group text-warning me-2"></i>{tenant.oragnizationSize}</td>
|
||||
<td><i className="bx bx-briefcase-alt text-dark me-2"></i>{tenant.industry?.name}</td>
|
||||
<td>
|
||||
<div className="dropdown">
|
||||
<button
|
||||
className="btn btn-sm text-secondary"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<i className="bx bx-dots-horizontal-rounded fs-4"></i>
|
||||
</button>
|
||||
<ul className="dropdown-menu">
|
||||
<li>
|
||||
<button className="dropdown-item" onClick={() => handleView(tenant)}>
|
||||
<i className="bx bx-show me-2 text-secondary"></i> View
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button className="dropdown-item" onClick={() => handleEdit(tenant)}>
|
||||
<i className="bx bx-edit-alt me-2 text-primary"></i> Edit
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button className="dropdown-item" onClick={() => handleDelete(tenant.id)}>
|
||||
<i className="bx bx-trash me-2 text-danger"></i> Delete
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{filteredTenants.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan="8" className="text-center py-3">
|
||||
No tenants found.
|
||||
No matching tenants found.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
256
src/components/Tenant/ViewTenant.jsx
Normal file
256
src/components/Tenant/ViewTenant.jsx
Normal file
@ -0,0 +1,256 @@
|
||||
import React, { useState } from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import Breadcrumb from "../common/Breadcrumb";
|
||||
|
||||
const ViewTenant = () => {
|
||||
const { state } = useLocation();
|
||||
const tenant = state?.formData;
|
||||
const navigate = useNavigate();
|
||||
const [activeTab, setActiveTab] = useState("profile");
|
||||
|
||||
if (!tenant) {
|
||||
return (
|
||||
<div className="container mt-4">
|
||||
<h5>No tenant data found.</h5>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<Breadcrumb
|
||||
data={[
|
||||
{ label: "Home", link: "/" },
|
||||
{ label: "Tenant", link: "/tenant/profile" },
|
||||
{ label: "View" },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className="d-flex justify-content-between align-items-center mt-3">
|
||||
<h4>Tenant Details</h4>
|
||||
<button className="btn btn-outline-secondary btn-sm" onClick={() => navigate(-1)}>
|
||||
<i className="bx bx-arrow-back me-1"></i>Back
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="card shadow-sm">
|
||||
<div className="card-header p-0">
|
||||
<ul className="nav nav-tabs" id="myTab" role="tablist">
|
||||
<li className="nav-item" role="presentation">
|
||||
<button
|
||||
className={`nav-link ${activeTab === "profile" ? "active" : ""} fs-6`}
|
||||
id="profile-tab"
|
||||
onClick={() => setActiveTab("profile")}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="profile-details"
|
||||
aria-selected={activeTab === "profile"}
|
||||
>
|
||||
<i className="bx bx-user me-1"></i>Profile Details
|
||||
</button>
|
||||
</li>
|
||||
<li className="nav-item" role="presentation">
|
||||
<button
|
||||
className={`nav-link ${activeTab === "subscription" ? "active" : ""} fs-6`}
|
||||
id="subscription-tab"
|
||||
onClick={() => setActiveTab("subscription")}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-controls="subscription-details"
|
||||
aria-selected={activeTab === "subscription"}
|
||||
>
|
||||
<i className="bx bx-receipt me-1"></i>Subscription Details
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div style={{ borderTop: "1px solid #dee2e6" }}></div>
|
||||
<div className="card-body" style={{ marginTop: "-30px" }}>
|
||||
<div className="tab-content" id="myTabContent">
|
||||
{/* Profile Details Tab Pane */}
|
||||
<div
|
||||
className={`tab-pane fade ${activeTab === "profile" ? "show active" : ""}`}
|
||||
id="profile-details"
|
||||
role="tabpanel"
|
||||
aria-labelledby="profile-tab"
|
||||
>
|
||||
<div className="text-start" style={{ marginLeft: "-24px" }}>
|
||||
{/* Contact Name */}
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "190px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-user bx-xs me-2 mt-1"></i>
|
||||
<strong>Contact Name</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "28px" }}>:</span>
|
||||
</div>
|
||||
<span>{tenant.contactName || "N/A"}</span>
|
||||
</div>
|
||||
{/* Email */}
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "190px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-envelope bx-xs me-2 mt-1"></i>
|
||||
<strong>Email</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "84px" }}>:</span>
|
||||
</div>
|
||||
<span>{tenant.email || "N/A"}</span>
|
||||
</div>
|
||||
{/* Phone */}
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "190px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-phone bx-xs me-2 mt-1"></i>
|
||||
<strong>Phone</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "80px" }}>:</span>
|
||||
</div>
|
||||
<span>{tenant.contactNumber || "N/A"}</span>
|
||||
</div>
|
||||
{/* Domain */}
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "190px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-globe bx-xs me-2 mt-1"></i>
|
||||
<strong>Domain</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "72px" }}>:</span>
|
||||
</div>
|
||||
<span>{tenant.domainName || "N/A"}</span>
|
||||
</div>
|
||||
{/* Organization Name */}
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "190px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-building bx-xs me-2 mt-1"></i>
|
||||
<strong>Organization</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "38px" }}>:</span>
|
||||
</div>
|
||||
<span>{tenant.name || "N/A"}</span>
|
||||
</div>
|
||||
{/* Organization Size */}
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "190px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-group bx-xs me-2 mt-1"></i>
|
||||
<strong>Size</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "92px" }}>:</span>
|
||||
</div>
|
||||
<span>{tenant.oragnizationSize || "N/A"}</span>
|
||||
</div>
|
||||
{/* Industry */}
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "190px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-briefcase bx-xs me-2 mt-1"></i>
|
||||
<strong>Industry</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "65px" }}>:</span>
|
||||
</div>
|
||||
<span>{tenant.industry?.name || "N/A"}</span>
|
||||
</div>
|
||||
{/* Tenant Status */}
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "190px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-check-circle bx-xs me-2 mt-1"></i>
|
||||
<strong>Status</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "77px" }}>:</span>
|
||||
</div>
|
||||
<span>
|
||||
<span className={`badge ${tenant.tenantStatus?.name === 'Active' ? 'bg-success' : 'bg-secondary'}`}>
|
||||
{tenant.tenantStatus?.name || "N/A"}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subscription Details Tab Pane (with hardcoded data as per original design) */}
|
||||
<div
|
||||
className={`tab-pane fade ${activeTab === "subscription" ? "show active" : ""}`}
|
||||
id="subscription-details"
|
||||
role="tabpanel"
|
||||
aria-labelledby="subscription-tab"
|
||||
>
|
||||
<div className="text-start" style={{ marginLeft: "-24px" }}>
|
||||
{/* Plan */}
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "170px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-credit-card-alt bx-xs me-2 mt-1"></i>
|
||||
<strong>Plan</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "72px" }}>:</span>
|
||||
</div>
|
||||
<span>Premium</span>
|
||||
</div>
|
||||
{/* Start Date */}
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "170px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-calendar bx-xs me-2 mt-1"></i>
|
||||
<strong>Start Date</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "35px" }}>:</span>
|
||||
</div>
|
||||
<span>01 July, 2025</span>
|
||||
</div>
|
||||
{/* End Date */}
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "170px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-calendar-x bx-xs me-2 mt-1"></i>
|
||||
<strong>End Date</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "44px" }}>:</span>
|
||||
</div>
|
||||
<span>01 July, 2026</span>
|
||||
</div>
|
||||
{/* Status */}
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "170px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-check-circle bx-xs me-2 mt-1"></i>
|
||||
<strong>Status</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "59px" }}>:</span>
|
||||
</div>
|
||||
<span><span className="badge bg-success">Active</span></span>
|
||||
</div>
|
||||
{/* Seats */}
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "170px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-chair bx-xs me-2 mt-1"></i>
|
||||
<strong>Seats</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "65px" }}>:</span>
|
||||
</div>
|
||||
<span>100 Users</span>
|
||||
</div>
|
||||
{/* Billing */}
|
||||
<div className="d-flex mb-5 align-items-start">
|
||||
<div className="d-flex" style={{ minWidth: "170px" }}>
|
||||
<span className="d-flex align-items-center">
|
||||
<i className="bx bx-money bx-xs me-2 mt-1"></i>
|
||||
<strong>Billing</strong>
|
||||
</span>
|
||||
<span style={{ marginLeft: "61px" }}>:</span>
|
||||
</div>
|
||||
<span>Annual</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewTenant;
|
||||
11
src/components/Tenant/apiTenant.jsx
Normal file
11
src/components/Tenant/apiTenant.jsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { api } from "../../utils/axiosClient";
|
||||
|
||||
export const apiTenant = {
|
||||
getIndustries: () => api.get("api/market/industries"),
|
||||
|
||||
getTenantList: (pageNumber, pageSize) =>
|
||||
api.get(`api/tenant/list?pageNumber=${pageNumber}&pageSize=${pageSize}`),
|
||||
|
||||
createTenant: (data) => api.post("api/tenant/create", data),
|
||||
|
||||
};
|
||||
60
src/components/Tenant/useTenants.js
Normal file
60
src/components/Tenant/useTenants.js
Normal file
@ -0,0 +1,60 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { apiTenant } from "./apiTenant";
|
||||
|
||||
export const useTenants = (page = 1, pageSize = 20) => {
|
||||
const [tenants, setTenants] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTenants = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await apiTenant.getTenantList(page, pageSize);
|
||||
console.log("Fetched tenants:", res.data); // ✅ Console here
|
||||
setTenants(res.data?.data || []);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch tenants:", err);
|
||||
setError(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchTenants();
|
||||
}, [page, pageSize]);
|
||||
|
||||
return { tenants, loading, error };
|
||||
};
|
||||
|
||||
|
||||
export const useCreateTenant = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
const createTenant = useCallback(async (tenantData) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setData(null);
|
||||
setSuccess(false);
|
||||
|
||||
try {
|
||||
const res = await apiTenant.createTenant(tenantData);
|
||||
setData(res.data);
|
||||
setSuccess(true);
|
||||
console.log("Tenant created successfully:", res.data);
|
||||
return res.data;
|
||||
} catch (err) {
|
||||
console.error("Failed to create tenant:", err);
|
||||
setError(err);
|
||||
setSuccess(false);
|
||||
return null;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return { createTenant, loading, data, error, success };
|
||||
};
|
||||
@ -41,18 +41,19 @@ import LoginWithOtp from "../pages/authentication/LoginWithOtp";
|
||||
import Tenant from "../components/Tenant/Tenant";
|
||||
import CreateTenant from "../components/Tenant/CreateTenant";
|
||||
import TenantSubscription from "../components/Tenant/TenantSubscription";
|
||||
import ViewTenant from "../components/Tenant/ViewTenant";
|
||||
|
||||
const router = createBrowserRouter(
|
||||
[
|
||||
{
|
||||
element: <AuthLayout />,
|
||||
children: [
|
||||
{path: "/auth/login", element: <LoginPage />},
|
||||
{path: "/auth/login-otp", element: <LoginWithOtp />},
|
||||
{ path: "/auth/login", element: <LoginPage /> },
|
||||
{ path: "/auth/login-otp", element: <LoginWithOtp /> },
|
||||
{ path: "/auth/reqest/demo", element: <RegisterPage /> },
|
||||
{ path: "/auth/forgot-password", element: <ForgotPasswordPage /> },
|
||||
{ path: "/reset-password", element: <ResetPasswordPage /> },
|
||||
{ path: "/legal-info", element: <LegalInfoCard /> },
|
||||
{ path: "/legal-info", element: <LegalInfoCard /> },
|
||||
{ path: "/auth/changepassword", element: <ChangePasswordPage /> },
|
||||
],
|
||||
},
|
||||
@ -81,8 +82,9 @@ const router = createBrowserRouter(
|
||||
{ path: "/gallary", element: <ImageGallary /> },
|
||||
{ path: "/masters", element: <MasterPage /> },
|
||||
{ path: "/tenant/profile", element: <Tenant /> },
|
||||
{ path: "/tenant/profile/create", element: <CreateTenant /> },
|
||||
{ path: "/tenant/profile/manage", element: <CreateTenant /> },
|
||||
{ path: "/tenant/profile/subscription", element: <TenantSubscription /> },
|
||||
{ path: "/tenant/profile/viewtenant", element: <ViewTenant /> },
|
||||
{ path: "/help/support", element: <Support /> },
|
||||
{ path: "/help/docs", element: <Documentation /> },
|
||||
{ path: "/help/connect", element: <Connect /> },
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user