Compare commits
5 Commits
main
...
Kartik_Tas
| Author | SHA1 | Date | |
|---|---|---|---|
| b96df88129 | |||
| 6c3f64bf24 | |||
| de854e87f3 | |||
| cb27f8c259 | |||
| c672e0cea0 |
261
package-lock.json
generated
261
package-lock.json
generated
@ -18,6 +18,7 @@
|
|||||||
"apexcharts": "^4.5.0",
|
"apexcharts": "^4.5.0",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"axios-retry": "^4.5.0",
|
"axios-retry": "^4.5.0",
|
||||||
|
"bootstrap": "^5.3.7",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"dotenv-webpack": "^8.1.0",
|
"dotenv-webpack": "^8.1.0",
|
||||||
"eventemitter3": "^5.0.1",
|
"eventemitter3": "^5.0.1",
|
||||||
@ -29,6 +30,7 @@
|
|||||||
"perfect-scrollbar": "^1.5.5",
|
"perfect-scrollbar": "^1.5.5",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-apexcharts": "^1.7.0",
|
"react-apexcharts": "^1.7.0",
|
||||||
|
"react-bootstrap": "^2.10.10",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"react-quill": "^2.0.0",
|
"react-quill": "^2.0.0",
|
||||||
@ -897,6 +899,31 @@
|
|||||||
"pako": "^1.0.10"
|
"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": {
|
"node_modules/@reduxjs/toolkit": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.0.tgz",
|
||||||
@ -928,6 +955,60 @@
|
|||||||
"node": ">=14.0.0"
|
"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": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.28.1",
|
"version": "4.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz",
|
"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==",
|
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@swc/types": {
|
||||||
"version": "0.1.17",
|
"version": "0.1.17",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz",
|
||||||
@ -1562,8 +1658,7 @@
|
|||||||
"node_modules/@types/prop-types": {
|
"node_modules/@types/prop-types": {
|
||||||
"version": "15.7.14",
|
"version": "15.7.14",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
|
||||||
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
|
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ=="
|
||||||
"devOptional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/@types/quill": {
|
"node_modules/@types/quill": {
|
||||||
"version": "1.3.10",
|
"version": "1.3.10",
|
||||||
@ -1578,7 +1673,6 @@
|
|||||||
"version": "18.3.16",
|
"version": "18.3.16",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.16.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.16.tgz",
|
||||||
"integrity": "sha512-oh8AMIC4Y2ciKufU8hnKgs+ufgbA/dhPTACaZPM86AbwX9QwnFtSoPWEeRUj8fge+v6kFt78BXcDhAU1SrrAsw==",
|
"integrity": "sha512-oh8AMIC4Y2ciKufU8hnKgs+ufgbA/dhPTACaZPM86AbwX9QwnFtSoPWEeRUj8fge+v6kFt78BXcDhAU1SrrAsw==",
|
||||||
"devOptional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
@ -1593,11 +1687,26 @@
|
|||||||
"@types/react": "^18.0.0"
|
"@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": {
|
"node_modules/@types/use-sync-external-store": {
|
||||||
"version": "0.0.6",
|
"version": "0.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
"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=="
|
"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": {
|
"node_modules/@types/web": {
|
||||||
"version": "0.0.216",
|
"version": "0.0.216",
|
||||||
"resolved": "https://registry.npmjs.org/@types/web/-/web-0.0.216.tgz",
|
"resolved": "https://registry.npmjs.org/@types/web/-/web-0.0.216.tgz",
|
||||||
@ -2140,6 +2249,25 @@
|
|||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
@ -2299,6 +2427,12 @@
|
|||||||
"node": ">=6.0"
|
"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": {
|
"node_modules/clone": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
|
||||||
@ -2401,8 +2535,7 @@
|
|||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
||||||
"devOptional": true
|
|
||||||
},
|
},
|
||||||
"node_modules/data-view-buffer": {
|
"node_modules/data-view-buffer": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
@ -2537,6 +2670,15 @@
|
|||||||
"node": ">=0.4.0"
|
"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": {
|
"node_modules/doctrine": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||||
@ -2549,6 +2691,16 @@
|
|||||||
"node": ">=6.0.0"
|
"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": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.4.7",
|
"version": "16.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||||
@ -3611,6 +3763,15 @@
|
|||||||
"node": ">= 0.4"
|
"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": {
|
"node_modules/is-arguments": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
|
||||||
@ -4646,6 +4807,19 @@
|
|||||||
"react-is": "^16.13.1"
|
"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": {
|
"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",
|
||||||
@ -4765,6 +4939,37 @@
|
|||||||
"react": ">=0.13"
|
"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": {
|
"node_modules/react-dom": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
"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": {
|
"node_modules/react-quill": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0.tgz",
|
||||||
@ -4885,6 +5096,22 @@
|
|||||||
"react-dom": "^18 || ^19"
|
"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": {
|
"node_modules/redux": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
@ -5746,6 +5973,21 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.20.0",
|
"version": "6.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
"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": {
|
"node_modules/watchpack": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
|
||||||
|
|||||||
@ -21,6 +21,7 @@
|
|||||||
"apexcharts": "^4.5.0",
|
"apexcharts": "^4.5.0",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"axios-retry": "^4.5.0",
|
"axios-retry": "^4.5.0",
|
||||||
|
"bootstrap": "^5.3.7",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"dotenv-webpack": "^8.1.0",
|
"dotenv-webpack": "^8.1.0",
|
||||||
"eventemitter3": "^5.0.1",
|
"eventemitter3": "^5.0.1",
|
||||||
@ -32,6 +33,7 @@
|
|||||||
"perfect-scrollbar": "^1.5.5",
|
"perfect-scrollbar": "^1.5.5",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-apexcharts": "^1.7.0",
|
"react-apexcharts": "^1.7.0",
|
||||||
|
"react-bootstrap": "^2.10.10",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"react-quill": "^2.0.0",
|
"react-quill": "^2.0.0",
|
||||||
|
|||||||
406
src/components/Tenant/CreateTenant.jsx
Normal file
406
src/components/Tenant/CreateTenant.jsx
Normal file
@ -0,0 +1,406 @@
|
|||||||
|
import React, { useEffect, useState, useCallback } from "react";
|
||||||
|
import { useNavigate, useLocation } from "react-router-dom";
|
||||||
|
import { Modal } from "react-bootstrap";
|
||||||
|
import Breadcrumb from "../common/Breadcrumb";
|
||||||
|
import { apiTenant } from "./apiTenant";
|
||||||
|
import { useCreateTenant } from "./useTenants";
|
||||||
|
import TenantSubscription from "./TenantSubscription";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
const defaultAvatar = "https://via.placeholder.com/100x100.png?text=Avatar";
|
||||||
|
const initialData = {
|
||||||
|
firstName: "",
|
||||||
|
lastName: "",
|
||||||
|
email: "",
|
||||||
|
phone: "",
|
||||||
|
mobile: "",
|
||||||
|
domainName: "",
|
||||||
|
organizationName: "",
|
||||||
|
description: "",
|
||||||
|
organizationSize: "",
|
||||||
|
industryId: "",
|
||||||
|
reference: "",
|
||||||
|
taxId: "",
|
||||||
|
billingAddress: "",
|
||||||
|
onBoardingDate: "",
|
||||||
|
status: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
// RequiredLabel component remains the same for mandatory fields
|
||||||
|
const RequiredLabel = ({ label, name }) => (
|
||||||
|
<label htmlFor={name} className="form-label small mb-1">
|
||||||
|
{label} <span className="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Regular label for non-mandatory fields
|
||||||
|
const RegularLabel = ({ label, name }) => (
|
||||||
|
<label htmlFor={name} className="form-label small mb-1">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
|
||||||
|
const validateForm = (form, step) => {
|
||||||
|
let errors = {};
|
||||||
|
let fieldsToValidate = [];
|
||||||
|
|
||||||
|
// Updated list of mandatory fields for each step
|
||||||
|
if (step === 1) {
|
||||||
|
fieldsToValidate = [
|
||||||
|
"firstName",
|
||||||
|
"lastName",
|
||||||
|
"email",
|
||||||
|
"phone",
|
||||||
|
"billingAddress",
|
||||||
|
];
|
||||||
|
} else if (step === 2) {
|
||||||
|
fieldsToValidate = [
|
||||||
|
"organizationName",
|
||||||
|
"onBoardingDate",
|
||||||
|
"organizationSize",
|
||||||
|
"industryId",
|
||||||
|
"reference",
|
||||||
|
"status",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldsToValidate.forEach((field) => {
|
||||||
|
if (!form?.[field] || String(form?.[field]).trim() === "") {
|
||||||
|
const fieldName = field.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase());
|
||||||
|
errors[field] = `${fieldName} is required.`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (step === 1 && form.phone && !/^[0-9]{10}$/.test(form.phone)) {
|
||||||
|
errors.phone = "Phone number must be a 10-digit number.";
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CreateTenant = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { state } = useLocation();
|
||||||
|
const formData = state?.formData || null;
|
||||||
|
// const { createTenant, updateTenant, loading } = useCreateTenant();
|
||||||
|
const { mutate: createTenant, isPending } = useCreateTenant();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const [form, setForm] = useState(initialData);
|
||||||
|
const [formErrors, setFormErrors] = useState({});
|
||||||
|
const [imagePreview, setImagePreview] = useState(defaultAvatar);
|
||||||
|
const [imageFile, setImageFile] = useState(null);
|
||||||
|
const [showImageModal, setShowImageModal] = useState(false);
|
||||||
|
const [showImageSizeModal, setShowImageSizeModal] = useState(false);
|
||||||
|
const [industryOptions, setIndustryOptions] = useState([]);
|
||||||
|
const [step, setStep] = useState(1);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (formData) {
|
||||||
|
const { contactName, contactNumber, logoImage, ...rest } = formData;
|
||||||
|
const [firstName, ...lastNameParts] = (contactName || "").trim().split(" ");
|
||||||
|
const lastName = lastNameParts.join(" ");
|
||||||
|
setForm({ ...initialData, ...rest, firstName, lastName, phone: contactNumber || "" });
|
||||||
|
if (logoImage) setImagePreview(logoImage);
|
||||||
|
}
|
||||||
|
}, [formData]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchIndustries = async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await apiTenant.getIndustries();
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
setIndustryOptions(data);
|
||||||
|
if (formData?.industry) {
|
||||||
|
const matchedIndustry = data.find((i) => i.name === formData.industry.name);
|
||||||
|
if (matchedIndustry) setForm((prev) => ({ ...prev, industryId: matchedIndustry.id }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to load industries:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchIndustries();
|
||||||
|
}, [formData]);
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
const sanitizedValue = name === "phone" ? value.replace(/\D/g, "") : value;
|
||||||
|
setForm((prev) => ({ ...prev, [name]: sanitizedValue }));
|
||||||
|
if (formErrors?.[name]) {
|
||||||
|
setFormErrors((prev) => {
|
||||||
|
const newErrors = { ...prev };
|
||||||
|
delete newErrors?.[name];
|
||||||
|
return newErrors;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImageChange = (e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
if (file.size > 200 * 1024) {
|
||||||
|
setShowImageSizeModal(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setImageFile(file);
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onloadend = () => setImagePreview(reader.result);
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImageReset = () => {
|
||||||
|
setImageFile(null);
|
||||||
|
setImagePreview(defaultAvatar);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearForm = () => {
|
||||||
|
setForm(initialData);
|
||||||
|
setFormErrors({});
|
||||||
|
handleImageReset();
|
||||||
|
setStep(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
const errors = validateForm(form, step);
|
||||||
|
if (Object.keys(errors).length > 0) {
|
||||||
|
setFormErrors(errors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setFormErrors({});
|
||||||
|
setStep((prev) => prev + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(
|
||||||
|
async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const errors = validateForm(form, 2);
|
||||||
|
if (Object.keys(errors).length > 0) {
|
||||||
|
setFormErrors(errors);
|
||||||
|
setStep(2);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setFormErrors({});
|
||||||
|
const finalLogoImage = imageFile || (imagePreview === defaultAvatar ? null : imagePreview);
|
||||||
|
const tenantPayload = {
|
||||||
|
...form,
|
||||||
|
logoImage: finalLogoImage,
|
||||||
|
contactNumber: form.phone,
|
||||||
|
contactName: `${form.firstName} ${form.lastName}`.trim(),
|
||||||
|
};
|
||||||
|
let result;
|
||||||
|
if (formData?.id) {
|
||||||
|
// result = await updateTenant(formData.id, tenantPayload);
|
||||||
|
if (result) navigate("/tenant/profile", { state: { newTenant: result } });
|
||||||
|
} else {
|
||||||
|
// result = await createTenant(submissionData);
|
||||||
|
// if (result) navigate("/tenant/profile/viewtenant", { state: { formData: result } });
|
||||||
|
createTenant(tenantPayload);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[form, imagePreview, imageFile, formData, navigate, createTenant]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderFormStep = () => {
|
||||||
|
switch (step) {
|
||||||
|
case 1:
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<RequiredLabel label="First Name" name="firstName" />
|
||||||
|
<input id="firstName" type="text" name="firstName" className="form-control form-control-sm" value={form.firstName} onChange={handleChange} required />
|
||||||
|
{formErrors?.firstName && (<p className="text-danger small mt-1">{formErrors.firstName}</p>)}
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<RequiredLabel label="Last Name" name="lastName" />
|
||||||
|
<input id="lastName" type="text" name="lastName" className="form-control form-control-sm" value={form.lastName} onChange={handleChange} required />
|
||||||
|
{formErrors?.lastName && (<p className="text-danger small mt-1">{formErrors.lastName}</p>)}
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<RequiredLabel label="Email" name="email" />
|
||||||
|
<input id="email" type="email" name="email" className="form-control form-control-sm" value={form.email} onChange={handleChange} required />
|
||||||
|
{formErrors?.email && (<p className="text-danger small mt-1">{formErrors.email}</p>)}
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<RequiredLabel label="Contact Number" name="phone" />
|
||||||
|
<input id="phone" type="text" name="phone" className="form-control form-control-sm" value={form.phone} onChange={handleChange} required maxLength="10" />
|
||||||
|
{formErrors?.phone && (<p className="text-danger small mt-1">{formErrors.phone}</p>)}
|
||||||
|
</div>
|
||||||
|
<div className="col-12">
|
||||||
|
<RequiredLabel label="Billing Address" name="billingAddress" />
|
||||||
|
<textarea id="billingAddress" name="billingAddress" className="form-control form-control-sm" rows="2" value={form.billingAddress} onChange={handleChange} required></textarea>
|
||||||
|
{formErrors?.billingAddress && (<p className="text-danger small mt-1">{formErrors.billingAddress}</p>)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
case 2:
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<RequiredLabel label="Onboarding Date" name="onBoardingDate" />
|
||||||
|
<input id="onBoardingDate" type="date" name="onBoardingDate" className="form-control form-control-sm" value={form.onBoardingDate} onChange={handleChange} required />
|
||||||
|
{formErrors?.onBoardingDate && (<p className="text-danger small mt-1">{formErrors.onBoardingDate}</p>)}
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<RequiredLabel label="Organization Name" name="organizationName" />
|
||||||
|
<input id="organizationName" type="text" name="organizationName" className="form-control form-control-sm" value={form.organizationName} onChange={handleChange} required />
|
||||||
|
{formErrors?.organizationName && (<p className="text-danger small mt-1">{formErrors.organizationName}</p>)}
|
||||||
|
</div>
|
||||||
|
<div className="col-md-4">
|
||||||
|
<RequiredLabel label="Organization Size" name="organizationSize" />
|
||||||
|
<select id="organizationSize" name="organizationSize" className="form-select form-select-sm" value={form.organizationSize} onChange={handleChange} required>
|
||||||
|
<option value="">Select</option>
|
||||||
|
<option>1-50</option>
|
||||||
|
<option>51-100</option>
|
||||||
|
<option>101-500</option>
|
||||||
|
<option>500+</option>
|
||||||
|
</select>
|
||||||
|
{formErrors?.organizationSize && (<p className="text-danger small mt-1">{formErrors.organizationSize}</p>)}
|
||||||
|
</div>
|
||||||
|
<div className="col-md-4">
|
||||||
|
<RequiredLabel label="Industry" name="industryId" />
|
||||||
|
<select id="industryId" name="industryId" className="form-select form-select-sm" value={form.industryId} onChange={handleChange} required>
|
||||||
|
<option value="">Select</option>
|
||||||
|
{industryOptions.map((industry) => (<option key={industry.id} value={industry.id}>{industry.name}</option>))}
|
||||||
|
</select>
|
||||||
|
{formErrors?.industryId && (<p className="text-danger small mt-1">{formErrors.industryId}</p>)}
|
||||||
|
</div>
|
||||||
|
<div className="col-md-4">
|
||||||
|
<RequiredLabel label="Reference" name="reference" />
|
||||||
|
<select id="reference" name="reference" className="form-select form-select-sm" value={form.reference} onChange={handleChange} required>
|
||||||
|
<option value="">Select</option>
|
||||||
|
<option>Google</option>
|
||||||
|
<option>Friend</option>
|
||||||
|
<option>Advertisement</option>
|
||||||
|
<option>Root Tenant</option>
|
||||||
|
</select>
|
||||||
|
{formErrors?.reference && (<p className="text-danger small mt-1">{formErrors.reference}</p>)}
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<RequiredLabel label="Status" name="status" />
|
||||||
|
<select id="status" name="status" className="form-select form-select-sm" value={form.status} onChange={handleChange} required>
|
||||||
|
<option value="">Select</option>
|
||||||
|
<option value="Active">Active</option>
|
||||||
|
<option value="Inactive">Inactive</option>
|
||||||
|
</select>
|
||||||
|
{formErrors?.status && (<p className="text-danger small mt-1">{formErrors.status}</p>)}
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<RegularLabel label="Tax ID" name="taxId" />
|
||||||
|
<input id="taxId" type="text" name="taxId" className="form-control form-control-sm" value={form.taxId} onChange={handleChange} />
|
||||||
|
{formErrors?.taxId && (<p className="text-danger small mt-1">{formErrors.taxId}</p>)}
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<RegularLabel label="Domain Name" name="domainName" />
|
||||||
|
<input id="domainName" type="text" name="domainName" className="form-control form-control-sm" value={form.domainName} onChange={handleChange} />
|
||||||
|
{formErrors?.domainName && (<p className="text-danger small mt-1">{formErrors.domainName}</p>)}
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6">
|
||||||
|
<RegularLabel label="Landline Number" name="mobile" />
|
||||||
|
<input id="mobile" type="text" name="mobile" className="form-control form-control-sm" value={form.mobile} onChange={handleChange} />
|
||||||
|
{formErrors?.mobile && (<p className="text-danger small mt-1">{formErrors.mobile}</p>)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-12">
|
||||||
|
<RegularLabel label="Description" name="description" />
|
||||||
|
<textarea id="description" name="description" className="form-control form-control-sm" rows="2" value={form.description} onChange={handleChange}></textarea>
|
||||||
|
{formErrors?.description && (<p className="text-danger small mt-1">{formErrors.description}</p>)}
|
||||||
|
</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 Logo</label>
|
||||||
|
</div>
|
||||||
|
<small className="text-muted">Allowed JPG, GIF or PNG. Max size of 200KB</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
case 3:
|
||||||
|
return (
|
||||||
|
<div className="col-12">
|
||||||
|
<TenantSubscription />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{ label: "Contact Info", subtitle: "Provide contact details", icon: "bx-user" },
|
||||||
|
{ label: "Organization Details", subtitle: "Enter organization info", icon: "bx-buildings" },
|
||||||
|
{ label: "Subscription", subtitle: "Select a plan", icon: "bx-credit-card" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container py-3">
|
||||||
|
<Breadcrumb data={[{ label: "Home", link: "/" }, { label: "Tenant", link: "/tenant/profile" }, { label: formData?.id ? "Update Tenant" : "Create Tenant" }]} />
|
||||||
|
<div className="card rounded-3 shadow-sm mt-3">
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h5 className="text-start mb-0">{formData?.id ? "Update Tenant" : "Create Tenant"}</h5>
|
||||||
|
{step !== 3 && (<button type="button" className="btn btn-sm btn-warning" onClick={handleClearForm}>Clear</button>)}
|
||||||
|
</div>
|
||||||
|
<div className="d-flex flex-column flex-md-row align-items-start">
|
||||||
|
{/* Steps Container with vertical divider */}
|
||||||
|
<div className="d-flex flex-column align-items-start me-md-4 mb-3 mb-md-0 pe-md-4 border-end">
|
||||||
|
{steps.map((s, index) => (
|
||||||
|
<div key={index} className="d-flex align-items-center position-relative py-2 py-md-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`btn btn-link p-0 text-start ${step >= index + 1 ? 'text-primary' : 'text-dark'}`}
|
||||||
|
style={{ cursor: step >= index + 1 ? 'pointer' : 'default', textDecoration: 'none' }}
|
||||||
|
onClick={() => setStep(index + 1)}
|
||||||
|
disabled={step < index + 1}
|
||||||
|
>
|
||||||
|
<div className="d-flex align-items-center">
|
||||||
|
<div
|
||||||
|
className={`d-flex align-items-center justify-content-center me-3 ${step === index + 1 ? 'bg-primary text-white' : 'bg-light text-secondary'} ${step > index + 1 ? 'bg-white text-primary border border-primary' : ''}`}
|
||||||
|
style={{ width: '40px', height: '40px', borderRadius: '8px' }}
|
||||||
|
>
|
||||||
|
<i className={`bx ${s.icon}`}></i>
|
||||||
|
</div>
|
||||||
|
<div className="d-flex flex-column text-start">
|
||||||
|
<span className={`small ${step === index + 1 ? 'fw-semibold' : 'text-muted'}`}>{s.label}</span>
|
||||||
|
<span className="text-muted small">{s.subtitle}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{/* Form Content */}
|
||||||
|
<div className="tab-content w-100 p-md-3 pt-md-0">
|
||||||
|
<form onSubmit={handleSubmit} noValidate>
|
||||||
|
<div className="row g-4 text-start">{renderFormStep()}</div>
|
||||||
|
<div className="mt-4 d-flex justify-content-end">
|
||||||
|
{step > 1 && (<button type="button" className="btn btn-sm btn-secondary me-2 px-4" onClick={() => setStep((prev) => prev - 1)}>Previous</button>)}
|
||||||
|
{step < 3 && (<button type="button" className="btn btn-sm btn-primary px-4" onClick={handleNext} disabled={isPending}>Next</button>)}
|
||||||
|
{step === 3 && (<button type="submit" className="btn btn-sm btn-primary px-4" disabled={isPending}>{isPending ? "Saving..." : "Save & Continue"}</button>)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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;
|
||||||
183
src/components/Tenant/Tenant.jsx
Normal file
183
src/components/Tenant/Tenant.jsx
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
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";
|
||||||
|
import TenantSkeleton from "./TenantSkeleton";
|
||||||
|
|
||||||
|
const Tenant = () => {
|
||||||
|
const { tenants, loading, error } = useTenants();
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [localTenants, setLocalTenants] = useState([]);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (tenants) {
|
||||||
|
setLocalTenants(tenants);
|
||||||
|
}
|
||||||
|
}, [tenants]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newTenant = location.state?.newTenant;
|
||||||
|
if (newTenant) {
|
||||||
|
setLocalTenants((prev) => {
|
||||||
|
const tenantIndex = prev.findIndex((t) => t.id === newTenant.id);
|
||||||
|
if (tenantIndex > -1) {
|
||||||
|
return prev.map((t) => (t.id === newTenant.id ? newTenant : t));
|
||||||
|
} else {
|
||||||
|
return [...prev, { ...newTenant, id: newTenant.id || Date.now() }];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [location.state]);
|
||||||
|
|
||||||
|
const handleCreate = () => {
|
||||||
|
navigate("/tenant/profile/manage");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (tenant) => {
|
||||||
|
navigate("/tenant/profile/manage", { state: { formData: tenant } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleView = (tenant) => {
|
||||||
|
navigate("/tenant/profile/viewtenant", { state: { formData: tenant } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (id) => {
|
||||||
|
if (window.confirm("Are you sure you want to delete this tenant?")) {
|
||||||
|
setLocalTenants((prev) => prev.filter((t) => t.id !== id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalize = (str) => (str?.toLowerCase().replace(/\s+/g, "") || "");
|
||||||
|
|
||||||
|
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.organizationSize).includes(term) ||
|
||||||
|
normalize(tenant.industry?.name).includes(term)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <TenantSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <div className="container mt-4">Error fetching tenants: {error.message}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<Breadcrumb data={[{ label: "Home", link: "/" }, { label: "Tenant" }]} />
|
||||||
|
|
||||||
|
<div className="card mt-3">
|
||||||
|
<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 Tenant
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="table-responsive p-3 pt-3" style={{ overflowX: "auto", whiteSpace: "nowrap" }}>
|
||||||
|
<table className="table text-start align-middle mb-0" style={{ minWidth: "1000px" }}>
|
||||||
|
<thead>
|
||||||
|
<tr className="fs-6">
|
||||||
|
<th className="px-4">Organization</th>
|
||||||
|
<th className="px-4">Name</th>
|
||||||
|
<th className="px-4">Phone</th>
|
||||||
|
<th className="px-4">User</th>
|
||||||
|
<th className="px-4">Industry</th>
|
||||||
|
<th className="px-4">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="table-border-bottom-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 className="px-4"><i className="bx bx-building text-secondary me-2"></i>{tenant.name}</td>
|
||||||
|
<td className="px-4">
|
||||||
|
<div className={`d-flex align-items-center ${tenant.logoImage ? "gap-2" : ""}`}>
|
||||||
|
{tenant.logoImage ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: "36px",
|
||||||
|
height: "36px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
border: "2px solid #ccc",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={tenant.logoImage}
|
||||||
|
alt={`${tenant.name} Logo`}
|
||||||
|
style={{
|
||||||
|
width: "32px",
|
||||||
|
height: "32px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
objectFit: "cover",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Avatar firstName={firstName} lastName={lastName} />
|
||||||
|
)}
|
||||||
|
<span className="ms-0">{tenant.contactName}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-4"><i className="bx bx-phone text-success me-2"></i>{tenant.contactNumber}</td>
|
||||||
|
<td className="px-4"><i className="bx bx-group text-warning me-2"></i>{tenant.organizationSize}</td>
|
||||||
|
<td className="px-4"><i className="bx bx-briefcase-alt text-dark me-2"></i>{tenant.industry?.name}</td>
|
||||||
|
<td className="px-4">
|
||||||
|
<div className="d-flex gap-2">
|
||||||
|
<i
|
||||||
|
className="bx bx-show text-secondary fs-5 cursor-pointer"
|
||||||
|
onClick={() => handleView(tenant)}
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
className="bx bx-edit text-primary fs-5 cursor-pointer"
|
||||||
|
onClick={() => handleEdit(tenant)}
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
className="bx bx-trash text-danger fs-5 cursor-pointer"
|
||||||
|
onClick={() => handleDelete(tenant.id)}
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{filteredTenants.length === 0 && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan="8" className="text-center py-3">
|
||||||
|
No matching tenants found.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tenant;
|
||||||
59
src/components/Tenant/TenantSkeleton.jsx
Normal file
59
src/components/Tenant/TenantSkeleton.jsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const SkeletonRow = () => (
|
||||||
|
<tr className="align-middle" style={{ height: "50px" }}>
|
||||||
|
{Array.from({ length: 5 }).map((_, idx) => (
|
||||||
|
<td className="px-4" key={idx}>
|
||||||
|
<div className="placeholder-glow w-100">
|
||||||
|
<span className="placeholder col-8" style={{ height: "20px", display: "inline-block" }}></span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
<td className="px-4">
|
||||||
|
<div className="d-flex gap-2">
|
||||||
|
{Array.from({ length: 3 }).map((_, idx) => (
|
||||||
|
<span key={idx} className="placeholder rounded" style={{ width: "24px", height: "24px" }}></span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
|
||||||
|
const TenantSkeleton = ({ rows = 6 }) => {
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="card mt-3">
|
||||||
|
<div className="d-flex justify-content-between align-items-center p-3 pb-0">
|
||||||
|
<div className="placeholder-glow w-25">
|
||||||
|
<span className="placeholder col-12" style={{ height: "38px", display: "inline-block" }}></span>
|
||||||
|
</div>
|
||||||
|
<div className="placeholder-glow" style={{ width: "150px" }}>
|
||||||
|
<span className="placeholder col-12" style={{ height: "38px", display: "inline-block" }}></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="table-responsive p-3 pt-3">
|
||||||
|
<table className="table text-start align-middle mb-0" style={{ minWidth: "1000px" }}>
|
||||||
|
<thead>
|
||||||
|
<tr className="fs-6">
|
||||||
|
<th className="px-4">Organization</th>
|
||||||
|
<th className="px-4">Name</th>
|
||||||
|
<th className="px-4">Phone</th>
|
||||||
|
<th className="px-4">Size</th>
|
||||||
|
<th className="px-4">Industry</th>
|
||||||
|
<th className="px-4">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="table-border-bottom-0">
|
||||||
|
{Array.from({ length: rows }).map((_, idx) => (
|
||||||
|
<SkeletonRow key={idx} />
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TenantSkeleton;
|
||||||
139
src/components/Tenant/TenantSubscription.jsx
Normal file
139
src/components/Tenant/TenantSubscription.jsx
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
// import { useSubscriptionPlans } from "./useTenant"; // adjust the path if needed
|
||||||
|
import { useSubscriptionPlans } from "./useTenants";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
const billingOptions = [
|
||||||
|
{ label: "Monthly", frequency: 0 },
|
||||||
|
{ label: "Quarterly", frequency: 1 },
|
||||||
|
{ label: "Half-Yearly", frequency: 2 },
|
||||||
|
{ label: "Yearly", frequency: 3 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const organizationSizes = ["1-10", "11-50", "51-200", "201-500", "501+"];
|
||||||
|
|
||||||
|
const TenantSubscription = () => {
|
||||||
|
const [selectedBillingIndex, setSelectedBillingIndex] = useState(null);
|
||||||
|
const [selectedPlanIndex, setSelectedPlanIndex] = useState(null);
|
||||||
|
const [selectedOrgSize, setSelectedOrgSize] = useState("");
|
||||||
|
const currentTenant = useSelector((state)=> state.globalVariables.currentTenant);
|
||||||
|
console.log("karti",currentTenant)
|
||||||
|
|
||||||
|
const selectedBilling = billingOptions[selectedBillingIndex];
|
||||||
|
const { plans, loading, error } = useSubscriptionPlans(selectedBilling?.frequency);
|
||||||
|
const currentPlan = plans?.[selectedPlanIndex] || null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container py-1">
|
||||||
|
{/* Breadcrumb placeholder */}
|
||||||
|
<div className="my-4">
|
||||||
|
<p className="mb-2">Choose your billing cycle:</p>
|
||||||
|
<div className="d-inline-flex border rounded-pill overflow-hidden shadow-sm">
|
||||||
|
{billingOptions.map((option, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
type="button"
|
||||||
|
className={`btn px-4 py-2 rounded-0 ${
|
||||||
|
selectedBillingIndex === index ? "btn-primary text-white" : "btn-light"
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
borderRight: index !== billingOptions.length - 1 ? "1px solid #dee2e6" : "none",
|
||||||
|
fontWeight: selectedBillingIndex === index ? "600" : "normal",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedBillingIndex(index);
|
||||||
|
setSelectedPlanIndex(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{loading && <p>Loading plans...</p>}
|
||||||
|
{error && <p className="text-danger">{error}</p>}
|
||||||
|
|
||||||
|
{plans.length > 0 && (
|
||||||
|
<div className="my-4">
|
||||||
|
<p className="mb-2">Select a plan:</p>
|
||||||
|
<div className="d-inline-flex border rounded-pill overflow-hidden shadow-sm">
|
||||||
|
{plans.map((plan, index) => (
|
||||||
|
<button
|
||||||
|
key={plan.id}
|
||||||
|
type="button"
|
||||||
|
className={`btn px-4 py-2 rounded-0 ${
|
||||||
|
selectedPlanIndex === index ? "btn-primary text-white" : "btn-light"
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
borderRight: index !== plans.length - 1 ? "1px solid #dee2e6" : "none",
|
||||||
|
fontWeight: selectedPlanIndex === index ? "600" : "normal",
|
||||||
|
}}
|
||||||
|
onClick={() => setSelectedPlanIndex(index)}
|
||||||
|
>
|
||||||
|
{plan.planName}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentPlan && (
|
||||||
|
<div className="row justify-content-start mt-3">
|
||||||
|
<div className="col-md-6 mb-4">
|
||||||
|
<div className="card h-100 shadow-sm">
|
||||||
|
<div className="card-body position-relative">
|
||||||
|
<div className="mb-3">
|
||||||
|
<i className="bx bxs-package" style={{ fontSize: "48px", color: "#6366f1" }}></i>
|
||||||
|
</div>
|
||||||
|
<h5 className="card-title">{currentPlan.planName}</h5>
|
||||||
|
<p className="text-muted">{currentPlan.description}</p>
|
||||||
|
<h2 className="fw-bold">
|
||||||
|
{currentPlan.currency?.symbol}
|
||||||
|
{currentPlan.price}{" "}
|
||||||
|
<small className="fs-6">/{selectedBilling?.label.toLowerCase()}</small>
|
||||||
|
</h2>
|
||||||
|
<ul className="list-unstyled mt-4 mb-4">
|
||||||
|
<li className="mb-2">
|
||||||
|
<i className="bx bx-check text-success me-2"></i>
|
||||||
|
Max Users: {currentPlan.maxUser}
|
||||||
|
</li>
|
||||||
|
<li className="mb-2">
|
||||||
|
<i className="bx bx-check text-success me-2"></i>
|
||||||
|
Storage: {currentPlan.maxStorage} MB
|
||||||
|
</li>
|
||||||
|
<li className="mb-2">
|
||||||
|
<i className="bx bx-check text-success me-2"></i>
|
||||||
|
Trial Days: {currentPlan.trialDays}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<button className="btn btn-primary">Choose Plan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="my-4">
|
||||||
|
<label htmlFor="organizationSize" className="form-label mb-2">
|
||||||
|
Organization Size
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="organizationSize"
|
||||||
|
className="form-select w-auto shadow-sm"
|
||||||
|
value={selectedOrgSize}
|
||||||
|
onChange={(e) => setSelectedOrgSize(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="" disabled>Select a size</option>
|
||||||
|
{organizationSizes.map((size, index) => (
|
||||||
|
<option key={index} value={size}>
|
||||||
|
{size}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TenantSubscription;
|
||||||
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>Tenant Profile
|
||||||
|
</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-0"></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-0"></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-0"></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-0"></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-0"></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-0"></i>
|
||||||
|
<strong>Size</strong>
|
||||||
|
</span>
|
||||||
|
<span style={{ marginLeft: "92px" }}>:</span>
|
||||||
|
</div>
|
||||||
|
<span>{tenant.organizationSize || "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-0"></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-0"></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-0"></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-0"></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-0"></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-0"></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-0"></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-0"></i>
|
||||||
|
<strong>Billing</strong>
|
||||||
|
</span>
|
||||||
|
<span style={{ marginLeft: "61px" }}>:</span>
|
||||||
|
</div>
|
||||||
|
<span>Annual</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ViewTenant;
|
||||||
15
src/components/Tenant/apiTenant.jsx
Normal file
15
src/components/Tenant/apiTenant.jsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
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),
|
||||||
|
|
||||||
|
getSubscriptionPlansTenant: (frequency) => api.get(`api/tenant/list/subscription-plan?frequency=${frequency}`),
|
||||||
|
|
||||||
|
createSubscriptionTenant: (data) => api.post("api/tenant/add-subscription", data),
|
||||||
|
|
||||||
|
};
|
||||||
138
src/components/Tenant/useTenants.js
Normal file
138
src/components/Tenant/useTenants.js
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { apiTenant } from "./apiTenant";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { setCurrentTenant } from "../../slices/globalVariablesSlice";
|
||||||
|
import showToast from "../../services/toastService";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
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 };
|
||||||
|
// };
|
||||||
|
|
||||||
|
|
||||||
|
export const useCreateTenant = ()=>{
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (tenantPayload) => {
|
||||||
|
const res = await apiTenant.createTenant(tenantPayload);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
onSuccess:(data,variables)=>{
|
||||||
|
dispatch(setCurrentTenant(data));
|
||||||
|
showToast("Tenant created successfully","sucess");
|
||||||
|
navigate("/tenant/profile/viewtenant", { state: { formData: result } });
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
showToast(error.message || "Failed to mark Tenant", "error");
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createSubscriptionTenant = ()=>{
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (subscriptionPayload) => {
|
||||||
|
const res = await apiTenant.createSubscriptionTenant(subscriptionPayload);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
onSuccess:(data,variables)=>{
|
||||||
|
dispatch(setCurrentTenant(data));
|
||||||
|
showToast("Tenant created successfully","sucess");
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
showToast(error.message || "Failed to mark Tenant", "error");
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSubscriptionPlans = (frequency) => {
|
||||||
|
const [plans, setPlans] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!frequency) return;
|
||||||
|
|
||||||
|
const fetchPlans = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiTenant.getSubscriptionPlansTenant(frequency);
|
||||||
|
if (response?.data) {
|
||||||
|
setPlans(response.data);
|
||||||
|
} else {
|
||||||
|
setError("Failed to fetch plans.");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message || "Something went wrong.");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchPlans();
|
||||||
|
}, [frequency]);
|
||||||
|
|
||||||
|
return { plans, loading, error };
|
||||||
|
};
|
||||||
|
|
||||||
@ -81,6 +81,16 @@
|
|||||||
"text": "Masters",
|
"text": "Masters",
|
||||||
"available": true,
|
"available": true,
|
||||||
"link": "/masters"
|
"link": "/masters"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Manage Subscription",
|
||||||
|
"available": true,
|
||||||
|
"link": "/tenant/manage"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Manage Tenants",
|
||||||
|
"available": true,
|
||||||
|
"link": "/tenant/profile"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@ -286,6 +286,7 @@ export const useMarkAttendance = () => {
|
|||||||
|
|
||||||
if(variables.forWhichTab !== 3) showToast("Attendance marked successfully", "success");
|
if(variables.forWhichTab !== 3) showToast("Attendance marked successfully", "success");
|
||||||
},
|
},
|
||||||
|
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
showToast(error.message || "Failed to mark attendance", "error");
|
showToast(error.message || "Failed to mark attendance", "error");
|
||||||
},
|
},
|
||||||
|
|||||||
@ -234,58 +234,7 @@ const EmployeeList = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{EmpForManageRole && (
|
<div className="container-fluid">
|
||||||
<GlobalModel
|
|
||||||
isOpen={EmpForManageRole}
|
|
||||||
closeModal={() => setEmpForManageRole(null)}
|
|
||||||
>
|
|
||||||
<ManageEmp
|
|
||||||
employeeId={EmpForManageRole}
|
|
||||||
onClosed={() => setEmpForManageRole(null)}
|
|
||||||
/>
|
|
||||||
</GlobalModel>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showModal && (
|
|
||||||
<GlobalModel
|
|
||||||
isOpen={showModal}
|
|
||||||
size="lg"
|
|
||||||
closeModal={() => setShowModal(false)}
|
|
||||||
>
|
|
||||||
<ManageEmployee
|
|
||||||
employeeId={selectedEmployeeId}
|
|
||||||
onClosed={() => setShowModal(false)}
|
|
||||||
IsAllEmployee={showAllEmployees}
|
|
||||||
/>
|
|
||||||
</GlobalModel>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{IsDeleteModalOpen && (
|
|
||||||
<div
|
|
||||||
className={`modal fade ${IsDeleteModalOpen ? "show" : ""}`}
|
|
||||||
tabIndex="-1"
|
|
||||||
role="dialog"
|
|
||||||
style={{
|
|
||||||
display: IsDeleteModalOpen ? "block" : "none",
|
|
||||||
backgroundColor: IsDeleteModalOpen
|
|
||||||
? "rgba(0,0,0,0.5)"
|
|
||||||
: "transparent",
|
|
||||||
}}
|
|
||||||
aria-hidden="false"
|
|
||||||
>
|
|
||||||
<ConfirmModal
|
|
||||||
type={"delete"}
|
|
||||||
header={"Suspend Employee"}
|
|
||||||
message={"Are you sure you want delete?"}
|
|
||||||
onSubmit={suspendEmployee}
|
|
||||||
onClose={() => setIsDeleteModalOpen(false)}
|
|
||||||
loading={employeeLodaing}
|
|
||||||
paramData={selectedEmpFordelete}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="container-fluid">
|
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
data={[
|
data={[
|
||||||
{ label: "Home", link: "/dashboard" },
|
{ label: "Home", link: "/dashboard" },
|
||||||
@ -306,44 +255,10 @@ const EmployeeList = () => {
|
|||||||
{/* Switches: All Employees + Inactive */}
|
{/* Switches: All Employees + Inactive */}
|
||||||
<div className="d-flex flex-wrap align-items-center gap-3">
|
<div className="d-flex flex-wrap align-items-center gap-3">
|
||||||
{/* All Employees Switch */}
|
{/* All Employees Switch */}
|
||||||
{ViewAllEmployee && (
|
|
||||||
<div className="form-check form-switch text-start">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="form-check-input"
|
|
||||||
role="switch"
|
|
||||||
id="allEmployeesCheckbox"
|
|
||||||
checked={showAllEmployees}
|
|
||||||
onChange={handleAllEmployeesToggle}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="form-check-label ms-0"
|
|
||||||
htmlFor="allEmployeesCheckbox"
|
|
||||||
>
|
|
||||||
All Employees
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Show Inactive Employees Switch */}
|
{/* Show Inactive Employees Switch */}
|
||||||
{showAllEmployees && (
|
|
||||||
<div className="form-check form-switch text-start">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="form-check-input"
|
|
||||||
role="switch"
|
|
||||||
id="inactiveEmployeesCheckbox"
|
|
||||||
checked={showInactive}
|
|
||||||
onChange={(e)=> setShowInactive(e.target.checked)}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
className="form-check-label ms-0"
|
|
||||||
htmlFor="inactiveEmployeesCheckbox"
|
|
||||||
>
|
|
||||||
Show Inactive Employees
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right side: Search + Export + Add Employee */}
|
{/* Right side: Search + Export + Add Employee */}
|
||||||
@ -363,69 +278,9 @@ const EmployeeList = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Export Dropdown */}
|
{/* Export Dropdown */}
|
||||||
<div className="dropdown">
|
|
||||||
<button
|
|
||||||
aria-label="Click me"
|
|
||||||
className="btn btn-sm btn-label-secondary dropdown-toggle"
|
|
||||||
type="button"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
aria-expanded="false"
|
|
||||||
>
|
|
||||||
<i className="bx bx-export me-2 bx-sm"></i>Export
|
|
||||||
</button>
|
|
||||||
<ul className="dropdown-menu">
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
className="dropdown-item"
|
|
||||||
href="#"
|
|
||||||
onClick={() => handleExport("print")}
|
|
||||||
>
|
|
||||||
<i className="bx bx-printer me-1"></i> Print
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
className="dropdown-item"
|
|
||||||
href="#"
|
|
||||||
onClick={() => handleExport("csv")}
|
|
||||||
>
|
|
||||||
<i className="bx bx-file me-1"></i> CSV
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
className="dropdown-item"
|
|
||||||
href="#"
|
|
||||||
onClick={() => handleExport("excel")}
|
|
||||||
>
|
|
||||||
<i className="bx bxs-file-export me-1"></i> Excel
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
className="dropdown-item"
|
|
||||||
href="#"
|
|
||||||
onClick={() => handleExport("pdf")}
|
|
||||||
>
|
|
||||||
<i className="bx bxs-file-pdf me-1"></i> PDF
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Add Employee Button */}
|
{/* Add Employee Button */}
|
||||||
{Manage_Employee && (
|
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-primary"
|
|
||||||
type="button"
|
|
||||||
onClick={() => handleEmployeeModel(null)}
|
|
||||||
>
|
|
||||||
<i className="bx bx-plus-circle me-2"></i>
|
|
||||||
<span className="d-none d-md-inline-block">
|
|
||||||
Add New Employee
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -38,18 +38,22 @@ import LegalInfoCard from "../pages/TermsAndConditions/LegalInfoCard";
|
|||||||
import ProtectedRoute from "./ProtectedRoute";
|
import ProtectedRoute from "./ProtectedRoute";
|
||||||
import Directory from "../pages/Directory/Directory";
|
import Directory from "../pages/Directory/Directory";
|
||||||
import LoginWithOtp from "../pages/authentication/LoginWithOtp";
|
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(
|
const router = createBrowserRouter(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
element: <AuthLayout />,
|
element: <AuthLayout />,
|
||||||
children: [
|
children: [
|
||||||
{path: "/auth/login", element: <LoginPage />},
|
{ path: "/auth/login", element: <LoginPage /> },
|
||||||
{path: "/auth/login-otp", element: <LoginWithOtp />},
|
{ path: "/auth/login-otp", element: <LoginWithOtp /> },
|
||||||
{ path: "/auth/reqest/demo", element: <RegisterPage /> },
|
{ path: "/auth/reqest/demo", element: <RegisterPage /> },
|
||||||
{ path: "/auth/forgot-password", element: <ForgotPasswordPage /> },
|
{ path: "/auth/forgot-password", element: <ForgotPasswordPage /> },
|
||||||
{ path: "/reset-password", element: <ResetPasswordPage /> },
|
{ path: "/reset-password", element: <ResetPasswordPage /> },
|
||||||
{ path: "/legal-info", element: <LegalInfoCard /> },
|
{ path: "/legal-info", element: <LegalInfoCard /> },
|
||||||
{ path: "/auth/changepassword", element: <ChangePasswordPage /> },
|
{ path: "/auth/changepassword", element: <ChangePasswordPage /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -77,6 +81,10 @@ const router = createBrowserRouter(
|
|||||||
{ path: "/activities/reports", element: <Reports /> },
|
{ path: "/activities/reports", element: <Reports /> },
|
||||||
{ path: "/gallary", element: <ImageGallary /> },
|
{ path: "/gallary", element: <ImageGallary /> },
|
||||||
{ path: "/masters", element: <MasterPage /> },
|
{ path: "/masters", element: <MasterPage /> },
|
||||||
|
{ path: "/tenant/profile", element: <Tenant /> },
|
||||||
|
{ 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/support", element: <Support /> },
|
||||||
{ path: "/help/docs", element: <Documentation /> },
|
{ path: "/help/docs", element: <Documentation /> },
|
||||||
{ path: "/help/connect", element: <Connect /> },
|
{ path: "/help/connect", element: <Connect /> },
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { createSlice } from "@reduxjs/toolkit";
|
import { createSlice, current } from "@reduxjs/toolkit";
|
||||||
|
|
||||||
const globalVariablesSlice = createSlice({
|
const globalVariablesSlice = createSlice({
|
||||||
name: "globalVariables",
|
name: "globalVariables",
|
||||||
initialState: {
|
initialState: {
|
||||||
loginUser:null
|
loginUser:null,
|
||||||
|
currentTenant:null
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
setGlobalVariable: (state, action) => {
|
setGlobalVariable: (state, action) => {
|
||||||
@ -13,9 +14,14 @@ const globalVariablesSlice = createSlice({
|
|||||||
setLoginUserPermmisions: ( state, action ) =>
|
setLoginUserPermmisions: ( state, action ) =>
|
||||||
{
|
{
|
||||||
state.loginUser = action.payload
|
state.loginUser = action.payload
|
||||||
|
},
|
||||||
|
setCurrentTenant:(state, action)=>
|
||||||
|
{
|
||||||
|
state.currentTenant = action.payload;
|
||||||
|
localStorage.setItem("currentTenant",state.currentTenant.id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { setGlobalVariable,setLoginUserPermmisions } = globalVariablesSlice.actions;
|
export const { setGlobalVariable,setLoginUserPermmisions,setCurrentTenant } = globalVariablesSlice.actions;
|
||||||
export default globalVariablesSlice.reducer;
|
export default globalVariablesSlice.reducer;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user