Merge branch 'Organization_Management' of https://git.marcoaiot.com/admin/marco.pms.web into Kartik_Bug#1180
This commit is contained in:
commit
fd46f294d7
119
package-lock.json
generated
119
package-lock.json
generated
@ -809,9 +809,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/source-map": {
|
"node_modules/@jridgewell/source-map": {
|
||||||
"version": "0.3.6",
|
"version": "0.3.11",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
|
||||||
"integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
|
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -1552,13 +1552,13 @@
|
|||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.13.13",
|
"version": "24.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.13.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz",
|
||||||
"integrity": "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ==",
|
"integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.20.0"
|
"undici-types": "~7.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/prop-types": {
|
"node_modules/@types/prop-types": {
|
||||||
@ -1835,9 +1835,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.14.0",
|
"version": "8.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@ -1845,6 +1846,19 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/acorn-import-phases": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"acorn": "^8.14.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn-jsx": {
|
"node_modules/acorn-jsx": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
|
||||||
@ -2625,9 +2639,9 @@
|
|||||||
"integrity": "sha512-ZpSAUOZ2Izby7qnZluSrAlGgGQzucmFbN0n64dYzocYxnxV5ufurpj3VgEe4cUp7ir9LmeLxNYo8bVnlM8bQHw=="
|
"integrity": "sha512-ZpSAUOZ2Izby7qnZluSrAlGgGQzucmFbN0n64dYzocYxnxV5ufurpj3VgEe4cUp7ir9LmeLxNYo8bVnlM8bQHw=="
|
||||||
},
|
},
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.18.1",
|
"version": "5.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||||
"integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
|
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -2741,9 +2755,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/es-module-lexer": {
|
"node_modules/es-module-lexer": {
|
||||||
"version": "1.6.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
||||||
"integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==",
|
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
@ -3138,9 +3152,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/fast-uri": {
|
"node_modules/fast-uri": {
|
||||||
"version": "3.0.6",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||||
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
|
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@ -5163,9 +5177,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/schema-utils": {
|
"node_modules/schema-utils": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
|
||||||
"integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==",
|
"integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -5567,24 +5581,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tapable": {
|
"node_modules/tapable": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz",
|
||||||
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
|
"integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/webpack"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/terser": {
|
"node_modules/terser": {
|
||||||
"version": "5.39.0",
|
"version": "5.44.0",
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz",
|
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz",
|
||||||
"integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==",
|
"integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/source-map": "^0.3.3",
|
"@jridgewell/source-map": "^0.3.3",
|
||||||
"acorn": "^8.8.2",
|
"acorn": "^8.15.0",
|
||||||
"commander": "^2.20.0",
|
"commander": "^2.20.0",
|
||||||
"source-map-support": "~0.5.20"
|
"source-map-support": "~0.5.20"
|
||||||
},
|
},
|
||||||
@ -5777,9 +5795,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.20.0",
|
"version": "7.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz",
|
||||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
|
"integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
@ -5907,9 +5925,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/watchpack": {
|
"node_modules/watchpack": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
|
||||||
"integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==",
|
"integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -5927,21 +5945,23 @@
|
|||||||
"license": "BSD-2-Clause"
|
"license": "BSD-2-Clause"
|
||||||
},
|
},
|
||||||
"node_modules/webpack": {
|
"node_modules/webpack": {
|
||||||
"version": "5.98.0",
|
"version": "5.101.3",
|
||||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz",
|
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz",
|
||||||
"integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==",
|
"integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/eslint-scope": "^3.7.7",
|
"@types/eslint-scope": "^3.7.7",
|
||||||
"@types/estree": "^1.0.6",
|
"@types/estree": "^1.0.8",
|
||||||
|
"@types/json-schema": "^7.0.15",
|
||||||
"@webassemblyjs/ast": "^1.14.1",
|
"@webassemblyjs/ast": "^1.14.1",
|
||||||
"@webassemblyjs/wasm-edit": "^1.14.1",
|
"@webassemblyjs/wasm-edit": "^1.14.1",
|
||||||
"@webassemblyjs/wasm-parser": "^1.14.1",
|
"@webassemblyjs/wasm-parser": "^1.14.1",
|
||||||
"acorn": "^8.14.0",
|
"acorn": "^8.15.0",
|
||||||
|
"acorn-import-phases": "^1.0.3",
|
||||||
"browserslist": "^4.24.0",
|
"browserslist": "^4.24.0",
|
||||||
"chrome-trace-event": "^1.0.2",
|
"chrome-trace-event": "^1.0.2",
|
||||||
"enhanced-resolve": "^5.17.1",
|
"enhanced-resolve": "^5.17.3",
|
||||||
"es-module-lexer": "^1.2.1",
|
"es-module-lexer": "^1.2.1",
|
||||||
"eslint-scope": "5.1.1",
|
"eslint-scope": "5.1.1",
|
||||||
"events": "^3.2.0",
|
"events": "^3.2.0",
|
||||||
@ -5951,11 +5971,11 @@
|
|||||||
"loader-runner": "^4.2.0",
|
"loader-runner": "^4.2.0",
|
||||||
"mime-types": "^2.1.27",
|
"mime-types": "^2.1.27",
|
||||||
"neo-async": "^2.6.2",
|
"neo-async": "^2.6.2",
|
||||||
"schema-utils": "^4.3.0",
|
"schema-utils": "^4.3.2",
|
||||||
"tapable": "^2.1.1",
|
"tapable": "^2.1.1",
|
||||||
"terser-webpack-plugin": "^5.3.11",
|
"terser-webpack-plugin": "^5.3.11",
|
||||||
"watchpack": "^2.4.1",
|
"watchpack": "^2.4.1",
|
||||||
"webpack-sources": "^3.2.3"
|
"webpack-sources": "^3.3.3"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"webpack": "bin/webpack.js"
|
"webpack": "bin/webpack.js"
|
||||||
@ -5974,15 +5994,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/webpack-sources": {
|
"node_modules/webpack-sources": {
|
||||||
"version": "3.2.3",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz",
|
||||||
"integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
|
"integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/webpack/node_modules/@types/estree": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/webpack/node_modules/eslint-scope": {
|
"node_modules/webpack/node_modules/eslint-scope": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
|
||||||
|
|||||||
@ -1,8 +1,12 @@
|
|||||||
:root,
|
:root,
|
||||||
[data-bs-theme="light"] {
|
[data-bs-theme="light"] {
|
||||||
--bs-nav-link-font-size: 0.7375rem;
|
--bs-nav-link-font-size: 0.7375rem;
|
||||||
|
--bg-border-color :#f8f6f6
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
padding: 0.5rem var(--bs-card-cap-padding-x);
|
padding: 0.5rem var(--bs-card-cap-padding-x);
|
||||||
}
|
}
|
||||||
|
.table_header_border {
|
||||||
|
border-bottom:2px solid var(--bs-table-border-color) ;
|
||||||
|
}
|
||||||
5
public/assets/img/SP-Placeholdeer.svg
Normal file
5
public/assets/img/SP-Placeholdeer.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="120" height="120" fill="#EFF1F3"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M33.2503 38.4816C33.2603 37.0472 34.4199 35.8864 35.8543 35.875H83.1463C84.5848 35.875 85.7503 37.0431 85.7503 38.4816V80.5184C85.7403 81.9528 84.5807 83.1136 83.1463 83.125H35.8543C34.4158 83.1236 33.2503 81.957 33.2503 80.5184V38.4816ZM80.5006 41.1251H38.5006V77.8751L62.8921 53.4783C63.9172 52.4536 65.5788 52.4536 66.6039 53.4783L80.5006 67.4013V41.1251ZM43.75 51.6249C43.75 54.5244 46.1005 56.8749 49 56.8749C51.8995 56.8749 54.25 54.5244 54.25 51.6249C54.25 48.7254 51.8995 46.3749 49 46.3749C46.1005 46.3749 43.75 48.7254 43.75 51.6249Z" fill="#687787"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 888 B |
BIN
public/assets/img/orglogo.png
Normal file
BIN
public/assets/img/orglogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
4
public/assets/vendor/css/core.css
vendored
4
public/assets/vendor/css/core.css
vendored
@ -18613,6 +18613,10 @@ li:not(:first-child) .dropdown-item,
|
|||||||
min-height: 70vh !important;
|
min-height: 70vh !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-min-h{
|
||||||
|
min-height: 60vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
.flex-fill {
|
.flex-fill {
|
||||||
flex: 1 1 auto !important;
|
flex: 1 1 auto !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { ToastContainer } from "react-toastify";
|
|||||||
import { QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
import { queryClient } from "./layouts/AuthLayout";
|
import { queryClient } from "./layouts/AuthLayout";
|
||||||
|
import ModalProvider from "./ModalProvider";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ const App = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="app">
|
<div className="app">
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<ModalProvider/>
|
||||||
<DireProvider>
|
<DireProvider>
|
||||||
<AppRoutes />
|
<AppRoutes />
|
||||||
</DireProvider>
|
</DireProvider>
|
||||||
|
|||||||
@ -1,112 +0,0 @@
|
|||||||
import React, {
|
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
useState,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
} from "react";
|
|
||||||
|
|
||||||
const ModalContext = createContext();
|
|
||||||
|
|
||||||
export const useModal = () => useContext(ModalContext);
|
|
||||||
|
|
||||||
// ModalProvider to manage modal state and expose functionality to the rest of the app
|
|
||||||
export const ModalProvider = ({ children }) => {
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const [modalContent, setModalContent] = useState(null);
|
|
||||||
const [onSubmit, setOnSubmit] = useState(null);
|
|
||||||
const [modalSize, setModalSize] = useState("lg");
|
|
||||||
|
|
||||||
// Ref to track the modal content element
|
|
||||||
const modalRef = useRef(null);
|
|
||||||
|
|
||||||
const openModal = (content, onSubmitCallback, size = "lg") => {
|
|
||||||
setModalContent(content); // Set modal content dynamically
|
|
||||||
setOnSubmit(() => onSubmitCallback); // Set the submit handler dynamically
|
|
||||||
setIsOpen(true); // Open the modal
|
|
||||||
setModalSize(size); // Set the modal size
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function to close the modal
|
|
||||||
const closeModal = () => {
|
|
||||||
setIsOpen(false); // Close the modal
|
|
||||||
setModalContent(null); // Clear modal content
|
|
||||||
setOnSubmit(null); // Clear the submit callback
|
|
||||||
setModalSize("lg"); // Reset modal size
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleEscape = (event) => {
|
|
||||||
if (event.key === "Escape") {
|
|
||||||
closeModal();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener("keydown", handleEscape);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("keydown", handleEscape);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleClickOutside = (event) => {
|
|
||||||
if (modalRef.current && !modalRef.current.contains(event.target)) {
|
|
||||||
closeModal();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalContext.Provider
|
|
||||||
value={{
|
|
||||||
isOpen,
|
|
||||||
openModal,
|
|
||||||
closeModal,
|
|
||||||
modalContent,
|
|
||||||
modalSize,
|
|
||||||
onSubmit,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
|
|
||||||
{isOpen && (
|
|
||||||
<div style={overlayStyles}>
|
|
||||||
<div
|
|
||||||
ref={modalRef}
|
|
||||||
style={{
|
|
||||||
...modalStyles,
|
|
||||||
maxWidth: modalSize === "sm" ? "400px" : "800px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>{modalContent}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</ModalContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const overlayStyles = {
|
|
||||||
position: "fixed",
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
zIndex: 1050,
|
|
||||||
};
|
|
||||||
|
|
||||||
const modalStyles = {
|
|
||||||
backgroundColor: "white",
|
|
||||||
padding: "20px",
|
|
||||||
borderRadius: "5px",
|
|
||||||
boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1)",
|
|
||||||
width: "90%",
|
|
||||||
maxWidth: "800px",
|
|
||||||
};
|
|
||||||
19
src/ModalProvider.jsx
Normal file
19
src/ModalProvider.jsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useOrganizationModal } from "./hooks/useOrganization";
|
||||||
|
import OrganizationModal from "./components/Organization/OrganizationModal";
|
||||||
|
import { useAuthModal } from "./hooks/useAuth";
|
||||||
|
import SwitchTenant from "./pages/authentication/SwitchTenant";
|
||||||
|
|
||||||
|
const ModalProvider = () => {
|
||||||
|
const { isOpen, onClose } = useOrganizationModal();
|
||||||
|
const { isOpen: isAuthOpen } = useAuthModal();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isOpen && <OrganizationModal />}
|
||||||
|
{isAuthOpen && <SwitchTenant />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalProvider;
|
||||||
@ -12,22 +12,19 @@ import { useQueryClient } from "@tanstack/react-query";
|
|||||||
import eventBus from "../../services/eventBus";
|
import eventBus from "../../services/eventBus";
|
||||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||||
|
|
||||||
const Attendance = ({ getRole, handleModalData, searchTerm }) => {
|
const Attendance = ({ getRole, handleModalData, searchTerm, projectId, organizationId, includeInactive, date }) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [todayDate, setTodayDate] = useState(new Date());
|
const [todayDate, setTodayDate] = useState(new Date());
|
||||||
const [ShowPending, setShowPending] = useState(false);
|
const [ShowPending, setShowPending] = useState(false);
|
||||||
// const selectedProject = useSelector(
|
|
||||||
// (store) => store.localVariables.projectId
|
|
||||||
// );
|
|
||||||
const selectedProject = useSelectedProject();
|
const selectedProject = useSelectedProject();
|
||||||
const {
|
const {
|
||||||
attendance,
|
attendance,
|
||||||
loading: attLoading,
|
loading: attLoading,
|
||||||
recall: attrecall,
|
recall: attrecall,
|
||||||
isFetching
|
isFetching
|
||||||
} = useAttendance(selectedProject);
|
} = useAttendance(selectedProject, organizationId, includeInactive, date);
|
||||||
const filteredAttendance = ShowPending
|
const filteredAttendance = ShowPending
|
||||||
? attendance?.filter(
|
? attendance?.filter(
|
||||||
(att) => att?.checkInTime !== null && att?.checkOutTime === null
|
(att) => att?.checkInTime !== null && att?.checkOutTime === null
|
||||||
@ -62,12 +59,11 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
|
|||||||
const role = item.jobRoleName?.toLowerCase() || "";
|
const role = item.jobRoleName?.toLowerCase() || "";
|
||||||
return (
|
return (
|
||||||
fullName.includes(lowercasedSearchTerm) ||
|
fullName.includes(lowercasedSearchTerm) ||
|
||||||
role.includes(lowercasedSearchTerm) // ✅ also search by role
|
role.includes(lowercasedSearchTerm) // also search by role
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}, [group1, group2, searchTerm]);
|
}, [group1, group2, searchTerm]);
|
||||||
|
|
||||||
|
|
||||||
const { currentPage, totalPages, currentItems, paginate } = usePagination(
|
const { currentPage, totalPages, currentItems, paginate } = usePagination(
|
||||||
finalFilteredData,
|
finalFilteredData,
|
||||||
ITEMS_PER_PAGE
|
ITEMS_PER_PAGE
|
||||||
@ -116,7 +112,7 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="table-responsive text-nowrap h-100"
|
className="table-responsive text-nowrap h-100"
|
||||||
style={{ minHeight: "200px" }} // 🔹 Ensures fixed height
|
style={{ minHeight: "200px" }} // Ensures fixed height
|
||||||
>
|
>
|
||||||
<div className="d-flex text-start align-items-center py-2">
|
<div className="d-flex text-start align-items-center py-2">
|
||||||
<strong>Date : {formatUTCToLocalTime(todayDate)}</strong>
|
<strong>Date : {formatUTCToLocalTime(todayDate)}</strong>
|
||||||
@ -142,6 +138,7 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
|
|||||||
<tr className="border-top-1">
|
<tr className="border-top-1">
|
||||||
<th colSpan={2}>Name</th>
|
<th colSpan={2}>Name</th>
|
||||||
<th>Role</th>
|
<th>Role</th>
|
||||||
|
<th>Organization</th>
|
||||||
<th>
|
<th>
|
||||||
<i className="bx bxs-down-arrow-alt text-success"></i>
|
<i className="bx bxs-down-arrow-alt text-success"></i>
|
||||||
Check-In
|
Check-In
|
||||||
@ -190,6 +187,8 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>{item.jobRoleName}</td>
|
<td>{item.jobRoleName}</td>
|
||||||
|
<td>{item.organizationName || "--"}</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
{item.checkInTime
|
{item.checkInTime
|
||||||
? convertShortTime(item.checkInTime)
|
? convertShortTime(item.checkInTime)
|
||||||
@ -213,7 +212,11 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
|
|||||||
))}
|
))}
|
||||||
{!attendance && (
|
{!attendance && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={6} className="text-center text-secondary" style={{ height: "200px" }}>
|
<td
|
||||||
|
colSpan={7}
|
||||||
|
className="text-center text-secondary"
|
||||||
|
style={{ height: "200px" }}
|
||||||
|
>
|
||||||
No employees assigned to the project!
|
No employees assigned to the project!
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -221,6 +224,7 @@ const Attendance = ({ getRole, handleModalData, searchTerm }) => {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
||||||
{!loading && finalFilteredData.length > ITEMS_PER_PAGE && (
|
{!loading && finalFilteredData.length > ITEMS_PER_PAGE && (
|
||||||
<nav aria-label="Page ">
|
<nav aria-label="Page ">
|
||||||
<ul className="pagination pagination-sm justify-content-end py-1">
|
<ul className="pagination pagination-sm justify-content-end py-1">
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import Avatar from "../common/Avatar";
|
|||||||
import { convertShortTime } from "../../utils/dateUtils";
|
import { convertShortTime } from "../../utils/dateUtils";
|
||||||
import RenderAttendanceStatus from "./RenderAttendanceStatus";
|
import RenderAttendanceStatus from "./RenderAttendanceStatus";
|
||||||
import { useSelector, useDispatch } from "react-redux";
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
import { fetchAttendanceData } from "../../slices/apiSlice/attedanceLogsSlice";
|
|
||||||
import DateRangePicker from "../common/DateRangePicker";
|
import DateRangePicker from "../common/DateRangePicker";
|
||||||
import { clearCacheKey, getCachedData, useSelectedProject } from "../../slices/apiDataManager";
|
import { clearCacheKey, getCachedData, useSelectedProject } from "../../slices/apiDataManager";
|
||||||
import eventBus from "../../services/eventBus";
|
import eventBus from "../../services/eventBus";
|
||||||
@ -34,7 +33,7 @@ const usePagination = (data, itemsPerPage) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const AttendanceLog = ({ handleModalData, searchTerm }) => {
|
const AttendanceLog = ({ handleModalData, searchTerm ,organizationId}) => {
|
||||||
// const selectedProject = useSelector(
|
// const selectedProject = useSelector(
|
||||||
// (store) => store.localVariables.projectId
|
// (store) => store.localVariables.projectId
|
||||||
// );
|
// );
|
||||||
@ -82,7 +81,8 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
|
|||||||
} = useAttendancesLogs(
|
} = useAttendancesLogs(
|
||||||
selectedProject,
|
selectedProject,
|
||||||
dateRange.startDate,
|
dateRange.startDate,
|
||||||
dateRange.endDate
|
dateRange.endDate,
|
||||||
|
organizationId
|
||||||
);
|
);
|
||||||
const filtering = (data) => {
|
const filtering = (data) => {
|
||||||
const filteredData = showPending
|
const filteredData = showPending
|
||||||
@ -151,6 +151,33 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
|
|||||||
});
|
});
|
||||||
}, [processedData, searchTerm]);
|
}, [processedData, searchTerm]);
|
||||||
|
|
||||||
|
// const filteredSearchData = useMemo(() => {
|
||||||
|
// let tempData = processedData;
|
||||||
|
|
||||||
|
// if (searchTerm) {
|
||||||
|
// const lowercasedSearchTerm = searchTerm.toLowerCase();
|
||||||
|
// tempData = tempData.filter((item) => {
|
||||||
|
// const fullName = `${item.firstName} ${item.lastName}`.toLowerCase();
|
||||||
|
// return fullName.includes(lowercasedSearchTerm);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (filters?.selectedOrganization) {
|
||||||
|
// tempData = tempData.filter(
|
||||||
|
// (item) => item.organization?.name === filters.selectedOrganization
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (filters?.selectedServices?.length > 0) {
|
||||||
|
// tempData = tempData.filter((item) =>
|
||||||
|
// filters.selectedServices.includes(item.service?.name)
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return tempData;
|
||||||
|
// }, [processedData, searchTerm, filters]);
|
||||||
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
currentPage,
|
currentPage,
|
||||||
totalPages,
|
totalPages,
|
||||||
@ -243,14 +270,7 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
|
|||||||
<label className="form-check-label ms-0">Show Pending</label>
|
<label className="form-check-label ms-0">Show Pending</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-2 m-0 text-end">
|
|
||||||
<i
|
|
||||||
className={`bx bx-refresh cursor-pointer fs-4 ${isFetching ? "spin" : ""
|
|
||||||
}`}
|
|
||||||
title="Refresh"
|
|
||||||
onClick={() => refetch()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="table-responsive text-nowrap" style={{ minHeight: "200px" }}>
|
<div className="table-responsive text-nowrap" style={{ minHeight: "200px" }}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@ -265,9 +285,9 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
|
|||||||
Name
|
Name
|
||||||
</th>
|
</th>
|
||||||
<th className="border-top-1">Date</th>
|
<th className="border-top-1">Date</th>
|
||||||
|
<th>Organization</th>
|
||||||
<th>
|
<th>
|
||||||
<i className="bx bxs-down-arrow-alt text-success"></i>{" "}
|
<i className="bx bxs-down-arrow-alt text-success"></i> Check-In
|
||||||
Check-In
|
|
||||||
</th>
|
</th>
|
||||||
<th>
|
<th>
|
||||||
<i className="bx bxs-up-arrow-alt text-danger"></i> Check-Out
|
<i className="bx bxs-up-arrow-alt text-danger"></i> Check-Out
|
||||||
@ -294,7 +314,7 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
|
|||||||
key={`header-${currentDate}`}
|
key={`header-${currentDate}`}
|
||||||
className="table-row-header"
|
className="table-row-header"
|
||||||
>
|
>
|
||||||
<td colSpan={6} className="text-start">
|
<td colSpan={8} className="text-start">
|
||||||
<strong>
|
<strong>
|
||||||
{moment(currentDate).format("DD-MM-YYYY")}
|
{moment(currentDate).format("DD-MM-YYYY")}
|
||||||
</strong>
|
</strong>
|
||||||
@ -324,6 +344,7 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
|
|||||||
attendance.checkInTime || attendance.checkOutTime
|
attendance.checkInTime || attendance.checkOutTime
|
||||||
).format("DD-MMM-YYYY")}
|
).format("DD-MMM-YYYY")}
|
||||||
</td>
|
</td>
|
||||||
|
<td>{attendance.organizationName || "--"}</td>
|
||||||
<td>{convertShortTime(attendance.checkInTime)}</td>
|
<td>{convertShortTime(attendance.checkInTime)}</td>
|
||||||
<td>
|
<td>
|
||||||
{attendance.checkOutTime
|
{attendance.checkOutTime
|
||||||
@ -345,7 +366,7 @@ const AttendanceLog = ({ handleModalData, searchTerm }) => {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
) : (
|
) : (
|
||||||
<div className="my-4"><span className="text-secondary">No Record Available !</span></div>
|
<div className="my-12"><span className="text-secondary">No data available for the selected date range. Please Select another date.</span></div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{paginatedAttendances?.length == 0 && filteredSearchData?.length > 0 && (
|
{paginatedAttendances?.length == 0 && filteredSearchData?.length > 0 && (
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import TimePicker from "../common/TimePicker";
|
import TimePicker from "../common/TimePicker";
|
||||||
import { usePositionTracker } from "../../hooks/usePositionTracker";
|
import { usePositionTracker } from "../../hooks/usePositionTracker";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { markAttendance } from "../../slices/apiSlice/attedanceLogsSlice";
|
|
||||||
import showToast from "../../services/toastService";
|
import showToast from "../../services/toastService";
|
||||||
import { checkIfCurrentDate } from "../../utils/dateUtils";
|
import { checkIfCurrentDate } from "../../utils/dateUtils";
|
||||||
import { useMarkAttendance } from "../../hooks/useAttendance";
|
import { useMarkAttendance } from "../../hooks/useAttendance";
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import "../../components/Project/ProjectInfra.css";
|
import "../../components/Project/ProjectInfra.css";
|
||||||
import BuildingModel from "../Project/Infrastructure/BuildingModel";
|
import BuildingModel from "../Project/Infrastructure/BuildingModel";
|
||||||
@ -8,25 +7,33 @@ import WorkAreaModel from "../Project/Infrastructure/WorkAreaModel";
|
|||||||
import TaskModel from "../Project/Infrastructure/TaskModel";
|
import TaskModel from "../Project/Infrastructure/TaskModel";
|
||||||
import ProjectRepository from "../../repositories/ProjectRepository";
|
import ProjectRepository from "../../repositories/ProjectRepository";
|
||||||
import Breadcrumb from "../../components/common/Breadcrumb";
|
import Breadcrumb from "../../components/common/Breadcrumb";
|
||||||
import {useProjectDetails, useProjectInfra, useProjects} from "../../hooks/useProjects";
|
import {
|
||||||
import {useHasUserPermission} from "../../hooks/useHasUserPermission";
|
useCurrentService,
|
||||||
import {APPROVE_TASK, ASSIGN_REPORT_TASK, MANAGE_PROJECT_INFRA} from "../../utils/constants";
|
useProjectDetails,
|
||||||
import {useDispatch, useSelector} from "react-redux";
|
useProjectInfra,
|
||||||
import {useProfile} from "../../hooks/useProfile";
|
useProjects,
|
||||||
import {refreshData, setProjectId} from "../../slices/localVariablesSlice";
|
} from "../../hooks/useProjects";
|
||||||
|
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||||
|
import {
|
||||||
|
APPROVE_TASK,
|
||||||
|
ASSIGN_REPORT_TASK,
|
||||||
|
MANAGE_PROJECT_INFRA,
|
||||||
|
} from "../../utils/constants";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { useProfile } from "../../hooks/useProfile";
|
||||||
|
import { refreshData, setProjectId } from "../../slices/localVariablesSlice";
|
||||||
import InfraTable from "../Project/Infrastructure/InfraTable";
|
import InfraTable from "../Project/Infrastructure/InfraTable";
|
||||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||||
import Loader from "../common/Loader";
|
import Loader from "../common/Loader";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const InfraPlanning = () => {
|
const InfraPlanning = () => {
|
||||||
const { profile: LoggedUser, refetch: fetchData } = useProfile();
|
const { profile: LoggedUser, refetch: fetchData } = useProfile();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const selectedProject = useSelectedProject();
|
const selectedProject = useSelectedProject();
|
||||||
|
const selectedService = useCurrentService();
|
||||||
|
|
||||||
const { projectInfra, isLoading, isError, error, isFetched } = useProjectInfra(selectedProject);
|
const { projectInfra, isLoading, isError, error, isFetched } =
|
||||||
|
useProjectInfra(selectedProject, selectedService || "" );
|
||||||
|
|
||||||
const canManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA);
|
const canManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA);
|
||||||
const canApproveTask = useHasUserPermission(APPROVE_TASK);
|
const canApproveTask = useHasUserPermission(APPROVE_TASK);
|
||||||
@ -55,7 +62,7 @@ const InfraPlanning = () => {
|
|||||||
|
|
||||||
if (isFetched && (!projectInfra || projectInfra.length === 0)) {
|
if (isFetched && (!projectInfra || projectInfra.length === 0)) {
|
||||||
return (
|
return (
|
||||||
<div className="card text-center">
|
<div className="text-center">
|
||||||
<p className="my-3">No Result Found</p>
|
<p className="my-3">No Result Found</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -63,16 +70,13 @@ const InfraPlanning = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="col-md-12 col-lg-12 col-xl-12 order-0 mb-4">
|
<div className="col-md-12 col-lg-12 col-xl-12 order-0 mb-4">
|
||||||
<div className="card">
|
|
||||||
<div className="card-body" style={{ padding: "0.5rem" }}>
|
<div className="card-body" style={{ padding: "0.5rem" }}>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<InfraTable buildings={projectInfra} projectId={selectedProject} />
|
<InfraTable buildings={projectInfra} projectId={selectedProject} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default InfraPlanning;
|
export default InfraPlanning;
|
||||||
|
|
||||||
|
|||||||
@ -10,13 +10,13 @@ import eventBus from "../../services/eventBus";
|
|||||||
import { cacheData, clearCacheKey, useSelectedProject } from "../../slices/apiDataManager";
|
import { cacheData, clearCacheKey, useSelectedProject } from "../../slices/apiDataManager";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
const Regularization = ({ handleRequest, searchTerm }) => {
|
const Regularization = ({ handleRequest, searchTerm,projectId, organizationId, IncludeInActive }) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
// var selectedProject = useSelector((store) => store.localVariables.projectId);
|
// var selectedProject = useSelector((store) => store.localVariables.projectId);
|
||||||
const selectedProject = useSelectedProject();
|
const selectedProject = useSelectedProject();
|
||||||
const [regularizesList, setregularizedList] = useState([]);
|
const [regularizesList, setregularizedList] = useState([]);
|
||||||
const { regularizes, loading, error, refetch } =
|
const { regularizes, loading, error, refetch } =
|
||||||
useRegularizationRequests(selectedProject);
|
useRegularizationRequests(selectedProject, organizationId, IncludeInActive);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setregularizedList(regularizes);
|
setregularizedList(regularizes);
|
||||||
@ -59,6 +59,36 @@ const Regularization = ({ handleRequest, searchTerm }) => {
|
|||||||
});
|
});
|
||||||
}, [regularizesList, searchTerm]);
|
}, [regularizesList, searchTerm]);
|
||||||
|
|
||||||
|
// const filteredSearchData = useMemo(() => {
|
||||||
|
// let sortedList = [...regularizesList].sort(sortByName);
|
||||||
|
|
||||||
|
// // Search filter
|
||||||
|
// if (searchTerm) {
|
||||||
|
// const lowercasedSearchTerm = searchTerm.toLowerCase();
|
||||||
|
// sortedList = sortedList.filter((item) => {
|
||||||
|
// const fullName = `${item.firstName} ${item.lastName}`.toLowerCase();
|
||||||
|
// return fullName.includes(lowercasedSearchTerm);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Organization filter
|
||||||
|
// if (filters?.selectedOrganization) {
|
||||||
|
// sortedList = sortedList.filter(
|
||||||
|
// (item) => item.organization?.name === filters.selectedOrganization
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Services filter
|
||||||
|
// if (filters?.selectedServices?.length > 0) {
|
||||||
|
// sortedList = sortedList.filter((item) =>
|
||||||
|
// filters.selectedServices.includes(item.service?.name)
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return sortedList;
|
||||||
|
// }, [regularizesList, searchTerm, filters]);
|
||||||
|
|
||||||
|
|
||||||
const { currentPage, totalPages, currentItems, paginate } =
|
const { currentPage, totalPages, currentItems, paginate } =
|
||||||
usePagination(filteredSearchData, 20);
|
usePagination(filteredSearchData, 20);
|
||||||
|
|
||||||
@ -98,6 +128,7 @@ const Regularization = ({ handleRequest, searchTerm }) => {
|
|||||||
<tr>
|
<tr>
|
||||||
<th colSpan={2}>Name</th>
|
<th colSpan={2}>Name</th>
|
||||||
<th>Date</th>
|
<th>Date</th>
|
||||||
|
<th>Organization</th>
|
||||||
<th>
|
<th>
|
||||||
<i className="bx bxs-down-arrow-alt text-success"></i>Check-In
|
<i className="bx bxs-down-arrow-alt text-success"></i>Check-In
|
||||||
</th>
|
</th>
|
||||||
@ -115,7 +146,7 @@ const Regularization = ({ handleRequest, searchTerm }) => {
|
|||||||
<Avatar
|
<Avatar
|
||||||
firstName={att.firstName}
|
firstName={att.firstName}
|
||||||
lastName={att.lastName}
|
lastName={att.lastName}
|
||||||
></Avatar>
|
/>
|
||||||
<div className="d-flex flex-column">
|
<div className="d-flex flex-column">
|
||||||
<a href="#" className="text-heading text-truncate">
|
<a href="#" className="text-heading text-truncate">
|
||||||
<span className="fw-normal">
|
<span className="fw-normal">
|
||||||
@ -126,6 +157,9 @@ const Regularization = ({ handleRequest, searchTerm }) => {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>{moment(att.checkOutTime).format("DD-MMM-YYYY")}</td>
|
<td>{moment(att.checkOutTime).format("DD-MMM-YYYY")}</td>
|
||||||
|
|
||||||
|
<td>{att.organizationName || "--"}</td>
|
||||||
|
|
||||||
<td>{convertShortTime(att.checkInTime)}</td>
|
<td>{convertShortTime(att.checkInTime)}</td>
|
||||||
<td>
|
<td>
|
||||||
{att.checkOutTime ? convertShortTime(att.checkOutTime) : "--"}
|
{att.checkOutTime ? convertShortTime(att.checkOutTime) : "--"}
|
||||||
@ -136,12 +170,12 @@ const Regularization = ({ handleRequest, searchTerm }) => {
|
|||||||
handleRequest={handleRequest}
|
handleRequest={handleRequest}
|
||||||
refresh={refetch}
|
refresh={refetch}
|
||||||
/>
|
/>
|
||||||
{/* </div> */}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
className="d-flex justify-content-center align-items-center"
|
className="d-flex justify-content-center align-items-center"
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
import React, { act, useEffect, useState } from 'react'
|
import React, { act, useEffect, useState } from 'react'
|
||||||
import useAttendanceStatus, { ACTIONS } from '../../hooks/useAttendanceStatus';
|
import useAttendanceStatus, { ACTIONS } from '../../hooks/useAttendanceStatus';
|
||||||
// import AttendanceRepository from '../../repositories/AttendanceRepository';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { usePositionTracker } from '../../hooks/usePositionTracker';
|
import { usePositionTracker } from '../../hooks/usePositionTracker';
|
||||||
import {markCurrentAttendance} from '../../slices/apiSlice/attendanceAllSlice';
|
|
||||||
import {cacheData, getCachedData, useSelectedProject} from '../../slices/apiDataManager';
|
import {cacheData, getCachedData, useSelectedProject} from '../../slices/apiDataManager';
|
||||||
import showToast from '../../services/toastService';
|
import showToast from '../../services/toastService';
|
||||||
import { useMarkAttendance } from '../../hooks/useAttendance';
|
import { useMarkAttendance } from '../../hooks/useAttendance';
|
||||||
|
|||||||
96
src/components/DailyProgressRport/TaskReportFilterPanel.jsx
Normal file
96
src/components/DailyProgressRport/TaskReportFilterPanel.jsx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { useCurrentService, useProjectInfra } from "../../hooks/useProjects";
|
||||||
|
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||||
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import {
|
||||||
|
TaskReportDefaultValue,
|
||||||
|
TaskReportFilterSchema,
|
||||||
|
} from "./TaskRportScheam";
|
||||||
|
import { DateRangePicker1 } from "../common/DateRangePicker";
|
||||||
|
import SelectMultiple from "../common/SelectMultiple";
|
||||||
|
import { localToUtc } from "../../utils/appUtils";
|
||||||
|
|
||||||
|
const TaskReportFilterPanel = ({ handleFilter }) => {
|
||||||
|
const [resetKey, setResetKey] = useState(0);
|
||||||
|
const selectedProjec = useSelectedProject();
|
||||||
|
const selectedService = useCurrentService();
|
||||||
|
const { projectInfra, isLoading, error, isFetched } = useProjectInfra(
|
||||||
|
selectedProjec,
|
||||||
|
selectedService
|
||||||
|
);
|
||||||
|
const methods = useForm({
|
||||||
|
resolver: zodResolver(TaskReportFilterSchema),
|
||||||
|
defaultValues: TaskReportDefaultValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
reset,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
} = methods;
|
||||||
|
|
||||||
|
const onSubmit = (formData) => {
|
||||||
|
console.log(formData)
|
||||||
|
const filterPayload = {
|
||||||
|
startDate:localToUtc(formData.startDate),
|
||||||
|
endDate:localToUtc(formData.endDate)
|
||||||
|
|
||||||
|
}
|
||||||
|
handleFilter(filterPayload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClear =()=>{
|
||||||
|
setResetKey((prev) => prev + 1);
|
||||||
|
handleFilter(TaskReportDefaultValue)
|
||||||
|
reset(TaskReportDefaultValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...methods}>
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
|
||||||
|
<div className="mb-3 w-100">
|
||||||
|
<label className="fw-semibold">Choose Date Range:</label>
|
||||||
|
<DateRangePicker1
|
||||||
|
placeholder="DD-MM-YYYY To DD-MM-YYYY"
|
||||||
|
startField="startDate"
|
||||||
|
endField="endDate"
|
||||||
|
resetSignal={resetKey}
|
||||||
|
defaultRange={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <div className="row g-2">
|
||||||
|
<SelectMultiple
|
||||||
|
name="buildingIds"
|
||||||
|
label="Projects"
|
||||||
|
options={projectInfra}
|
||||||
|
labelKey="buildingName"
|
||||||
|
valueKey="id"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="row g-2">
|
||||||
|
<SelectMultiple
|
||||||
|
name="floorIds"
|
||||||
|
label="Floor"
|
||||||
|
options={projectInfra}
|
||||||
|
labelKey="floorName"
|
||||||
|
valueKey="id"
|
||||||
|
/>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
<div className="d-flex justify-content-end py-3 gap-2">
|
||||||
|
<button type="button" className="btn btn-label-secondary btn-sm" onClick={onClear}>
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="btn btn-primary btn-sm">
|
||||||
|
Apply
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TaskReportFilterPanel;
|
||||||
301
src/components/DailyProgressRport/TaskReportList.jsx
Normal file
301
src/components/DailyProgressRport/TaskReportList.jsx
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
import React, { useState, useEffect, useMemo } from "react";
|
||||||
|
import { useTaskList } from "../../hooks/useTasks";
|
||||||
|
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||||
|
import { useProjectName } from "../../hooks/useProjects";
|
||||||
|
import DailyProgrssReport, {
|
||||||
|
useDailyProgrssContext,
|
||||||
|
} from "../../pages/DailyProgressReport/DailyProgrssReport";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { setProjectId } from "../../slices/localVariablesSlice";
|
||||||
|
import {
|
||||||
|
APPROVE_TASK,
|
||||||
|
ASSIGN_REPORT_TASK,
|
||||||
|
ITEMS_PER_PAGE,
|
||||||
|
} from "../../utils/constants";
|
||||||
|
import { formatNumber, formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||||
|
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||||
|
import Pagination from "../common/Pagination";
|
||||||
|
import { TaskReportListSkeleton } from "./TaskRepprtListSkeleton";
|
||||||
|
import HoverPopup from "../common/HoverPopup";
|
||||||
|
|
||||||
|
const TaskReportList = () => {
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [filters, setFilters] = useState({
|
||||||
|
selectedBuilding: "",
|
||||||
|
selectedFloors: [],
|
||||||
|
selectedActivities: [],
|
||||||
|
});
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK);
|
||||||
|
const ReportTaskRights = useHasUserPermission(ASSIGN_REPORT_TASK);
|
||||||
|
|
||||||
|
const { service, openModal, closeModal,filter } = useDailyProgrssContext();
|
||||||
|
const selectedProject = useSelectedProject();
|
||||||
|
const { projectNames } = useProjectName();
|
||||||
|
|
||||||
|
const { data, isLoading, isError, error } = useTaskList(
|
||||||
|
selectedProject,
|
||||||
|
ITEMS_PER_PAGE,
|
||||||
|
currentPage,
|
||||||
|
service,filter
|
||||||
|
);
|
||||||
|
|
||||||
|
const ProgrssReportColumn = [
|
||||||
|
{
|
||||||
|
key: "activity",
|
||||||
|
label: "Activity",
|
||||||
|
getValue: (task) => task.workItem.activityMaster?.activityName || "N/A",
|
||||||
|
align: "text-start",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "assigned",
|
||||||
|
label: "Total Assigned",
|
||||||
|
getValue: (task) => task.plannedTask ?? "N/A",
|
||||||
|
align: "text-start",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "completed",
|
||||||
|
label: "Completed",
|
||||||
|
getValue: (task) => task.completedTask ?? "N/A",
|
||||||
|
align: "text-start",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "assignAt",
|
||||||
|
label: "Assign Date",
|
||||||
|
getValue: (task) =>
|
||||||
|
task.assignmentDate ? formatUTCToLocalTime(task.assignmentDate) : "N/A",
|
||||||
|
align: "text-start",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "team",
|
||||||
|
label: "Team",
|
||||||
|
getValue: (task) =>
|
||||||
|
task.teamMembers?.map((m) => `${m.firstName} ${m.lastName}`).join(", ") ||
|
||||||
|
"N/A",
|
||||||
|
align: "text-start",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const paginate = (page) => {
|
||||||
|
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
|
||||||
|
setCurrentPage(page);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedProject && projectNames.length > 0) {
|
||||||
|
dispatch(setProjectId(projectNames[0].id));
|
||||||
|
}
|
||||||
|
}, [selectedProject, projectNames, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFilters({
|
||||||
|
selectedBuilding: "",
|
||||||
|
selectedFloors: [],
|
||||||
|
selectedActivities: [],
|
||||||
|
});
|
||||||
|
}, [selectedProject]);
|
||||||
|
|
||||||
|
// Filter and Group wise data
|
||||||
|
|
||||||
|
const filteredTasks = useMemo(() => {
|
||||||
|
if (!data?.data) return [];
|
||||||
|
return data?.data.filter((task) => {
|
||||||
|
const { selectedBuilding, selectedFloors, selectedActivities } = filters;
|
||||||
|
|
||||||
|
if (
|
||||||
|
selectedBuilding &&
|
||||||
|
task?.workItem?.workArea?.floor?.building?.name !== selectedBuilding
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
if (
|
||||||
|
selectedFloors.length > 0 &&
|
||||||
|
!selectedFloors.includes(task?.workItem?.workArea?.floor?.floorName)
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
if (
|
||||||
|
selectedActivities.length > 0 &&
|
||||||
|
!selectedActivities.includes(
|
||||||
|
task?.workItem?.activityMaster?.activityName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}, [data?.data, filters, currentPage]);
|
||||||
|
|
||||||
|
const groupedTasks = useMemo(() => {
|
||||||
|
const groups = {};
|
||||||
|
filteredTasks.forEach((task) => {
|
||||||
|
const date = task.assignmentDate.split("T")[0];
|
||||||
|
if (!groups[date]) groups[date] = [];
|
||||||
|
groups[date].push(task);
|
||||||
|
});
|
||||||
|
return Object.keys(groups)
|
||||||
|
.sort((a, b) => new Date(b) - new Date(a))
|
||||||
|
.map((date) => ({ date, tasks: groups[date] }));
|
||||||
|
}, [filteredTasks, paginate, currentPage, selectedProject]);
|
||||||
|
|
||||||
|
const renderTeamMembers = (task, refIndex) => (
|
||||||
|
<div
|
||||||
|
key={refIndex}
|
||||||
|
tabIndex="0"
|
||||||
|
className="d-flex align-items-center avatar-group justify-content-center"
|
||||||
|
data-bs-toggle="popover"
|
||||||
|
data-bs-trigger="focus"
|
||||||
|
data-bs-placement="left"
|
||||||
|
data-bs-html="true"
|
||||||
|
data-bs-content={`
|
||||||
|
<div class="border border-secondary rounded custom-popover p-2 px-3">
|
||||||
|
${task.teamMembers
|
||||||
|
.map(
|
||||||
|
(m) => `
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-2">
|
||||||
|
<div class="avatar avatar-xs">
|
||||||
|
<span class="avatar-initial rounded-circle bg-label-primary">
|
||||||
|
${m?.firstName?.charAt(0) || ""}${m?.lastName?.charAt(0) || ""
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span>${m.firstName} ${m.lastName}</span>
|
||||||
|
</div>`
|
||||||
|
)
|
||||||
|
.join("")}
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{task.teamMembers.slice(0, 3).map((m) => (
|
||||||
|
<div
|
||||||
|
key={m.id}
|
||||||
|
className="avatar avatar-xs"
|
||||||
|
title={`${m.firstName} ${m.lastName}`}
|
||||||
|
>
|
||||||
|
<span className="avatar-initial rounded-circle bg-label-primary">
|
||||||
|
{m?.firstName.slice(0, 1)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{task.teamMembers.length > 3 && (
|
||||||
|
<div
|
||||||
|
className="avatar avatar-xs"
|
||||||
|
title={`${task.teamMembers.length - 3} more`}
|
||||||
|
>
|
||||||
|
<span className="avatar-initial rounded-circle bg-label-secondary">
|
||||||
|
+{task.teamMembers.length - 3}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading) return <TaskReportListSkeleton />;
|
||||||
|
if (isError) return <div>Loading....</div>;
|
||||||
|
return (
|
||||||
|
<div className="mt-2">
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="text-start">Activity</th>
|
||||||
|
<th>
|
||||||
|
<span>
|
||||||
|
Total Pending{" "}
|
||||||
|
<HoverPopup
|
||||||
|
title="Total Pending Task"
|
||||||
|
content={<p>This shows the total pending tasks for each activity on that date.</p>}
|
||||||
|
>
|
||||||
|
<i className="bx bx-xs ms-1 bx-info-circle cursor-pointer"></i>
|
||||||
|
</HoverPopup>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<span>
|
||||||
|
Reported/Planned{" "}
|
||||||
|
<HoverPopup
|
||||||
|
title="Reported and Planned Task"
|
||||||
|
content={<p>This shows the reported versus planned tasks for each activity on that date.</p>}
|
||||||
|
>
|
||||||
|
<i className="bx bx-xs ms-1 bx-info-circle cursor-pointer"></i>
|
||||||
|
</HoverPopup>
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<th>Assign Date</th>
|
||||||
|
<th>Team</th>
|
||||||
|
<th className="text-center">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{groupedTasks.length === 0 && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={6} className="text-center align-middle" style={{ height: "200px", borderBottom: "none" }}>
|
||||||
|
No reports available
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{groupedTasks.map(({ date, tasks }) => (
|
||||||
|
<React.Fragment key={date}>
|
||||||
|
<tr className="table-row-header text-start">
|
||||||
|
<td colSpan={6}>
|
||||||
|
<strong>{formatUTCToLocalTime(date)}</strong>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{tasks.map((task, idx) => (
|
||||||
|
<tr key={task.id || idx}>
|
||||||
|
<td className="flex-wrap text-start">
|
||||||
|
<div>
|
||||||
|
{task.workItem.activityMaster?.activityName || "No Activity Name"}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm py-2">
|
||||||
|
{task.workItem.workArea?.floor?.building?.name} ›{" "}
|
||||||
|
{task.workItem.workArea?.floor?.floorName} ›{" "}
|
||||||
|
{task.workItem.workArea?.areaName}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{formatNumber(task.workItem.plannedWork)}
|
||||||
|
</td>
|
||||||
|
<td>{`${formatNumber(task.completedTask)} / ${formatNumber(task.plannedTask)}`}</td>
|
||||||
|
<td>{formatUTCToLocalTime(task.assignmentDate)}</td>
|
||||||
|
<td className="text-center">{renderTeamMembers(task, idx)}</td>
|
||||||
|
<td className="text-center">
|
||||||
|
<div className="d-flex justify-content-end gap-2">
|
||||||
|
{ReportTaskRights && !task.reportedDate && (
|
||||||
|
<button className="btn btn-xs btn-primary" onClick={() => openModal("report", task)}>
|
||||||
|
Report
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{ApprovedTaskRights && task.reportedDate && !task.approvedBy && (
|
||||||
|
<button
|
||||||
|
className="btn btn-xs btn-warning"
|
||||||
|
onClick={() => openModal("comments", { task, isActionAllow: true })}
|
||||||
|
>
|
||||||
|
QC
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className="btn btn-xs btn-primary"
|
||||||
|
onClick={() => openModal("comments", { task, isActionAllow: false })}
|
||||||
|
>
|
||||||
|
Comment
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{data?.data?.length > 0 && (
|
||||||
|
<Pagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={data.totalPages}
|
||||||
|
onPageChange={paginate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TaskReportList;
|
||||||
62
src/components/DailyProgressRport/TaskRepprtListSkeleton.jsx
Normal file
62
src/components/DailyProgressRport/TaskRepprtListSkeleton.jsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
|
||||||
|
const SkeletonLine = ({ height = 20, width = "100%", className = "" }) => (
|
||||||
|
<div
|
||||||
|
className={`skeleton mb-2 ${className}`}
|
||||||
|
style={{
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
export const TaskReportListSkeleton = () => {
|
||||||
|
const skeletonRows = 8; // Number of placeholder rows
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Activity</th>
|
||||||
|
<th>Assigned</th>
|
||||||
|
<th>Completed</th>
|
||||||
|
<th>Assign On</th>
|
||||||
|
<th>Team</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{[...Array(skeletonRows)].map((_, idx) => (
|
||||||
|
<tr key={idx}>
|
||||||
|
<td>
|
||||||
|
<SkeletonLine height={16} width="70%" />
|
||||||
|
<SkeletonLine height={12} width="50%" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<SkeletonLine height={16} width="60%" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<SkeletonLine height={16} width="60%" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<SkeletonLine height={16} width="80%" />
|
||||||
|
</td>
|
||||||
|
<td className="text-center">
|
||||||
|
<div className="d-flex justify-content-center gap-1">
|
||||||
|
{[...Array(3)].map((_, i) => (
|
||||||
|
<SkeletonLine key={i} height={24} width={24} className="rounded-circle" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div className="d-flex justify-content-end gap-2">
|
||||||
|
<SkeletonLine height={24} width="60px" />
|
||||||
|
<SkeletonLine height={24} width="60px" />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
15
src/components/DailyProgressRport/TaskRportScheam.jsx
Normal file
15
src/components/DailyProgressRport/TaskRportScheam.jsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const TaskReportFilterSchema = z.object({
|
||||||
|
// buildingIds: z.array(z.string()).optional(),
|
||||||
|
// floorIds: z.array(z.string()).optional(),
|
||||||
|
startDate: z.string().optional(),
|
||||||
|
endDate: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TaskReportDefaultValue = {
|
||||||
|
// buildingIds:[],
|
||||||
|
// floorIds:[],
|
||||||
|
startDate:null,
|
||||||
|
endDate:null
|
||||||
|
}
|
||||||
@ -1,194 +1,194 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
// import React, { useState, useEffect } from "react";
|
||||||
import LineChart from "../Charts/LineChart";
|
// import LineChart from "../Charts/LineChart";
|
||||||
import { useProjects } from "../../hooks/useProjects";
|
// import { useProjects } from "../../hooks/useProjects";
|
||||||
import { useDashboard_ActivityData } from "../../hooks/useDashboard_Data";
|
// import { useDashboard_ActivityData } from "../../hooks/useDashboard_Data";
|
||||||
import ApexChart from "../Charts/Circlechart";
|
// import ApexChart from "../Charts/Circlechart";
|
||||||
|
|
||||||
const LOCAL_STORAGE_PROJECT_KEY = "selectedActivityProjectId";
|
// const LOCAL_STORAGE_PROJECT_KEY = "selectedActivityProjectId";
|
||||||
|
|
||||||
const Activity = () => {
|
// const Activity = () => {
|
||||||
const { projects } = useProjects();
|
// const { projects } = useProjects();
|
||||||
const today = new Date().toISOString().split("T")[0]; // Format: YYYY-MM-DD
|
// const today = new Date().toISOString().split("T")[0]; // Format: YYYY-MM-DD
|
||||||
const [selectedDate, setSelectedDate] = useState(today);
|
// const [selectedDate, setSelectedDate] = useState(today);
|
||||||
const storedProjectId = localStorage.getItem(LOCAL_STORAGE_PROJECT_KEY);
|
// const storedProjectId = localStorage.getItem(LOCAL_STORAGE_PROJECT_KEY);
|
||||||
const initialProjectId = storedProjectId || "all";
|
// const initialProjectId = storedProjectId || "all";
|
||||||
const [selectedProjectId, setSelectedProjectId] = useState(initialProjectId);
|
// const [selectedProjectId, setSelectedProjectId] = useState(initialProjectId);
|
||||||
const [displayedProjectName, setDisplayedProjectName] = useState("Select Project");
|
// const [displayedProjectName, setDisplayedProjectName] = useState("Select Project");
|
||||||
const [activeTab, setActiveTab] = useState("all");
|
// const [activeTab, setActiveTab] = useState("all");
|
||||||
|
|
||||||
const { dashboard_Activitydata: ActivityData, isLoading, error: isError } =
|
// const { dashboard_Activitydata: ActivityData, isLoading, error: isError } =
|
||||||
useDashboard_ActivityData(selectedDate, selectedProjectId);
|
// useDashboard_ActivityData(selectedDate, selectedProjectId);
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
if (selectedProjectId === "all") {
|
// if (selectedProjectId === "all") {
|
||||||
setDisplayedProjectName("All Projects");
|
// setDisplayedProjectName("All Projects");
|
||||||
} else if (projects) {
|
// } else if (projects) {
|
||||||
const foundProject = projects.find((p) => p.id === selectedProjectId);
|
// const foundProject = projects.find((p) => p.id === selectedProjectId);
|
||||||
setDisplayedProjectName(foundProject ? foundProject.name : "Select Project");
|
// setDisplayedProjectName(foundProject ? foundProject.name : "Select Project");
|
||||||
} else {
|
// } else {
|
||||||
setDisplayedProjectName("Select Project");
|
// setDisplayedProjectName("Select Project");
|
||||||
}
|
// }
|
||||||
|
|
||||||
localStorage.setItem(LOCAL_STORAGE_PROJECT_KEY, selectedProjectId);
|
// localStorage.setItem(LOCAL_STORAGE_PROJECT_KEY, selectedProjectId);
|
||||||
}, [selectedProjectId, projects]);
|
// }, [selectedProjectId, projects]);
|
||||||
|
|
||||||
const handleProjectSelect = (projectId) => {
|
// const handleProjectSelect = (projectId) => {
|
||||||
setSelectedProjectId(projectId);
|
// setSelectedProjectId(projectId);
|
||||||
};
|
// };
|
||||||
|
|
||||||
const handleDateChange = (e) => {
|
// const handleDateChange = (e) => {
|
||||||
setSelectedDate(e.target.value);
|
// setSelectedDate(e.target.value);
|
||||||
};
|
// };
|
||||||
|
|
||||||
return (
|
// return (
|
||||||
<div className="card h-100">
|
// <div className="card h-100">
|
||||||
<div className="card-header">
|
// <div className="card-header">
|
||||||
<div className="d-flex flex-wrap justify-content-between align-items-center mb-0">
|
// <div className="d-flex flex-wrap justify-content-between align-items-center mb-0">
|
||||||
<div className="card-title mb-0 text-start">
|
// <div className="card-title mb-0 text-start">
|
||||||
<h5 className="mb-1">Activity</h5>
|
// <h5 className="mb-1">Activity</h5>
|
||||||
<p className="card-subtitle">Activity Progress Chart</p>
|
// <p className="card-subtitle text-primary">Activity Progress Chart</p>
|
||||||
</div>
|
// </div>
|
||||||
|
|
||||||
<div className="btn-group">
|
// <div className="btn-group">
|
||||||
<button
|
// <button
|
||||||
className="btn btn-outline-primary btn-sm dropdown-toggle"
|
// className="btn btn-outline-primary btn-sm dropdown-toggle"
|
||||||
type="button"
|
// type="button"
|
||||||
data-bs-toggle="dropdown"
|
// data-bs-toggle="dropdown"
|
||||||
aria-expanded="false"
|
// aria-expanded="false"
|
||||||
>
|
// >
|
||||||
{displayedProjectName}
|
// {displayedProjectName}
|
||||||
</button>
|
// </button>
|
||||||
<ul className="dropdown-menu">
|
// <ul className="dropdown-menu">
|
||||||
<li>
|
// <li>
|
||||||
<button className="dropdown-item" onClick={() => handleProjectSelect("all")}>
|
// <button className="dropdown-item" onClick={() => handleProjectSelect("all")}>
|
||||||
All Projects
|
// All Projects
|
||||||
</button>
|
// </button>
|
||||||
</li>
|
// </li>
|
||||||
{projects?.map((project) => (
|
// {projects?.map((project) => (
|
||||||
<li key={project.id}>
|
// <li key={project.id}>
|
||||||
<button
|
// <button
|
||||||
className="dropdown-item"
|
// className="dropdown-item"
|
||||||
onClick={() => handleProjectSelect(project.id)}
|
// onClick={() => handleProjectSelect(project.id)}
|
||||||
>
|
// >
|
||||||
{project.name}
|
// {project.name}
|
||||||
</button>
|
// </button>
|
||||||
</li>
|
// </li>
|
||||||
))}
|
// ))}
|
||||||
</ul>
|
// </ul>
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
|
|
||||||
{/* ✅ Date Picker Aligned Left with Padding */}
|
// {/* ✅ Date Picker Aligned Left with Padding */}
|
||||||
<div className="d-flex justify-content-start ps-3 mb-3">
|
// <div className="d-flex justify-content-start ps-3 mb-3">
|
||||||
<div style={{ width: "150px" }}>
|
// <div style={{ width: "150px" }}>
|
||||||
<input
|
// <input
|
||||||
type="date"
|
// type="date"
|
||||||
className="form-control"
|
// className="form-control"
|
||||||
value={selectedDate}
|
// value={selectedDate}
|
||||||
onChange={handleDateChange}
|
// onChange={handleDateChange}
|
||||||
/>
|
// />
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
|
|
||||||
{/* Tabs */}
|
// {/* Tabs */}
|
||||||
<ul className="nav nav-tabs " role="tablist">
|
// <ul className="nav nav-tabs " role="tablist">
|
||||||
<li className="nav-item">
|
// <li className="nav-item">
|
||||||
<button
|
// <button
|
||||||
type="button"
|
// type="button"
|
||||||
className={`nav-link ${activeTab === "all" ? "active" : ""}`}
|
// className={`nav-link ${activeTab === "all" ? "active" : ""}`}
|
||||||
onClick={() => setActiveTab("all")}
|
// onClick={() => setActiveTab("all")}
|
||||||
data-bs-toggle="tab"
|
// data-bs-toggle="tab"
|
||||||
>
|
// >
|
||||||
Summary
|
// Summary
|
||||||
</button>
|
// </button>
|
||||||
</li>
|
// </li>
|
||||||
<li className="nav-item">
|
// <li className="nav-item">
|
||||||
<button
|
// <button
|
||||||
type="button"
|
// type="button"
|
||||||
className={`nav-link ${activeTab === "logs" ? "active" : ""}`}
|
// className={`nav-link ${activeTab === "logs" ? "active" : ""}`}
|
||||||
onClick={() => setActiveTab("logs")}
|
// onClick={() => setActiveTab("logs")}
|
||||||
data-bs-toggle="tab"
|
// data-bs-toggle="tab"
|
||||||
>
|
// >
|
||||||
Details
|
// Details
|
||||||
</button>
|
// </button>
|
||||||
</li>
|
// </li>
|
||||||
</ul>
|
// </ul>
|
||||||
|
|
||||||
<div className="card-body">
|
// <div className="card-body">
|
||||||
{activeTab === "all" && (
|
// {activeTab === "all" && (
|
||||||
<div className="row justify-content-between">
|
// <div className="row justify-content-between">
|
||||||
<div className="col-md-6 d-flex flex-column align-items-center text-center mb-4">
|
// <div className="col-md-6 d-flex flex-column align-items-center text-center mb-4">
|
||||||
{isLoading ? (
|
// {isLoading ? (
|
||||||
<p>Loading activity data...</p>
|
// <p>Loading activity data...</p>
|
||||||
) : isError ? (
|
// ) : isError ? (
|
||||||
<p>No data available.</p>
|
// <p>No data available.</p>
|
||||||
) : (
|
// ) : (
|
||||||
ActivityData && (
|
// ActivityData && (
|
||||||
<>
|
// <>
|
||||||
<h5 className="fw-bold mb-0 text-start w-80">
|
// <h5 className="fw-bold mb-0 text-start w-80">
|
||||||
<i className="bx bx-task text-info"></i> Allocated Task
|
// <i className="bx bx-task text-info"></i> Allocated Task
|
||||||
</h5>
|
// </h5>
|
||||||
<h4 className="mb-0 fw-bold">
|
// <h4 className="mb-0 fw-bold">
|
||||||
{ActivityData.totalCompletedWork?.toLocaleString()}/
|
// {ActivityData.totalCompletedWork?.toLocaleString()}/
|
||||||
{ActivityData.totalPlannedWork?.toLocaleString()}
|
// {ActivityData.totalPlannedWork?.toLocaleString()}
|
||||||
</h4>
|
// </h4>
|
||||||
<small className="text-muted">Completed / Assigned</small>
|
// <small className="text-muted">Completed / Assigned</small>
|
||||||
<div style={{ maxWidth: "180px" }}>
|
// <div style={{ maxWidth: "180px" }}>
|
||||||
<ApexChart />
|
// <ApexChart />
|
||||||
</div>
|
// </div>
|
||||||
</>
|
// </>
|
||||||
)
|
// )
|
||||||
)}
|
// )}
|
||||||
</div>
|
// </div>
|
||||||
|
|
||||||
<div className="col-md-6 d-flex flex-column align-items-center text-center mb-4">
|
// <div className="col-md-6 d-flex flex-column align-items-center text-center mb-4">
|
||||||
{!isLoading && !isError && ActivityData && (
|
// {!isLoading && !isError && ActivityData && (
|
||||||
<>
|
// <>
|
||||||
<h5 className="fw-bold mb-0 text-start w-110">
|
// <h5 className="fw-bold mb-0 text-start w-110">
|
||||||
<i className="bx bx-task text-info"></i> Activities
|
// <i className="bx bx-task text-info"></i> Activities
|
||||||
</h5>
|
// </h5>
|
||||||
<h4 className="mb-0 fw-bold">
|
// <h4 className="mb-0 fw-bold">
|
||||||
{ActivityData.totalCompletedWork?.toLocaleString()}/
|
// {ActivityData.totalCompletedWork?.toLocaleString()}/
|
||||||
{ActivityData.totalPlannedWork?.toLocaleString()}
|
// {ActivityData.totalPlannedWork?.toLocaleString()}
|
||||||
</h4>
|
// </h4>
|
||||||
<small className="text-muted ">Pending / Assigned</small>
|
// <small className="text-muted ">Pending / Assigned</small>
|
||||||
<div style={{ maxWidth: "180px" }}>
|
// <div style={{ maxWidth: "180px" }}>
|
||||||
<ApexChart />
|
// <ApexChart />
|
||||||
</div>
|
// </div>
|
||||||
</>
|
// </>
|
||||||
)}
|
// )}
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
)}
|
// )}
|
||||||
|
|
||||||
{activeTab === "logs" && (
|
// {activeTab === "logs" && (
|
||||||
<div className="table-responsive">
|
// <div className="table-responsive">
|
||||||
<table className="table table-bordered table-hover">
|
// <table className="table table-bordered table-hover">
|
||||||
<thead>
|
// <thead>
|
||||||
<tr>
|
// <tr>
|
||||||
<th>Activity / Location</th>
|
// <th>Activity / Location</th>
|
||||||
<th>Assigned / Completed</th>
|
// <th>Assigned / Completed</th>
|
||||||
</tr>
|
// </tr>
|
||||||
</thead>
|
// </thead>
|
||||||
<tbody>
|
// <tbody>
|
||||||
{[{
|
// {[{
|
||||||
activity: "Code Review / Remote",
|
// activity: "Code Review / Remote",
|
||||||
assignedToday: 3,
|
// assignedToday: 3,
|
||||||
completed: 2
|
// completed: 2
|
||||||
}].map((log, index) => (
|
// }].map((log, index) => (
|
||||||
<tr key={index}>
|
// <tr key={index}>
|
||||||
<td>{log.activity}</td>
|
// <td>{log.activity}</td>
|
||||||
<td>{log.assignedToday} / {log.completed}</td>
|
// <td>{log.assignedToday} / {log.completed}</td>
|
||||||
</tr>
|
// </tr>
|
||||||
))}
|
// ))}
|
||||||
</tbody>
|
// </tbody>
|
||||||
</table>
|
// </table>
|
||||||
</div>
|
// </div>
|
||||||
)}
|
// )}
|
||||||
</div>
|
// </div>
|
||||||
</div>
|
// </div>
|
||||||
);
|
// );
|
||||||
};
|
// };
|
||||||
|
|
||||||
export default Activity;
|
// export default Activity;
|
||||||
|
|||||||
@ -1,21 +1,16 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useMemo } from "react";
|
||||||
import LineChart from "../Charts/LineChart";
|
import ApexChart from "../Charts/Circle";
|
||||||
import { useProjects } from "../../hooks/useProjects";
|
import { useProjects } from "../../hooks/useProjects";
|
||||||
import { useDashboard_AttendanceData } from "../../hooks/useDashboard_Data";
|
import { useDashboard_AttendanceData } from "../../hooks/useDashboard_Data";
|
||||||
import ApexChart from "../Charts/Circle";
|
import { useSelectedProject } from "../../hooks/useSelectedProject"; // ✅ your custom hook
|
||||||
|
|
||||||
const LOCAL_STORAGE_PROJECT_KEY = "selectedAttendanceProjectId";
|
|
||||||
|
|
||||||
const Attendance = () => {
|
const Attendance = () => {
|
||||||
const { projects } = useProjects();
|
const { projects } = useProjects();
|
||||||
const today = new Date().toISOString().split("T")[0]; // Format: YYYY-MM-DD
|
const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
||||||
const [selectedDate, setSelectedDate] = useState(today);
|
const [selectedDate, setSelectedDate] = useState(today);
|
||||||
const storedProjectId = localStorage.getItem(LOCAL_STORAGE_PROJECT_KEY);
|
|
||||||
const initialProjectId = storedProjectId || "all";
|
// central project selection hook
|
||||||
const [selectedProjectId, setSelectedProjectId] = useState(initialProjectId);
|
const selectedProjectId = useSelectedProject()
|
||||||
const [displayedProjectName, setDisplayedProjectName] =
|
|
||||||
useState("Select Project");
|
|
||||||
const [activeTab, setActiveTab] = useState("Summary");
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
dashboard_Attendancedata: AttendanceData,
|
dashboard_Attendancedata: AttendanceData,
|
||||||
@ -23,38 +18,24 @@ const Attendance = () => {
|
|||||||
error: isError,
|
error: isError,
|
||||||
} = useDashboard_AttendanceData(selectedDate, selectedProjectId);
|
} = useDashboard_AttendanceData(selectedDate, selectedProjectId);
|
||||||
|
|
||||||
useEffect(() => {
|
// project name derived once
|
||||||
if (selectedProjectId === "all") {
|
const displayedProjectName = useMemo(() => {
|
||||||
setDisplayedProjectName("All Projects");
|
if (selectedProjectId === "all") return "All Projects";
|
||||||
} else if (projects) {
|
const found = projects?.find((p) => p.id === selectedProjectId);
|
||||||
const foundProject = projects.find((p) => p.id === selectedProjectId);
|
return found?.name || "Select Project";
|
||||||
setDisplayedProjectName(
|
|
||||||
foundProject ? foundProject.name : "Select Project"
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setDisplayedProjectName("Select Project");
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem(LOCAL_STORAGE_PROJECT_KEY, selectedProjectId);
|
|
||||||
}, [selectedProjectId, projects]);
|
}, [selectedProjectId, projects]);
|
||||||
|
|
||||||
const handleProjectSelect = (projectId) => {
|
|
||||||
setSelectedProjectId(projectId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDateChange = (e) => {
|
|
||||||
setSelectedDate(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card h-100">
|
<div className="card h-100">
|
||||||
<div className="card-header mb-1 pb-0 ">
|
{/* Header */}
|
||||||
<div className="d-flex flex-wrap justify-content-between align-items-center mb-0 pb-0 ">
|
<div className="card-header mb-1 pb-0">
|
||||||
|
<div className="d-flex flex-wrap justify-content-between align-items-center">
|
||||||
<div className="card-title mb-0 text-start">
|
<div className="card-title mb-0 text-start">
|
||||||
<h5 className="mb-1">Attendance</h5>
|
<h5 className="mb-1">Attendance</h5>
|
||||||
<p className="card-subtitle">Daily Attendance Data</p>
|
<p className="card-subtitle">Daily Attendance Data</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Project Dropdown */}
|
||||||
<div className="btn-group">
|
<div className="btn-group">
|
||||||
<button
|
<button
|
||||||
className="btn btn-outline-primary btn-sm dropdown-toggle"
|
className="btn btn-outline-primary btn-sm dropdown-toggle"
|
||||||
@ -68,7 +49,7 @@ const Attendance = () => {
|
|||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
className="dropdown-item"
|
className="dropdown-item"
|
||||||
onClick={() => handleProjectSelect("all")}
|
onClick={() => setSelectedProjectId("all")}
|
||||||
>
|
>
|
||||||
All Projects
|
All Projects
|
||||||
</button>
|
</button>
|
||||||
@ -77,7 +58,7 @@ const Attendance = () => {
|
|||||||
<li key={project.id}>
|
<li key={project.id}>
|
||||||
<button
|
<button
|
||||||
className="dropdown-item"
|
className="dropdown-item"
|
||||||
onClick={() => handleProjectSelect(project.id)}
|
onClick={() => setSelectedProjectId(project.id)}
|
||||||
>
|
>
|
||||||
{project.name}
|
{project.name}
|
||||||
</button>
|
</button>
|
||||||
@ -88,18 +69,14 @@ const Attendance = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="d-flex flex-wrap justify-content-between align-items-center mb-0 mt-0 me-5 ms-5">
|
{/* Tabs + Date Picker */}
|
||||||
{/* Tabs */}
|
<div className="d-flex flex-wrap justify-content-between align-items-center me-5 ms-5">
|
||||||
<div>
|
<ul className="nav nav-tabs">
|
||||||
<ul className="nav nav-tabs " role="tablist">
|
|
||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`nav-link ${
|
className={`nav-link ${AttendanceData?.activeTab === "Summary" ? "active" : ""}`}
|
||||||
activeTab === "Summary" ? "active" : ""
|
onClick={() => (AttendanceData.activeTab = "Summary")}
|
||||||
}`}
|
|
||||||
onClick={() => setActiveTab("Summary")}
|
|
||||||
data-bs-toggle="tab"
|
|
||||||
>
|
>
|
||||||
Summary
|
Summary
|
||||||
</button>
|
</button>
|
||||||
@ -107,33 +84,28 @@ const Attendance = () => {
|
|||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`nav-link ${
|
className={`nav-link ${AttendanceData?.activeTab === "Details" ? "active" : ""}`}
|
||||||
activeTab === "Details" ? "active" : ""
|
onClick={() => (AttendanceData.activeTab = "Details")}
|
||||||
}`}
|
|
||||||
onClick={() => setActiveTab("Details")}
|
|
||||||
data-bs-toggle="tab"
|
|
||||||
>
|
>
|
||||||
Details
|
Details
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
<div className="ps-6 mb-3">
|
||||||
{/* ✅ Date Picker Aligned Left with Padding */}
|
|
||||||
<div className="ps-6 mb-3 mt-0">
|
|
||||||
<div style={{ width: "120px" }}>
|
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
className="form-control p-1"
|
className="form-control p-1"
|
||||||
// style={{ fontSize: "1rem" }}
|
style={{ width: "120px" }}
|
||||||
value={selectedDate}
|
value={selectedDate}
|
||||||
onChange={handleDateChange}
|
onChange={(e) => setSelectedDate(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{/* Body */}
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
{activeTab === "Summary" && (
|
{/* Summary */}
|
||||||
|
{AttendanceData?.activeTab === "Summary" && (
|
||||||
<div className="row justify-content-center">
|
<div className="row justify-content-center">
|
||||||
<div className="col-12 col-md-6 d-flex flex-column align-items-center text-center mb-4">
|
<div className="col-12 col-md-6 d-flex flex-column align-items-center text-center mb-4">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@ -143,7 +115,7 @@ const Attendance = () => {
|
|||||||
) : (
|
) : (
|
||||||
AttendanceData && (
|
AttendanceData && (
|
||||||
<>
|
<>
|
||||||
<h5 className="fw-bold mb-0 text-center w-100">
|
<h5 className="fw-bold mb-0">
|
||||||
<i className="bx bx-task text-info"></i> Attendance
|
<i className="bx bx-task text-info"></i> Attendance
|
||||||
</h5>
|
</h5>
|
||||||
<h4 className="mb-0 fw-bold">
|
<h4 className="mb-0 fw-bold">
|
||||||
@ -164,11 +136,9 @@ const Attendance = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === "Details" && (
|
{/* Details */}
|
||||||
<div
|
{AttendanceData?.activeTab === "Details" && (
|
||||||
className="table-responsive"
|
<div className="table-responsive" style={{ maxHeight: "300px" }}>
|
||||||
style={{ maxHeight: "300px", overflowY: "auto" }}
|
|
||||||
>
|
|
||||||
<table className="table table-hover mb-0 text-start">
|
<table className="table table-hover mb-0 text-start">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -178,32 +148,17 @@ const Attendance = () => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{AttendanceData?.attendanceTable &&
|
{AttendanceData?.attendanceTable?.length ? (
|
||||||
AttendanceData.attendanceTable.length > 0 ? (
|
AttendanceData.attendanceTable.map((r, i) => (
|
||||||
AttendanceData.attendanceTable.map((record, index) => (
|
<tr key={i}>
|
||||||
<tr key={index}>
|
<td>{r.firstName} {r.lastName}</td>
|
||||||
<td>
|
<td>{r.inTime ? new Date(r.inTime).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "-"}</td>
|
||||||
{record.firstName} {record.lastName}
|
<td>{r.outTime ? new Date(r.outTime).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : "-"}</td>
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{new Date(record.inTime).toLocaleTimeString([], {
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
})}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{new Date(record.outTime).toLocaleTimeString([], {
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
})}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan="3" className="text-center">
|
<td colSpan="3" className="text-center">No attendance data available</td>
|
||||||
No attendance data available
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@ -132,7 +132,7 @@ const AttendanceOverview = () => {
|
|||||||
onClick={() => setView("table")}
|
onClick={() => setView("table")}
|
||||||
title="Table View"
|
title="Table View"
|
||||||
>
|
>
|
||||||
<i class="bx bx-list-ul fs-5"></i>
|
<i className="bx bx-list-ul fs-5"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,33 +1,28 @@
|
|||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useDashboardProjectsCardData } from "../../hooks/useDashboard_Data";
|
import { useDashboardProjectsCardData } from "../../hooks/useDashboard_Data";
|
||||||
import eventBus from "../../services/eventBus";
|
import eventBus from "../../services/eventBus";
|
||||||
import GlobalRepository from "../../repositories/GlobalRepository";
|
|
||||||
|
|
||||||
const Projects = () => {
|
const Projects = () => {
|
||||||
const { projectsCardData } = useDashboardProjectsCardData();
|
const {
|
||||||
const [projectData, setProjectsData] = useState(projectsCardData);
|
data: projectsCardData,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
|
refetch,
|
||||||
|
} = useDashboardProjectsCardData();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setProjectsData(projectsCardData);
|
// When "project" event happens, just refetch
|
||||||
}, [projectsCardData]);
|
const handler = () => {
|
||||||
|
refetch();
|
||||||
|
};
|
||||||
|
|
||||||
const handler = useCallback(
|
|
||||||
async (msg) => {
|
|
||||||
try {
|
|
||||||
const response =
|
|
||||||
await GlobalRepository.getDashboardProjectsCardData();
|
|
||||||
setProjectsData(response.data);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[GlobalRepository]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
eventBus.on("project", handler);
|
eventBus.on("project", handler);
|
||||||
return () => eventBus.off("project", handler);
|
return () => eventBus.off("project", handler);
|
||||||
}, [handler]);
|
}, [refetch]);
|
||||||
|
|
||||||
|
const totalProjects = projectsCardData?.totalProjects ?? 0;
|
||||||
|
const ongoingProjects = projectsCardData?.ongoingProjects ?? 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card p-3 h-100 text-center d-flex justify-content-between">
|
<div className="card p-3 h-100 text-center d-flex justify-content-between">
|
||||||
@ -37,20 +32,29 @@ const Projects = () => {
|
|||||||
Projects
|
Projects
|
||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="d-flex justify-content-center align-items-center flex-grow-1">
|
||||||
|
<div className="spinner-border text-primary" role="status">
|
||||||
|
<span className="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : isError ? (
|
||||||
|
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center">
|
||||||
|
{error?.message || "Error loading data"}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="d-flex justify-content-around align-items-start mt-n2">
|
<div className="d-flex justify-content-around align-items-start mt-n2">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="mb-0 fw-bold">
|
<h4 className="mb-0 fw-bold">{totalProjects.toLocaleString()}</h4>
|
||||||
{projectData.totalProjects?.toLocaleString()}
|
|
||||||
</h4>
|
|
||||||
<small className="text-muted">Total</small>
|
<small className="text-muted">Total</small>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="mb-0 fw-bold">
|
<h4 className="mb-0 fw-bold">{ongoingProjects.toLocaleString()}</h4>
|
||||||
{projectData.ongoingProjects?.toLocaleString()}
|
|
||||||
</h4>
|
|
||||||
<small className="text-muted">Ongoing</small>
|
<small className="text-muted">Ongoing</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,10 +1,16 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||||
import { useDashboardTasksCardData } from "../../hooks/useDashboard_Data";
|
import { useDashboardTasksCardData } from "../../hooks/useDashboard_Data";
|
||||||
|
|
||||||
const TasksCard = () => {
|
const TasksCard = () => {
|
||||||
const projectId = useSelector((store) => store.localVariables?.projectId);
|
const projectId = useSelectedProject();
|
||||||
const { tasksCardData, loading, error } = useDashboardTasksCardData(projectId);
|
|
||||||
|
const {
|
||||||
|
data: tasksCardData,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
|
} = useDashboardTasksCardData(projectId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card p-3 h-100 text-center d-flex justify-content-between">
|
<div className="card p-3 h-100 text-center d-flex justify-content-between">
|
||||||
@ -14,28 +20,30 @@ const TasksCard = () => {
|
|||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{isLoading ? (
|
||||||
// Loader will be displayed when loading is true
|
// Loader while fetching
|
||||||
<div className="d-flex justify-content-center align-items-center flex-grow-1">
|
<div className="d-flex justify-content-center align-items-center flex-grow-1">
|
||||||
<div className="spinner-border text-primary" role="status">
|
<div className="spinner-border text-primary" role="status">
|
||||||
<span className="visually-hidden">Loading...</span>
|
<span className="visually-hidden">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : error ? (
|
) : isError ? (
|
||||||
// Error message if there's an error
|
// Show error
|
||||||
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center">{error}</div>
|
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center">
|
||||||
|
{error?.message || "Error loading data"}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// Actual data when loaded successfully
|
// Show data
|
||||||
<div className="d-flex justify-content-around align-items-start mt-n2">
|
<div className="d-flex justify-content-around align-items-start mt-n2">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="mb-0 fw-bold">
|
<h4 className="mb-0 fw-bold">
|
||||||
{tasksCardData?.totalTasks?.toLocaleString()}
|
{tasksCardData?.totalTasks?.toLocaleString() ?? 0}
|
||||||
</h4>
|
</h4>
|
||||||
<small className="text-muted">Total</small>
|
<small className="text-muted">Total</small>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="mb-0 fw-bold">
|
<h4 className="mb-0 fw-bold">
|
||||||
{tasksCardData?.completedTasks?.toLocaleString()}
|
{tasksCardData?.completedTasks?.toLocaleString() ?? 0}
|
||||||
</h4>
|
</h4>
|
||||||
<small className="text-muted">Completed</small>
|
<small className="text-muted">Completed</small>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,33 +1,45 @@
|
|||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { useCallback, useEffect } from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { useDashboardTeamsCardData } from "../../hooks/useDashboard_Data";
|
import { useDashboardTeamsCardData } from "../../hooks/useDashboard_Data";
|
||||||
import eventBus from "../../services/eventBus";
|
import eventBus from "../../services/eventBus";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||||
|
|
||||||
const Teams = () => {
|
const Teams = () => {
|
||||||
const projectId = useSelector((store) => store.localVariables?.projectId);
|
const queryClient = useQueryClient();
|
||||||
const { teamsCardData, loading, error } = useDashboardTeamsCardData(projectId);
|
const projectId = useSelectedProject()
|
||||||
|
|
||||||
const [totalEmployees, setTotalEmployee] = useState(0);
|
const {
|
||||||
const [inToday, setInToday] = useState(0);
|
data: teamsCardData,
|
||||||
|
isLoading,
|
||||||
// Update state when API data arrives
|
isError,
|
||||||
useEffect(() => {
|
error,
|
||||||
setTotalEmployee(teamsCardData?.totalEmployees || 0);
|
} = useDashboardTeamsCardData(projectId);
|
||||||
setInToday(teamsCardData?.inToday || 0);
|
|
||||||
}, [teamsCardData]);
|
|
||||||
|
|
||||||
// Handle real-time updates via eventBus
|
// Handle real-time updates via eventBus
|
||||||
const handler = useCallback((msg) => {
|
const handler = useCallback(
|
||||||
|
(msg) => {
|
||||||
if (msg.activity === 1) {
|
if (msg.activity === 1) {
|
||||||
setInToday((prev) => prev + 1);
|
queryClient.setQueryData(["dashboardTeams", projectId], (old) => {
|
||||||
|
if (!old) return old;
|
||||||
|
return {
|
||||||
|
...old,
|
||||||
|
inToday: (old.inToday || 0) + 1,
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
},
|
||||||
|
[queryClient, projectId]
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
eventBus.on("attendance", handler);
|
eventBus.on("attendance", handler);
|
||||||
return () => eventBus.off("attendance", handler);
|
return () => eventBus.off("attendance", handler);
|
||||||
}, [handler]);
|
}, [handler]);
|
||||||
|
|
||||||
|
const inToday = teamsCardData?.inToday ?? 0;
|
||||||
|
const totalEmployees = teamsCardData?.totalEmployees ?? 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card p-3 h-100 text-center d-flex justify-content-between">
|
<div className="card p-3 h-100 text-center d-flex justify-content-between">
|
||||||
<div className="d-flex justify-content-start align-items-center mb-3">
|
<div className="d-flex justify-content-start align-items-center mb-3">
|
||||||
@ -36,18 +48,17 @@ const Teams = () => {
|
|||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{isLoading ? (
|
||||||
// Blue spinner loader
|
|
||||||
<div className="d-flex justify-content-center align-items-center flex-grow-1">
|
<div className="d-flex justify-content-center align-items-center flex-grow-1">
|
||||||
<div className="spinner-border text-primary" role="status">
|
<div className="spinner-border text-primary" role="status">
|
||||||
<span className="visually-hidden">Loading...</span>
|
<span className="visually-hidden">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : error ? (
|
) : isError ? (
|
||||||
// Error message if data fetching fails
|
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center">
|
||||||
<div className="text-danger flex-grow-1 d-flex justify-content-center align-items-center">{error}</div>
|
{error?.message || "Error loading data"}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
// Display data once loaded
|
|
||||||
<div className="d-flex justify-content-around align-items-start mt-n2">
|
<div className="d-flex justify-content-around align-items-start mt-n2">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="mb-0 fw-bold">{totalEmployees.toLocaleString()}</h4>
|
<h4 className="mb-0 fw-bold">{totalEmployees.toLocaleString()}</h4>
|
||||||
|
|||||||
@ -105,7 +105,7 @@ const ListViewContact = ({ data, Pagination }) => {
|
|||||||
<div className="dataTables_wrapper no-footer mx-5 pb-2">
|
<div className="dataTables_wrapper no-footer mx-5 pb-2">
|
||||||
<table className="table dataTable text-nowrap">
|
<table className="table dataTable text-nowrap">
|
||||||
<thead>
|
<thead>
|
||||||
<tr style={{ borderBottom: "2px solid var(--bs-table-border-color)"}}>
|
<tr className="table_header_border">
|
||||||
{contactList?.map((col) => (
|
{contactList?.map((col) => (
|
||||||
<th key={col.key} className={col.align}>
|
<th key={col.key} className={col.align}>
|
||||||
{col.label}
|
{col.label}
|
||||||
|
|||||||
@ -96,7 +96,7 @@ const DocumentFilterPanel = ({ entityTypeId, onApply }) => {
|
|||||||
placeholder="DD-MM-YYYY To DD-MM-YYYY"
|
placeholder="DD-MM-YYYY To DD-MM-YYYY"
|
||||||
startField="startDate"
|
startField="startDate"
|
||||||
endField="endDate"
|
endField="endDate"
|
||||||
defaultRange={true}
|
defaultRange={false}
|
||||||
resetSignal={resetKey}
|
resetSignal={resetKey}
|
||||||
maxDate={new Date()}
|
maxDate={new Date()}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -118,7 +118,7 @@ const Documents = ({ Document_Entity, Entity }) => {
|
|||||||
return (
|
return (
|
||||||
<DocumentContext.Provider value={contextValues}>
|
<DocumentContext.Provider value={contextValues}>
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
<div className="card d-flex p-2">
|
<div className="card page-min-h d-flex p-2">
|
||||||
<div className="row align-items-center">
|
<div className="row align-items-center">
|
||||||
{/* Search */}
|
{/* Search */}
|
||||||
<div className="d-flex col-8 col-md-8 col-lg-4 mb-md-0 align-items-center">
|
<div className="d-flex col-8 col-md-8 col-lg-4 mb-md-0 align-items-center">
|
||||||
@ -149,30 +149,11 @@ const Documents = ({ Document_Entity, Entity }) => {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<div className="col-6 col-md-6 col-lg-8 text-end">
|
<div className="col-6 col-md-6 col-lg-8 text-end">
|
||||||
{/* <span
|
|
||||||
className="text-tiny text-muted p-1 border-0 bg-none lead mx-3 cursor-pointer"
|
|
||||||
disabled={isRefetching}
|
|
||||||
onClick={() => {
|
|
||||||
setSearchText("");
|
|
||||||
setFilter(DocumentFilterDefaultValues);
|
|
||||||
refetchFn && refetchFn();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Refresh
|
|
||||||
<i
|
|
||||||
className={`bx bx-refresh ms-1 ${
|
|
||||||
isRefetching ? "bx-spin" : ""
|
|
||||||
}`}
|
|
||||||
></i>
|
|
||||||
</span> */}
|
|
||||||
|
|
||||||
{(isSelf || canUploadDocument) && (
|
{(isSelf || canUploadDocument) && (
|
||||||
<button
|
<button
|
||||||
|
className="btn btn-sm btn-primary me-3"
|
||||||
type="button"
|
type="button"
|
||||||
title="Add New Document"
|
|
||||||
className="p-1 bg-primary rounded-circle cursor-pointer"
|
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setManageDoc({
|
setManageDoc({
|
||||||
document: null,
|
document: null,
|
||||||
@ -180,7 +161,10 @@ const Documents = ({ Document_Entity, Entity }) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<i className="bx bx-plus fs-4 text-white"></i>
|
<i className="bx bx-plus-circle me-2"></i>
|
||||||
|
<span className="d-none d-md-inline-block">
|
||||||
|
Add New Document
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,172 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
const DemoTable = () => {
|
|
||||||
return (
|
|
||||||
<div className="content-wrapper">
|
|
||||||
<div className="container-fluid">
|
|
||||||
<div className="card">
|
|
||||||
<div className="card-datatable table-responsive">
|
|
||||||
<table className="datatables-basic table border-top">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<th></th>
|
|
||||||
<th>id</th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Email</th>
|
|
||||||
<th>Date</th>
|
|
||||||
<th>Salary</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Action</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="offcanvas offcanvas-end" id="add-new-record">
|
|
||||||
<div className="offcanvas-header border-bottom">
|
|
||||||
<h5 className="offcanvas-title" id="exampleModalLabel">
|
|
||||||
New Record
|
|
||||||
</h5>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn-close text-reset"
|
|
||||||
data-bs-dismiss="offcanvas"
|
|
||||||
aria-label="Close"
|
|
||||||
></button>
|
|
||||||
</div>
|
|
||||||
<div className="offcanvas-body flex-grow-1">
|
|
||||||
<form
|
|
||||||
className="add-new-record pt-0 row g-2"
|
|
||||||
id="form-add-new-record"
|
|
||||||
onsubmit="return false"
|
|
||||||
>
|
|
||||||
<div className="col-sm-12">
|
|
||||||
<label className="form-label" for="basicFullname">
|
|
||||||
Full Name
|
|
||||||
</label>
|
|
||||||
<div className="input-group input-group-merge">
|
|
||||||
<span id="basicFullname2" className="input-group-text">
|
|
||||||
<i className="bx bx-user"></i>
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="basicFullname"
|
|
||||||
className="form-control dt-full-name"
|
|
||||||
name="basicFullname"
|
|
||||||
placeholder="John Doe"
|
|
||||||
aria-label="John Doe"
|
|
||||||
aria-describedby="basicFullname2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-sm-12">
|
|
||||||
<label className="form-label" for="basicPost">
|
|
||||||
Post
|
|
||||||
</label>
|
|
||||||
<div className="input-group input-group-merge">
|
|
||||||
<span id="basicPost2" className="input-group-text">
|
|
||||||
<i className="bx bxs-briefcase"></i>
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="basicPost"
|
|
||||||
name="basicPost"
|
|
||||||
className="form-control dt-post"
|
|
||||||
placeholder="Web Developer"
|
|
||||||
aria-label="Web Developer"
|
|
||||||
aria-describedby="basicPost2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-sm-12">
|
|
||||||
<label className="form-label" for="basicEmail">
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<div className="input-group input-group-merge">
|
|
||||||
<span className="input-group-text">
|
|
||||||
<i className="bx bx-envelope"></i>
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="basicEmail"
|
|
||||||
name="basicEmail"
|
|
||||||
className="form-control dt-email"
|
|
||||||
placeholder="john.doe@example.com"
|
|
||||||
aria-label="john.doe@example.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-text">
|
|
||||||
You can use letters, numbers & periods
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-sm-12">
|
|
||||||
<label className="form-label" for="basicDate">
|
|
||||||
Joining Date
|
|
||||||
</label>
|
|
||||||
<div className="input-group input-group-merge">
|
|
||||||
<span id="basicDate2" className="input-group-text">
|
|
||||||
<i className="bx bx-calendar"></i>
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className="form-control dt-date"
|
|
||||||
id="basicDate"
|
|
||||||
name="basicDate"
|
|
||||||
aria-describedby="basicDate2"
|
|
||||||
placeholder="MM/DD/YYYY"
|
|
||||||
aria-label="MM/DD/YYYY"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-sm-12">
|
|
||||||
<label className="form-label" for="basicSalary">
|
|
||||||
Salary
|
|
||||||
</label>
|
|
||||||
<div className="input-group input-group-merge">
|
|
||||||
<span id="basicSalary2" className="input-group-text">
|
|
||||||
<i className="bx bx-dollar"></i>
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
id="basicSalary"
|
|
||||||
name="basicSalary"
|
|
||||||
className="form-control dt-salary"
|
|
||||||
placeholder="12000"
|
|
||||||
aria-label="12000"
|
|
||||||
aria-describedby="basicSalary2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-sm-12">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="btn btn-primary data-submit me-sm-4 me-1"
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="reset"
|
|
||||||
className="btn btn-outline-secondary"
|
|
||||||
data-bs-dismiss="offcanvas"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr className="my-12" />
|
|
||||||
|
|
||||||
<hr className="my-12" />
|
|
||||||
|
|
||||||
<hr className="my-12" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="content-backdrop fade"></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DemoTable;
|
|
||||||
@ -2,7 +2,6 @@ import React, { useState, useEffect } from "react";
|
|||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import DateRangePicker, { DateRangePicker1 } from "../common/DateRangePicker";
|
import DateRangePicker, { DateRangePicker1 } from "../common/DateRangePicker";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { fetchEmployeeAttendanceData } from "../../slices/apiSlice/employeeAttendanceSlice";
|
|
||||||
import usePagination from "../../hooks/usePagination";
|
import usePagination from "../../hooks/usePagination";
|
||||||
import Avatar from "../common/Avatar";
|
import Avatar from "../common/Avatar";
|
||||||
import { convertShortTime } from "../../utils/dateUtils";
|
import { convertShortTime } from "../../utils/dateUtils";
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
const EmployeeList = () => {
|
|
||||||
return <div>EmployeeList</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EmployeeList;
|
|
||||||
124
src/components/Employee/EmployeeSchema.jsx
Normal file
124
src/components/Employee/EmployeeSchema.jsx
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
|
||||||
|
const mobileNumberRegex = /^[0-9]\d{9}$/;
|
||||||
|
|
||||||
|
export const employeeSchema =
|
||||||
|
z.object({
|
||||||
|
firstName: z.string().min(1, { message: "First Name is required" }),
|
||||||
|
middleName: z.string().optional(),
|
||||||
|
lastName: z.string().min(1, { message: "Last Name is required" }),
|
||||||
|
email: z
|
||||||
|
.string()
|
||||||
|
.max(80, "Email cannot exceed 80 characters")
|
||||||
|
.optional()
|
||||||
|
.refine((val) => !val || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val), {
|
||||||
|
message: "Invalid email format",
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(val) => {
|
||||||
|
if (!val) return true;
|
||||||
|
const [local, domain] = val.split("@");
|
||||||
|
return (
|
||||||
|
val.length <= 320 && local?.length <= 64 && domain?.length <= 255
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Email local or domain part is too long",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
currentAddress: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "Current Address is required" })
|
||||||
|
.max(500, { message: "Address cannot exceed 500 characters" }),
|
||||||
|
birthDate: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "Birth Date is required" })
|
||||||
|
.refine(
|
||||||
|
(date, ctx) => {
|
||||||
|
return new Date(date) <= new Date();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Birth date cannot be in the future",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
joiningDate: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "Joining Date is required" })
|
||||||
|
.refine(
|
||||||
|
(date, ctx) => {
|
||||||
|
return new Date(date) <= new Date();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "Joining date cannot be in the future",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
emergencyPhoneNumber: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "Phone Number is required" })
|
||||||
|
.regex(mobileNumberRegex, { message: "Invalid phone number " }),
|
||||||
|
emergencyContactPerson: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "Emergency Contact Person is required" })
|
||||||
|
.regex(/^[A-Za-z\s]+$/, {
|
||||||
|
message: "Emergency Contact Person must contain only letters",
|
||||||
|
}),
|
||||||
|
aadharNumber: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.refine((val) => !val || /^\d{12}$/.test(val), {
|
||||||
|
message: "Aadhar card must be exactly 12 digits long",
|
||||||
|
}),
|
||||||
|
gender: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "Gender is required" })
|
||||||
|
.refine((val) => val !== "Select Gender", {
|
||||||
|
message: "Please select a gender",
|
||||||
|
}),
|
||||||
|
panNumber: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.refine((val) => !val || /^[A-Z]{5}[0-9]{4}[A-Z]{1}$/.test(val), {
|
||||||
|
message: "Invalid PAN number",
|
||||||
|
}),
|
||||||
|
permanentAddress: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "Permanent Address is required" })
|
||||||
|
.max(500, { message: "Address cannot exceed 500 characters" }),
|
||||||
|
phoneNumber: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "Phone Number is required" })
|
||||||
|
.regex(mobileNumberRegex, { message: "Invalid phone number " }),
|
||||||
|
jobRoleId: z.string().min(1, { message: "Role is required" }),
|
||||||
|
organizationId:z.string().min(1,{message:"Organization is required"}),
|
||||||
|
hasApplicationAccess:z.boolean().default(false),
|
||||||
|
}).refine((data) => {
|
||||||
|
if (data.hasApplicationAccess) {
|
||||||
|
return data.email && data.email.trim() !== "";
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}, {
|
||||||
|
message: "Email is required when employee has access",
|
||||||
|
path: ["email"],
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export const defatEmployeeObj = {
|
||||||
|
firstName: "",
|
||||||
|
middleName: "",
|
||||||
|
lastName: "",
|
||||||
|
email: "",
|
||||||
|
currentAddress: "",
|
||||||
|
birthDate: "",
|
||||||
|
joiningDate: "",
|
||||||
|
emergencyPhoneNumber: "",
|
||||||
|
emergencyContactPerson: "",
|
||||||
|
aadharNumber: "",
|
||||||
|
gender: "",
|
||||||
|
panNumber: "",
|
||||||
|
permanentAddress: "",
|
||||||
|
phoneNumber: "",
|
||||||
|
jobRoleId: null,
|
||||||
|
organizationId:"",
|
||||||
|
hasApplicationAccess:false
|
||||||
|
}
|
||||||
@ -1,36 +1,37 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import showToast from "../../services/toastService";
|
|
||||||
import EmployeeRepository from "../../repositories/EmployeeRepository";
|
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
|
||||||
import useMaster from "../../hooks/masterHook/useMaster";
|
import useMaster from "../../hooks/masterHook/useMaster";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
import { changeMaster } from "../../slices/localVariablesSlice";
|
import { changeMaster } from "../../slices/localVariablesSlice";
|
||||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||||
import { formatDate } from "../../utils/dateUtils";
|
import { formatDate } from "../../utils/dateUtils";
|
||||||
import { useEmployeeProfile, useUpdateEmployee } from "../../hooks/useEmployees";
|
|
||||||
import {
|
import {
|
||||||
cacheData,
|
useEmployeeProfile,
|
||||||
clearCacheKey,
|
useUpdateEmployee,
|
||||||
getCachedData,
|
} from "../../hooks/useEmployees";
|
||||||
} from "../../slices/apiDataManager";
|
|
||||||
import { clearApiCacheKey } from "../../slices/apiCacheSlice";
|
|
||||||
import { useMutation } from "@tanstack/react-query";
|
|
||||||
import Label from "../common/Label";
|
import Label from "../common/Label";
|
||||||
import DatePicker from "../common/DatePicker";
|
import DatePicker from "../common/DatePicker";
|
||||||
|
import { defatEmployeeObj, employeeSchema } from "./EmployeeSchema";
|
||||||
const mobileNumberRegex = /^[0-9]\d{9}$/;
|
import { useOrganizationsList } from "../../hooks/useOrganization";
|
||||||
|
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
||||||
|
|
||||||
const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { mutate: updateEmployee, isPending } = useUpdateEmployee();
|
const { mutate: updateEmployee, isPending } = useUpdateEmployee();
|
||||||
|
const {
|
||||||
|
data: organzationList,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
error: EempError,
|
||||||
|
} = useOrganizationsList(ITEMS_PER_PAGE, 1, true);
|
||||||
const {
|
const {
|
||||||
employee,
|
employee,
|
||||||
error,
|
error,
|
||||||
loading: empLoading,
|
loading: empLoading,
|
||||||
refetch
|
refetch,
|
||||||
} = useEmployeeProfile(employeeId);
|
} = useEmployeeProfile(employeeId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -38,6 +39,7 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
|||||||
}, [employeeId]);
|
}, [employeeId]);
|
||||||
|
|
||||||
const [disabledEmail, setDisabledEmail] = useState(false);
|
const [disabledEmail, setDisabledEmail] = useState(false);
|
||||||
|
|
||||||
const { data: job_role, loading } = useMaster();
|
const { data: job_role, loading } = useMaster();
|
||||||
const [isloading, setLoading] = useState(false);
|
const [isloading, setLoading] = useState(false);
|
||||||
const navigation = useNavigate();
|
const navigation = useNavigate();
|
||||||
@ -45,98 +47,9 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
|||||||
const [currentAddressLength, setCurrentAddressLength] = useState(0);
|
const [currentAddressLength, setCurrentAddressLength] = useState(0);
|
||||||
const [permanentAddressLength, setPermanentAddressLength] = useState(0);
|
const [permanentAddressLength, setPermanentAddressLength] = useState(0);
|
||||||
|
|
||||||
const userSchema = z.object({
|
|
||||||
...(employeeId ? { id: z.string().optional() } : {}),
|
|
||||||
firstName: z.string().min(1, { message: "First Name is required" }),
|
|
||||||
middleName: z.string().optional(),
|
|
||||||
lastName: z.string().min(1, { message: "Last Name is required" }),
|
|
||||||
email: z
|
|
||||||
.string()
|
|
||||||
.max(80, "Email cannot exceed 80 characters")
|
|
||||||
.optional()
|
|
||||||
.refine((val) => !val || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val), {
|
|
||||||
message: "Invalid email format",
|
|
||||||
})
|
|
||||||
.refine(
|
|
||||||
(val) => {
|
|
||||||
if (!val) return true;
|
|
||||||
const [local, domain] = val.split("@");
|
|
||||||
return (
|
|
||||||
val.length <= 320 && local?.length <= 64 && domain?.length <= 255
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "Email local or domain part is too long",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
currentAddress: z
|
|
||||||
.string()
|
|
||||||
.min(1, { message: "Current Address is required" })
|
|
||||||
.max(500, { message: "Address cannot exceed 500 characters" }),
|
|
||||||
birthDate: z
|
|
||||||
.string()
|
|
||||||
.min(1, { message: "Birth Date is required" })
|
|
||||||
.refine(
|
|
||||||
(date, ctx) => {
|
|
||||||
return new Date(date) <= new Date();
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "Birth date cannot be in the future",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
joiningDate: z
|
|
||||||
.string()
|
|
||||||
.min(1, { message: "Joining Date is required" })
|
|
||||||
.refine(
|
|
||||||
(date, ctx) => {
|
|
||||||
return new Date(date) <= new Date();
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "Joining date cannot be in the future",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
emergencyPhoneNumber: z
|
|
||||||
.string()
|
|
||||||
.min(1, { message: "Phone Number is required" })
|
|
||||||
.regex(mobileNumberRegex, { message: "Invalid phone number " }),
|
|
||||||
emergencyContactPerson: z
|
|
||||||
.string()
|
|
||||||
.min(1, { message: "Emergency Contact Person is required" })
|
|
||||||
.regex(/^[A-Za-z\s]+$/, {
|
|
||||||
message: "Emergency Contact Person must contain only letters",
|
|
||||||
}),
|
|
||||||
aadharNumber: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.refine((val) => !val || /^\d{12}$/.test(val), {
|
|
||||||
message: "Aadhar card must be exactly 12 digits long",
|
|
||||||
}),
|
|
||||||
gender: z
|
|
||||||
.string()
|
|
||||||
.min(1, { message: "Gender is required" })
|
|
||||||
.refine((val) => val !== "Select Gender", {
|
|
||||||
message: "Please select a gender",
|
|
||||||
}),
|
|
||||||
panNumber: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.refine((val) => !val || /^[A-Z]{5}[0-9]{4}[A-Z]{1}$/.test(val), {
|
|
||||||
message: "Invalid PAN number",
|
|
||||||
}),
|
|
||||||
permanentAddress: z
|
|
||||||
.string()
|
|
||||||
.min(1, { message: "Permanent Address is required" })
|
|
||||||
.max(500, { message: "Address cannot exceed 500 characters" }),
|
|
||||||
phoneNumber: z
|
|
||||||
.string()
|
|
||||||
.min(1, { message: "Phone Number is required" })
|
|
||||||
.regex(mobileNumberRegex, { message: "Invalid phone number " }),
|
|
||||||
jobRoleId: z.string().min(1, { message: "Role is required" }),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refetch()
|
refetch();
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@ -147,25 +60,8 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
|||||||
reset,
|
reset,
|
||||||
getValues,
|
getValues,
|
||||||
} = useForm({
|
} = useForm({
|
||||||
resolver: zodResolver(userSchema),
|
resolver: zodResolver(employeeSchema),
|
||||||
defaultValues: {
|
defaultValues: defatEmployeeObj,
|
||||||
id: currentEmployee?.id || null,
|
|
||||||
firstName: currentEmployee?.firstName || "",
|
|
||||||
middleName: currentEmployee?.middleName || "",
|
|
||||||
lastName: currentEmployee?.lastName || "",
|
|
||||||
email: currentEmployee?.email || "",
|
|
||||||
currentAddress: currentEmployee?.currentAddress || "",
|
|
||||||
birthDate: formatDate(currentEmployee?.birthDate) || "",
|
|
||||||
joiningDate: formatDate(currentEmployee?.joiningDate) || "",
|
|
||||||
emergencyPhoneNumber: currentEmployee?.emergencyPhoneNumber || "",
|
|
||||||
emergencyContactPerson: currentEmployee?.emergencyContactPerson || "",
|
|
||||||
aadharNumber: currentEmployee?.aadharNumber || "",
|
|
||||||
gender: currentEmployee?.gender || "",
|
|
||||||
panNumber: currentEmployee?.panNumber || "",
|
|
||||||
permanentAddress: currentEmployee?.permanentAddress || "",
|
|
||||||
phoneNumber: currentEmployee?.phoneNumber || "",
|
|
||||||
jobRoleId: currentEmployee?.jobRoleId.toString() || null,
|
|
||||||
},
|
|
||||||
mode: "onChange",
|
mode: "onChange",
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -176,7 +72,13 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
|||||||
data.email = null;
|
data.email = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateEmployee({ ...data, IsAllEmployee }, {
|
const payload = { ...data, IsAllEmployee };
|
||||||
|
|
||||||
|
if (employeeId) {
|
||||||
|
payload.id = employeeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateEmployee(payload, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
reset();
|
reset();
|
||||||
onClosed();
|
onClosed();
|
||||||
@ -184,7 +86,6 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loading && !error && employee) {
|
if (!loading && !error && employee) {
|
||||||
setCurrentEmployee(employee);
|
setCurrentEmployee(employee);
|
||||||
@ -212,6 +113,8 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
|||||||
permanentAddress: currentEmployee.permanentAddress || "",
|
permanentAddress: currentEmployee.permanentAddress || "",
|
||||||
phoneNumber: currentEmployee.phoneNumber || "",
|
phoneNumber: currentEmployee.phoneNumber || "",
|
||||||
jobRoleId: currentEmployee.jobRoleId?.toString() || "",
|
jobRoleId: currentEmployee.jobRoleId?.toString() || "",
|
||||||
|
organizationId: currentEmployee.organizationId || "",
|
||||||
|
hasApplicationAccess: currentEmployee.hasApplicationAccess || false,
|
||||||
}
|
}
|
||||||
: {}
|
: {}
|
||||||
);
|
);
|
||||||
@ -219,13 +122,21 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
|||||||
setPermanentAddressLength(currentEmployee?.permanentAddress?.length || 0);
|
setPermanentAddressLength(currentEmployee?.permanentAddress?.length || 0);
|
||||||
}, [currentEmployee, reset]);
|
}, [currentEmployee, reset]);
|
||||||
|
|
||||||
|
const hasAccessAplication = watch("hasApplicationAccess");
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="p-sm-0 p-2">
|
<form onSubmit={handleSubmit(onSubmit)} className="p-sm-0 p-2">
|
||||||
<div className="text-center"><p className="fs-5 fw-semibold"> {employee ? "Update Employee" : "Create Employee"}</p> </div>
|
<div className="text-center">
|
||||||
|
<p className="fs-5 fw-semibold">
|
||||||
|
{" "}
|
||||||
|
{employee ? "Update Employee" : "Create Employee"}
|
||||||
|
</p>{" "}
|
||||||
|
</div>
|
||||||
<div className="row mb-3">
|
<div className="row mb-3">
|
||||||
<div className="col-sm-4">
|
<div className="col-sm-4">
|
||||||
<Label className="form-text text-start" required>First Name</Label>
|
<Label className="form-text text-start" required>
|
||||||
|
First Name
|
||||||
|
</Label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="firstName"
|
name="firstName"
|
||||||
@ -244,7 +155,10 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{errors.firstName && (
|
{errors.firstName && (
|
||||||
<div className="danger-text text-start" style={{ fontSize: "12px" }}>
|
<div
|
||||||
|
className="danger-text text-start"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
|
>
|
||||||
{errors.firstName.message}
|
{errors.firstName.message}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -267,14 +181,18 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{errors.middleName && (
|
{errors.middleName && (
|
||||||
<div className="danger-text text-start " style={{ fontSize: "12px" }}>
|
<div
|
||||||
|
className="danger-text text-start "
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
|
>
|
||||||
{errors.middleName.message}
|
{errors.middleName.message}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-sm-4">
|
<div className="col-sm-4">
|
||||||
<Label className="form-text text-start" required>Last Name</Label>
|
<Label className="form-text text-start" required>
|
||||||
|
Last Name
|
||||||
|
</Label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
{...register("lastName", {
|
{...register("lastName", {
|
||||||
@ -291,16 +209,24 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{errors.lastName && (
|
{errors.lastName && (
|
||||||
<div className="danger-text text-start" style={{ fontSize: "12px" }}>
|
<div
|
||||||
|
className="danger-text text-start"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
|
>
|
||||||
{errors.lastName.message}
|
{errors.lastName.message}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className="row mb-3">
|
<div className="row mb-3">
|
||||||
<div className="col-sm-6">
|
<div className="col-sm-6">
|
||||||
<div className="form-text text-start">Email</div>
|
<Label
|
||||||
|
htmlFor="email"
|
||||||
|
className="text-start form-text"
|
||||||
|
required={hasAccessAplication}
|
||||||
|
>
|
||||||
|
Email
|
||||||
|
</Label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
id="email"
|
id="email"
|
||||||
@ -321,7 +247,9 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-sm-6">
|
<div className="col-sm-6">
|
||||||
<Label className="form-text text-start" required>Phone Number</Label>
|
<Label className="form-text text-start" required>
|
||||||
|
Phone Number
|
||||||
|
</Label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
keyboardType="numeric"
|
keyboardType="numeric"
|
||||||
@ -345,7 +273,9 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
|||||||
<div className="row mb-3"></div>
|
<div className="row mb-3"></div>
|
||||||
<div className="row mb-3">
|
<div className="row mb-3">
|
||||||
<div className="col-sm-4">
|
<div className="col-sm-4">
|
||||||
<Label className="form-text text-start" required>Gender</Label>
|
<Label className="form-text text-start" required>
|
||||||
|
Gender
|
||||||
|
</Label>
|
||||||
|
|
||||||
<div className="input-group">
|
<div className="input-group">
|
||||||
<select
|
<select
|
||||||
@ -387,7 +317,10 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{errors.birthDate && (
|
{errors.birthDate && (
|
||||||
<div className="danger-text text-start" style={{ fontSize: "12px" }}>
|
<div
|
||||||
|
className="danger-text text-start"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
|
>
|
||||||
{errors.birthDate.message}
|
{errors.birthDate.message}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -408,7 +341,10 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{errors.joiningDate && (
|
{errors.joiningDate && (
|
||||||
<div className="danger-text text-start" style={{ fontSize: "12px" }}>
|
<div
|
||||||
|
className="danger-text text-start"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
|
>
|
||||||
{errors.joiningDate.message}
|
{errors.joiningDate.message}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -416,7 +352,9 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="row mb-3">
|
<div className="row mb-3">
|
||||||
<div className="col-sm-6">
|
<div className="col-sm-6">
|
||||||
<Label className="form-text text-start" required>Current Address</Label>
|
<Label className="form-text text-start" required>
|
||||||
|
Current Address
|
||||||
|
</Label>
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
id="currentAddress"
|
id="currentAddress"
|
||||||
@ -428,15 +366,11 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
|||||||
maxLength={500}
|
maxLength={500}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setCurrentAddressLength(e.target.value.length);
|
setCurrentAddressLength(e.target.value.length);
|
||||||
// let react-hook-form still handle it
|
|
||||||
register("currentAddress").onChange(e);
|
register("currentAddress").onChange(e);
|
||||||
}}
|
}}
|
||||||
></textarea>
|
></textarea>
|
||||||
<div className="text-end muted">
|
<div className="text-end muted">
|
||||||
<small>
|
<small> {500 - currentAddressLength} characters left</small>
|
||||||
{" "}
|
|
||||||
{500 - currentAddressLength} characters left
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
{errors.currentAddress && (
|
{errors.currentAddress && (
|
||||||
<div
|
<div
|
||||||
@ -466,9 +400,7 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
|||||||
}}
|
}}
|
||||||
></textarea>
|
></textarea>
|
||||||
<div className="text-end muted">
|
<div className="text-end muted">
|
||||||
<small>
|
<small>{500 - permanentAddressLength} characters left</small>
|
||||||
{500 - permanentAddressLength} characters left
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
{errors.permanentAddress && (
|
{errors.permanentAddress && (
|
||||||
<div
|
<div
|
||||||
@ -480,6 +412,55 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* -------------- */}
|
||||||
|
<div className="row mb-3">
|
||||||
|
<div className="col-sm-6">
|
||||||
|
<Label className="form-text text-start" required>
|
||||||
|
Organization
|
||||||
|
</Label>
|
||||||
|
<div className="input-group">
|
||||||
|
<select
|
||||||
|
className="form-select form-select-sm"
|
||||||
|
{...register("organizationId")}
|
||||||
|
id="organizationId"
|
||||||
|
aria-label=""
|
||||||
|
>
|
||||||
|
<option disabled value="">
|
||||||
|
Select Role
|
||||||
|
</option>
|
||||||
|
{organzationList?.data
|
||||||
|
.sort((a, b) => a?.name?.localeCompare(b?.name))
|
||||||
|
.map((item) => (
|
||||||
|
<option value={item?.id} key={item?.id}>
|
||||||
|
{item?.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{errors.organizationId && (
|
||||||
|
<div
|
||||||
|
className="danger-text text-start justify-content-center"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
|
>
|
||||||
|
{errors.organizationId.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-sm-6 d-flex align-items-center mt-2">
|
||||||
|
<label className="form-check-label d-flex align-items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-check-input me-2"
|
||||||
|
{...register("hasApplicationAccess")}
|
||||||
|
/>
|
||||||
|
Has Application Access ?
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* --------------- */}
|
||||||
<div className="row mb-3">
|
<div className="row mb-3">
|
||||||
{" "}
|
{" "}
|
||||||
<div className="divider">
|
<div className="divider">
|
||||||
@ -488,7 +469,9 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="row mb-3">
|
<div className="row mb-3">
|
||||||
<div className="col-sm-4">
|
<div className="col-sm-4">
|
||||||
<Label className="form-text text-start" required>Official Designation</Label>
|
<Label className="form-text text-start" required>
|
||||||
|
Official Designation
|
||||||
|
</Label>
|
||||||
<div className="input-group">
|
<div className="input-group">
|
||||||
<select
|
<select
|
||||||
className="form-select form-select-sm"
|
className="form-select form-select-sm"
|
||||||
@ -601,15 +584,6 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{employeeId && (
|
|
||||||
<div className="row mb-3 d-none">
|
|
||||||
<div className="col-sm-12">
|
|
||||||
<input type="text" name="id" {...register("id")} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="row text-end">
|
<div className="row text-end">
|
||||||
<div className="col-sm-12">
|
<div className="col-sm-12">
|
||||||
<button
|
<button
|
||||||
@ -626,18 +600,11 @@ const ManageEmployee = ({ employeeId, onClosed, IsAllEmployee }) => {
|
|||||||
className="btn btn-sm btn-primary"
|
className="btn btn-sm btn-primary"
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
>
|
>
|
||||||
{isPending
|
{isPending ? "Please Wait..." : employeeId ? "Update" : "Create"}
|
||||||
? "Please Wait..."
|
|
||||||
: employeeId
|
|
||||||
? "Update"
|
|
||||||
: "Create"}
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState,useMemo } from "react";
|
import React, { useEffect, useState, useMemo } from "react";
|
||||||
import { FormProvider, useForm, Controller } from "react-hook-form";
|
import { FormProvider, useForm, Controller } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { defaultFilter, SearchSchema } from "./ExpenseSchema";
|
import { defaultFilter, SearchSchema } from "./ExpenseSchema";
|
||||||
@ -16,8 +16,11 @@ import { ExpenseFilterSkeleton } from "./ExpenseSkeleton";
|
|||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
||||||
const selectedProjectId = useSelector((store) => store.localVariables.projectId);
|
const selectedProjectId = useSelector(
|
||||||
const { data, isLoading,isError,error,isFetching , isFetched} = useExpenseFilter();
|
(store) => store.localVariables.projectId
|
||||||
|
);
|
||||||
|
const { data, isLoading, isError, error, isFetching, isFetched } =
|
||||||
|
useExpenseFilter();
|
||||||
|
|
||||||
const groupByList = useMemo(() => {
|
const groupByList = useMemo(() => {
|
||||||
return [
|
return [
|
||||||
@ -27,11 +30,10 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
|||||||
{ id: "project", name: "Project" },
|
{ id: "project", name: "Project" },
|
||||||
{ id: "paymentMode", name: "Payment Mode" },
|
{ id: "paymentMode", name: "Payment Mode" },
|
||||||
{ id: "expensesType", name: "Expense Type" },
|
{ id: "expensesType", name: "Expense Type" },
|
||||||
{ id: "createdAt", name: "Submitted Date" }
|
{ id: "createdAt", name: "Submitted Date" },
|
||||||
].sort((a, b) => a.name.localeCompare(b.name));
|
].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
const [selectedGroup, setSelectedGroup] = useState(groupByList[0]);
|
const [selectedGroup, setSelectedGroup] = useState(groupByList[0]);
|
||||||
const [resetKey, setResetKey] = useState(0);
|
const [resetKey, setResetKey] = useState(0);
|
||||||
|
|
||||||
@ -40,7 +42,7 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
|||||||
defaultValues: defaultFilter,
|
defaultValues: defaultFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { control, register, handleSubmit, reset, watch } = methods;
|
const { control, handleSubmit, reset, setValue, watch } = methods;
|
||||||
const isTransactionDate = watch("isTransactionDate");
|
const isTransactionDate = watch("isTransactionDate");
|
||||||
|
|
||||||
const closePanel = () => {
|
const closePanel = () => {
|
||||||
@ -78,28 +80,37 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
|||||||
}, [location]);
|
}, [location]);
|
||||||
|
|
||||||
if (isLoading || isFetching) return <ExpenseFilterSkeleton />;
|
if (isLoading || isFetching) return <ExpenseFilterSkeleton />;
|
||||||
if(isError && isFetched) return <div>Something went wrong Here- {error.message} </div>
|
if (isError && isFetched)
|
||||||
|
return <div>Something went wrong Here- {error.message} </div>;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
|
<form onSubmit={handleSubmit(onSubmit)} className="p-2 text-start">
|
||||||
<div className="mb-3 w-100">
|
<div className="mb-3 w-100">
|
||||||
<div className="d-flex align-items-center mb-2">
|
<div className="d-flex align-items-center mb-2">
|
||||||
<label className="form-label me-2">Choose Date:</label>
|
<label className="form-label me-2">Filter By:</label>
|
||||||
<div className="form-check form-switch m-0">
|
<div className="d-inline-flex border rounded-pill mb-1 overflow-hidden shadow-none">
|
||||||
<input
|
<button
|
||||||
className="form-check-input"
|
type="button"
|
||||||
type="checkbox"
|
className={`btn px-2 py-1 rounded-0 text-tiny ${
|
||||||
id="switchOption1"
|
isTransactionDate ? "active btn-primary text-white" : ""
|
||||||
{...register("isTransactionDate")}
|
}`}
|
||||||
/>
|
onClick={() => setValue("isTransactionDate", true)}
|
||||||
|
>
|
||||||
|
Transaction Date
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`btn px-2 py-1 rounded-0 text-tiny ${
|
||||||
|
!isTransactionDate ? "active btn-primary text-white" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => setValue("isTransactionDate", false)}
|
||||||
|
>
|
||||||
|
Submitted Date
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<label className="form-label mb-0 ms-2">
|
|
||||||
{isTransactionDate ? "Submitted": "Transaction" }
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
<label className="fw-semibold">Choose Date Range:</label>
|
||||||
<DateRangePicker1
|
<DateRangePicker1
|
||||||
placeholder="DD-MM-YYYY To DD-MM-YYYY"
|
placeholder="DD-MM-YYYY To DD-MM-YYYY"
|
||||||
startField="startDate"
|
startField="startDate"
|
||||||
@ -169,7 +180,9 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-2 text-start ">
|
<div className="mb-2 text-start ">
|
||||||
<label htmlFor="groupBySelect" className="form-label">Group By :</label>
|
<label htmlFor="groupBySelect" className="form-label">
|
||||||
|
Group By :
|
||||||
|
</label>
|
||||||
<select
|
<select
|
||||||
id="groupBySelect"
|
id="groupBySelect"
|
||||||
className="form-select form-select-sm"
|
className="form-select form-select-sm"
|
||||||
@ -187,18 +200,17 @@ const ExpenseFilterPanel = ({ onApply, handleGroupBy }) => {
|
|||||||
<div className="d-flex justify-content-end py-3 gap-2">
|
<div className="d-flex justify-content-end py-3 gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-label-secondary btn-xs"
|
className="btn btn-label-secondary btn-sm"
|
||||||
onClick={onClear}
|
onClick={onClear}
|
||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" className="btn btn-primary btn-xs">
|
<button type="submit" className="btn btn-primary btn-sm">
|
||||||
Apply
|
Apply
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import { useProjectName } from "../../hooks/useProjects";
|
|||||||
import eventBus from "../../services/eventBus";
|
import eventBus from "../../services/eventBus";
|
||||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||||
import { MANAGE_PROJECT } from "../../utils/constants";
|
import { MANAGE_PROJECT } from "../../utils/constants";
|
||||||
|
import { useAuthModal, useLogout } from "../../hooks/useAuth";
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const { profile } = useProfile();
|
const { profile } = useProfile();
|
||||||
@ -26,10 +27,9 @@ const Header = () => {
|
|||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { data, loading } = useMaster();
|
const { data, loading } = useMaster();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const {onOpen} = useAuthModal()
|
||||||
const HasManageProjectPermission = useHasUserPermission(MANAGE_PROJECT);
|
const HasManageProjectPermission = useHasUserPermission(MANAGE_PROJECT);
|
||||||
// {
|
const { mutate : logout,isPending:logouting} = useLogout()
|
||||||
// console.log(location.pathname);
|
|
||||||
// }
|
|
||||||
|
|
||||||
const isDashboardPath =
|
const isDashboardPath =
|
||||||
/^\/dashboard$/.test(location.pathname) || /^\/$/.test(location.pathname);
|
/^\/dashboard$/.test(location.pathname) || /^\/$/.test(location.pathname);
|
||||||
@ -59,41 +59,9 @@ const Header = () => {
|
|||||||
return role ? role.name : "User";
|
return role ? role.name : "User";
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogout = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
logout();
|
|
||||||
};
|
|
||||||
|
|
||||||
const logout = async () => {
|
|
||||||
try {
|
|
||||||
let data = {
|
|
||||||
refreshToken: localStorage.getItem("refreshToken"),
|
|
||||||
};
|
|
||||||
|
|
||||||
AuthRepository.logout(data)
|
|
||||||
.then(() => {
|
|
||||||
localStorage.removeItem("jwtToken");
|
|
||||||
localStorage.removeItem("refreshToken");
|
|
||||||
localStorage.removeItem("user");
|
|
||||||
localStorage.clear();
|
|
||||||
clearAllCache();
|
|
||||||
window.location.href = "/auth/login";
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
localStorage.removeItem("jwtToken");
|
|
||||||
localStorage.removeItem("refreshToken");
|
|
||||||
localStorage.removeItem("user");
|
|
||||||
localStorage.clear();
|
|
||||||
clearAllCache();
|
|
||||||
window.location.href = "/auth/login";
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
"Error during logout:",
|
|
||||||
error?.response?.data || error.message
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleProfilePage = () => {
|
const handleProfilePage = () => {
|
||||||
navigate(`/employee/${profile?.employeeInfo?.id}`);
|
navigate(`/employee/${profile?.employeeInfo?.id}`);
|
||||||
@ -426,6 +394,15 @@ const Header = () => {
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<div className="dropdown-divider"></div>
|
<div className="dropdown-divider"></div>
|
||||||
|
</li>
|
||||||
|
<li onClick={()=>onOpen()}>
|
||||||
|
{" "}
|
||||||
|
<a
|
||||||
|
className="dropdown-item cusor-pointer"
|
||||||
|
>
|
||||||
|
<i className="bx bx-transfer-alt me-2"></i>
|
||||||
|
<span className="align-middle">Switch Workspace</span>
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li onClick={handleProfilePage}>
|
<li onClick={handleProfilePage}>
|
||||||
<a
|
<a
|
||||||
@ -455,6 +432,8 @@ const Header = () => {
|
|||||||
<span className="align-middle">Change Password</span>
|
<span className="align-middle">Change Password</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<div className="dropdown-divider"></div>
|
<div className="dropdown-divider"></div>
|
||||||
</li>
|
</li>
|
||||||
@ -462,11 +441,10 @@ const Header = () => {
|
|||||||
<a
|
<a
|
||||||
aria-label="click to log out"
|
aria-label="click to log out"
|
||||||
className="dropdown-item cusor-pointer"
|
className="dropdown-item cusor-pointer"
|
||||||
href="/logout"
|
onClick={()=>logout()}
|
||||||
onClick={handleLogout}
|
|
||||||
>
|
>
|
||||||
<i className="bx bx-power-off me-2"></i>
|
{logouting ? "Please Wait":<> <i className="bx bx-log-out me-2"></i>
|
||||||
<span className="align-middle">Log Out</span>
|
<span className="align-middle">SignOut</span></>}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
268
src/components/Organization/AssignOrg.jsx
Normal file
268
src/components/Organization/AssignOrg.jsx
Normal file
@ -0,0 +1,268 @@
|
|||||||
|
import React, { useMemo, useState } from "react";
|
||||||
|
import { useProjectAssignedServices } from "../../hooks/useProjects";
|
||||||
|
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||||
|
import {
|
||||||
|
useOrganizationType,
|
||||||
|
useServices,
|
||||||
|
} from "../../hooks/masterHook/useMaster";
|
||||||
|
import Label from "../common/Label";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { assignedOrgToProject } from "./OrganizationSchema";
|
||||||
|
import {
|
||||||
|
useAssignOrgToProject,
|
||||||
|
useAssignOrgToTenant,
|
||||||
|
useOrganizationModal,
|
||||||
|
} from "../../hooks/useOrganization";
|
||||||
|
|
||||||
|
const AssignOrg = ({ setStep }) => {
|
||||||
|
const { isOpen, orgData, startStep, onOpen, flowType, prevStep, onClose } =
|
||||||
|
useOrganizationModal();
|
||||||
|
const selectedProject = useSelectedProject();
|
||||||
|
const { data: masterService, isLoading: isMasterserviceLoading } =
|
||||||
|
useServices();
|
||||||
|
const { data: projectServices, isLoading } =
|
||||||
|
useProjectAssignedServices(selectedProject);
|
||||||
|
const { data: orgType, isLoading: orgLoading } = useOrganizationType();
|
||||||
|
|
||||||
|
const { mutate: AssignToProject, isPending: isPendingProject } =
|
||||||
|
useAssignOrgToProject(() => onClose());
|
||||||
|
const { mutate: AssignToTenant, isPending: isPendingTenat } =
|
||||||
|
useAssignOrgToTenant(() => {
|
||||||
|
onClose();
|
||||||
|
});
|
||||||
|
const isPending = isPendingProject || isPendingTenat;
|
||||||
|
const mergedServices = useMemo(() => {
|
||||||
|
if (!masterService || !projectServices) return [];
|
||||||
|
const combined = [...masterService?.data, ...projectServices];
|
||||||
|
return combined.filter(
|
||||||
|
(item, index, self) => index === self.findIndex((s) => s.id === item.id)
|
||||||
|
);
|
||||||
|
}, [masterService, projectServices]);
|
||||||
|
|
||||||
|
const resolver =
|
||||||
|
flowType === "default" ? undefined : zodResolver(assignedOrgToProject);
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm({
|
||||||
|
resolver,
|
||||||
|
defaultValues: {
|
||||||
|
organizationTypeId: "",
|
||||||
|
serviceIds: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = (formData) => {
|
||||||
|
if (flowType === "default") {
|
||||||
|
const payload = orgData.id;
|
||||||
|
|
||||||
|
AssignToTenant(payload);
|
||||||
|
} else {
|
||||||
|
const payload = {
|
||||||
|
...formData,
|
||||||
|
projectId: selectedProject,
|
||||||
|
organizationId: orgData.id,
|
||||||
|
parentOrganizationId: null,
|
||||||
|
};
|
||||||
|
AssignToProject(payload);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = () => {
|
||||||
|
onOpen({ startStep: 4, orgData });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
if (prevStep === 1 && flowType === "assign") {
|
||||||
|
onOpen({ startStep: prevStep });
|
||||||
|
} else if (prevStep === 1 && flowType !== "assign") {
|
||||||
|
onOpen({ startStep: 1 });
|
||||||
|
} else {
|
||||||
|
onOpen({ startStep: 2 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isMasterserviceLoading || isLoading)
|
||||||
|
return <div className="text-center">Loading....</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="row text-black text-start mb-3">
|
||||||
|
{/* Organization Info Display */}
|
||||||
|
<div className="col-12 mb-3">
|
||||||
|
<div className="d-flex justify-content-between align-items-center text-start mb-1">
|
||||||
|
<div className="d-flex flex-row gap-2 align-items-center text-wrap">
|
||||||
|
<img
|
||||||
|
src="/public/assets/img/orgLogo.png"
|
||||||
|
alt="logo"
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
/> <p className="fw-semibold fs-6 m-0">{orgData.name}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleEdit}
|
||||||
|
className="btn btn-link p-0"
|
||||||
|
>
|
||||||
|
<i className="bx bx-edit text-secondary"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="d-flex text-secondary mb-2"> <i className="bx bx-sm bx-info-circle me-1" /> Organization Info</div>
|
||||||
|
{/* Contact Info */}
|
||||||
|
<div className="col-md-6 mb-3">
|
||||||
|
<div className="d-flex">
|
||||||
|
<label
|
||||||
|
className="form-label me-2 mb-0 fw-semibold"
|
||||||
|
style={{ minWidth: "130px" }}
|
||||||
|
>
|
||||||
|
<i className="bx bx-sm bx-user me-1" /> Contact Person :
|
||||||
|
</label>
|
||||||
|
<div className="text-muted">{orgData.contactPerson}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6 mb-3">
|
||||||
|
<div className="d-flex">
|
||||||
|
<label
|
||||||
|
className="form-label me-2 mb-0 fw-semibold"
|
||||||
|
style={{ minWidth: "130px" }}
|
||||||
|
>
|
||||||
|
<i className='bx bx-sm me-1 bx-phone'></i> Contact Number :
|
||||||
|
</label>
|
||||||
|
<div className="text-muted">{orgData.contactNumber}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6 mb-3">
|
||||||
|
<div className="d-flex">
|
||||||
|
<label
|
||||||
|
className="form-label me-2 mb-0 fw-semibold"
|
||||||
|
style={{ minWidth: "130px" }}
|
||||||
|
>
|
||||||
|
<i className='bx bx-sm me-1 bx-envelope'></i> Email Address :
|
||||||
|
</label>
|
||||||
|
<div className="text-muted">{orgData.email}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 mb-3">
|
||||||
|
<div className="d-flex">
|
||||||
|
<label
|
||||||
|
className="form-label me-2 mb-0 fw-semibold"
|
||||||
|
style={{ maxWidth: "130px" }}
|
||||||
|
>
|
||||||
|
<i className="bx bx-sm me-1 bx-barcode"></i>
|
||||||
|
Service Provider Id (SPRID) :
|
||||||
|
</label>
|
||||||
|
<div className="text-muted">{orgData.sprid}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 mb-3">
|
||||||
|
<div className="d-flex">
|
||||||
|
<label
|
||||||
|
className="form-label me-1 mb-0 fw-semibold"
|
||||||
|
style={{ minWidth: "130px" }}
|
||||||
|
>
|
||||||
|
<i className='bx bx-sm me-1 bx-map'></i> Address :
|
||||||
|
</label>
|
||||||
|
<div className="text-muted text-start">{orgData.address}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<div className="text-black text-start">
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
{/* Show fields only if flowType is NOT default */}
|
||||||
|
{flowType !== "default" && (
|
||||||
|
<>
|
||||||
|
{/* Organization Type */}
|
||||||
|
<div className="mb-3 text-start">
|
||||||
|
<Label htmlFor="organizationTypeId" className="mb-3 fw-semibold" required>
|
||||||
|
Organization Type
|
||||||
|
</Label>
|
||||||
|
<div className="d-flex flex-wrap gap-3 mt-1">
|
||||||
|
{orgType?.data.map((type) => (
|
||||||
|
<div
|
||||||
|
key={type.id}
|
||||||
|
className="form-check d-flex align-items-center gap-2 p-0 m-0"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id={`organizationType-${type.id}`}
|
||||||
|
value={type.id}
|
||||||
|
{...register("organizationTypeId")}
|
||||||
|
className="form-check-input m-0"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
className="form-check-label m-0"
|
||||||
|
htmlFor={`organizationType-${type.id}`}
|
||||||
|
>
|
||||||
|
{type.name}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{errors.organizationTypeId && (
|
||||||
|
<span className="text-danger">
|
||||||
|
{errors.organizationTypeId.message}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Services */}
|
||||||
|
<div className="mb-3">
|
||||||
|
<Label htmlFor="serviceIds" className="mb-3 fw-semibold" required>
|
||||||
|
Select Services
|
||||||
|
</Label>
|
||||||
|
{mergedServices?.map((service) => (
|
||||||
|
<div key={service.id} className="form-check mb-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
value={service.id}
|
||||||
|
{...register("serviceIds")}
|
||||||
|
className="form-check-input"
|
||||||
|
/>
|
||||||
|
<label className="form-check-label">{service.name}</label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{errors.serviceIds && (
|
||||||
|
<div className="text-danger small">
|
||||||
|
{errors.serviceIds.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Buttons: Always visible */}
|
||||||
|
<div className="d-flex justify-content-between mt-5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm btn-outline-secondary"
|
||||||
|
onClick={handleBack}
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
<i className="bx bx-chevron-left"></i>Back
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-sm btn-primary"
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
{isPending
|
||||||
|
? "Please wait..."
|
||||||
|
: flowType === "default"
|
||||||
|
? "Assign to Organization"
|
||||||
|
: "Assign to Project"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AssignOrg;
|
||||||
212
src/components/Organization/ManagOrg.jsx
Normal file
212
src/components/Organization/ManagOrg.jsx
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
|
import {
|
||||||
|
useCreateOrganization,
|
||||||
|
useOrganization,
|
||||||
|
useOrganizationModal,
|
||||||
|
useUpdateOrganization,
|
||||||
|
} from "../../hooks/useOrganization";
|
||||||
|
import {
|
||||||
|
defaultOrganizationValues,
|
||||||
|
organizationSchema,
|
||||||
|
} from "./OrganizationSchema";
|
||||||
|
import Label from "../common/Label";
|
||||||
|
import { useGlobalServices } from "../../hooks/masterHook/useMaster";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import SelectMultiple from "../common/SelectMultiple";
|
||||||
|
|
||||||
|
const ManagOrg = () => {
|
||||||
|
const { data: service, isLoading } = useGlobalServices();
|
||||||
|
const { flowType, orgData, startStep, onOpen, onClose, prevStep } =
|
||||||
|
useOrganizationModal();
|
||||||
|
const {
|
||||||
|
data: organization,
|
||||||
|
isLoading: organizationLoading,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
|
} = useOrganization(orgData?.id);
|
||||||
|
|
||||||
|
const method = useForm({
|
||||||
|
resolver: zodResolver(organizationSchema),
|
||||||
|
defaultValues: defaultOrganizationValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
register,
|
||||||
|
reset,
|
||||||
|
formState: { errors },
|
||||||
|
} = method;
|
||||||
|
|
||||||
|
// Create & Update mutations
|
||||||
|
const { mutate: createOrganization, isPending: isCreating } =
|
||||||
|
useCreateOrganization(() => {
|
||||||
|
reset(defaultOrganizationValues);
|
||||||
|
onOpen({ startStep: 1 });
|
||||||
|
onClose();
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: updateOrganization, isPending: isUpdating } =
|
||||||
|
useUpdateOrganization(() => {
|
||||||
|
reset(defaultOrganizationValues);
|
||||||
|
onOpen({ startStep: 1 });
|
||||||
|
onClose();
|
||||||
|
});
|
||||||
|
// Prefill form if editing
|
||||||
|
useEffect(() => {
|
||||||
|
if (organization) {
|
||||||
|
reset({
|
||||||
|
name: organization.name || "",
|
||||||
|
contactPerson: organization.contactPerson || "",
|
||||||
|
contactNumber: organization.contactNumber || "",
|
||||||
|
email: organization.email || "",
|
||||||
|
serviceIds: organization.services?.map((s) => s.id) || [],
|
||||||
|
address: organization.address || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [organization, reset, service?.data]);
|
||||||
|
|
||||||
|
const onSubmit = (formData) => {
|
||||||
|
let payload = { ...formData };
|
||||||
|
if (organization?.id) {
|
||||||
|
updateOrganization({
|
||||||
|
orgId: organization.id,
|
||||||
|
payload: { ...payload, id: organization.id },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
createOrganization(payload);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleBack = () => {
|
||||||
|
if (flowType === "edit") {
|
||||||
|
onClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flowType === "assign") {
|
||||||
|
if (prevStep === 1) {
|
||||||
|
onOpen({ startStep: 1 });
|
||||||
|
} else {
|
||||||
|
onOpen({ startStep: prevStep ?? 2 });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onOpen({ startStep: 2 });
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<FormProvider {...method}>
|
||||||
|
<form className="form" onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="mb-1 text-start">
|
||||||
|
<Label htmlFor="name" required>
|
||||||
|
Organization Name
|
||||||
|
</Label>
|
||||||
|
<input
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
{...register("name")}
|
||||||
|
/>
|
||||||
|
{errors.name && (
|
||||||
|
<span className="danger-text">{errors.name.message}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-1 text-start">
|
||||||
|
<Label htmlFor="contactPerson" required>
|
||||||
|
Contact Person
|
||||||
|
</Label>
|
||||||
|
<input
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
{...register("contactPerson")}
|
||||||
|
/>
|
||||||
|
{errors.contactPerson && (
|
||||||
|
<span className="danger-text">{errors.contactPerson.message}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-1 text-start">
|
||||||
|
<Label htmlFor="contactNumber" required>
|
||||||
|
Contact Number
|
||||||
|
</Label>
|
||||||
|
<input
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
{...register("contactNumber")}
|
||||||
|
/>
|
||||||
|
{errors.contactNumber && (
|
||||||
|
<span className="danger-text">{errors.contactNumber.message}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-1 text-start">
|
||||||
|
<Label htmlFor="email" required>
|
||||||
|
Email Address
|
||||||
|
</Label>
|
||||||
|
<input
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
{...register("email")}
|
||||||
|
/>
|
||||||
|
{errors.email && (
|
||||||
|
<span className="danger-text">{errors.email.message}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-1 text-start">
|
||||||
|
<SelectMultiple
|
||||||
|
name="serviceIds"
|
||||||
|
label="Select Service"
|
||||||
|
options={service?.data}
|
||||||
|
labelKey="name"
|
||||||
|
valueKey="id"
|
||||||
|
IsLoading={isLoading}
|
||||||
|
/>
|
||||||
|
{errors.serviceIds && (
|
||||||
|
<span className="danger-text">{errors.serviceIds.message}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-1 text-start">
|
||||||
|
<Label htmlFor="address" required>
|
||||||
|
Address
|
||||||
|
</Label>
|
||||||
|
<textarea
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
{...register("address")}
|
||||||
|
rows={2}
|
||||||
|
/>
|
||||||
|
{errors.address && (
|
||||||
|
<span className="danger-text">{errors.address.message}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="d-flex justify-content-between gap-2 my-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm btn-outline-secondary"
|
||||||
|
onClick={handleBack}
|
||||||
|
>
|
||||||
|
{flowType === "edit" ? (
|
||||||
|
"Close"
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<i className="bx bx-chevron-left"></i>Back
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-sm btn-primary"
|
||||||
|
disabled={isCreating || isUpdating || isLoading}
|
||||||
|
>
|
||||||
|
{isCreating || isUpdating
|
||||||
|
? "Please Wait..."
|
||||||
|
: orgData
|
||||||
|
? "Update"
|
||||||
|
: "Submit"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ManagOrg;
|
||||||
24
src/components/Organization/OrgPicker.css
Normal file
24
src/components/Organization/OrgPicker.css
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
/* Default: hide scrollbar */
|
||||||
|
.scrollable-tbody {
|
||||||
|
max-height: 350px;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
|
.scrollable-tbody::-webkit-scrollbar {
|
||||||
|
width: 0; /* Chrome, Safari */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* On hover: show scrollbar */
|
||||||
|
.scrollable-tbody:hover {
|
||||||
|
scrollbar-width: thin; /* Firefox */
|
||||||
|
}
|
||||||
|
.scrollable-tbody:hover::-webkit-scrollbar {
|
||||||
|
width: 6px; /* Adjust width */
|
||||||
|
}
|
||||||
|
.scrollable-tbody:hover::-webkit-scrollbar-thumb {
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.scrollable-tbody:hover::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
152
src/components/Organization/OrgPickerFromSPId.jsx
Normal file
152
src/components/Organization/OrgPickerFromSPId.jsx
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
useAssignOrgToTenant,
|
||||||
|
useOrganizationBySPRID,
|
||||||
|
useOrganizationModal,
|
||||||
|
} from "../../hooks/useOrganization";
|
||||||
|
import Label from "../common/Label";
|
||||||
|
import { useDebounce } from "../../utils/appUtils";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { spridSchema } from "./OrganizationSchema";
|
||||||
|
import { OrgCardSkeleton } from "./OrganizationSkeleton";
|
||||||
|
|
||||||
|
// Zod schema: only allow exactly 4 digits
|
||||||
|
|
||||||
|
const OrgPickerFromSPId = ({ title, placeholder }) => {
|
||||||
|
const { onClose, startStep, flowType, onOpen, prevStep } =
|
||||||
|
useOrganizationModal();
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
watch,
|
||||||
|
} = useForm({
|
||||||
|
resolver: zodResolver(spridSchema),
|
||||||
|
defaultValues: { spridSearchText: "" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const [SPRID, setSPRID] = useState("");
|
||||||
|
|
||||||
|
const { data, isLoading, isError, error, refetch } =
|
||||||
|
useOrganizationBySPRID(SPRID);
|
||||||
|
|
||||||
|
const onSubmit = (formdata) => {
|
||||||
|
setSPRID(formdata.spridSearchText);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOrg = (orgId) => {};
|
||||||
|
const SP = watch("spridSearchText");
|
||||||
|
return (
|
||||||
|
<div className="d-block">
|
||||||
|
<form
|
||||||
|
className="d-flex flex-row gap-6 text-start align-items-center"
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<div className="d-flex flex-row align-items-center gap-2">
|
||||||
|
<Label className="text-secondary">Search by SPRID</Label>
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
{...register("spridSearchText")}
|
||||||
|
className="form-control form-control-sm w-auto"
|
||||||
|
placeholder="Enter SPRID"
|
||||||
|
maxLength={4}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" className="btn btn-sm btn-primary">
|
||||||
|
<i className="bx bx-sm bx-search-alt-2"></i> Search
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div className="text-start danger-text">
|
||||||
|
{" "}
|
||||||
|
{errors.spridSearchText && (
|
||||||
|
<p className="text-danger small mt-1">
|
||||||
|
{errors.spridSearchText.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ---- Organization list ---- */}
|
||||||
|
{isLoading ? (
|
||||||
|
<OrgCardSkeleton />
|
||||||
|
) : data && data?.data.length > 0 ? (
|
||||||
|
<div className="py-2 text-tiny text-center">
|
||||||
|
<div className="d-flex flex-column gap-2 border-0 bg-none">
|
||||||
|
{data.data.map((org) => (
|
||||||
|
<div className="d-flex flex-row gap-2 text-start text-black ">
|
||||||
|
<div className="mt-1">
|
||||||
|
<img
|
||||||
|
src="/public/assets/img/orgLogo.png"
|
||||||
|
alt="logo"
|
||||||
|
width={50}
|
||||||
|
height={50}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="d-flex flex-column p-0 m-0 cursor-pointer">
|
||||||
|
<span className="fs-6 fw-semibold">{org.name}</span>
|
||||||
|
<div className="d-flex gap-2">
|
||||||
|
<small
|
||||||
|
className=" fw-semibold text-uppercase"
|
||||||
|
style={{ letterSpacing: "1px" }}
|
||||||
|
>
|
||||||
|
SPRID :{" "}
|
||||||
|
</small>
|
||||||
|
<small className="fs-6">{org.sprid}</small>
|
||||||
|
</div>
|
||||||
|
<div className="d-flex flex-row gap-2">
|
||||||
|
<small className="text-small fw-semibold">Address:</small>
|
||||||
|
<div className="d-flex text-wrap">{org.address}</div>
|
||||||
|
</div>
|
||||||
|
<div className="m-0 p-0">
|
||||||
|
{" "}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-sm btn-primary"
|
||||||
|
onClick={() => onOpen({ startStep: 3, orgData: org })}
|
||||||
|
>
|
||||||
|
Select
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : SPRID ? (
|
||||||
|
<div className="py-3 text-center text-secondary">
|
||||||
|
No organization found for "{SPRID}"
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="py-12 text-center text-tiny text-black">
|
||||||
|
<small className="d-block text-secondary">
|
||||||
|
Do not have SPRID or could not find organization ?
|
||||||
|
</small>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm btn-primary mt-3"
|
||||||
|
onClick={() => onOpen({ startStep: 4 })}
|
||||||
|
>
|
||||||
|
<i className="bx bx-plus-circle me-2"></i>
|
||||||
|
Create New Organization
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ---- Footer buttons ---- */}
|
||||||
|
<div className={`d-flex text-secondary mt-3`}>
|
||||||
|
{flowType !== "default" && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-xs btn-outline-secondary"
|
||||||
|
onClick={() => onOpen({ startStep: prevStep })}
|
||||||
|
>
|
||||||
|
<i className="bx bx-chevron-left"></i> Back
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrgPickerFromSPId;
|
||||||
182
src/components/Organization/OrgPickerfromTenant.jsx
Normal file
182
src/components/Organization/OrgPickerfromTenant.jsx
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
useOrganizationModal,
|
||||||
|
useOrganizationsList,
|
||||||
|
} from "../../hooks/useOrganization";
|
||||||
|
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
||||||
|
import Label from "../common/Label";
|
||||||
|
import Pagination from "../common/Pagination";
|
||||||
|
import "./OrgPicker.css"
|
||||||
|
|
||||||
|
const OrgPickerfromTenant = ({ title }) => {
|
||||||
|
const [searchText, setSearchText] = useState("");
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const { data, isLoading } = useOrganizationsList(
|
||||||
|
ITEMS_PER_PAGE - 10,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
searchText
|
||||||
|
);
|
||||||
|
|
||||||
|
const paginate = (page) => {
|
||||||
|
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
|
||||||
|
setCurrentPage(page);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const { isOpen, orgData, startStep, onOpen, flowType, prevStep } =
|
||||||
|
useOrganizationModal();
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
if (prevStep == 1 && flowType == "assign") {
|
||||||
|
onOpen({ startStep: prevStep });
|
||||||
|
} else if (prevStep == 1 && flowType == "assign") {
|
||||||
|
onOpen({ startStep: 1 });
|
||||||
|
} else {
|
||||||
|
onOpen({ startStep: 2 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const contactList = [
|
||||||
|
{
|
||||||
|
key: "name",
|
||||||
|
label: "Name",
|
||||||
|
getValue: (org) => (
|
||||||
|
<div className="d-flex gap-2 py-1 ">
|
||||||
|
<i class="bx bx-buildings"></i>
|
||||||
|
<span
|
||||||
|
className="text-truncate d-inline-block "
|
||||||
|
style={{ maxWidth: "150px" }}
|
||||||
|
>
|
||||||
|
{org?.name || "N/A"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
align: "text-start",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "sprid",
|
||||||
|
label: "SPRID",
|
||||||
|
getValue: (org) => (
|
||||||
|
<span
|
||||||
|
className="text-truncate d-inline-block"
|
||||||
|
style={{ maxWidth: "200px" }}
|
||||||
|
>
|
||||||
|
{org?.sprid || "N/A"}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
align: "text-center",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="d-block">
|
||||||
|
<div className="d-flex align-items-center gap-2 mb-1">
|
||||||
|
<Label className="mb-0">{title}</Label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => setSearchText?.(e.target.value)}
|
||||||
|
className="form-control form-control-sm w-auto"
|
||||||
|
placeholder="Enter Organization Name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{/* ---- Organization list ---- */}
|
||||||
|
{isLoading ? (
|
||||||
|
<div>Loading....</div>
|
||||||
|
) : data && data?.data?.length > 0 ? (
|
||||||
|
<div className="dataTables_wrapper no-footer pb-5">
|
||||||
|
<table className="table dataTable text-nowrap">
|
||||||
|
<thead>
|
||||||
|
<tr className="table_header_border">
|
||||||
|
{contactList.map((col) => (
|
||||||
|
<th key={col.key} className={col.align}>
|
||||||
|
{col.label}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
<th className="sticky-action-column bg-white text-center">
|
||||||
|
Action
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="scrollable-tbody overflow-y-auto"
|
||||||
|
style={{ maxHeight: "350px" }}
|
||||||
|
>
|
||||||
|
<table className="table dataTable text-nowrap mb-0">
|
||||||
|
<tbody>
|
||||||
|
{Array.isArray(data.data) && data.data.length > 0
|
||||||
|
? data.data.map((row, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
{contactList.map((col) => (
|
||||||
|
<td key={col.key} className={col.align}>
|
||||||
|
{col.getValue(row)}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
<td className="sticky-action-column p-0 bg-white">
|
||||||
|
<div className="p-1">
|
||||||
|
<span
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-sm"
|
||||||
|
onClick={() =>
|
||||||
|
onOpen({ startStep: 3, orgData: row })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<i class='bx bx-right-arrow-circle text-primary'></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="d-flex flex-column align-items-center text-center text-wrap text-black gap-2">
|
||||||
|
<small className="mb-1">
|
||||||
|
Could not find organization in your database? Please search within the
|
||||||
|
global database.
|
||||||
|
</small>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm btn-primary w-auto"
|
||||||
|
onClick={() => onOpen({ startStep: 2 })}
|
||||||
|
>
|
||||||
|
Search Using SPRID
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* ---- Footer buttons ---- */}
|
||||||
|
<div
|
||||||
|
className={`d-flex justify-content-end
|
||||||
|
text-secondary mt-3`}
|
||||||
|
>
|
||||||
|
{flowType == "default" && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm btn-outline-secondary"
|
||||||
|
onClick={handleBack}
|
||||||
|
>
|
||||||
|
<i className="bx bx-left-arrow-alt"></i> Back
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* <button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm btn-secondary"
|
||||||
|
onClick={() => onOpen({ startStep: 4 })}
|
||||||
|
>
|
||||||
|
<i className="bx bx-plus-circle me-2"></i>
|
||||||
|
Add New Organization
|
||||||
|
</button> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrgPickerfromTenant;
|
||||||
117
src/components/Organization/OrganizationModal.jsx
Normal file
117
src/components/Organization/OrganizationModal.jsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import React, { useMemo, useState } from "react";
|
||||||
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
|
import {
|
||||||
|
defaultOrganizationValues,
|
||||||
|
organizationSchema,
|
||||||
|
} from "./OrganizationSchema";
|
||||||
|
import Modal from "../common/Modal";
|
||||||
|
import {
|
||||||
|
useCreateOrganization,
|
||||||
|
useOrganizationBySPRID,
|
||||||
|
useOrganizationModal,
|
||||||
|
useOrganizationsList,
|
||||||
|
} from "../../hooks/useOrganization";
|
||||||
|
import Label from "../common/Label";
|
||||||
|
import SelectMultiple from "../common/SelectMultiple";
|
||||||
|
import { useServices } from "../../hooks/masterHook/useMaster";
|
||||||
|
import AssignOrg from "./AssignOrg";
|
||||||
|
import ManagOrg from "./ManagOrg";
|
||||||
|
import OrgPickerFromSPId from "./OrgPickerFromSPId";
|
||||||
|
import OrgPickerfromTenant from "./OrgPickerfromTenant";
|
||||||
|
import ViewOrganization from "./ViewOrganization";
|
||||||
|
|
||||||
|
const OrganizationModal = () => {
|
||||||
|
const { isOpen, orgData, startStep, onOpen, onClose, onToggle } =
|
||||||
|
useOrganizationModal();
|
||||||
|
const { data: masterService, isLoading } = useServices();
|
||||||
|
const [searchText, setSearchText] = useState();
|
||||||
|
const [SPRID, setSPRID] = useState("");
|
||||||
|
|
||||||
|
const [Organization, setOrganization] = useState({});
|
||||||
|
|
||||||
|
const method = useForm({
|
||||||
|
resolver: zodResolver(organizationSchema),
|
||||||
|
defaultValues: defaultOrganizationValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
register,
|
||||||
|
reset,
|
||||||
|
formState: { errors },
|
||||||
|
} = method;
|
||||||
|
|
||||||
|
const { mutate: CreateOrganization, isPending } = useCreateOrganization(
|
||||||
|
() => {
|
||||||
|
reset(defaultOrganizationValues);
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSubmit = (OrgPayload) => {
|
||||||
|
CreateOrganization(OrgPayload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RenderTitle = useMemo(() => {
|
||||||
|
if (orgData && startStep === 3 ) {
|
||||||
|
return "Assign Organization";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startStep === 1) {
|
||||||
|
return orgData && orgData !== null
|
||||||
|
? "Add Organization"
|
||||||
|
: "Choose Organization";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startStep === 2) {
|
||||||
|
return "Add Organization";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startStep === 3) {
|
||||||
|
return "Assign Organization";
|
||||||
|
}
|
||||||
|
if(startStep === 5){
|
||||||
|
return "Organization Details"
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${orgData ? "Update":"Create"} Organization`;
|
||||||
|
}, [startStep, orgData]);
|
||||||
|
|
||||||
|
const contentBody = (
|
||||||
|
<div>
|
||||||
|
{/* ---------- STEP 1: Service Provider- Form Own Tenant list ---------- */}
|
||||||
|
{startStep === 1 && <OrgPickerfromTenant title="Find Organization" />}
|
||||||
|
|
||||||
|
{startStep === 2 && (
|
||||||
|
<OrgPickerFromSPId
|
||||||
|
title="Find Organization"
|
||||||
|
placeholder="Enter Service Provider Id"
|
||||||
|
projectOrganizations={orgData}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ---------- STEP 2: Existing Organization Details ---------- */}
|
||||||
|
{startStep === 3 && Organization && (
|
||||||
|
<AssignOrg Organization={Organization} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ---------- STEP 3: Add New Organization ---------- */}
|
||||||
|
{startStep === 4 && <ManagOrg />}
|
||||||
|
|
||||||
|
{/* ---------- STEP 3: View Organization ---------- */}
|
||||||
|
{startStep === 5 && <ViewOrganization orgId={orgData}/>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
title={RenderTitle}
|
||||||
|
body={contentBody}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrganizationModal;
|
||||||
57
src/components/Organization/OrganizationSchema.js
Normal file
57
src/components/Organization/OrganizationSchema.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { array, z } from "zod";
|
||||||
|
|
||||||
|
const phoneRegex = /^\+?[1-9]\d{6,14}$/;
|
||||||
|
|
||||||
|
export const organizationSchema = z.object({
|
||||||
|
name: z.string().min(1, { message: "Name is required" }),
|
||||||
|
contactNumber: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(7, { message: "Contact number must be at least 7 digits" })
|
||||||
|
.max(20, { message: "Contact number cannot exceed 12 digits" })
|
||||||
|
|
||||||
|
.regex(phoneRegex, { message: "Invalid phone number" }),
|
||||||
|
contactPerson: z.string().min(1, { message: "Person name required" }),
|
||||||
|
address: z.string().min(1, { message: "Address is required!" }),
|
||||||
|
email: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "Email is required" })
|
||||||
|
.email("Invalid email address"),
|
||||||
|
serviceIds: z
|
||||||
|
.array(z.string())
|
||||||
|
.min(1, { message: "Service isrequired" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const defaultOrganizationValues = {
|
||||||
|
name: "",
|
||||||
|
contactNumber: "",
|
||||||
|
contactPerson: "",
|
||||||
|
address: "",
|
||||||
|
email: "",
|
||||||
|
serviceIds: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const assignedOrgToProject = z.object({
|
||||||
|
// projectId: z.string().uuid({ message: "Invalid projectId format" }),
|
||||||
|
// organizationId: z.string().string({ message: "Invalid organizationId format" }),
|
||||||
|
// parentOrganizationId: z
|
||||||
|
// .string()
|
||||||
|
// .nullable()
|
||||||
|
// .optional(),
|
||||||
|
serviceIds: z.preprocess(
|
||||||
|
(val) => (Array.isArray(val) ? val : []),
|
||||||
|
z
|
||||||
|
.array(z.string().uuid({ message: "Invalid serviceId format" }))
|
||||||
|
.nonempty({ message: "At least one service must be selected" })
|
||||||
|
),
|
||||||
|
|
||||||
|
organizationTypeId: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "Organization is required" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const spridSchema = z.object({
|
||||||
|
spridSearchText: z
|
||||||
|
.string()
|
||||||
|
.regex(/^\d{4}$/, { message: "SPRID must be exactly 4 digits" }),
|
||||||
|
});
|
||||||
123
src/components/Organization/OrganizationSkeleton.jsx
Normal file
123
src/components/Organization/OrganizationSkeleton.jsx
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
const SkeletonLine = ({ height = 20, width = "100%", className = "" }) => (
|
||||||
|
<div
|
||||||
|
className={`skeleton mb-2 ${className}`}
|
||||||
|
style={{
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
);
|
||||||
|
export const OrgCardSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<div className="row p-3">
|
||||||
|
{[...Array(1)].map((_, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className="list-group-item d-flex flex-row gap-2 align-items-start border-0 shadow-sm rounded mb-3 p-3"
|
||||||
|
>
|
||||||
|
{/* Left: Logo/avatar placeholder */}
|
||||||
|
<div className="mt-1">
|
||||||
|
<SkeletonLine height={50} width={50} className="rounded-circle" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right: Info section */}
|
||||||
|
<div className="d-flex flex-column flex-grow-1 text-start">
|
||||||
|
{/* Org name */}
|
||||||
|
<SkeletonLine height={18} width="160px" className="mb-2" />
|
||||||
|
|
||||||
|
{/* SPRID */}
|
||||||
|
<div className="d-flex gap-2 mb-2">
|
||||||
|
<SkeletonLine height={14} width="60px" />
|
||||||
|
<SkeletonLine height={14} width="100px" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Address */}
|
||||||
|
<div className="d-flex gap-2">
|
||||||
|
<SkeletonLine height={14} width="70px" />
|
||||||
|
<SkeletonLine height={14} width="100%" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const OrgDetailsSkeleton = () => {
|
||||||
|
return (
|
||||||
|
<div className="row text-start p-3">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="col-12 mb-3">
|
||||||
|
<div className="d-flex justify-content-between align-items-center">
|
||||||
|
{/* Logo + Name */}
|
||||||
|
<div className="d-flex flex-row gap-2 align-items-center">
|
||||||
|
<SkeletonLine height={40} width={40} className="rounded-circle" />
|
||||||
|
<SkeletonLine height={18} width="180px" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Badge */}
|
||||||
|
<SkeletonLine height={20} width="70px" className="rounded-pill" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section Title */}
|
||||||
|
<div className="d-flex text-secondary mb-2">
|
||||||
|
<SkeletonLine height={16} width="140px" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contact Person */}
|
||||||
|
<div className="col-md-6 mb-3">
|
||||||
|
<div className="d-flex">
|
||||||
|
<SkeletonLine height={16} width="130px" className="me-2" />
|
||||||
|
<SkeletonLine height={16} width="140px" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contact Number */}
|
||||||
|
<div className="col-md-6 mb-3">
|
||||||
|
<div className="d-flex">
|
||||||
|
<SkeletonLine height={16} width="130px" className="me-2" />
|
||||||
|
<SkeletonLine height={16} width="140px" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Email */}
|
||||||
|
<div className="col-md-12 mb-3">
|
||||||
|
<div className="d-flex">
|
||||||
|
<SkeletonLine height={16} width="130px" className="me-2" />
|
||||||
|
<SkeletonLine height={16} width="220px" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* SPRID */}
|
||||||
|
<div className="col-6 mb-3">
|
||||||
|
<div className="d-flex">
|
||||||
|
<SkeletonLine height={16} width="130px" className="me-2" />
|
||||||
|
<SkeletonLine height={16} width="160px" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Employees */}
|
||||||
|
<div className="col-6 mb-3">
|
||||||
|
<div className="d-flex">
|
||||||
|
<SkeletonLine height={16} width="130px" className="me-2" />
|
||||||
|
<SkeletonLine height={16} width="60px" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Address */}
|
||||||
|
<div className="col-12 mb-3">
|
||||||
|
<div className="d-flex">
|
||||||
|
<SkeletonLine height={16} width="130px" className="me-2" />
|
||||||
|
<SkeletonLine height={16} width="100%" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section Title 2 */}
|
||||||
|
<div className="d-flex text-secondary mb-2">
|
||||||
|
<SkeletonLine height={16} width="200px" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
164
src/components/Organization/OrganizationsList.jsx
Normal file
164
src/components/Organization/OrganizationsList.jsx
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { useOrganizationModal, useOrganizationsList } from "../../hooks/useOrganization";
|
||||||
|
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
||||||
|
import Avatar from "../common/Avatar";
|
||||||
|
import { useDebounce } from "../../utils/appUtils";
|
||||||
|
import Pagination from "../common/Pagination";
|
||||||
|
|
||||||
|
const OrganizationsList = ({searchText}) => {
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const searchString = useDebounce(searchText,500)
|
||||||
|
const {
|
||||||
|
data = [],
|
||||||
|
isLoading,
|
||||||
|
isFetching,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
|
} = useOrganizationsList(ITEMS_PER_PAGE, 1, true,null,searchString);
|
||||||
|
const {onClose, startStep, flowType, onOpen,orgData } = useOrganizationModal();
|
||||||
|
|
||||||
|
const organizationsColumns = [
|
||||||
|
{
|
||||||
|
key: "name",
|
||||||
|
label: "Organization Name",
|
||||||
|
getValue: (org) => (
|
||||||
|
<div className="d-flex gap-2 py-1 ">
|
||||||
|
<i class="bx bx-buildings"></i>
|
||||||
|
<span
|
||||||
|
className="text-truncate d-inline-block "
|
||||||
|
style={{ maxWidth: "150px" }}
|
||||||
|
>
|
||||||
|
{org?.name || "N/A"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
align: "text-start",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "contactPerson",
|
||||||
|
label: "Contact Person",
|
||||||
|
getValue: (org) => (
|
||||||
|
(
|
||||||
|
<div className="d-flex align-items-center ps-1">
|
||||||
|
<Avatar
|
||||||
|
size="xs"
|
||||||
|
classAvatar="m-0"
|
||||||
|
firstName={(org?.name || "").trim().split(" ")[0] || ""}
|
||||||
|
lastName={(org?.name || "").trim().split(" ")[1] || ""}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="text-truncate d-inline-block "
|
||||||
|
style={{ maxWidth: "150px" }}
|
||||||
|
>
|
||||||
|
{org?.name || "N/A"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
),
|
||||||
|
align: "text-start",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "contactNumber",
|
||||||
|
label: "Phone Number",
|
||||||
|
getValue: (org) => org.contactNumber || "N/A",
|
||||||
|
align: "text-start",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "email",
|
||||||
|
label: "Email",
|
||||||
|
getValue: (org) => (
|
||||||
|
<span
|
||||||
|
className="text-truncate d-inline-block"
|
||||||
|
style={{ maxWidth: "200px" }}
|
||||||
|
>
|
||||||
|
{org?.email || "N/A"}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
align: "text-start",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "sprid",
|
||||||
|
label: "SPRID Id",
|
||||||
|
getValue: (org) => org.sprid || "N/A",
|
||||||
|
align: "text-center",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const paginate = (page) => {
|
||||||
|
if (page >= 1 && page <= (data?.totalPages ?? 1)) {
|
||||||
|
setCurrentPage(page);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (isFetching && !isFetching) return <div>Loading...</div>;
|
||||||
|
if (isError) return <div>{error?.message || "Something went wrong"}</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card px-0 px-sm-4 pb-12 pt-5">
|
||||||
|
<div className="card-datatable table-responsive" id="horizontal-example">
|
||||||
|
<div className="dataTables_wrapper no-footer px-2">
|
||||||
|
<table className="table border-top dataTable text-nowrap">
|
||||||
|
<thead>
|
||||||
|
<tr className="table_header_border">
|
||||||
|
{organizationsColumns.map((col) => (
|
||||||
|
<th
|
||||||
|
key={col.key}
|
||||||
|
className="sorting d-table-cell"
|
||||||
|
aria-sort="descending"
|
||||||
|
>
|
||||||
|
<div className={`${col.align}`}>{col.label}</div>
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
<th className="sticky-action-column bg-white text-center">
|
||||||
|
Action
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{data?.data?.length > 0 ? (
|
||||||
|
data?.data?.map((org) => (
|
||||||
|
<tr key={org.id}>
|
||||||
|
{organizationsColumns.map((col) => (
|
||||||
|
<td
|
||||||
|
key={col.key}
|
||||||
|
className={`d-table-cell ${col.align ?? ""}`}
|
||||||
|
>
|
||||||
|
{col.customRender
|
||||||
|
? col.customRender(org)
|
||||||
|
: col.getValue(org)}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
<td className="sticky-action-column ">
|
||||||
|
<div className="d-flex justify-content-center gap-2">
|
||||||
|
<i className="bx bx-show text-primary cursor-pointer" onClick={()=>onOpen({startStep:5,orgData:org.id,flowType:"view"})}></i>
|
||||||
|
<i className="bx bx-edit text-secondary cursor-pointer" onClick={()=>onOpen({startStep:4,orgData:org,flowType:"edit"})}></i>
|
||||||
|
<i className="bx bx-trash text-danger cursor-pointer"></i>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colSpan={organizationsColumns.length + 1}
|
||||||
|
className="text-center"
|
||||||
|
>
|
||||||
|
<p className="fw-semibold">Not Found</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{data?.data?.length > 0 && (
|
||||||
|
<Pagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
totalPages={data.totalPages}
|
||||||
|
onPageChange={paginate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OrganizationsList;
|
||||||
103
src/components/Organization/ViewOrganization.jsx
Normal file
103
src/components/Organization/ViewOrganization.jsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useOrganization } from "../../hooks/useOrganization";
|
||||||
|
import { OrgDetailsSkeleton } from "./OrganizationSkeleton";
|
||||||
|
|
||||||
|
const VieworgDataanization = ({ orgId }) => {
|
||||||
|
const { data, isLoading, isError, error } = useOrganization(orgId);
|
||||||
|
if (isLoading) return <OrgDetailsSkeleton/>;
|
||||||
|
if (isError) return <div>{error.message}</div>;
|
||||||
|
return (
|
||||||
|
<div className="row text-black text-black text-start ">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="col-12 mb-3">
|
||||||
|
<div className="d-flex justify-content-between align-items-center text-start mb-1">
|
||||||
|
<div className="d-flex flex-row gap-2 align-items-center text-wrap">
|
||||||
|
<img
|
||||||
|
src="/public/assets/img/orgLogo.png"
|
||||||
|
alt="logo"
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
/> <p className="fw-semibold fs-6 m-0">{data?.data?.name}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-end">
|
||||||
|
<span className={`badge bg-label-${data?.data.isActive ? "primary":"secondary"} `}>{data?.data.isActive ? "Active":"In-Active"} </span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="d-flex text-secondary mb-2"> <i className="bx bx-sm bx-info-circle me-1" /> Organization Info</div>
|
||||||
|
{/* Contact Info */}
|
||||||
|
<div className="col-md-6 mb-3">
|
||||||
|
<div className="d-flex">
|
||||||
|
<label
|
||||||
|
className="form-label me-2 mb-0 fw-semibold"
|
||||||
|
style={{ minWidth: "130px" }}
|
||||||
|
>
|
||||||
|
<i className="bx bx-sm bx-user me-1" /> Contact Person :
|
||||||
|
</label>
|
||||||
|
<div className="text-muted">{data?.data?.contactPerson}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-6 mb-3">
|
||||||
|
<div className="d-flex">
|
||||||
|
<label
|
||||||
|
className="form-label me-2 mb-0 fw-semibold"
|
||||||
|
style={{ minWidth: "130px" }}
|
||||||
|
>
|
||||||
|
<i className='bx bx-sm me-1 bx-phone'></i> Contact Number :
|
||||||
|
</label>
|
||||||
|
<div className="text-muted">{data?.data?.contactNumber}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-12 mb-3">
|
||||||
|
<div className="d-flex">
|
||||||
|
<label
|
||||||
|
className="form-label me-2 mb-0 fw-semibold"
|
||||||
|
style={{ minWidth: "130px" }}
|
||||||
|
>
|
||||||
|
<i className='bx bx-sm me-1 bx-envelope'></i> Email Address :
|
||||||
|
</label>
|
||||||
|
<div className="text-muted">{data?.data?.email}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-6 mb-3">
|
||||||
|
<div className="d-flex">
|
||||||
|
<label
|
||||||
|
className="form-label me-2 mb-0 fw-semibold"
|
||||||
|
style={{ maxWidth: "130px" }}
|
||||||
|
>
|
||||||
|
<i className="bx bx-sm me-1 bx-barcode"></i>
|
||||||
|
Service Provider Id (SPRID) :
|
||||||
|
</label>
|
||||||
|
<div className="text-muted">{data?.data?.sprid}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-6 mb-3">
|
||||||
|
<div className="d-flex">
|
||||||
|
<label
|
||||||
|
className="form-label me-2 mb-0 fw-semibold"
|
||||||
|
style={{ maxWidth: "130px" }}
|
||||||
|
>
|
||||||
|
<i className="bx bx-sm me-1 bx-group"></i>
|
||||||
|
Employees :
|
||||||
|
</label>
|
||||||
|
<div className="text-muted">{data?.data?.activeEmployeeCount}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-12 mb-3">
|
||||||
|
<div className="d-flex">
|
||||||
|
<label
|
||||||
|
className="form-label me-1 mb-0 fw-semibold"
|
||||||
|
style={{ minWidth: "130px" }}
|
||||||
|
>
|
||||||
|
<i className='bx bx-sm me-1 bx-map'></i> Address :
|
||||||
|
</label>
|
||||||
|
<div className="text-muted text-start">{data?.data?.address}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="d-flex text-secondary mb-2"> <i className="bx bx-sm bx-briefcase me-1" /> Projects And Services</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VieworgDataanization;
|
||||||
1
src/components/Organization/tempCodeRunnerFile.jsx
Normal file
1
src/components/Organization/tempCodeRunnerFile.jsx
Normal file
@ -0,0 +1 @@
|
|||||||
|
useAssignOrgToTenant
|
||||||
@ -1,24 +1,25 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { changeMaster } from "../../slices/localVariablesSlice";
|
import { changeMaster } from "../../slices/localVariablesSlice";
|
||||||
import useMaster from "../../hooks/masterHook/useMaster";
|
import useMaster, { useServices } from "../../hooks/masterHook/useMaster";
|
||||||
import { useForm, Controller } from "react-hook-form";
|
import { useForm, Controller } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { clearCacheKey, getCachedData } from "../../slices/apiDataManager";
|
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||||
import { useEmployeesAllOrByProjectId } from "../../hooks/useEmployees";
|
import { useEmployeesAllOrByProjectId } from "../../hooks/useEmployees";
|
||||||
import { TasksRepository } from "../../repositories/ProjectRepository";
|
import { TasksRepository } from "../../repositories/ProjectRepository";
|
||||||
import showToast from "../../services/toastService";
|
import showToast from "../../services/toastService";
|
||||||
import { useProjectDetails } from "../../hooks/useProjects";
|
import {
|
||||||
|
useEmployeeForTaskAssign,
|
||||||
|
useProjectAssignedOrganizations,
|
||||||
|
useProjectDetails,
|
||||||
|
} from "../../hooks/useProjects";
|
||||||
import eventBus from "../../services/eventBus";
|
import eventBus from "../../services/eventBus";
|
||||||
import { useCreateTask } from "../../hooks/useTasks";
|
import { useCreateTask } from "../../hooks/useTasks";
|
||||||
import Label from "../common/Label";
|
import Label from "../common/Label";
|
||||||
|
|
||||||
const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
const TaskSchema = (maxPlanned) => {
|
||||||
const maxPlanned =
|
return z.object({
|
||||||
assignData?.workItem?.plannedWork - assignData?.workItem?.completedWork;
|
|
||||||
|
|
||||||
const schema = z.object({
|
|
||||||
selectedEmployees: z
|
selectedEmployees: z
|
||||||
.array(z.string())
|
.array(z.string())
|
||||||
.min(1, { message: "At least one employee must be selected" }),
|
.min(1, { message: "At least one employee must be selected" }),
|
||||||
@ -37,20 +38,26 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
|||||||
})
|
})
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
||||||
|
const planned = assignData?.workItem?.plannedWork || 0;
|
||||||
|
const completed = assignData?.workItem?.completedWork || 0;
|
||||||
|
const maxPlanned = planned - completed;
|
||||||
|
|
||||||
const [isHelpVisibleTarget, setIsHelpVisibleTarget] = useState(false);
|
const [isHelpVisibleTarget, setIsHelpVisibleTarget] = useState(false);
|
||||||
const helpPopupRefTarget = useRef(null);
|
const helpPopupRefTarget = useRef(null);
|
||||||
const [isHelpVisible, setIsHelpVisible] = useState(false);
|
const [isHelpVisible, setIsHelpVisible] = useState(false);
|
||||||
|
const [selectedService, setSelectedService] = useState(null);
|
||||||
|
const [selectedOrganization, setSelectedOrganization] = useState(null);
|
||||||
|
|
||||||
const { mutate: assignTask, isPending: isSubmitting } = useCreateTask({
|
const { mutate: assignTask, isPending: isSubmitting } = useCreateTask({
|
||||||
onSuccessCallback: () => {
|
onSuccessCallback: closedModel,
|
||||||
closedModel();
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const dropdownRef = useRef(null);
|
const dropdownRef = useRef(null);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
// Close dropdown on outside click
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event) => {
|
const handleClickOutside = (event) => {
|
||||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||||
@ -63,48 +70,47 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
|||||||
|
|
||||||
const infoRef = useRef(null);
|
const infoRef = useRef(null);
|
||||||
const infoRef1 = useRef(null);
|
const infoRef1 = useRef(null);
|
||||||
|
|
||||||
// State for search term
|
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof bootstrap !== "undefined") {
|
if (typeof bootstrap !== "undefined") {
|
||||||
if (infoRef.current) {
|
infoRef.current &&
|
||||||
new bootstrap.Popover(infoRef.current, {
|
new bootstrap.Popover(infoRef.current, {
|
||||||
trigger: "focus",
|
trigger: "focus",
|
||||||
placement: "right",
|
placement: "right",
|
||||||
html: true,
|
html: true,
|
||||||
content: `<div>Total Pending tasks of the Activity</div>`,
|
content: `<div>Total Pending tasks of the Activity</div>`,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (infoRef1.current) {
|
infoRef1.current &&
|
||||||
new bootstrap.Popover(infoRef1.current, {
|
new bootstrap.Popover(infoRef1.current, {
|
||||||
trigger: "focus",
|
trigger: "focus",
|
||||||
placement: "right",
|
placement: "right",
|
||||||
html: true,
|
html: true,
|
||||||
content: `<div>Target task for today</div>`,
|
content: `<div>Target task for today</div>`,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.warn("Bootstrap is not available. Popovers might not function.");
|
console.warn("Bootstrap is not available. Popovers might not function.");
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
const selectedProject = useSelector(
|
|
||||||
(store) => store.localVariables.projectId
|
|
||||||
);
|
|
||||||
const {
|
|
||||||
employees,
|
|
||||||
loading: employeeLoading,
|
|
||||||
recallEmployeeData,
|
|
||||||
} = useEmployeesAllOrByProjectId(false, selectedProject, false);
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const { loading } = useMaster();
|
|
||||||
const { data: jobRoleData } = useMaster();
|
|
||||||
|
|
||||||
// Changed to an array to hold multiple selected roles
|
const selectedProject = useSelectedProject();
|
||||||
|
const { data: serviceList, isLoading: isServiceLoading } = useServices();
|
||||||
|
const { data: organizationList, isLoading: isOrgLoading } =
|
||||||
|
useProjectAssignedOrganizations(selectedProject);
|
||||||
|
const { data: employees, isLoading: isEmployeeLoading } =
|
||||||
|
useEmployeeForTaskAssign(
|
||||||
|
selectedProject,
|
||||||
|
selectedService,
|
||||||
|
selectedOrganization
|
||||||
|
);
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { loading, data: jobRoleData } = useMaster();
|
||||||
|
|
||||||
const [selectedRoles, setSelectedRoles] = useState(["all"]);
|
const [selectedRoles, setSelectedRoles] = useState(["all"]);
|
||||||
const [displayedSelection, setDisplayedSelection] = useState("");
|
const [displayedSelection, setDisplayedSelection] = useState("");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
@ -114,133 +120,98 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
|||||||
reset,
|
reset,
|
||||||
trigger,
|
trigger,
|
||||||
} = useForm({
|
} = useForm({
|
||||||
defaultValues: {
|
defaultValues: { selectedEmployees: [], description: "", plannedTask: "" },
|
||||||
selectedEmployees: [],
|
resolver: zodResolver(TaskSchema(maxPlanned)),
|
||||||
description: "",
|
|
||||||
plannedTask: "",
|
|
||||||
},
|
|
||||||
resolver: zodResolver(schema),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleCheckboxChange = (event, user) => {
|
const handleCheckboxChange = (event, user) => {
|
||||||
const isChecked = event.target.checked;
|
const updatedSelectedEmployees = event.target.checked
|
||||||
let updatedSelectedEmployees = watch("selectedEmployees") || [];
|
? [...(watch("selectedEmployees") || []), user.id].filter(
|
||||||
|
(v, i, a) => a.indexOf(v) === i
|
||||||
|
)
|
||||||
|
: (watch("selectedEmployees") || []).filter((id) => id !== user.id);
|
||||||
|
|
||||||
if (isChecked) {
|
|
||||||
if (!updatedSelectedEmployees.includes(user.id)) {
|
|
||||||
updatedSelectedEmployees = [...updatedSelectedEmployees, user.id];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updatedSelectedEmployees = updatedSelectedEmployees?.filter(
|
|
||||||
(id) => id !== user.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
setValue("selectedEmployees", updatedSelectedEmployees);
|
setValue("selectedEmployees", updatedSelectedEmployees);
|
||||||
trigger("selectedEmployees");
|
trigger("selectedEmployees");
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(changeMaster("Job Role"));
|
dispatch(changeMaster("Job Role"));
|
||||||
// Initial state should reflect "All Roles" selected
|
|
||||||
setSelectedRoles(["all"]);
|
setSelectedRoles(["all"]);
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
// Modified handleRoleChange to handle multiple selections
|
|
||||||
const handleRoleChange = (event, roleId) => {
|
const handleRoleChange = (event, roleId) => {
|
||||||
// If 'all' is selected, clear other selections
|
setSelectedRoles((prev) => {
|
||||||
if (roleId === "all") {
|
if (roleId === "all") return ["all"];
|
||||||
setSelectedRoles(["all"]);
|
const newRoles = prev.filter((r) => r !== "all");
|
||||||
} else {
|
return newRoles.includes(roleId)
|
||||||
setSelectedRoles((prevSelectedRoles) => {
|
? newRoles.filter((r) => r !== roleId)
|
||||||
// If "all" was previously selected, remove it
|
: [...newRoles, roleId];
|
||||||
const newRoles = prevSelectedRoles.filter((role) => role !== "all");
|
|
||||||
if (newRoles.includes(roleId)) {
|
|
||||||
// If role is already selected, unselect it
|
|
||||||
return newRoles.filter((id) => id !== roleId);
|
|
||||||
} else {
|
|
||||||
// If role is not selected, add it
|
|
||||||
return [...newRoles, roleId];
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Update displayedSelection based on selectedRoles
|
|
||||||
if (selectedRoles.includes("all")) {
|
if (selectedRoles.includes("all")) {
|
||||||
setDisplayedSelection("All Roles");
|
setDisplayedSelection("All Roles");
|
||||||
} else if (selectedRoles.length > 0) {
|
} else if (selectedRoles.length > 0) {
|
||||||
const selectedRoleNames = selectedRoles.map(roleId => {
|
setDisplayedSelection(
|
||||||
const role = jobRoleData?.find(r => String(r.id) === roleId);
|
selectedRoles
|
||||||
return role ? role.name : '';
|
.map((id) => jobRoleData?.find((r) => String(r.id) === id)?.name)
|
||||||
}).filter(Boolean); // Filter out empty strings for roles not found
|
.filter(Boolean)
|
||||||
setDisplayedSelection(selectedRoleNames.join(', '));
|
.join(", ")
|
||||||
} else {
|
);
|
||||||
setDisplayedSelection("Select Roles");
|
} else setDisplayedSelection("Select Roles");
|
||||||
}
|
|
||||||
}, [selectedRoles, jobRoleData]);
|
}, [selectedRoles, jobRoleData]);
|
||||||
|
|
||||||
|
const handleSearchChange = (e) => setSearchTerm(e.target.value);
|
||||||
|
|
||||||
const handleSearchChange = (event) => {
|
const filteredEmployees = employees?.data?.filter((emp) => {
|
||||||
setSearchTerm(event.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Filter employees first by role, then by search term AND job role name
|
|
||||||
const filteredEmployees = employees?.filter((emp) => {
|
|
||||||
const matchesRole =
|
const matchesRole =
|
||||||
selectedRoles.includes("all") || selectedRoles.includes(String(emp.jobRoleId));
|
selectedRoles.includes("all") ||
|
||||||
// Convert both first and last names and job role name to lowercase for case-insensitive matching
|
selectedRoles.includes(String(emp.jobRoleId));
|
||||||
const fullName = `${emp.firstName} ${emp.lastName}`.toLowerCase();
|
|
||||||
|
|
||||||
const jobRoleName = jobRoleData?.find((role) => role.id === emp.jobRoleId)?.name?.toLowerCase() || "";
|
|
||||||
|
|
||||||
const searchLower = searchTerm.toLowerCase();
|
const searchLower = searchTerm.toLowerCase();
|
||||||
// Check if the full name OR job role name includes the search term
|
const fullName = `${emp.firstName} ${emp.lastName}`.toLowerCase();
|
||||||
const matchesSearch = fullName.includes(searchLower) || jobRoleName.includes(searchLower);
|
const jobRoleName =
|
||||||
return matchesRole && matchesSearch;
|
jobRoleData
|
||||||
|
?.find((role) => role.id === emp.jobRoleId)
|
||||||
|
?.name?.toLowerCase() || "";
|
||||||
|
return (
|
||||||
|
matchesRole &&
|
||||||
|
(fullName.includes(searchLower) || jobRoleName.includes(searchLower))
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Determine unique job role IDs from the filtered employees (for dropdown options)
|
const jobRolesForDropdown = jobRoleData?.filter((role) =>
|
||||||
const uniqueJobRoleIdsInFilteredEmployees = new Set(
|
new Set(employees?.data?.map((emp) => emp.jobRoleId).filter(Boolean)).has(
|
||||||
employees?.map(emp => emp.jobRoleId).filter(Boolean)
|
role.id
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Filter jobRoleData to only include roles present in the uniqueJobRoleIdsInFilteredEmployees
|
|
||||||
const jobRolesForDropdown = jobRoleData?.filter(role =>
|
|
||||||
uniqueJobRoleIdsInFilteredEmployees.has(role.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Calculate the count of selected roles for display
|
|
||||||
const selectedRolesCount = selectedRoles.includes("all")
|
const selectedRolesCount = selectedRoles.includes("all")
|
||||||
? 0 // "All Roles" doesn't contribute to a specific count
|
? 0
|
||||||
: selectedRoles.length;
|
: selectedRoles.length;
|
||||||
|
|
||||||
const onSubmit = (data) => {
|
const onSubmit = (data) => {
|
||||||
const selectedEmployeeIds = data.selectedEmployees;
|
assignTask({
|
||||||
|
payload: {
|
||||||
const taskTeamWithDetails = selectedEmployeeIds
|
taskTeam: data.selectedEmployees.filter(Boolean),
|
||||||
?.map((empId) => empId)
|
|
||||||
?.filter(Boolean);
|
|
||||||
const formattedData = {
|
|
||||||
taskTeam: taskTeamWithDetails,
|
|
||||||
plannedTask: data.plannedTask,
|
plannedTask: data.plannedTask,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
assignmentDate: new Date().toISOString(),
|
assignmentDate: new Date().toISOString(),
|
||||||
workItemId: assignData?.workItem.id,
|
workItemId: assignData?.workItem.id,
|
||||||
};
|
},
|
||||||
assignTask({
|
|
||||||
payload: formattedData,
|
|
||||||
workAreaId: assignData?.workArea?.id,
|
workAreaId: assignData?.workArea?.id,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const closedModel = () => {
|
function closedModel() {
|
||||||
reset();
|
reset();
|
||||||
onClose();
|
onClose();
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fs-5 text-dark text-center d-flex align-items-center justify-content-center flex-wrap">
|
<div className="fs-5 text-dark text-center d-flex align-items-center justify-content-center flex-wrap">
|
||||||
<p className="align-items-center flex-wrap m-0 ">Assign Task</p>
|
<p className="align-items-center flex-wrap m-0">Assign Task</p>
|
||||||
<div className="container my-3">
|
<div className="container my-3">
|
||||||
<div className="mb-1">
|
<div className="mb-1">
|
||||||
<p className="mb-0">
|
<p className="mb-0">
|
||||||
@ -252,7 +223,7 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
|||||||
assignData?.workArea?.areaName,
|
assignData?.workArea?.areaName,
|
||||||
assignData?.workItem?.activityMaster?.activityName,
|
assignData?.workItem?.activityMaster?.activityName,
|
||||||
]
|
]
|
||||||
.filter(Boolean) // Filter out any undefined/null values
|
.filter(Boolean)
|
||||||
.map((item, index, array) => (
|
.map((item, index, array) => (
|
||||||
<span key={index} className="d-flex align-items-center">
|
<span key={index} className="d-flex align-items-center">
|
||||||
{item}
|
{item}
|
||||||
@ -268,17 +239,61 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
|||||||
<div className="form-label text-start">
|
<div className="form-label text-start">
|
||||||
<div className="row mb-1">
|
<div className="row mb-1">
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<div className="form-text text-start">
|
<div className="row text-start">
|
||||||
|
<div className="col-12 col-md-8 d-flex flex-row gap-3 align-items-center">
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
className="form-select form-select-sm"
|
||||||
|
value={selectedOrganization || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setSelectedOrganization(e.target.value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{isServiceLoading ? (
|
||||||
|
<option>Loading...</option>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<option value="">--Select Organization--</option>
|
||||||
|
{organizationList?.map((org,index) => (
|
||||||
|
<option key={`${org.id}-${index}`} value={org.id}>
|
||||||
|
{org.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
className="form-select form-select-sm"
|
||||||
|
value={selectedService || ""}
|
||||||
|
onChange={(e) => setSelectedService(e.target.value)}
|
||||||
|
>
|
||||||
|
{isOrgLoading ? (
|
||||||
|
<option>Loading...</option>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<option value="">--Select Service--</option>
|
||||||
|
{serviceList?.data?.map((service,index) => (
|
||||||
|
<option key={`${service.id}-${index}`} value={service.id}>
|
||||||
|
{service.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className="d-flex align-items-center form-text fs-7"
|
className="col-12 col-md-4 d-flex flex-row gap-3 align-items-center justify-content-end"
|
||||||
ref={dropdownRef}
|
ref={dropdownRef}
|
||||||
>
|
>
|
||||||
<span className="text-dark">Select Team</span>
|
|
||||||
|
|
||||||
{/* Dropdown */}
|
{/* Dropdown */}
|
||||||
<div className="dropdown position-relative d-inline-block">
|
<div className="dropdown position-relative d-inline-block">
|
||||||
<a
|
<a
|
||||||
className={`dropdown-toggle hide-arrow cursor-pointer ${selectedRoles.includes("all") || selectedRoles.length === 0
|
className={`dropdown-toggle hide-arrow cursor-pointer ${
|
||||||
|
selectedRoles.includes("all") ||
|
||||||
|
selectedRoles.length === 0
|
||||||
? "text-secondary"
|
? "text-secondary"
|
||||||
: "text-primary"
|
: "text-primary"
|
||||||
}`}
|
}`}
|
||||||
@ -290,7 +305,7 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
|||||||
{/* Badge */}
|
{/* Badge */}
|
||||||
{selectedRolesCount > 0 && (
|
{selectedRolesCount > 0 && (
|
||||||
<span
|
<span
|
||||||
className="position-absolute top-0 start-100 translate-middle badge rounded-circle bg-warning text-white"
|
className="position-absolute top-0 start-100 translate-middle badge rounded-circle bg-warning text-white text-tiny"
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.65rem",
|
fontSize: "0.65rem",
|
||||||
minWidth: "18px",
|
minWidth: "18px",
|
||||||
@ -320,7 +335,9 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
|||||||
id="checkboxAllRoles"
|
id="checkboxAllRoles"
|
||||||
value="all"
|
value="all"
|
||||||
checked={selectedRoles.includes("all")}
|
checked={selectedRoles.includes("all")}
|
||||||
onChange={(e) => handleRoleChange(e, e.target.value)}
|
onChange={(e) =>
|
||||||
|
handleRoleChange(e, e.target.value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
className="form-check-label ms-2"
|
className="form-check-label ms-2"
|
||||||
@ -340,8 +357,12 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
id={`checkboxRole-${role.id}`}
|
id={`checkboxRole-${role.id}`}
|
||||||
value={role.id}
|
value={role.id}
|
||||||
checked={selectedRoles.includes(String(role.id))}
|
checked={selectedRoles.includes(
|
||||||
onChange={(e) => handleRoleChange(e, e.target.value)}
|
String(role.id)
|
||||||
|
)}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleRoleChange(e, e.target.value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
className="form-check-label ms-2"
|
className="form-check-label ms-2"
|
||||||
@ -357,18 +378,19 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search Box */}
|
{/* Search Box */}
|
||||||
|
<div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control form-control-sm ms-auto mb-2 mt-2"
|
className="form-control form-control-sm ms-auto mb-2 mt-2"
|
||||||
placeholder="Search employees or roles..."
|
placeholder="Search employees or roles..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
style={{ maxWidth: "200px" }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Employees list */}
|
{/* Employees list */}
|
||||||
<div
|
<div
|
||||||
@ -381,19 +403,19 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
|||||||
>
|
>
|
||||||
{selectedRoles?.length > 0 && (
|
{selectedRoles?.length > 0 && (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
{employeeLoading ? (
|
{isEmployeeLoading ? (
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<p className="text-center">Loading employees...</p>
|
<p className="text-center">Loading employees...</p>
|
||||||
</div>
|
</div>
|
||||||
) : filteredEmployees?.length > 0 ? (
|
) : filteredEmployees?.length > 0 ? (
|
||||||
filteredEmployees.map((emp) => {
|
filteredEmployees.map((emp,index) => {
|
||||||
const jobRole = jobRoleData?.find(
|
const jobRole = jobRoleData?.find(
|
||||||
(role) => role?.id === emp?.jobRoleId
|
(role) => role?.id === emp?.jobRoleId
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={emp.id}
|
key={`${emp.index}-${index}`}
|
||||||
className="col-6 col-md-4 col-lg-3 mb-3"
|
className="col-6 col-md-4 col-lg-3 mb-3"
|
||||||
>
|
>
|
||||||
<div className="form-check d-flex align-items-start">
|
<div className="form-check d-flex align-items-start">
|
||||||
@ -441,7 +463,7 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<p className="text-center">
|
<p className="text-center">
|
||||||
No employees found for the selected role.
|
No employees found for the selected filter.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -456,12 +478,14 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
|||||||
{watch("selectedEmployees")?.length > 0 && (
|
{watch("selectedEmployees")?.length > 0 && (
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<div className="text-start px-2">
|
<div className="text-start px-2">
|
||||||
{watch("selectedEmployees")?.map((empId) => {
|
{watch("selectedEmployees")?.map((empId,ind) => {
|
||||||
const emp = employees.find((emp) => emp.id === empId);
|
const emp = employees?.data?.find(
|
||||||
|
(emp) => emp.id === empId
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
emp && (
|
emp && (
|
||||||
<span
|
<span
|
||||||
key={empId}
|
key={`${empId}-${ind}`}
|
||||||
className="badge rounded-pill bg-label-primary d-inline-flex align-items-center me-1 mb-1"
|
className="badge rounded-pill bg-label-primary d-inline-flex align-items-center me-1 mb-1"
|
||||||
>
|
>
|
||||||
{emp.firstName} {emp.lastName}
|
{emp.firstName} {emp.lastName}
|
||||||
@ -506,7 +530,12 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
|||||||
{assignData?.workItem?.plannedWork -
|
{assignData?.workItem?.plannedWork -
|
||||||
assignData?.workItem?.completedWork}
|
assignData?.workItem?.completedWork}
|
||||||
</strong>{" "}
|
</strong>{" "}
|
||||||
<u>{assignData?.workItem?.activityMaster?.unitOfMeasurement}</u>
|
<u>
|
||||||
|
{
|
||||||
|
assignData?.workItem?.activityMaster
|
||||||
|
?.unitOfMeasurement
|
||||||
|
}
|
||||||
|
</u>
|
||||||
</label>
|
</label>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@ -537,8 +566,15 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
|||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
<span style={{ paddingLeft: "6px", whiteSpace: "nowrap" }}>
|
<span
|
||||||
<u>{assignData?.workItem?.activityMaster?.unitOfMeasurement}</u>
|
style={{ paddingLeft: "6px", whiteSpace: "nowrap" }}
|
||||||
|
>
|
||||||
|
<u>
|
||||||
|
{
|
||||||
|
assignData?.workItem?.activityMaster
|
||||||
|
?.unitOfMeasurement
|
||||||
|
}
|
||||||
|
</u>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -546,19 +582,17 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{errors.plannedTask && (
|
{errors.plannedTask && (
|
||||||
<div className="danger-text mt-1">{errors.plannedTask.message}</div>
|
<div className="danger-text mt-1">
|
||||||
|
{errors.plannedTask.message}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<Label
|
||||||
{/* <label
|
|
||||||
className="form-text fs-7 m-1 text-lg text-dark"
|
className="form-text fs-7 m-1 text-lg text-dark"
|
||||||
htmlFor="descriptionTextarea" // Changed htmlFor for better accessibility
|
htmlFor="descriptionTextarea"
|
||||||
|
required
|
||||||
>
|
>
|
||||||
Description
|
Description
|
||||||
</label> */}
|
|
||||||
<Label className="form-text fs-7 m-1 text-lg text-dark"
|
|
||||||
htmlFor="descriptionTextarea" required>
|
|
||||||
Description
|
|
||||||
</Label>
|
</Label>
|
||||||
<Controller
|
<Controller
|
||||||
name="description"
|
name="description"
|
||||||
@ -592,7 +626,6 @@ const AssignTask = ({ assignData, onClose, setAssigned }) => {
|
|||||||
{isSubmitting ? "Please Wait" : "Submit"}
|
{isSubmitting ? "Please Wait" : "Submit"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -32,8 +32,7 @@ const EditActivityModal = ({
|
|||||||
building,
|
building,
|
||||||
floor,
|
floor,
|
||||||
onClose,
|
onClose,
|
||||||
} ) =>
|
}) => {
|
||||||
{
|
|
||||||
|
|
||||||
const { activities, loading: loadingActivities } = useActivitiesMaster();
|
const { activities, loading: loadingActivities } = useActivitiesMaster();
|
||||||
const { categories, loading: loadingCategories } = useWorkCategoriesMaster();
|
const { categories, loading: loadingCategories } = useWorkCategoriesMaster();
|
||||||
@ -57,13 +56,12 @@ const EditActivityModal = ({
|
|||||||
comment: "",
|
comment: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { mutate: UpdateTask, isPending } = useManageTask({
|
const { mutate: UpdateTask, isPending } = useManageTask({
|
||||||
onSuccessCallback: (response) =>
|
onSuccessCallback: (response) => {
|
||||||
{
|
showToast(response?.message, "success")
|
||||||
showToast( response?.message, "success" )
|
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
} );
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -82,7 +80,7 @@ const { mutate: UpdateTask, isPending } = useManageTask({
|
|||||||
[categories]
|
[categories]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!workItem) return;
|
if (!workItem) return;
|
||||||
console.log(workItem)
|
console.log(workItem)
|
||||||
reset({
|
reset({
|
||||||
@ -98,17 +96,16 @@ useEffect(() => {
|
|||||||
workItem?.workItem?.completedWork || workItem?.completedWork || 0,
|
workItem?.workItem?.completedWork || workItem?.completedWork || 0,
|
||||||
comment: workItem?.workItem?.description || workItem?.description || "",
|
comment: workItem?.workItem?.description || workItem?.description || "",
|
||||||
});
|
});
|
||||||
}, [workItem?.id,selectedActivity]);
|
}, [workItem?.id, selectedActivity]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const selected = activities?.find((a) => a.id === activityID);
|
const selected = activities?.find((a) => a.id === activityID);
|
||||||
setSelectedActivity( selected || null );
|
setSelectedActivity(selected || null);
|
||||||
}, [activityID, activities]);
|
}, [activityID, activities]);
|
||||||
|
|
||||||
const onSubmitForm = (data) =>
|
const onSubmitForm = (data) => {
|
||||||
{
|
|
||||||
const payload = {
|
const payload = {
|
||||||
...data,
|
...data,
|
||||||
id: workItem?.workItem?.id ?? workItem?.id,
|
id: workItem?.workItem?.id ?? workItem?.id,
|
||||||
@ -125,7 +122,7 @@ useEffect(() => {
|
|||||||
buildingId: building?.id,
|
buildingId: building?.id,
|
||||||
floorId: floor?.id,
|
floorId: floor?.id,
|
||||||
workAreaId: workArea?.id,
|
workAreaId: workArea?.id,
|
||||||
previousCompletedWork:completedTask
|
previousCompletedWork: completedTask
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@ -162,14 +159,26 @@ useEffect(() => {
|
|||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-12 text-start">
|
||||||
|
<label className="form-label">Select Service</label>
|
||||||
|
<input
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
value={
|
||||||
|
workItem?.activityMaster?.activityGroupMaster?.service?.name || ""
|
||||||
|
}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="col-12 text-start">
|
<div className="col-12 text-start">
|
||||||
<label className="form-label">Select Activity</label>
|
<label className="form-label">Select Activity</label>
|
||||||
<select
|
<select
|
||||||
{...register("activityID")}
|
{...register("activityID")}
|
||||||
className="form-select form-select-sm"
|
className="form-select form-select-sm"
|
||||||
|
disabled
|
||||||
>
|
>
|
||||||
<option disabled>Select Activity</option>
|
<option >Select Activity</option>
|
||||||
{loadingActivities ? (
|
{loadingActivities ? (
|
||||||
<option>Loading...</option>
|
<option>Loading...</option>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
getCachedData,
|
getCachedData,
|
||||||
} from "../../../slices/apiDataManager";
|
} from "../../../slices/apiDataManager";
|
||||||
|
|
||||||
const InfraTable = ({ buildings, projectId}) => {
|
const InfraTable = ({ buildings, projectId }) => {
|
||||||
const [projectBuilding, setProjectBuilding] = useState([]);
|
const [projectBuilding, setProjectBuilding] = useState([]);
|
||||||
const [expandedBuildings, setExpandedBuildings] = useState([]);
|
const [expandedBuildings, setExpandedBuildings] = useState([]);
|
||||||
const [showFloorModal, setShowFloorModal] = useState(false);
|
const [showFloorModal, setShowFloorModal] = useState(false);
|
||||||
@ -143,7 +143,7 @@ const InfraTable = ({ buildings, projectId}) => {
|
|||||||
// }, [handler]);
|
// }, [handler]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="px-6">
|
||||||
{projectBuilding && projectBuilding.length > 0 && (
|
{projectBuilding && projectBuilding.length > 0 && (
|
||||||
<table className="table table-bordered">
|
<table className="table table-bordered">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@ -3,17 +3,22 @@ import { useForm } from "react-hook-form";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import {
|
import {
|
||||||
|
useActivitiesByGroups,
|
||||||
useActivitiesMaster,
|
useActivitiesMaster,
|
||||||
|
useGroups,
|
||||||
useWorkCategoriesMaster,
|
useWorkCategoriesMaster,
|
||||||
} from "../../../hooks/masterHook/useMaster";
|
} from "../../../hooks/masterHook/useMaster";
|
||||||
import { useManageTask } from "../../../hooks/useProjects";
|
import { useManageTask, useProjectAssignedOrganizations, useProjectAssignedServices } from "../../../hooks/useProjects";
|
||||||
import showToast from "../../../services/toastService";
|
import showToast from "../../../services/toastService";
|
||||||
import Label from "../../common/Label";
|
import Label from "../../common/Label";
|
||||||
|
import { useSelectedProject } from "../../../slices/apiDataManager";
|
||||||
|
|
||||||
const taskSchema = z.object({
|
const taskSchema = z.object({
|
||||||
buildingID: z.string().min(1, "Building is required"),
|
buildingID: z.string().min(1, "Building is required"),
|
||||||
floorId: z.string().min(1, "Floor is required"),
|
floorId: z.string().min(1, "Floor is required"),
|
||||||
workAreaId: z.string().min(1, "Work Area is required"),
|
workAreaId: z.string().min(1, "Work Area is required"),
|
||||||
|
serviceId: z.string().min(1, "Service is required"),
|
||||||
|
activityGroupId: z.string().min(1, "Activity Group is required"),
|
||||||
activityID: z.string().min(1, "Activity is required"),
|
activityID: z.string().min(1, "Activity is required"),
|
||||||
workCategoryId: z.string().min(1, "Work Category is required"),
|
workCategoryId: z.string().min(1, "Work Category is required"),
|
||||||
plannedWork: z.number().min(1, "Planned Work must be greater than 0"),
|
plannedWork: z.number().min(1, "Planned Work must be greater than 0"),
|
||||||
@ -26,6 +31,8 @@ const defaultModel = {
|
|||||||
buildingID: "",
|
buildingID: "",
|
||||||
floorId: "",
|
floorId: "",
|
||||||
workAreaId: "",
|
workAreaId: "",
|
||||||
|
serviceId: "",
|
||||||
|
activityGroupId: "",
|
||||||
activityID: "",
|
activityID: "",
|
||||||
workCategoryId: "",
|
workCategoryId: "",
|
||||||
plannedWork: 0,
|
plannedWork: 0,
|
||||||
@ -34,9 +41,42 @@ const defaultModel = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TaskModel = ({ project, onSubmit, onClose }) => {
|
const TaskModel = ({ project, onSubmit, onClose }) => {
|
||||||
const { activities, loading: activityLoading } = useActivitiesMaster();
|
// const { activities, loading: activityLoading } = useActivitiesMaster();
|
||||||
const { categories, categoryLoading } = useWorkCategoriesMaster();
|
const { categories, categoryLoading } = useWorkCategoriesMaster();
|
||||||
|
|
||||||
|
const projectId = useSelectedProject();
|
||||||
|
|
||||||
|
const { data: assignedServices, isLoading: servicesLoading } = useProjectAssignedServices(projectId);
|
||||||
|
const { data: assignedOrganizations, isLoading: orgLoading } = useProjectAssignedOrganizations(projectId);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const [selectedService, setSelectedService] = useState("");
|
||||||
|
const [selectedGroup, setSelectedGroup] = useState("");
|
||||||
|
const { data: groupsResponse, isLoading: groupsLoading } = useGroups(selectedService);
|
||||||
|
const groups = groupsResponse?.data ?? [];
|
||||||
|
|
||||||
|
const { data: activitiesResponse, isLoading: activitiesLoading } = useActivitiesByGroups(selectedGroup);
|
||||||
|
const activities = activitiesResponse?.data ?? [];
|
||||||
|
// Fetch Assigned Organizations (Activity Groups)
|
||||||
|
|
||||||
|
const [selectedOrg, setSelectedOrg] = useState("");
|
||||||
|
const handleServiceChange = (e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setSelectedService(value);
|
||||||
|
setSelectedGroup("");
|
||||||
|
setValue("activityGroupId", "");
|
||||||
|
setValue("activityID", "");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGroupChange = (e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
setSelectedGroup(value);
|
||||||
|
setValue("activityGroupId", value);
|
||||||
|
setValue("activityID", "");
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@ -72,7 +112,7 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
|
|||||||
const { mutate: CreateTask, isPending } = useManageTask({
|
const { mutate: CreateTask, isPending } = useManageTask({
|
||||||
onSuccessCallback: (response) => {
|
onSuccessCallback: (response) => {
|
||||||
showToast(response?.message, "success");
|
showToast(response?.message, "success");
|
||||||
onClose?.();
|
// onClose?.();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -97,9 +137,11 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
|
|||||||
|
|
||||||
const onSubmitForm = async (data) => {
|
const onSubmitForm = async (data) => {
|
||||||
const payload = [data];
|
const payload = [data];
|
||||||
CreateTask({payload:payload,buildingId: data.buildingID,
|
CreateTask({
|
||||||
|
payload: payload, buildingId: data.buildingID,
|
||||||
floorId: data.floorId,
|
floorId: data.floorId,
|
||||||
workAreaId: data.workAreaId, PreviousPlannedWork:0,previousCompletedWork:0});
|
workAreaId: data.workAreaId, PreviousPlannedWork: 0, previousCompletedWork: 0
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -150,6 +192,7 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Work Area Selection */}
|
||||||
{selectedFloor && (
|
{selectedFloor && (
|
||||||
<div className="col-12 text-start">
|
<div className="col-12 text-start">
|
||||||
<Label className="form-label" required>Select Work Area</Label>
|
<Label className="form-label" required>Select Work Area</Label>
|
||||||
@ -172,23 +215,64 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Services Selection */}
|
||||||
{selectedWorkArea && (
|
{selectedWorkArea && (
|
||||||
<div className="col-12 text-start">
|
<div className="col-12 text-start">
|
||||||
<Label className="form-label" required>Select Activity</Label>
|
<Label className="form-label" required>Select Service</Label>
|
||||||
<select
|
<select
|
||||||
className="form-select form-select-sm"
|
className="form-select form-select-sm"
|
||||||
{...register("activityID")}
|
{...register("serviceId")}
|
||||||
|
value={selectedService}
|
||||||
|
// onChange={handleServiceChange}
|
||||||
|
onChange={(e) => {
|
||||||
|
handleServiceChange(e);
|
||||||
|
setValue("serviceId", e.target.value);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<option value="">Select Activity</option>
|
<option value="">Select Service</option>
|
||||||
{activityData.map((a) => (
|
{servicesLoading && <option>Loading...</option>}
|
||||||
<option key={a.id} value={a.id}>
|
{assignedServices?.map((service) => (
|
||||||
{a.activityName}
|
<option key={service.id} value={service.id}>
|
||||||
|
{service.name}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
{errors.activityID && (
|
</div>
|
||||||
<p className="danger-text">{errors.activityID.message}</p>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Activity Group (Organization) Selection */}
|
||||||
|
{selectedService && (
|
||||||
|
<div className="col-12 text-start">
|
||||||
|
<Label className="form-label" required>Select Activity Group</Label>
|
||||||
|
<select
|
||||||
|
className="form-select form-select-sm"
|
||||||
|
{...register("activityGroupId")}
|
||||||
|
value={selectedGroup}
|
||||||
|
onChange={handleGroupChange}
|
||||||
|
>
|
||||||
|
<option value="">Select Group</option>
|
||||||
|
{groupsLoading && <option>Loading...</option>}
|
||||||
|
{groups?.map((g) => (
|
||||||
|
<option key={g.id} value={g.id}>{g.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{errors.activityGroupId && <p className="danger-text">{errors.activityGroupId.message}</p>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
{/* Activity Selection */}
|
||||||
|
{selectedGroup && (
|
||||||
|
<div className="col-12 text-start">
|
||||||
|
<Label className="form-label" required>Select Activity</Label>
|
||||||
|
<select className="form-select form-select-sm" {...register("activityID")}>
|
||||||
|
<option value="">Select Activity</option>
|
||||||
|
{activitiesLoading && <option>Loading...</option>}
|
||||||
|
{activities?.map((a) => (
|
||||||
|
<option key={a.id} value={a.id}>{a.activityName}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{errors.activityID && <p className="danger-text">{errors.activityID.message}</p>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -272,9 +356,10 @@ const TaskModel = ({ project, onSubmit, onClose }) => {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn btn-sm btn-primary"
|
className="btn btn-sm btn-primary"
|
||||||
disabled={isSubmitting}
|
// disabled={isSubmitting}
|
||||||
|
disabled={isPending}
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Please Wait..." : "Add Task"}
|
{isPending ? "Please Wait..." : "Add Task"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import WorkItem from "./WorkItem";
|
import WorkItem from "./WorkItem";
|
||||||
import { useProjectDetails, useProjectTasks } from "../../../hooks/useProjects";
|
import { useCurrentService, useProjectDetails, useProjectTasks } from "../../../hooks/useProjects";
|
||||||
import { cacheData } from "../../../slices/apiDataManager";
|
import { cacheData, useSelectedProject } from "../../../slices/apiDataManager";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { refreshData } from "../../../slices/localVariablesSlice";
|
import { refreshData } from "../../../slices/localVariablesSlice";
|
||||||
import ProjectRepository from "../../../repositories/ProjectRepository";
|
import ProjectRepository from "../../../repositories/ProjectRepository";
|
||||||
@ -14,20 +14,20 @@ import {
|
|||||||
} from "../../../utils/constants";
|
} from "../../../utils/constants";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import ProgressBar from "../../common/ProgressBar";
|
import ProgressBar from "../../common/ProgressBar";
|
||||||
import {formatNumber} from "../../../utils/dateUtils";
|
import { formatNumber } from "../../../utils/dateUtils";
|
||||||
|
import { useServices } from "../../../hooks/masterHook/useMaster";
|
||||||
|
|
||||||
const WorkArea = ({ workArea, floor, forBuilding }) => {
|
const WorkArea = ({ workArea, floor, forBuilding }) => {
|
||||||
const selectedProject = useSelector((store) => store.localVariables.projectId);
|
const selectedProject = useSelectedProject()
|
||||||
|
const selectedService = useCurrentService()
|
||||||
const { projects_Details, loading } = useProjectDetails(selectedProject);
|
const { projects_Details, loading } = useProjectDetails(selectedProject);
|
||||||
const [IsExpandedArea, setIsExpandedArea] = useState(false);
|
const [IsExpandedArea, setIsExpandedArea] = useState(false);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const [Project, setProject] = useState();
|
const [Project, setProject] = useState();
|
||||||
// const { projectId } = useParams();
|
|
||||||
|
|
||||||
const ManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA);
|
const ManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA);
|
||||||
const ManageAndAssignTak = useHasUserPermission(ASSIGN_REPORT_TASK);
|
const ManageAndAssignTak = useHasUserPermission(ASSIGN_REPORT_TASK);
|
||||||
|
const { ProjectTaskList, isLoading } = useProjectTasks(workArea.id, selectedService, IsExpandedArea);
|
||||||
const { ProjectTaskList, isLoading } = useProjectTasks(workArea.id, IsExpandedArea);
|
|
||||||
|
|
||||||
const [workAreaStatus, setWorkAreaStatus] = useState({
|
const [workAreaStatus, setWorkAreaStatus] = useState({
|
||||||
completed: 0,
|
completed: 0,
|
||||||
@ -82,8 +82,7 @@ const WorkArea = ({ workArea, floor, forBuilding }) => {
|
|||||||
aria-controls={`collapse-${workArea.id}`}
|
aria-controls={`collapse-${workArea.id}`}
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
className={`bx me-2 toggle-icon ${
|
className={`bx me-2 toggle-icon ${IsExpandedArea ? "bx-minus-circle" : "bx-plus-circle"
|
||||||
IsExpandedArea ? "bx-minus-circle" : "bx-plus-circle"
|
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
fontSize: "1.2rem",
|
fontSize: "1.2rem",
|
||||||
@ -109,7 +108,7 @@ const WorkArea = ({ workArea, floor, forBuilding }) => {
|
|||||||
<ProgressBar
|
<ProgressBar
|
||||||
completedWork={formatNumber(workArea?.completedWork)}
|
completedWork={formatNumber(workArea?.completedWork)}
|
||||||
plannedWork={formatNumber(workArea?.plannedWork)}
|
plannedWork={formatNumber(workArea?.plannedWork)}
|
||||||
className="m-0 text-info"
|
className="m-0 my-2 me-6 text-info"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -121,12 +120,12 @@ const WorkArea = ({ workArea, floor, forBuilding }) => {
|
|||||||
className="accordion-collapse collapse"
|
className="accordion-collapse collapse"
|
||||||
aria-labelledby={`heading-${workArea.id}`}
|
aria-labelledby={`heading-${workArea.id}`}
|
||||||
>
|
>
|
||||||
<div className="accordion-body px-1">
|
<div className="accordion-body px-6">
|
||||||
{isLoading || ProjectTaskList === undefined ? (
|
{isLoading || ProjectTaskList === undefined ? (
|
||||||
<div className="text-center py-2 text-muted">Loading activities...</div>
|
<div className="text-center py-2 text-muted">Loading activities...</div>
|
||||||
) : ProjectTaskList?.length === 0 ? (
|
) : ProjectTaskList?.length === 0 ? (
|
||||||
<div className="text-center py-2 text-muted">No activities available for this work area.</div>
|
<div className="text-center py-2 text-muted">No activities available for this work area.</div>
|
||||||
):ProjectTaskList?.length > 0 ? (
|
) : ProjectTaskList?.length > 0 ? (
|
||||||
<table className="table table-sm mx-1">
|
<table className="table table-sm mx-1">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -152,7 +151,7 @@ const WorkArea = ({ workArea, floor, forBuilding }) => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="table-border-bottom-0">
|
<tbody className="table-border-bottom-0">
|
||||||
{ProjectTaskList.map((workItem,index) => (
|
{ProjectTaskList.map((workItem, index) => (
|
||||||
<WorkItem
|
<WorkItem
|
||||||
key={workItem.workItemId || `fallback-${index}`}
|
key={workItem.workItemId || `fallback-${index}`}
|
||||||
workItem={workItem}
|
workItem={workItem}
|
||||||
|
|||||||
@ -50,8 +50,8 @@ const WorkAreaModel = ({ project, onSubmit, onClose }) => {
|
|||||||
: "Work Area created Successfully",
|
: "Work Area created Successfully",
|
||||||
"success"
|
"success"
|
||||||
);
|
);
|
||||||
reset({ id: "0", buildingId: "0", areaName: "", floorId: "0" });
|
setValue("id", "0");
|
||||||
// onClose?.();
|
setValue("areaName", "");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -77,13 +77,7 @@ const ManageProject = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isProjectCreated && (
|
|
||||||
<div>
|
|
||||||
<h3>Additional Content</h3>
|
|
||||||
<p>Now that the project is created, you can access this content.</p>
|
|
||||||
{/* Add more content here */}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,11 +1,24 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { projectSchema, projectDefault } from "./ProjectSchema";
|
||||||
import { useForm, Controller } from "react-hook-form";
|
import { useForm, Controller } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { z } from "zod";
|
|
||||||
import Label from "../common/Label";
|
import Label from "../common/Label";
|
||||||
import DatePicker from "../common/DatePicker";
|
import DatePicker from "../common/DatePicker";
|
||||||
|
import { useCreateProject, useProjectDetails, useUpdateProject } from "../../hooks/useProjects";
|
||||||
|
|
||||||
const currentDate = new Date().toLocaleDateString('en-CA');
|
import {
|
||||||
|
DEFAULT_EMPTY_STATUS_ID,
|
||||||
|
ITEMS_PER_PAGE,
|
||||||
|
PROJECT_STATUS,
|
||||||
|
} from "../../utils/constants";
|
||||||
|
import {
|
||||||
|
useOrganizationModal,
|
||||||
|
useOrganizationsList,
|
||||||
|
} from "../../hooks/useOrganization";
|
||||||
|
import { localToUtc } from "../../utils/appUtils";
|
||||||
|
|
||||||
|
const currentDate = new Date().toLocaleDateString("en-CA");
|
||||||
const formatDate = (date) => {
|
const formatDate = (date) => {
|
||||||
if (!date) {
|
if (!date) {
|
||||||
return currentDate;
|
return currentDate;
|
||||||
@ -14,54 +27,23 @@ const formatDate = (date) => {
|
|||||||
if (isNaN(d.getTime())) {
|
if (isNaN(d.getTime())) {
|
||||||
return currentDate;
|
return currentDate;
|
||||||
}
|
}
|
||||||
return d.toLocaleDateString('en-CA');
|
return d.toLocaleDateString("en-CA");
|
||||||
};
|
};
|
||||||
const ManageProjectInfo = ({ project, handleSubmitForm, onClose, isPending }) => {
|
const ManageProjectInfo = ({ project, onClose }) => {
|
||||||
const [CurrentProject, setCurrentProject] = useState();
|
|
||||||
const [addressLength, setAddressLength] = useState(0);
|
const [addressLength, setAddressLength] = useState(0);
|
||||||
const maxAddressLength = 500;
|
const maxAddressLength = 500;
|
||||||
|
const { onOpen, startStep, flowType } = useOrganizationModal();
|
||||||
|
|
||||||
const ACTIVE_STATUS_ID = "b74da4c2-d07e-46f2-9919-e75e49b12731";
|
const ACTIVE_STATUS_ID = "b74da4c2-d07e-46f2-9919-e75e49b12731";
|
||||||
const DEFAULT_EMPTY_STATUS_ID = "00000000-0000-0000-0000-000000000000";
|
|
||||||
|
|
||||||
const projectSchema = z
|
const { projects_Details, loading } = useProjectDetails(project);
|
||||||
.object({
|
const { data, isLoading, isError, error } = useOrganizationsList(
|
||||||
...(project?.id ? { id: z.string().optional() } : {}),
|
ITEMS_PER_PAGE,
|
||||||
name: z.string().min(1, { message: "Project Name is required" }),
|
1,
|
||||||
shortName: z.string().optional(),
|
true
|
||||||
contactPerson: z
|
|
||||||
.string()
|
|
||||||
.min(1, { message: "Contact Person Name is required" })
|
|
||||||
.regex(/^[A-Za-z\s]+$/, {
|
|
||||||
message: "Contact Person must contain only letters",
|
|
||||||
}),
|
|
||||||
projectAddress: z
|
|
||||||
.string()
|
|
||||||
.min(1, { message: "Address is required" })
|
|
||||||
.max(500, "Address must not exceed 150 characters"),
|
|
||||||
startDate: z
|
|
||||||
.string()
|
|
||||||
.min(1, { message: "Start Date is required" })
|
|
||||||
.default(currentDate),
|
|
||||||
endDate: z
|
|
||||||
.string()
|
|
||||||
.min(1, { message: "End Date is required" })
|
|
||||||
.default(currentDate),
|
|
||||||
projectStatusId: z
|
|
||||||
.string()
|
|
||||||
.min(1, { message: "Status is required" })
|
|
||||||
})
|
|
||||||
.refine(
|
|
||||||
(data) => {
|
|
||||||
const start = new Date(data.startDate);
|
|
||||||
const end = new Date(data.endDate);
|
|
||||||
return end >= start;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ["endDate"], // attaches the error to the endDate field
|
|
||||||
message: "End Date must be greater than Start Date",
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
const { mutate: UpdateProject, isPending } = useUpdateProject(() => {onClose?.()});
|
||||||
|
const {mutate:CeateProject,isPending:isCreating}= useCreateProject(()=>onClose?.())
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@ -72,80 +54,67 @@ const ManageProjectInfo = ({ project, handleSubmitForm, onClose, isPending }) =>
|
|||||||
getValues,
|
getValues,
|
||||||
} = useForm({
|
} = useForm({
|
||||||
resolver: zodResolver(projectSchema),
|
resolver: zodResolver(projectSchema),
|
||||||
defaultValues: {
|
defaultValues: projectDefault,
|
||||||
id: project?.id || "",
|
|
||||||
name: project?.name || "",
|
|
||||||
shortName: project?.shortName || "",
|
|
||||||
contactPerson: project?.contactPerson || "",
|
|
||||||
projectAddress: project?.projectAddress || "",
|
|
||||||
startDate: formatDate(project?.startDate) || currentDate,
|
|
||||||
endDate: formatDate(project?.endDate) || currentDate,
|
|
||||||
// projectStatusId: String(project?.projectStatusId || "00000000-0000-0000-0000-000000000000"),
|
|
||||||
projectStatusId: project?.projectStatusId && project.projectStatusId !== DEFAULT_EMPTY_STATUS_ID
|
|
||||||
|
|
||||||
? String(project.projectStatusId)
|
|
||||||
|
|
||||||
: ACTIVE_STATUS_ID,
|
|
||||||
},
|
|
||||||
mode: "onChange",
|
mode: "onChange",
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentProject(project);
|
if (project && projects_Details)
|
||||||
reset(
|
reset({
|
||||||
project
|
name: projects_Details?.name || "",
|
||||||
? {
|
shortName: projects_Details?.shortName || "",
|
||||||
id: project?.id || "",
|
contactPerson: projects_Details?.contactPerson || "",
|
||||||
name: project?.name || "",
|
projectAddress: projects_Details?.projectAddress || "",
|
||||||
shortName: project?.shortName || "",
|
startDate: formatDate(projects_Details?.startDate) || "",
|
||||||
contactPerson: project?.contactPerson || "",
|
endDate: formatDate(projects_Details?.endDate) || "",
|
||||||
projectAddress: project?.projectAddress || "",
|
projectStatusId:
|
||||||
startDate: formatDate(project?.startDate) || "",
|
String(projects_Details?.projectStatus?.id) ||
|
||||||
endDate: formatDate(project?.endDate) || "",
|
DEFAULT_EMPTY_STATUS_IDF,
|
||||||
projectStatusId: String(project?.projectStatus?.id) || "00000000-0000-0000-0000-000000000000",
|
promoterId: projects_Details.promoter.id || "",
|
||||||
|
pmcId: projects_Details.pmc.id || "",
|
||||||
|
});
|
||||||
|
setAddressLength(projects_Details?.projectAddress?.length || 0);
|
||||||
|
}, [project, projects_Details, reset]);
|
||||||
|
|
||||||
|
const onSubmitForm = (formData) => {
|
||||||
|
if (project) {
|
||||||
|
let payload = {
|
||||||
|
...formData,
|
||||||
|
startDate: localToUtc(formData.startDate),
|
||||||
|
endDate: localToUtc(formData.endDate),
|
||||||
|
id: project,
|
||||||
|
};
|
||||||
|
console.log(payload);
|
||||||
|
UpdateProject({ projectId: project, payload: payload });
|
||||||
|
}else{
|
||||||
|
let payload = {
|
||||||
|
...formData,
|
||||||
|
startDate: localToUtc(formData.startDate),
|
||||||
|
endDate: localToUtc(formData.endDate),
|
||||||
|
};
|
||||||
|
CeateProject(payload)
|
||||||
}
|
}
|
||||||
: {}
|
|
||||||
);
|
|
||||||
setAddressLength(project?.projectAddress?.length || 0);
|
|
||||||
}, [project, reset]);
|
|
||||||
|
|
||||||
/**
|
|
||||||
|
|
||||||
* Handles the form submission.
|
|
||||||
|
|
||||||
* @param {object} updatedProject - The project data from the form.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
const onSubmitForm = (updatedProject) => {
|
|
||||||
|
|
||||||
handleSubmitForm(updatedProject);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
reset({
|
reset(projectDefault);
|
||||||
id: project?.id || "",
|
|
||||||
name: project?.name || "",
|
|
||||||
shortName: project?.shortName || "",
|
|
||||||
contactPerson: project?.contactPerson || "",
|
|
||||||
projectAddress: project?.projectAddress || "",
|
|
||||||
startDate: formatDate(project?.startDate) || currentDate,
|
|
||||||
endDate: formatDate(project?.endDate) || currentDate,
|
|
||||||
projectStatusId: String(project?.projectStatus?.id || "00000000-0000-0000-0000-000000000000"),
|
|
||||||
});
|
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
const handleOrganizaioFinder = () => {
|
||||||
|
onClose();
|
||||||
|
onOpen({ startStep: 2, flowType: "default" });
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<div className="p-sm-2 p-2">
|
<div className="p-sm-2 p-2">
|
||||||
|
|
||||||
<div className="text-center mb-2">
|
<div className="text-center mb-2">
|
||||||
<h5 className="mb-2">
|
<h5 className="mb-2">{project ? "Edit Project" : "Create Project"}</h5>
|
||||||
{project?.id ? "Edit Project" : "Create Project"}
|
|
||||||
</h5>
|
|
||||||
</div>
|
</div>
|
||||||
<form className="row g-2 text-start" onSubmit={handleSubmit(onSubmitForm)}>
|
<form
|
||||||
|
className="row g-2 text-start"
|
||||||
|
onSubmit={handleSubmit(onSubmitForm)}
|
||||||
|
>
|
||||||
<div className="col-12 col-md-12">
|
<div className="col-12 col-md-12">
|
||||||
<Label htmlFor="name" required>
|
<Label htmlFor="name" required>
|
||||||
Project Name
|
Project Name
|
||||||
@ -225,7 +194,10 @@ const ManageProjectInfo = ({ project, handleSubmitForm, onClose, isPending }) =>
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{errors.startDate && (
|
{errors.startDate && (
|
||||||
<div className="danger-text text-start" style={{ fontSize: "12px" }}>
|
<div
|
||||||
|
className="danger-text text-start"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
|
>
|
||||||
{errors.startDate.message}
|
{errors.startDate.message}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -245,13 +217,16 @@ const ManageProjectInfo = ({ project, handleSubmitForm, onClose, isPending }) =>
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{errors.endDate && (
|
{errors.endDate && (
|
||||||
<div className="danger-text text-start" style={{ fontSize: "12px" }}>
|
<div
|
||||||
|
className="danger-text text-start"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
|
>
|
||||||
{errors.endDate.message}
|
{errors.endDate.message}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 col-md-6">
|
<div className="col-12 ">
|
||||||
<label className="form-label" htmlFor="modalEditUserStatus">
|
<label className="form-label" htmlFor="modalEditUserStatus">
|
||||||
Status
|
Status
|
||||||
</label>
|
</label>
|
||||||
@ -265,15 +240,11 @@ const ManageProjectInfo = ({ project, handleSubmitForm, onClose, isPending }) =>
|
|||||||
valueAsNumber: false,
|
valueAsNumber: false,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{/* <option disabled>Status</option>
|
{PROJECT_STATUS.map((status) => (
|
||||||
<option value="b74da4c2-d07e-46f2-9919-e75e49b12731">Active</option> */}
|
<option key={status.id} value={status.id}>
|
||||||
<option value={ACTIVE_STATUS_ID}>Active</option>
|
{status.label}
|
||||||
<option value="603e994b-a27f-4e5d-a251-f3d69b0498ba">On Hold</option>
|
</option>
|
||||||
|
))}
|
||||||
<option value="cdad86aa-8a56-4ff4-b633-9c629057dfef">In Progress</option>
|
|
||||||
<option value="ef1c356e-0fe0-42df-a5d3-8daee355492d">Inactive</option>
|
|
||||||
|
|
||||||
<option value="33deaef9-9af1-4f2a-b443-681ea0d04f81">Completed</option>
|
|
||||||
</select>
|
</select>
|
||||||
{errors.projectStatusId && (
|
{errors.projectStatusId && (
|
||||||
<div
|
<div
|
||||||
@ -284,6 +255,83 @@ const ManageProjectInfo = ({ project, handleSubmitForm, onClose, isPending }) =>
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-12 ">
|
||||||
|
<label className="form-label" htmlFor="modalEditUserStatus">
|
||||||
|
Promoter
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="select2 form-select form-select-sm"
|
||||||
|
aria-label="Default select example"
|
||||||
|
{...register("promoterId", {
|
||||||
|
required: "Promoter is required",
|
||||||
|
valueAsNumber: false,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<option>Loading...</option>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<option value="">Select Promoter</option>
|
||||||
|
{data?.data?.map((org) => (
|
||||||
|
<option key={org.id} value={org.id}>
|
||||||
|
{org.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
{errors.promoterId && (
|
||||||
|
<div
|
||||||
|
className="danger-text text-start"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
|
>
|
||||||
|
{errors.promoterId.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="col-12 ">
|
||||||
|
<label className="form-label" htmlFor="modalEditUserStatus">
|
||||||
|
PMC
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="select2 form-select form-select-sm"
|
||||||
|
aria-label="Default select example"
|
||||||
|
{...register("pmcId", {
|
||||||
|
required: "Promoter is required",
|
||||||
|
valueAsNumber: false,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<option>Loading...</option>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<option value="">Select PMC</option>
|
||||||
|
{data?.data?.map((org) => (
|
||||||
|
<option key={org.id} value={org.id}>
|
||||||
|
{org.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
{errors.pmcId && (
|
||||||
|
<div
|
||||||
|
className="danger-text text-start"
|
||||||
|
style={{ fontSize: "12px" }}
|
||||||
|
>
|
||||||
|
{errors.pmcId.message}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="d-flex justify-content-between text-secondary text-tiny text-wrap">
|
||||||
|
<span>
|
||||||
|
<i className="bx bx-sm bx-info-circle"></i> Not found PMC and
|
||||||
|
Pomoter, find through SPRID or create new
|
||||||
|
</span>
|
||||||
|
<small className="cursor-pointer" onClick={handleOrganizaioFinder}>
|
||||||
|
<i className="bx bx-plus-circle text-primary"></i>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="col-12 col-md-12">
|
<div className="col-12 col-md-12">
|
||||||
<Label htmlFor="projectAddress" required>
|
<Label htmlFor="projectAddress" required>
|
||||||
@ -301,6 +349,7 @@ const ManageProjectInfo = ({ project, handleSubmitForm, onClose, isPending }) =>
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-end" style={{ fontSize: "12px" }}>
|
<div className="text-end" style={{ fontSize: "12px" }}>
|
||||||
{maxAddressLength - addressLength} characters left
|
{maxAddressLength - addressLength} characters left
|
||||||
</div>
|
</div>
|
||||||
@ -319,19 +368,18 @@ const ManageProjectInfo = ({ project, handleSubmitForm, onClose, isPending }) =>
|
|||||||
className="btn btn-label-secondary btn-sm me-2"
|
className="btn btn-label-secondary btn-sm me-2"
|
||||||
onClick={handleCancel}
|
onClick={handleCancel}
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
disabled={isPending}
|
disabled={isPending || isCreating}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="btn btn-primary btn-sm"
|
className="btn btn-primary btn-sm"
|
||||||
disabled={isPending}
|
disabled={isPending || isCreating}
|
||||||
>
|
>
|
||||||
{isPending ? "Please Wait..." : project?.id ? "Update" : "Submit"}
|
{isPending||isCreating ? "Please Wait..." : project ? "Update" : "Submit"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,211 +0,0 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import EmployeeRepository from "../../repositories/EmployeeRepository";
|
|
||||||
import { useAllEmployees } from "../../hooks/useEmployees";
|
|
||||||
import useSearch from "../../hooks/useSearch";
|
|
||||||
import AssignEmployeeTable from "./AssignEmployeeTable";
|
|
||||||
import showToast from "../../services/toastService";
|
|
||||||
import "./MapUser.css";
|
|
||||||
|
|
||||||
const MapUsers = ({
|
|
||||||
projectId,
|
|
||||||
onClose,
|
|
||||||
empJobRoles,
|
|
||||||
onSubmit,
|
|
||||||
allocation,
|
|
||||||
assignedLoading,
|
|
||||||
setAssignedLoading,
|
|
||||||
}) => {
|
|
||||||
const {
|
|
||||||
employeesList,
|
|
||||||
loading: employeeLoading,
|
|
||||||
error,
|
|
||||||
} = useAllEmployees(false);
|
|
||||||
const [selectedEmployees, setSelectedEmployees] = useState([]);
|
|
||||||
const [searchText, setSearchText] = useState("");
|
|
||||||
|
|
||||||
const handleAllocationData = Array.isArray(allocation) ? allocation : [];
|
|
||||||
|
|
||||||
const allocationEmployees = employeesList.map((employee) => {
|
|
||||||
const allocationItem = handleAllocationData.find(
|
|
||||||
(alloc) => alloc.employeeId === employee.id
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
...employee,
|
|
||||||
isActive: allocationItem ? allocationItem.isActive : false,
|
|
||||||
jobRoleId: allocationItem ? allocationItem.jobRoleId : employee.jobRoleId,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
function parseDate(dateStr) {
|
|
||||||
return new Date(dateStr.split(".")[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const latestAllocations = handleAllocationData.reduce((acc, alloc) => {
|
|
||||||
const existingAlloc = acc[alloc.employeeId];
|
|
||||||
|
|
||||||
if (!existingAlloc) {
|
|
||||||
acc[alloc.employeeId] = alloc;
|
|
||||||
} else {
|
|
||||||
const existingDate = parseDate(
|
|
||||||
existingAlloc.reAllocationDate || existingAlloc.allocationDate
|
|
||||||
);
|
|
||||||
const newDate = parseDate(alloc.reAllocationDate || alloc.allocationDate);
|
|
||||||
|
|
||||||
if (newDate > existingDate) {
|
|
||||||
acc[alloc.employeeId] = alloc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const allocationEmployeesData = employeesList
|
|
||||||
.map((employee) => {
|
|
||||||
const allocationItem = latestAllocations[employee.id];
|
|
||||||
return {
|
|
||||||
...employee,
|
|
||||||
isActive: allocationItem ? allocationItem.isActive : false,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.filter((employee) => employee.isActive === false);
|
|
||||||
|
|
||||||
const { filteredData, setSearchQuery } = useSearch(
|
|
||||||
allocationEmployeesData,
|
|
||||||
searchText
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleRoleChange = (employeeId, newRoleId) => {
|
|
||||||
setSelectedEmployees((prevSelectedEmployees) =>
|
|
||||||
prevSelectedEmployees.map((emp) =>
|
|
||||||
emp.id === employeeId ? { ...emp, jobRoleId: newRoleId } : emp
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCheckboxChange = (employeeId) => {
|
|
||||||
setSelectedEmployees((prevSelectedEmployees) => {
|
|
||||||
const updatedEmployees = [...prevSelectedEmployees];
|
|
||||||
const employeeIndex = updatedEmployees.findIndex(
|
|
||||||
(emp) => emp.id === employeeId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (employeeIndex !== -1) {
|
|
||||||
const isSelected = !updatedEmployees[employeeIndex].isSelected;
|
|
||||||
updatedEmployees[employeeIndex].isSelected = isSelected;
|
|
||||||
} else {
|
|
||||||
updatedEmployees.push({
|
|
||||||
id: employeeId,
|
|
||||||
isSelected: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return updatedEmployees;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
setAssignedLoading(true);
|
|
||||||
const selected = selectedEmployees
|
|
||||||
.filter((emp) => emp.isSelected)
|
|
||||||
.map((emp) => ({ empID: emp.id, jobRoleId: emp.jobRoleId }));
|
|
||||||
if (selected.length > 0) {
|
|
||||||
onSubmit(selected);
|
|
||||||
setSelectedEmployees([]);
|
|
||||||
} else {
|
|
||||||
showToast("Please select Employee", "error");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="modal-dialog modal-dialog-scrollable mx-sm-auto mx-1 modal-lg modal-simple modal-edit-user">
|
|
||||||
<div className="modal-content">
|
|
||||||
<div className="modal-header text-center">
|
|
||||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p className="m-0 fw-semibold fs-5">Assign Employee</p>
|
|
||||||
|
|
||||||
<div className="px-4 mt-4 col-md-4 text-start">
|
|
||||||
{(filteredData.length > 0 ||
|
|
||||||
allocationEmployeesData.length > 0) && (
|
|
||||||
<div className="input-group input-group-sm mb-2">
|
|
||||||
<input
|
|
||||||
type="search"
|
|
||||||
className="form-control"
|
|
||||||
placeholder="Search employees..."
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<p className="mb-0 small text-muted fw-semibold">Select Employee</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="modal-body p-sm-4 p-0">
|
|
||||||
<table
|
|
||||||
className="datatables-users table border-top dataTable no-footer dtr-column "
|
|
||||||
id="DataTables_Table_0"
|
|
||||||
aria-describedby="DataTables_Table_0_info"
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
>
|
|
||||||
<tbody>
|
|
||||||
{employeeLoading && allocationEmployeesData.length === 0 && (
|
|
||||||
<tr>
|
|
||||||
<td>Loading..</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!employeeLoading &&
|
|
||||||
allocationEmployeesData.length === 0 &&
|
|
||||||
filteredData.length === 0 && (
|
|
||||||
<tr>
|
|
||||||
<td>All employee assigned to Project.</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!employeeLoading &&
|
|
||||||
allocationEmployeesData.length > 0 &&
|
|
||||||
filteredData.length === 0 && (
|
|
||||||
<tr>
|
|
||||||
<td>No matching employees found.</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(filteredData.length > 0 ||
|
|
||||||
allocationEmployeesData.length > 0) &&
|
|
||||||
filteredData.map((emp) => (
|
|
||||||
<AssignEmployeeTable
|
|
||||||
key={emp.id}
|
|
||||||
employee={emp}
|
|
||||||
jobRoles={empJobRoles}
|
|
||||||
isChecked={emp.isSelected}
|
|
||||||
onRoleChange={handleRoleChange}
|
|
||||||
onCheckboxChange={handleCheckboxChange}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div className="modal-footer mt-5 d-flex justify-content-end gap-0">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-sm btn-label-secondary"
|
|
||||||
data-dismiss="modal"
|
|
||||||
aria-label="Close"
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{(filteredData.length > 0 || allocationEmployeesData.length > 0) && (
|
|
||||||
<button className="btn btn-sm btn-primary" onClick={handleSubmit}>
|
|
||||||
{assignedLoading ? "Please Wait..." : "Assign to Project"}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MapUsers;
|
|
||||||
@ -14,41 +14,15 @@ import {
|
|||||||
getProjectStatusName,
|
getProjectStatusName,
|
||||||
} from "../../utils/projectStatus";
|
} from "../../utils/projectStatus";
|
||||||
import GlobalModel from "../common/GlobalModel";
|
import GlobalModel from "../common/GlobalModel";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
import { setProjectId } from "../../slices/localVariablesSlice";
|
import { setProjectId } from "../../slices/localVariablesSlice";
|
||||||
|
import { useProjectContext } from "../../pages/project/ProjectPage";
|
||||||
|
|
||||||
const ProjectCard = ({ projectData, recall }) => {
|
const ProjectCard = ({ project }) => {
|
||||||
const [ projectInfo, setProjectInfo ] = useState( projectData );
|
const dispatch = useDispatch();
|
||||||
const { projects_Details, loading, error, refetch } = useProjectDetails(
|
|
||||||
projectInfo?.id,false
|
|
||||||
);
|
|
||||||
const [showModal, setShowModal] = useState(false);
|
|
||||||
const dispatch = useDispatch()
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const ManageProject = useHasUserPermission(MANAGE_PROJECT);
|
const ManageProject = useHasUserPermission(MANAGE_PROJECT);
|
||||||
const {
|
const { setMangeProject } = useProjectContext();
|
||||||
mutate: updateProject,
|
|
||||||
isPending,
|
|
||||||
isSuccess,
|
|
||||||
isError,
|
|
||||||
} = useUpdateProject({
|
|
||||||
onSuccessCallback: () => {
|
|
||||||
setShowModal(false);
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
useEffect(()=>{
|
|
||||||
setProjectInfo(projectData);
|
|
||||||
}, [ projectData ] )
|
|
||||||
|
|
||||||
const handleShow = async () => {
|
|
||||||
try {
|
|
||||||
const { data } = await refetch();
|
|
||||||
setShowModal(true);
|
|
||||||
} catch (err) {
|
|
||||||
showToast("Failed to load project details", "error");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getProgress = (planned, completed) => {
|
const getProgress = (planned, completed) => {
|
||||||
return (completed * 100) / planned + "%";
|
return (completed * 100) / planned + "%";
|
||||||
@ -60,35 +34,18 @@ const ProjectCard = ({ projectData, recall }) => {
|
|||||||
const handleClose = () => setShowModal(false);
|
const handleClose = () => setShowModal(false);
|
||||||
|
|
||||||
const handleViewProject = () => {
|
const handleViewProject = () => {
|
||||||
dispatch(setProjectId(projectInfo.id))
|
dispatch(setProjectId(project.id));
|
||||||
navigate(`/projects/details`);
|
navigate(`/projects/details`);
|
||||||
};
|
};
|
||||||
|
const handleViewActivities = () => {
|
||||||
const handleFormSubmit = (updatedProject) => {
|
dispatch(setProjectId(project.id));
|
||||||
if (projectInfo?.id) {
|
navigate(`/activities/records?project=${project.id}`);
|
||||||
updateProject({
|
};
|
||||||
projectId: projectInfo.id,
|
|
||||||
updatedData: updatedProject,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
||||||
{showModal && projects_Details && (
|
|
||||||
<GlobalModel isOpen={showModal} closeModal={handleClose}>
|
|
||||||
<ManageProjectInfo
|
|
||||||
project={projects_Details}
|
|
||||||
handleSubmitForm={handleFormSubmit}
|
|
||||||
onClose={handleClose}
|
|
||||||
isPending={isPending}
|
|
||||||
/>
|
|
||||||
</GlobalModel>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="col-md-6 col-lg-4 col-xl-4 order-0 mb-4">
|
<div className="col-md-6 col-lg-4 col-xl-4 order-0 mb-4">
|
||||||
<div className={`card cursor-pointer ${isPending ? "bg-light opacity-50 pointer-events-none" : ""}`}>
|
<div className={`card cursor-pointer`}>
|
||||||
<div className="card-header pb-4">
|
<div className="card-header pb-4">
|
||||||
<div className="d-flex align-items-start">
|
<div className="d-flex align-items-start">
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center">
|
||||||
@ -103,12 +60,10 @@ const ProjectCard = ({ projectData, recall }) => {
|
|||||||
className="mb-0 stretched-link text-heading text-start"
|
className="mb-0 stretched-link text-heading text-start"
|
||||||
onClick={handleViewProject}
|
onClick={handleViewProject}
|
||||||
>
|
>
|
||||||
{projectInfo.shortName
|
{project?.shortName ? project?.shortName : project?.name}
|
||||||
? projectInfo.shortName
|
|
||||||
: projectInfo.name}
|
|
||||||
</h5>
|
</h5>
|
||||||
<div className="client-info text-body">
|
<div className="client-info text-body">
|
||||||
<span>{projectInfo.shortName ? projectInfo.name : ""}</span>
|
<span>{project.shortName ? project.name : ""}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -120,14 +75,6 @@ const ProjectCard = ({ projectData, recall }) => {
|
|||||||
data-bs-toggle="dropdown"
|
data-bs-toggle="dropdown"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
>
|
>
|
||||||
{loading ? (
|
|
||||||
<div
|
|
||||||
className="spinner-border spinner-border-sm text-secondary"
|
|
||||||
role="status"
|
|
||||||
>
|
|
||||||
<span className="visually-hidden">Loading...</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<i
|
<i
|
||||||
className="bx bx-dots-vertical-rounded bx-sm text-muted"
|
className="bx bx-dots-vertical-rounded bx-sm text-muted"
|
||||||
data-bs-toggle="tooltip"
|
data-bs-toggle="tooltip"
|
||||||
@ -136,7 +83,6 @@ const ProjectCard = ({ projectData, recall }) => {
|
|||||||
data-bs-custom-class="tooltip-dark"
|
data-bs-custom-class="tooltip-dark"
|
||||||
title="More Action"
|
title="More Action"
|
||||||
></i>
|
></i>
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
<ul className="dropdown-menu dropdown-menu-end">
|
<ul className="dropdown-menu dropdown-menu-end">
|
||||||
<li>
|
<li>
|
||||||
@ -150,19 +96,18 @@ const ProjectCard = ({ projectData, recall }) => {
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li onClick={handleShow}>
|
<li>
|
||||||
<a className="dropdown-item">
|
<a className="dropdown-item" onClick={() =>
|
||||||
|
setMangeProject({
|
||||||
|
isOpen: true,
|
||||||
|
Project: project.id,
|
||||||
|
})
|
||||||
|
}>
|
||||||
<i className="bx bx-pencil me-2"></i>
|
<i className="bx bx-pencil me-2"></i>
|
||||||
<span className="align-left">Modify</span>
|
<span className="align-left">Modify</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li
|
<li onClick={handleViewActivities}>
|
||||||
onClick={() =>
|
|
||||||
navigate(
|
|
||||||
`/activities/records?project=${projectInfo.id}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<a className="dropdown-item">
|
<a className="dropdown-item">
|
||||||
<i className="bx bx-task me-2"></i>
|
<i className="bx bx-task me-2"></i>
|
||||||
<span className="align-left">Activities</span>
|
<span className="align-left">Activities</span>
|
||||||
@ -180,22 +125,22 @@ const ProjectCard = ({ projectData, recall }) => {
|
|||||||
<span className="text-heading fw-medium">
|
<span className="text-heading fw-medium">
|
||||||
Contact Person:{" "}
|
Contact Person:{" "}
|
||||||
</span>
|
</span>
|
||||||
{projectInfo.contactPerson ? projectInfo.contactPerson : "NA"}
|
{project.contactPerson ? project.contactPerson : "NA"}
|
||||||
</p>
|
</p>
|
||||||
<p className="mb-1">
|
<p className="mb-1">
|
||||||
<span className="text-heading fw-medium">Start Date: </span>
|
<span className="text-heading fw-medium">Start Date: </span>
|
||||||
{projectInfo.startDate
|
{project.startDate
|
||||||
? moment(projectInfo.startDate).format("DD-MMM-YYYY")
|
? moment(project.startDate).format("DD-MMM-YYYY")
|
||||||
: "NA"}
|
: "NA"}
|
||||||
</p>
|
</p>
|
||||||
<p className="mb-1">
|
<p className="mb-1">
|
||||||
<span className="text-heading fw-medium">Deadline: </span>
|
<span className="text-heading fw-medium">Deadline: </span>
|
||||||
|
|
||||||
{projectInfo.endDate
|
{project.endDate
|
||||||
? moment(projectInfo.endDate).format("DD-MMM-YYYY")
|
? moment(project.endDate).format("DD-MMM-YYYY")
|
||||||
: "NA"}
|
: "NA"}
|
||||||
</p>
|
</p>
|
||||||
<p className="mb-0">{projectInfo.projectAddress}</p>
|
<p className="mb-0">{project.projectAddress}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -205,36 +150,37 @@ const ProjectCard = ({ projectData, recall }) => {
|
|||||||
<span
|
<span
|
||||||
className={
|
className={
|
||||||
`badge rounded-pill ` +
|
`badge rounded-pill ` +
|
||||||
getProjectStatusColor(projectInfo.projectStatusId)
|
getProjectStatusColor(project.projectStatusId)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{getProjectStatusName(projectInfo.projectStatusId)}
|
{getProjectStatusName(project.projectStatusId)}
|
||||||
</span>
|
</span>
|
||||||
</p>{" "}
|
</p>{" "}
|
||||||
{getDateDifferenceInDays(projectInfo.endDate, Date()) >= 0 && (
|
{getDateDifferenceInDays(project.endDate, Date()) >= 0 && (
|
||||||
<span className="badge bg-label-success ms-auto">
|
<span className="badge bg-label-success ms-auto">
|
||||||
{projectInfo.endDate &&
|
{project.endDate &&
|
||||||
getDateDifferenceInDays(projectInfo.endDate, Date())}{" "}
|
getDateDifferenceInDays(project.endDate, Date())}{" "}
|
||||||
Days left
|
Days left
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{getDateDifferenceInDays(projectInfo.endDate, Date()) < 0 && (
|
{getDateDifferenceInDays(project.endDate, Date()) < 0 && (
|
||||||
<span className="badge bg-label-danger ms-auto">
|
<span className="badge bg-label-danger ms-auto">
|
||||||
{projectInfo.endDate &&
|
{project.endDate &&
|
||||||
getDateDifferenceInDays(projectInfo.endDate, Date())}{" "}
|
getDateDifferenceInDays(project.endDate, Date())}{" "}
|
||||||
Days overdue
|
Days overdue
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="d-flex justify-content-between align-items-center mb-2">
|
<div className="d-flex justify-content-between align-items-center mb-2">
|
||||||
<small className="text-body">
|
<small className="text-body">
|
||||||
Task: {formatNumber(projectInfo.completedWork)} / {formatNumber(projectInfo.plannedWork)}
|
Task: {formatNumber(project.completedWork)} /{" "}
|
||||||
|
{formatNumber(project.plannedWork)}
|
||||||
</small>
|
</small>
|
||||||
<small className="text-body">
|
<small className="text-body">
|
||||||
{Math.floor(
|
{Math.floor(
|
||||||
getProgressInNumber(
|
getProgressInNumber(
|
||||||
projectInfo.plannedWork,
|
project.plannedWork,
|
||||||
projectInfo.completedWork
|
project.completedWork
|
||||||
)
|
)
|
||||||
) || 0}{" "}
|
) || 0}{" "}
|
||||||
% Completed
|
% Completed
|
||||||
@ -246,22 +192,20 @@ const ProjectCard = ({ projectData, recall }) => {
|
|||||||
role="progressbar"
|
role="progressbar"
|
||||||
style={{
|
style={{
|
||||||
width: getProgress(
|
width: getProgress(
|
||||||
projectInfo.plannedWork,
|
project.plannedWork,
|
||||||
projectInfo.completedWork
|
project.completedWork
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
aria-valuenow={projectInfo.completedWork}
|
aria-valuenow={project.completedWork}
|
||||||
aria-valuemin="0"
|
aria-valuemin="0"
|
||||||
aria-valuemax={projectInfo.plannedWork}
|
aria-valuemax={project.plannedWork}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="d-flex align-items-center justify-content-between">
|
<div className="d-flex align-items-center justify-content-between">
|
||||||
{/* <div className="d-flex align-items-center ">
|
|
||||||
</div> */}
|
|
||||||
<div>
|
<div>
|
||||||
<a className="text-muted d-flex " alt="Active team size">
|
<a className="text-muted d-flex " alt="Active team size">
|
||||||
<i className="bx bx-group bx-sm me-1_5"></i>
|
<i className="bx bx-group bx-sm me-1_5"></i>
|
||||||
{projectInfo?.teamSize} Members
|
{project?.teamSize} Members
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
70
src/components/Project/ProjectCardView.jsx
Normal file
70
src/components/Project/ProjectCardView.jsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { useProjects } from '../../hooks/useProjects'
|
||||||
|
import Loader from '../common/Loader'
|
||||||
|
import ProjectCard from './ProjectCard'
|
||||||
|
|
||||||
|
const ProjectCardView = ({currentItems,setCurrentPage,totalPages }) => {
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
|
||||||
|
<div className="row page-min-h">
|
||||||
|
|
||||||
|
{ currentItems.length === 0 && (
|
||||||
|
<p className="text-center text-muted">No projects found.</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentItems.map((project) => (
|
||||||
|
<ProjectCard
|
||||||
|
key={project.id}
|
||||||
|
project={project}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
|
||||||
|
{ totalPages > 1 && (
|
||||||
|
<nav>
|
||||||
|
<ul className="pagination pagination-sm justify-content-end py-2">
|
||||||
|
<li className={`page-item ${currentPage === 1 && "disabled"}`}>
|
||||||
|
<button
|
||||||
|
className="page-link"
|
||||||
|
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
||||||
|
>
|
||||||
|
«
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{[...Array(totalPages)].map((_, i) => (
|
||||||
|
<li
|
||||||
|
key={i}
|
||||||
|
className={`page-item ${currentPage === i + 1 && "active"}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="page-link"
|
||||||
|
onClick={() => setCurrentPage(i + 1)}
|
||||||
|
>
|
||||||
|
{i + 1}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<li
|
||||||
|
className={`page-item ${currentPage === totalPages && "disabled"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="page-link"
|
||||||
|
onClick={() =>
|
||||||
|
setCurrentPage((p) => Math.min(totalPages, p + 1))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
»
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProjectCardView
|
||||||
@ -17,84 +17,140 @@ import {
|
|||||||
getCachedData,
|
getCachedData,
|
||||||
useSelectedProject,
|
useSelectedProject,
|
||||||
} from "../../slices/apiDataManager";
|
} from "../../slices/apiDataManager";
|
||||||
import { useProjectDetails, useProjectInfra } from "../../hooks/useProjects";
|
import {
|
||||||
|
useCurrentService,
|
||||||
|
useProjectAssignedServices,
|
||||||
|
useProjectDetails,
|
||||||
|
useProjectInfra,
|
||||||
|
} from "../../hooks/useProjects";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { refreshData } from "../../slices/localVariablesSlice";
|
import { refreshData } from "../../slices/localVariablesSlice";
|
||||||
import eventBus from "../../services/eventBus";
|
import eventBus from "../../services/eventBus";
|
||||||
import {useParams} from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import GlobalModel from "../common/GlobalModel";
|
import GlobalModel from "../common/GlobalModel";
|
||||||
|
import { setService } from "../../slices/globalVariablesSlice";
|
||||||
|
|
||||||
const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) =>
|
const ProjectInfra = ({ data, onDataChange, eachSiteEngineer }) => {
|
||||||
{
|
|
||||||
// const projectId = useSelector((store)=>store.localVariables.projectId)
|
|
||||||
const projectId = useSelectedProject();
|
const projectId = useSelectedProject();
|
||||||
|
const selectedService = useCurrentService();
|
||||||
const reloadedData = useSelector((store) => store.localVariables.reload);
|
const reloadedData = useSelector((store) => store.localVariables.reload);
|
||||||
const [ expandedBuildings, setExpandedBuildings ] = useState( [] );
|
const [expandedBuildings, setExpandedBuildings] = useState([]);
|
||||||
const {projectInfra,isLoading,error} = useProjectInfra(projectId)
|
const { projectInfra, isLoading, error } = useProjectInfra(
|
||||||
|
projectId,
|
||||||
|
selectedService
|
||||||
|
);
|
||||||
const { projects_Details, refetch, loading } = useProjectDetails(data?.id);
|
const { projects_Details, refetch, loading } = useProjectDetails(data?.id);
|
||||||
const [ project, setProject ] = useState( projects_Details );
|
const [project, setProject] = useState(projects_Details);
|
||||||
const ManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA);
|
const ManageInfra = useHasUserPermission(MANAGE_PROJECT_INFRA);
|
||||||
const ManageTask = useHasUserPermission(MANAGE_TASK)
|
const ManageTask = useHasUserPermission(MANAGE_TASK);
|
||||||
const [showModalFloor, setshowModalFloor] = useState(false);
|
const [showModalFloor, setshowModalFloor] = useState(false);
|
||||||
const [showModalWorkArea, setshowModalWorkArea] = useState(false);
|
const [showModalWorkArea, setshowModalWorkArea] = useState(false);
|
||||||
const [showModalTask, setshowModalTask] = useState(false);
|
const [showModalTask, setshowModalTask] = useState(false);
|
||||||
const [showModalBuilding, setshowModalBuilding] = useState(false);
|
const [showModalBuilding, setshowModalBuilding] = useState(false);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const { data: assignedServices, isLoading: servicesLoading } =
|
||||||
|
useProjectAssignedServices(projectId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setProject(projectInfra);
|
setProject(projectInfra);
|
||||||
}, [data, projects_Details]);
|
}, [data, projects_Details]);
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (reloadedData) {
|
|
||||||
// refetch();
|
|
||||||
// dispatch(refreshData(false));
|
|
||||||
// }
|
|
||||||
// }, [reloadedData]);
|
|
||||||
|
|
||||||
const signalRHandler = (response) => {
|
const signalRHandler = (response) => {
|
||||||
setProject(response);
|
setProject(response);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{showModalBuilding && <GlobalModel isOpen={showModalBuilding} size="md" closeModal={() => setshowModalBuilding( false )}>
|
{showModalBuilding && (
|
||||||
|
<GlobalModel
|
||||||
|
isOpen={showModalBuilding}
|
||||||
|
size="md"
|
||||||
|
closeModal={() => setshowModalBuilding(false)}
|
||||||
|
>
|
||||||
<BuildingModel
|
<BuildingModel
|
||||||
project={projectInfra}
|
project={projectInfra}
|
||||||
onClose={() => setshowModalBuilding( false )}
|
onClose={() => setshowModalBuilding(false)}
|
||||||
/>
|
/>
|
||||||
</GlobalModel>}
|
</GlobalModel>
|
||||||
{showModalFloor && <GlobalModel isOpen={showModalFloor} size="md" closeModal={()=>setshowModalFloor(false)}>
|
)}
|
||||||
|
{showModalFloor && (
|
||||||
|
<GlobalModel
|
||||||
|
isOpen={showModalFloor}
|
||||||
|
size="md"
|
||||||
|
closeModal={() => setshowModalFloor(false)}
|
||||||
|
>
|
||||||
<FloorModel
|
<FloorModel
|
||||||
project={projectInfra}
|
project={projectInfra}
|
||||||
onClose={()=>setshowModalFloor(false)}
|
onClose={() => setshowModalFloor(false)}
|
||||||
/>
|
/>
|
||||||
</GlobalModel>}
|
</GlobalModel>
|
||||||
{showModalWorkArea && <GlobalModel isOpen={showModalWorkArea} size="lg" closeModal={()=>setshowModalWorkArea(false)} >
|
)}
|
||||||
|
{showModalWorkArea && (
|
||||||
|
<GlobalModel
|
||||||
|
isOpen={showModalWorkArea}
|
||||||
|
size="lg"
|
||||||
|
closeModal={() => setshowModalWorkArea(false)}
|
||||||
|
>
|
||||||
<WorkAreaModel
|
<WorkAreaModel
|
||||||
project={projectInfra}
|
project={projectInfra}
|
||||||
onClose={()=>setshowModalWorkArea(false)}
|
onClose={() => setshowModalWorkArea(false)}
|
||||||
/>
|
/>
|
||||||
</GlobalModel>}
|
</GlobalModel>
|
||||||
{showModalTask && ( <GlobalModel isOpen={showModalTask} size="lg" closeModal={()=>setshowModalTask(false)}>
|
)}
|
||||||
|
{showModalTask && (
|
||||||
|
<GlobalModel
|
||||||
|
isOpen={showModalTask}
|
||||||
|
size="lg"
|
||||||
|
closeModal={() => setshowModalTask(false)}
|
||||||
|
>
|
||||||
<TaskModel
|
<TaskModel
|
||||||
project={projectInfra}
|
project={projectInfra}
|
||||||
onClose={()=>setshowModalTask(false)}
|
onClose={() => setshowModalTask(false)}
|
||||||
/>
|
/>
|
||||||
</GlobalModel>)}
|
</GlobalModel>
|
||||||
|
)}
|
||||||
<div className="col-md-12 col-lg-12 col-xl-12 order-0 mb-4">
|
<div className="col-md-12 col-lg-12 col-xl-12 order-0 mb-4">
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-body" style={{ padding: "0.5rem" }}>
|
<div className="card-body" style={{ padding: "0.5rem" }}>
|
||||||
<div className="align-items-center">
|
<div className="align-items-center">
|
||||||
<div className="row ">
|
<div className="row ">
|
||||||
<div
|
<div
|
||||||
className={`col-12 text-end mb-1 `}
|
className="dataTables_length text-start py-2 px-6 col-md-4 col-12"
|
||||||
|
id="DataTables_Table_0_length"
|
||||||
>
|
>
|
||||||
{ManageInfra && (<>
|
{!servicesLoading &&
|
||||||
|
assignedServices?.length > 0 &&
|
||||||
|
(assignedServices.length > 1 ? (
|
||||||
|
<label>
|
||||||
|
<select
|
||||||
|
name="DataTables_Table_0_length"
|
||||||
|
aria-controls="DataTables_Table_0"
|
||||||
|
className="form-select form-select-sm"
|
||||||
|
aria-label="Select Service"
|
||||||
|
value={selectedService}
|
||||||
|
onChange={(e) => dispatch(setService(e.target.value))}
|
||||||
|
>
|
||||||
|
<option value="">All Services</option>
|
||||||
|
{assignedServices.map((service) => (
|
||||||
|
<option key={service.id} value={service.id}>
|
||||||
|
{service.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
) : (
|
||||||
|
<h5>{assignedServices[0].name}</h5>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Buttons Section (aligned to right) */}
|
||||||
|
<div className="col-md-8 col-12 text-end mb-1">
|
||||||
|
{ManageInfra && (
|
||||||
|
<>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="link-button btn btn-xs rounded-md link-button-sm m-1 btn-primary"
|
className="link-button btn btn-xs rounded-md link-button-sm m-1 btn-primary"
|
||||||
onClick={()=>setshowModalBuilding(true)}
|
onClick={() => setshowModalBuilding(true)}
|
||||||
>
|
>
|
||||||
<i className="bx bx-plus-circle me-2"></i>
|
<i className="bx bx-plus-circle me-2"></i>
|
||||||
Manage Building
|
Manage Building
|
||||||
@ -102,7 +158,7 @@ const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) =>
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="link-button btn btn-xs rounded-md m-1 btn-primary"
|
className="link-button btn btn-xs rounded-md m-1 btn-primary"
|
||||||
onClick={()=>setshowModalFloor(true)}
|
onClick={() => setshowModalFloor(true)}
|
||||||
>
|
>
|
||||||
<i className="bx bx-plus-circle me-2"></i>
|
<i className="bx bx-plus-circle me-2"></i>
|
||||||
Manage Floors
|
Manage Floors
|
||||||
@ -114,16 +170,20 @@ const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) =>
|
|||||||
>
|
>
|
||||||
<i className="bx bx-plus-circle me-2"></i>
|
<i className="bx bx-plus-circle me-2"></i>
|
||||||
Manage Work Areas
|
Manage Work Areas
|
||||||
</button></>)}
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{(ManageTask || ManageInfra) && (<button
|
{(ManageTask || ManageInfra) && (
|
||||||
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="link-button btn btn-xs rounded-md m-1 btn-primary"
|
className="link-button btn btn-xs rounded-md m-1 btn-primary"
|
||||||
onClick={()=>setshowModalTask(true)}
|
onClick={() => setshowModalTask(true)}
|
||||||
>
|
>
|
||||||
<i className="bx bx-plus-circle me-2"></i>
|
<i className="bx bx-plus-circle me-2"></i>
|
||||||
Create Tasks
|
Create Tasks
|
||||||
</button>)}
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="row ">
|
<div className="row ">
|
||||||
@ -132,11 +192,14 @@ const ProjectInfra = ( {data, onDataChange, eachSiteEngineer} ) =>
|
|||||||
<InfraTable
|
<InfraTable
|
||||||
buildings={projectInfra}
|
buildings={projectInfra}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
// handleFloor={submitData}
|
serviceId={selectedService}
|
||||||
// signalRHandler ={signalRHandler}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isLoading && projectInfra?.length == 0 && <div className="mt-5"><p>No Infra Avaiable</p></div>}
|
{!isLoading && projectInfra?.length == 0 && (
|
||||||
|
<div className="mt-5">
|
||||||
|
<p>No Infra Avaiable</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
280
src/components/Project/ProjectListView.jsx
Normal file
280
src/components/Project/ProjectListView.jsx
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { MANAGE_PROJECT, PROJECT_STATUS } from "../../utils/constants";
|
||||||
|
import { useProjects } from "../../hooks/useProjects";
|
||||||
|
import { formatNumber, formatUTCToLocalTime } from "../../utils/dateUtils";
|
||||||
|
import ProgressBar from "../common/ProgressBar";
|
||||||
|
import {
|
||||||
|
getProjectStatusColor,
|
||||||
|
getProjectStatusName,
|
||||||
|
} from "../../utils/projectStatus";
|
||||||
|
import { useDispatch } from "react-redux";
|
||||||
|
import { setProjectId } from "../../slices/localVariablesSlice";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||||
|
import { useProjectContext } from "../../pages/project/ProjectPage";
|
||||||
|
import usePagination from "../../hooks/usePagination";
|
||||||
|
|
||||||
|
const ProjectListView = ({
|
||||||
|
currentItems,
|
||||||
|
selectedStatuses,
|
||||||
|
handleStatusChange,
|
||||||
|
setCurrentPage,
|
||||||
|
totalPages,
|
||||||
|
isLoading,
|
||||||
|
}) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { setMangeProject } = useProjectContext();
|
||||||
|
// const { data, isLoading, isError, error } = useProjects();
|
||||||
|
|
||||||
|
// check Permissions
|
||||||
|
const canManageProject = useHasUserPermission(MANAGE_PROJECT);
|
||||||
|
|
||||||
|
const projectColumns = [
|
||||||
|
{
|
||||||
|
key: "projectName",
|
||||||
|
label: "Project Name",
|
||||||
|
className: "text-start py-3",
|
||||||
|
getValue: (p) => (
|
||||||
|
<div
|
||||||
|
className="text-primary cursor-pointer fw-bold py-3"
|
||||||
|
onClick={() => {
|
||||||
|
dispatch(setProjectId(p.id));
|
||||||
|
navigate(`/projects/details`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{p.shortName ? `${p.name} (${p.shortName})` : p.name}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "contactPerson",
|
||||||
|
label: "Contact Person",
|
||||||
|
className: "text-start small",
|
||||||
|
getValue: (p) => `${p?.contactPerson ?? ""}`.trim() || "N/A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "startDate",
|
||||||
|
label: "Start Date",
|
||||||
|
className: "text-center small",
|
||||||
|
getValue: (p) => formatUTCToLocalTime(p?.startDate) || "N/A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "deadline",
|
||||||
|
label: "Deadline",
|
||||||
|
className: "text-center small",
|
||||||
|
getValue: (p) => formatUTCToLocalTime(p?.endDate) || "N/A",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "task",
|
||||||
|
label: "Task",
|
||||||
|
colSpan: 2,
|
||||||
|
className: "text-center small",
|
||||||
|
getValue: (p) => formatNumber(p?.plannedWork) || "0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "progress",
|
||||||
|
label: "Progress",
|
||||||
|
className: "text-start small",
|
||||||
|
|
||||||
|
getValue: (p) => (
|
||||||
|
<ProgressBar
|
||||||
|
plannedWork={p.plannedWork}
|
||||||
|
completedWork={p.completedWork}
|
||||||
|
className="mb-0"
|
||||||
|
height="6px"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "status",
|
||||||
|
label: "Status",
|
||||||
|
className: "text-center small",
|
||||||
|
isFilter: true,
|
||||||
|
customRender: (_, selectedStatuses, handleStatusChange) => (
|
||||||
|
<div className="dropdown">
|
||||||
|
<a
|
||||||
|
className="dropdown-toggle hide-arrow cursor-pointer"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
Status <i className="bx bx-filter bx-sm"></i>
|
||||||
|
</a>
|
||||||
|
<ul className="dropdown-menu p-2 text-capitalize">
|
||||||
|
{PROJECT_STATUS.map(({ id, label }) => (
|
||||||
|
<li key={id}>
|
||||||
|
<div className="form-check">
|
||||||
|
<input
|
||||||
|
className="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedStatuses.includes(id)}
|
||||||
|
onChange={() => handleStatusChange(id)}
|
||||||
|
/>
|
||||||
|
<label className="form-check-label">{label}</label>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
getValue: (p) => (
|
||||||
|
<span className={`badge ${getProjectStatusColor(p.projectStatusId)}`}>
|
||||||
|
{getProjectStatusName(p.projectStatusId)}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleViewActivities = (project) => {
|
||||||
|
dispatch(setProjectId(project));
|
||||||
|
navigate(`/activities/records?project=${project}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card page-min-h py-4 px-6 shadow-sm">
|
||||||
|
<table className="table table-hover align-middle m-0">
|
||||||
|
<thead className="border-bottom">
|
||||||
|
<tr>
|
||||||
|
{projectColumns.map((col) => (
|
||||||
|
<th key={col.key} colSpan={col.colSpan} className={col.className}>
|
||||||
|
{col.label}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
<th className="text-center py-3">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{currentItems?.map((project) => (
|
||||||
|
<tr key={project.id}>
|
||||||
|
{projectColumns.map((col) => (
|
||||||
|
<td
|
||||||
|
key={col.key}
|
||||||
|
colSpan={col.colSpan}
|
||||||
|
className={`${col.className} py-5`}
|
||||||
|
style={{ paddingTop: "20px", paddingBottom: "20px" }}
|
||||||
|
>
|
||||||
|
{col.getValue
|
||||||
|
? col.getValue(project)
|
||||||
|
: project[col.key] || "N/A"}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
<td
|
||||||
|
className={`mx-2 ${
|
||||||
|
canManageProject ? "d-sm-table-cell" : "d-none"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="dropdown z-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="bx bx-dots-vertical-rounded bx-sm text-muted"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-offset="0,8"
|
||||||
|
data-bs-placement="top"
|
||||||
|
data-bs-custom-class="tooltip-dark"
|
||||||
|
title="More Action"
|
||||||
|
></i>
|
||||||
|
</button>
|
||||||
|
<ul className="dropdown-menu dropdown-menu-end">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
aria-label="click to View details"
|
||||||
|
className="dropdown-item cursor-pointer"
|
||||||
|
>
|
||||||
|
<i className="bx bx-detail me-2"></i>
|
||||||
|
<span className="align-left">View details</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
className="dropdown-item cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setMangeProject({
|
||||||
|
isOpen: true,
|
||||||
|
Project: project.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<i className="bx bx-pencil me-2"></i>
|
||||||
|
<span className="align-left">Modify</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li onClick={() => handleViewActivities(project.id)}>
|
||||||
|
<a className="dropdown-item cursor-pointer">
|
||||||
|
<i className="bx bx-task me-2"></i>
|
||||||
|
<span className="align-left">Activities</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<div className="py-4">
|
||||||
|
{" "}
|
||||||
|
{isLoading && <p className="text-center">Loading...</p>}
|
||||||
|
{!isLoading && filteredProjects.length === 0 && (
|
||||||
|
<p className="text-center text-muted">No projects found.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isLoading && currentItems.length === 0 && (
|
||||||
|
<div className="py-6">
|
||||||
|
<p className="text-center text-muted">No projects found.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isLoading && totalPages > 1 && (
|
||||||
|
<nav>
|
||||||
|
<ul className="pagination pagination-sm justify-content-end py-2">
|
||||||
|
<li className={`page-item ${currentPage === 1 && "disabled"}`}>
|
||||||
|
<button
|
||||||
|
className="page-link"
|
||||||
|
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
||||||
|
>
|
||||||
|
«
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{[...Array(totalPages)].map((_, i) => (
|
||||||
|
<li
|
||||||
|
key={i}
|
||||||
|
className={`page-item ${currentPage === i + 1 && "active"}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="page-link"
|
||||||
|
onClick={() => setCurrentPage(i + 1)}
|
||||||
|
>
|
||||||
|
{i + 1}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<li
|
||||||
|
className={`page-item ${
|
||||||
|
currentPage === totalPages && "disabled"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="page-link"
|
||||||
|
onClick={() =>
|
||||||
|
setCurrentPage((p) => Math.min(totalPages, p + 1))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
»
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectListView;
|
||||||
@ -1,8 +1,7 @@
|
|||||||
import React from 'react'
|
import React from "react";
|
||||||
import AssignRole from './AssignTask'
|
import AssignRole from "./AssignTask";
|
||||||
|
|
||||||
const ProjectModal = ({modalConfig,closeModal}) => {
|
|
||||||
|
|
||||||
|
const ProjectModal = ({ modalConfig, closeModal }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="modal fade"
|
className="modal fade"
|
||||||
@ -23,14 +22,14 @@ const ProjectModal = ({modalConfig,closeModal}) => {
|
|||||||
></button>
|
></button>
|
||||||
<div className="text-center mb-2"></div>
|
<div className="text-center mb-2"></div>
|
||||||
|
|
||||||
|
{modalConfig?.type === "assignRole" && (
|
||||||
{modalConfig?.type === "assignRole" && <AssignRole assignData={modalConfig?.data} onClose={closeModal} />}
|
<AssignRole assignData={modalConfig?.data} onClose={closeModal} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ProjectModal
|
export default ProjectModal;
|
||||||
|
|||||||
@ -1,6 +1,4 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { hasUserPermission } from "../../utils/authUtils";
|
|
||||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
|
||||||
import {
|
import {
|
||||||
DIRECTORY_ADMIN,
|
DIRECTORY_ADMIN,
|
||||||
DIRECTORY_MANAGER,
|
DIRECTORY_MANAGER,
|
||||||
@ -13,6 +11,7 @@ import {
|
|||||||
VIEW_DOCUMENT,
|
VIEW_DOCUMENT,
|
||||||
VIEW_PROJECT_INFRA,
|
VIEW_PROJECT_INFRA,
|
||||||
} from "../../utils/constants";
|
} from "../../utils/constants";
|
||||||
|
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||||
|
|
||||||
const ProjectNav = ({ onPillClick, activePill }) => {
|
const ProjectNav = ({ onPillClick, activePill }) => {
|
||||||
const HasViewInfraStructure = useHasUserPermission(VIEW_PROJECT_INFRA);
|
const HasViewInfraStructure = useHasUserPermission(VIEW_PROJECT_INFRA);
|
||||||
@ -22,7 +21,7 @@ const ProjectNav = ({ onPillClick, activePill }) => {
|
|||||||
const DireManager = useHasUserPermission(DIRECTORY_MANAGER);
|
const DireManager = useHasUserPermission(DIRECTORY_MANAGER);
|
||||||
const DirUser = useHasUserPermission(DIRECTORY_USER);
|
const DirUser = useHasUserPermission(DIRECTORY_USER);
|
||||||
const isManageTeam = useHasUserPermission(MANAGE_TEAM)
|
const isManageTeam = useHasUserPermission(MANAGE_TEAM)
|
||||||
const isViewDocuments = hasUserPermission(VIEW_DOCUMENT);
|
const isViewDocuments = useHasUserPermission(VIEW_DOCUMENT);
|
||||||
const isUploadDocument = useHasUserPermission(UPLOAD_DOCUMENT)
|
const isUploadDocument = useHasUserPermission(UPLOAD_DOCUMENT)
|
||||||
const isModifyDocument = useHasUserPermission(MODIFY_DOCUMENT)
|
const isModifyDocument = useHasUserPermission(MODIFY_DOCUMENT)
|
||||||
|
|
||||||
@ -42,6 +41,7 @@ const ProjectNav = ({ onPillClick, activePill }) => {
|
|||||||
hidden: !(DirAdmin || DireManager || DirUser),
|
hidden: !(DirAdmin || DireManager || DirUser),
|
||||||
},
|
},
|
||||||
{ key: "documents", icon: "bx bx-folder-open", label: "Documents",hidden:!(isViewDocuments || isModifyDocument || isUploadDocument) },
|
{ key: "documents", icon: "bx bx-folder-open", label: "Documents",hidden:!(isViewDocuments || isModifyDocument || isUploadDocument) },
|
||||||
|
{ key: "organization", icon: "bx bx-buildings", label: "Organization"},
|
||||||
{ key: "setting", icon: "bx bxs-cog", label: "Setting",hidden:!isManageTeam },
|
{ key: "setting", icon: "bx bxs-cog", label: "Setting",hidden:!isManageTeam },
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -0,0 +1,109 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useProjectAssignedOrganizations } from "../../../hooks/useProjects";
|
||||||
|
import { useSelectedProject } from "../../../slices/apiDataManager";
|
||||||
|
|
||||||
|
const ProjectAssignedOrgs = () => {
|
||||||
|
const selectedProject = useSelectedProject();
|
||||||
|
const { data, isLoading, isError, error } =
|
||||||
|
useProjectAssignedOrganizations(selectedProject);
|
||||||
|
|
||||||
|
const orgList = [
|
||||||
|
{
|
||||||
|
key: "name",
|
||||||
|
label: "Organization Name",
|
||||||
|
getValue: (org) => (
|
||||||
|
<div className="d-flex gap-2 py-1 ">
|
||||||
|
<i class="bx bx-buildings"></i>
|
||||||
|
<span
|
||||||
|
className="text-truncate d-inline-block "
|
||||||
|
style={{ maxWidth: "150px" }}
|
||||||
|
>
|
||||||
|
{org?.name || "N/A"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
align: "text-start",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "service",
|
||||||
|
label: "Service Name",
|
||||||
|
getValue: (org) => (
|
||||||
|
<div className="d-flex gap-2 py-1 ">
|
||||||
|
{org?.service?.name}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
align: "text-start",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "sprid",
|
||||||
|
label: "Service Provider Id",
|
||||||
|
getValue: (org) => (
|
||||||
|
<span
|
||||||
|
className="text-truncate d-inline-block"
|
||||||
|
style={{ maxWidth: "200px" }}
|
||||||
|
>
|
||||||
|
{org?.sprid || "N/A"}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
align: "text-center",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "isActive",
|
||||||
|
label: "Status",
|
||||||
|
getValue: (org) => (
|
||||||
|
<span
|
||||||
|
className={`text-truncate d-inline-block badge bg-label-${org.isActive ? "primary" :"secondary"}`}
|
||||||
|
style={{ maxWidth: "80px" }}
|
||||||
|
>
|
||||||
|
{org?.isActive ? "Active" : "Inactive"}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
align: "text-start",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isLoading) return <div>Loading...</div>;
|
||||||
|
if (isError) return <div>{error.message}</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="dataTables_wrapper no-footer mx-5 pb-2 page">
|
||||||
|
<table className="table dataTable text-nowrap">
|
||||||
|
<thead>
|
||||||
|
<tr className="table_header_border">
|
||||||
|
{orgList.map((col) => (
|
||||||
|
<th key={col.key} className={col.align}>
|
||||||
|
{col.label}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{Array.isArray(data) && data.length > 0 ? (
|
||||||
|
data.map((row, i) => (
|
||||||
|
<tr key={i}>
|
||||||
|
{orgList.map((col) => (
|
||||||
|
<td key={col.key} className={col.align}>
|
||||||
|
{col.getValue(row)}
|
||||||
|
</td>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<tr style={{ height: "200px" }}>
|
||||||
|
<td
|
||||||
|
colSpan={orgList.length}
|
||||||
|
className="text-center align-middle"
|
||||||
|
>
|
||||||
|
Not Assigned yet
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectAssignedOrgs;
|
||||||
31
src/components/Project/ProjectOrganizations.jsx
Normal file
31
src/components/Project/ProjectOrganizations.jsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { useOrganizationModal } from "../../hooks/useOrganization";
|
||||||
|
import { useSelectedProject } from "../../slices/apiDataManager";
|
||||||
|
import ProjectAssignedOrgs from "./ProjectOrganization/ProjectAssignedOrgs";
|
||||||
|
|
||||||
|
const ProjectOrganizations = () => {
|
||||||
|
const { onOpen, startStep, flowType } = useOrganizationModal();
|
||||||
|
const selectedProject = useSelectedProject();
|
||||||
|
return (
|
||||||
|
<div className="card pb-10" >
|
||||||
|
<div className="card-header">
|
||||||
|
<div className="d-flex justify-content-end px-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="link-button btn btn-sm rounded-md link-button-sm m-1 btn-primary"
|
||||||
|
onClick={() => onOpen({ startStep: 1, flowType: "assign" })}
|
||||||
|
>
|
||||||
|
<i className="bx bx-plus-circle me-2"></i>
|
||||||
|
Add Organization
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="row">
|
||||||
|
<ProjectAssignedOrgs />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectOrganizations;
|
||||||
@ -10,9 +10,9 @@ import ReactApexChart from "react-apexcharts";
|
|||||||
import Chart from "react-apexcharts";
|
import Chart from "react-apexcharts";
|
||||||
|
|
||||||
const ProjectOverview = ({ project }) => {
|
const ProjectOverview = ({ project }) => {
|
||||||
const { projects } = useProjects();
|
const { data } = useProjects();
|
||||||
const [current_project, setCurrentProject] = useState(
|
const [current_project, setCurrentProject] = useState(
|
||||||
projects.find((pro) => pro.id == project)
|
data?.find((pro) => pro.id == project)
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedProject = useSelector(
|
const selectedProject = useSelector(
|
||||||
@ -154,7 +154,7 @@ const ProjectOverview = ({ project }) => {
|
|||||||
}, [current_project]);
|
}, [current_project]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentProject(projects.find((pro) => pro.id == selectedProject));
|
setCurrentProject(data?.find((pro) => pro.id == selectedProject));
|
||||||
if (current_project) {
|
if (current_project) {
|
||||||
let val = getProgressInPercentage(
|
let val = getProgressInPercentage(
|
||||||
current_project.plannedWork,
|
current_project.plannedWork,
|
||||||
|
|||||||
59
src/components/Project/ProjectSchema.jsx
Normal file
59
src/components/Project/ProjectSchema.jsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { DEFAULT_EMPTY_STATUS_ID } from "../../utils/constants";
|
||||||
|
const currentDate = new Date()
|
||||||
|
|
||||||
|
|
||||||
|
export const projectDefault = {
|
||||||
|
name: "",
|
||||||
|
shortName: "",
|
||||||
|
contactPerson: "",
|
||||||
|
projectAddress: "",
|
||||||
|
startDate: currentDate.toISOString().split("T")[0],
|
||||||
|
endDate: currentDate.toISOString().split("T")[0],
|
||||||
|
projectStatusId: DEFAULT_EMPTY_STATUS_ID,
|
||||||
|
promoterId: "",
|
||||||
|
pmcId: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const projectSchema = z
|
||||||
|
.object({
|
||||||
|
name: z.string().min(1, { message: "Project Name is required" }),
|
||||||
|
shortName: z.string().optional(),
|
||||||
|
contactPerson: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "Contact Person Name is required" })
|
||||||
|
.regex(/^[A-Za-z\s]+$/, {
|
||||||
|
message: "Contact Person must contain only letters",
|
||||||
|
}),
|
||||||
|
projectAddress: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "Address is required" })
|
||||||
|
.max(500, "Address must not exceed 150 characters"),
|
||||||
|
startDate: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "Start Date is required" })
|
||||||
|
.default(projectDefault),
|
||||||
|
endDate: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "End Date is required" })
|
||||||
|
.default(projectDefault),
|
||||||
|
projectStatusId: z.string().min(1, { message: "Status is required" }),
|
||||||
|
promoterId: z.string().min(1, { message: "Promoter is required" }),
|
||||||
|
pmcId: z.string().min(1, { message: "PMC is required" }),
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
const start = new Date(data.startDate);
|
||||||
|
const end = new Date(data.endDate);
|
||||||
|
return end >= start;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ["endDate"],
|
||||||
|
message: "End Date must be greater than Start Date",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
80
src/components/Project/Team/TeamAssignToProject.jsx
Normal file
80
src/components/Project/Team/TeamAssignToProject.jsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import TeamEmployeeList from "./TeamEmployeeList";
|
||||||
|
import { useOrganization } from "../../../hooks/useDirectory";
|
||||||
|
import { useOrganizationsList } from "../../../hooks/useOrganization";
|
||||||
|
import { useProjectAssignedOrganizations } from "../../../hooks/useProjects";
|
||||||
|
import { useSelectedProject } from "../../../slices/apiDataManager";
|
||||||
|
|
||||||
|
const TeamAssignToProject = ({ closeModal }) => {
|
||||||
|
const [searchText, setSearchText] = useState("");
|
||||||
|
const [selectedOrg, setSelectedOrg] = useState(null);
|
||||||
|
const project = useSelectedProject();
|
||||||
|
const { data, isLoading, isError, error } =
|
||||||
|
useProjectAssignedOrganizations(project);
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<p className="fs-5 fs-seminbod ">Assign Employee To Project </p>
|
||||||
|
|
||||||
|
<div class="row align-items-center gx-5">
|
||||||
|
<div class="col">
|
||||||
|
<div className="d-flex flex-grow-1 align-items-center gap-2">
|
||||||
|
{isLoading ? (
|
||||||
|
<select className="form-select form-select-sm w-100" disabled>
|
||||||
|
<option value="">Loading...</option>
|
||||||
|
</select>
|
||||||
|
) : data?.length === 0 ? (
|
||||||
|
<p className="mb-0 badge bg-label-secondary">No organizations found</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<label
|
||||||
|
htmlFor="organization"
|
||||||
|
className="form-label mb-0 text-nowrap"
|
||||||
|
>
|
||||||
|
Select Organization
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="organization"
|
||||||
|
className="form-select form-select-sm w-100"
|
||||||
|
value={selectedOrg || ""}
|
||||||
|
onChange={(e) => setSelectedOrg(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">Select</option>
|
||||||
|
{data.map((org) => (
|
||||||
|
<option key={org.id} value={org.id}>
|
||||||
|
{org.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div className="d-flex flex-grow-1 align-items-center gap-2">
|
||||||
|
<label htmlFor="search" className="form-label mb-0 text-nowrap">
|
||||||
|
Search Employee
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="search"
|
||||||
|
type="search"
|
||||||
|
className="form-control form-control-sm w-100"
|
||||||
|
placeholder="Search..."
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div >
|
||||||
|
<TeamEmployeeList
|
||||||
|
organizationId={selectedOrg}
|
||||||
|
searchTerm={searchText}
|
||||||
|
closeModal={closeModal}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TeamAssignToProject;
|
||||||
250
src/components/Project/Team/TeamEmployeeList.jsx
Normal file
250
src/components/Project/Team/TeamEmployeeList.jsx
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import Avatar from "../../common/Avatar";
|
||||||
|
import { useDebounce } from "../../../utils/appUtils";
|
||||||
|
import { useSelectedProject } from "../../../slices/apiDataManager";
|
||||||
|
import { useOrganizationEmployees } from "../../../hooks/useOrganization";
|
||||||
|
import {
|
||||||
|
useEmployeesByProjectAllocated,
|
||||||
|
useManageProjectAllocation,
|
||||||
|
} from "../../../hooks/useProjects";
|
||||||
|
import useMaster, { useServices } from "../../../hooks/masterHook/useMaster";
|
||||||
|
import showToast from "../../../services/toastService";
|
||||||
|
|
||||||
|
const TeamEmployeeList = ({ organizationId, searchTerm, closeModal }) => {
|
||||||
|
const selectedProject = useSelectedProject();
|
||||||
|
const debounceSearchTerm = useDebounce(searchTerm, 500);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: employeesData = [],
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
|
} = useOrganizationEmployees(
|
||||||
|
selectedProject,
|
||||||
|
organizationId,
|
||||||
|
debounceSearchTerm
|
||||||
|
);
|
||||||
|
|
||||||
|
const { projectEmployees, loading: employeeLodaing } =
|
||||||
|
useEmployeesByProjectAllocated(selectedProject, null);
|
||||||
|
|
||||||
|
const { data: jobRoles } = useMaster();
|
||||||
|
const { data: services } = useServices();
|
||||||
|
|
||||||
|
const [employees, setEmployees] = useState([]);
|
||||||
|
|
||||||
|
const { mutate: handleAssignEmployee, isPending } =
|
||||||
|
useManageProjectAllocation({
|
||||||
|
onSuccessCallback: () => {
|
||||||
|
closeModal();
|
||||||
|
},
|
||||||
|
onErrorCallback: () => {
|
||||||
|
closeModal();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (employeesData?.data?.length > 0) {
|
||||||
|
const available = employeesData.data.filter((emp) => {
|
||||||
|
const projEmp = projectEmployees.find((pe) => pe.employeeId === emp.id);
|
||||||
|
return !projEmp || projEmp.isActive === false;
|
||||||
|
});
|
||||||
|
|
||||||
|
setEmployees(
|
||||||
|
available.map((emp) => ({
|
||||||
|
...emp,
|
||||||
|
isChecked: false,
|
||||||
|
jobRole: emp?.jobRoleId || null,
|
||||||
|
serviceId: "",
|
||||||
|
errors: {},
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [employeesData, projectEmployees, organizationId]);
|
||||||
|
|
||||||
|
const handleCheckboxChange = (index) => {
|
||||||
|
setEmployees((prev) => {
|
||||||
|
const newArr = [...prev];
|
||||||
|
newArr[index].isChecked = !newArr[index].isChecked;
|
||||||
|
newArr[index].errors = {};
|
||||||
|
return newArr;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectChange = (index, field, value) => {
|
||||||
|
setEmployees((prev) => {
|
||||||
|
const newArr = [...prev];
|
||||||
|
newArr[index][field] = value;
|
||||||
|
newArr[index].errors[field] = "";
|
||||||
|
return newArr;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = () => {
|
||||||
|
const checkedEmployees = employees.filter((emp) => emp.isChecked);
|
||||||
|
|
||||||
|
setEmployees((prev) => prev.map((emp) => ({ ...emp, errors: {} })));
|
||||||
|
|
||||||
|
if (checkedEmployees.length === 0) {
|
||||||
|
showToast("Select at least one employee", "info");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasError = false;
|
||||||
|
const newEmployees = employees.map((emp) => {
|
||||||
|
const empErrors = {};
|
||||||
|
if (emp.isChecked) {
|
||||||
|
if (!emp.jobRole) {
|
||||||
|
empErrors.jobRole = "Job role is required";
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
if (!emp.serviceId) {
|
||||||
|
empErrors.serviceId = "Service is required";
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { ...emp, errors: empErrors };
|
||||||
|
});
|
||||||
|
|
||||||
|
setEmployees(newEmployees);
|
||||||
|
|
||||||
|
if (hasError) return; // stop submit if validation fails
|
||||||
|
|
||||||
|
const payload = checkedEmployees.map((emp) => ({
|
||||||
|
employeeId: emp.id,
|
||||||
|
jobRoleId: emp.jobRole,
|
||||||
|
serviceId: emp.serviceId,
|
||||||
|
projectId: selectedProject,
|
||||||
|
status: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
handleAssignEmployee({ payload,actionType:"assign"} );
|
||||||
|
|
||||||
|
setEmployees((prev) =>
|
||||||
|
prev.map((emp) => ({
|
||||||
|
...emp,
|
||||||
|
isChecked: false,
|
||||||
|
jobRole: "",
|
||||||
|
serviceId: "",
|
||||||
|
errors: {},
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return ( <div className="page-min-h d-flex justify-content-center align-items-center "><p className="text-muted">Loading employees...</p></div>) ;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return (
|
||||||
|
<div className="page-min-h d-flex justify-content-center align-items-center ">
|
||||||
|
{error?.status === 400 ? (
|
||||||
|
<p className="m-0">Enter employee you want to find.</p>
|
||||||
|
) : (
|
||||||
|
<p className="m-0 dange-text">Something went wrong. Please try again later.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (employees.length === 0) {
|
||||||
|
return(<div className="page-min-h d-flex justify-content-center align-items-center "><p className="text-muted">No available employees to assign.</p></div>) ;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className=" position-relative">
|
||||||
|
<table className="table" style={{ maxHeight: "80px", overflowY: "auto" }}>
|
||||||
|
<thead className=" position-sticky top-0">
|
||||||
|
<tr>
|
||||||
|
<th>Employee</th>
|
||||||
|
<th>Service</th>
|
||||||
|
<th>Job Role</th>
|
||||||
|
<th>Select</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{employees.map((emp, index) => (
|
||||||
|
<tr key={emp.id}>
|
||||||
|
<td>
|
||||||
|
<div className="d-flex align-items-center">
|
||||||
|
<Avatar firstName={emp.firstName} lastName={emp.lastName} />
|
||||||
|
<span className="ms-2 fw-semibold">
|
||||||
|
{emp.firstName} {emp.lastName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select
|
||||||
|
value={emp.serviceId}
|
||||||
|
disabled={!emp.isChecked}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleSelectChange(index, "serviceId", e.target.value)
|
||||||
|
}
|
||||||
|
className={`form-select form-select-sm w-auto border-none rounded-0 py-1 px-auto ${
|
||||||
|
emp.errors.serviceId ? "is-invalid" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<option value="">Select Service</option>
|
||||||
|
{services?.data?.map((s) => (
|
||||||
|
<option key={s.id} value={s.id}>
|
||||||
|
{s.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{emp.errors.serviceId && (
|
||||||
|
<div className="danger-text">{emp.errors.serviceId}</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<select
|
||||||
|
value={emp.jobRole}
|
||||||
|
disabled={!emp.isChecked}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleSelectChange(index, "jobRole", e.target.value)
|
||||||
|
}
|
||||||
|
className={`form-select form-select-sm w-auto border-none rounded-0 py-1 px-auto ${
|
||||||
|
emp.errors.jobRole ? "is-invalid" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<option value="">Select Job Role</option>
|
||||||
|
{jobRoles?.map((r) => (
|
||||||
|
<option key={r.id} value={r.id}>
|
||||||
|
{r.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{emp.errors.jobRole && (
|
||||||
|
<div className="danger-text">{emp.errors.jobRole}</div>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-check-input"
|
||||||
|
checked={emp.isChecked}
|
||||||
|
onChange={() => handleCheckboxChange(index)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div className="position-sticky bottom-0 bg-white d-flex justify-content-end gap-3 z-25 ">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm btn-label-secondary"
|
||||||
|
onClick={() => closeModal()}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button onClick={onSubmit} className="btn btn-primary">
|
||||||
|
{isPending ? "Please Wait..." : "Assign to Project"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TeamEmployeeList;
|
||||||
333
src/components/Project/Team/Teams.jsx
Normal file
333
src/components/Project/Team/Teams.jsx
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
||||||
|
import { Link, NavLink, useNavigate, useParams } from "react-router-dom";
|
||||||
|
|
||||||
|
import showToast from "../../../services/toastService";
|
||||||
|
import Avatar from "../../common/Avatar";
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
|
import ProjectRepository from "../../../repositories/ProjectRepository";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { changeMaster } from "../../../slices/localVariablesSlice";
|
||||||
|
import useMaster from "../../../hooks/masterHook/useMaster";
|
||||||
|
import { useHasUserPermission } from "../../../hooks/useHasUserPermission";
|
||||||
|
import { ASSIGN_TO_PROJECT } from "../../../utils/constants";
|
||||||
|
import ConfirmModal from "../../common/ConfirmModal";
|
||||||
|
import eventBus from "../../../services/eventBus";
|
||||||
|
import {
|
||||||
|
useEmployeesByProjectAllocated,
|
||||||
|
useManageProjectAllocation,
|
||||||
|
useProjectAssignedServices,
|
||||||
|
} from "../../../hooks/useProjects";
|
||||||
|
import { useSelectedProject } from "../../../slices/apiDataManager";
|
||||||
|
import GlobalModel from "../../common/GlobalModel";
|
||||||
|
import TeamAssignToProject from "./TeamAssignToProject";
|
||||||
|
|
||||||
|
const Teams = () => {
|
||||||
|
const selectedProject = useSelectedProject();
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const [AssigTeam, setAssignTeam] = useState(false);
|
||||||
|
const [employees, setEmployees] = useState([]);
|
||||||
|
const [selectedEmployee, setSelectedEmployee] = useState(null);
|
||||||
|
const [deleteEmployee, setDeleteEmplyee] = useState(null);
|
||||||
|
const [searchTerm, setSearchTerm] = useState(""); // State for search term
|
||||||
|
const [selectedService, setSelectedService] = useState(null);
|
||||||
|
const [activeEmployee, setActiveEmployee] = useState(false);
|
||||||
|
|
||||||
|
const { data: assignedServices, isLoading: servicesLoading } =
|
||||||
|
useProjectAssignedServices(selectedProject);
|
||||||
|
const { data: empJobRoles, loading } = useMaster();
|
||||||
|
const handleToggleActive = (e) => setActiveEmployee(e.target.checked);
|
||||||
|
|
||||||
|
const handleServiceChange = (e) => {
|
||||||
|
setSelectedService(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const HasAssignUserPermission = useHasUserPermission(ASSIGN_TO_PROJECT);
|
||||||
|
const [IsDeleteModal, setIsDeleteModal] = useState(false);
|
||||||
|
const {
|
||||||
|
projectEmployees,
|
||||||
|
loading: employeeLodaing,
|
||||||
|
refetch,
|
||||||
|
} = useEmployeesByProjectAllocated(
|
||||||
|
selectedProject,
|
||||||
|
selectedService,
|
||||||
|
null,
|
||||||
|
activeEmployee
|
||||||
|
);
|
||||||
|
const {
|
||||||
|
mutate: submitAllocations,
|
||||||
|
isPending,
|
||||||
|
isSuccess,
|
||||||
|
isError,
|
||||||
|
} = useManageProjectAllocation({
|
||||||
|
onSuccessCallback: () => {
|
||||||
|
setSelectedEmployee(null);
|
||||||
|
},
|
||||||
|
onErrorCallback: () => {
|
||||||
|
setSelectedEmployee(null);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDelete = (employee) => {
|
||||||
|
let payload = [
|
||||||
|
{
|
||||||
|
employeeId: employee?.employeeId,
|
||||||
|
jobRoleId: employee?.jobRoleId,
|
||||||
|
projectId: selectedProject,
|
||||||
|
serviceId: employee?.serviceId,
|
||||||
|
status: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
submitAllocations({ payload: payload, actionType: "remove" });
|
||||||
|
};
|
||||||
|
const getJobRole = (jobRoleId) => {
|
||||||
|
if (loading) return "Loading...";
|
||||||
|
if (!Array.isArray(empJobRoles)) return "Unassigned";
|
||||||
|
if (!jobRoleId) return "Unassigned";
|
||||||
|
|
||||||
|
const role = empJobRoles.find((b) => b.id == jobRoleId);
|
||||||
|
return role ? role.name : "Unassigned";
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredEmployees = useMemo(() => {
|
||||||
|
if (!projectEmployees || !searchTerm?.trim()) return projectEmployees;
|
||||||
|
|
||||||
|
const lower = searchTerm.toLowerCase();
|
||||||
|
|
||||||
|
return projectEmployees?.filter((emp) => {
|
||||||
|
const fullName = `${emp.firstName ?? ""} ${
|
||||||
|
emp.lastName ?? ""
|
||||||
|
}`.toLowerCase();
|
||||||
|
|
||||||
|
const joberole = getJobRole(emp?.jobRoleId)?.toLowerCase();
|
||||||
|
|
||||||
|
return fullName?.includes(lower) || joberole?.includes(lower);
|
||||||
|
});
|
||||||
|
}, [projectEmployees, searchTerm]);
|
||||||
|
const handleSearch = (e) => setSearchTerm(e.target.value);
|
||||||
|
const employeeHandler = useCallback(
|
||||||
|
(msg) => {
|
||||||
|
if (filteredEmployees.some((emp) => emp.employeeId == msg.employeeId)) {
|
||||||
|
refetch();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[filteredEmployees, refetch]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
eventBus.on("employee", employeeHandler);
|
||||||
|
return () => eventBus.off("employee", employeeHandler);
|
||||||
|
}, [employeeHandler]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{AssigTeam && (
|
||||||
|
<GlobalModel
|
||||||
|
size="lg"
|
||||||
|
isOpen={AssigTeam}
|
||||||
|
closeModal={() => setAssignTeam(false)}
|
||||||
|
>
|
||||||
|
<TeamAssignToProject closeModal={() => setAssignTeam(false)} />
|
||||||
|
</GlobalModel>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
type="delete"
|
||||||
|
header={"Remove Employee"}
|
||||||
|
message={"Are you sure you want to remove?"}
|
||||||
|
isOpen={!!selectedEmployee}
|
||||||
|
loading={isPending}
|
||||||
|
onSubmit={() => handleDelete(selectedEmployee)}
|
||||||
|
onClose={() => setSelectedEmployee(null)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="card card-action mb-6">
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="row align-items-center justify-content-between mb-4 g-3">
|
||||||
|
<div className="col-md-6 col-12 algin-items-center">
|
||||||
|
<div className="d-flex flex-wrap align-items-center gap-3">
|
||||||
|
<div>
|
||||||
|
{!servicesLoading && (
|
||||||
|
<>
|
||||||
|
{(!assignedServices || assignedServices.length === 0) && (
|
||||||
|
<span className="badge bg-label-secondary">
|
||||||
|
Not Service Assigned
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{assignedServices?.length === 1 && (
|
||||||
|
<span className="badge bg-label-secondary">
|
||||||
|
{assignedServices[0].name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{assignedServices?.length > 1 && (
|
||||||
|
<select
|
||||||
|
className="form-select form-select-sm"
|
||||||
|
aria-label="Select Service"
|
||||||
|
value={selectedService}
|
||||||
|
onChange={handleServiceChange}
|
||||||
|
>
|
||||||
|
<option value="">All Services</option>
|
||||||
|
{assignedServices.map((service) => (
|
||||||
|
<option key={service.id} value={service.id}>
|
||||||
|
{service.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-check form-switch d-flex align-items-center text-nowrap">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="form-check-input"
|
||||||
|
id="activeEmployeeSwitch"
|
||||||
|
checked={activeEmployee}
|
||||||
|
onChange={handleToggleActive}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
className="form-check-label ms-2"
|
||||||
|
htmlFor="activeEmployeeSwitch"
|
||||||
|
>
|
||||||
|
{activeEmployee ? "Active Employees" : "Include Inactive Employees"}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-md-6 col-12 d-flex justify-content-md-end align-items-center justify-content-start gap-3">
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
className="form-control form-control-sm"
|
||||||
|
placeholder="Search by Name or Role"
|
||||||
|
aria-controls="DataTables_Table_0"
|
||||||
|
style={{ maxWidth: "200px" }}
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={handleSearch}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{HasAssignUserPermission && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary btn-sm text-nowrap"
|
||||||
|
onClick={() => setAssignTeam(true)}
|
||||||
|
>
|
||||||
|
<i className="bx bx-plus-circle me-1"></i>
|
||||||
|
Assign Employee
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="table-responsive text-nowrap modal-min-h">
|
||||||
|
{employeeLodaing && <p>Loading..</p>}
|
||||||
|
{projectEmployees && projectEmployees.length > 0 && (
|
||||||
|
<table className="table ">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<div className="text-start ms-5">Name</div>
|
||||||
|
</th>
|
||||||
|
<th>Services</th>
|
||||||
|
<th>Organization</th>
|
||||||
|
<th>Assigned Date</th>
|
||||||
|
{activeEmployee && <th>Release Date</th>}
|
||||||
|
<th>Project Role</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="table-border-bottom-0">
|
||||||
|
{filteredEmployees &&
|
||||||
|
filteredEmployees
|
||||||
|
.sort((a, b) =>
|
||||||
|
(a.firstName || "").localeCompare(b.firstName || "")
|
||||||
|
)
|
||||||
|
.map((emp) => (
|
||||||
|
<tr key={emp.id}>
|
||||||
|
<td>
|
||||||
|
<div className="d-flex justify-content-start align-items-center">
|
||||||
|
<Avatar
|
||||||
|
firstName={emp.firstName}
|
||||||
|
lastName={emp.lastName}
|
||||||
|
/>
|
||||||
|
<div className="d-flex flex-column">
|
||||||
|
<a
|
||||||
|
onClick={() =>
|
||||||
|
navigate(
|
||||||
|
`/employee/${emp.employeeId}?for=attendance`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="text-heading text-truncate cursor-pointer"
|
||||||
|
>
|
||||||
|
<span className="fw-normal">
|
||||||
|
{emp.firstName}
|
||||||
|
{emp.lastName}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>{emp.serviceName || "N/A"}</td>
|
||||||
|
<td>{emp.organizationName || "N/A"}</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{moment(emp.allocationDate).format("DD-MMM-YYYY")}
|
||||||
|
</td>
|
||||||
|
{activeEmployee && (
|
||||||
|
<td>
|
||||||
|
{emp.reAllocationDate
|
||||||
|
? moment(emp.reAllocationDate).format(
|
||||||
|
"DD-MMM-YYYY"
|
||||||
|
)
|
||||||
|
: "Present"}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
<td>
|
||||||
|
<span className="badge bg-label-primary me-1">
|
||||||
|
{getJobRole(emp.jobRoleId)}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{emp.isActive ? (
|
||||||
|
<button
|
||||||
|
aria-label="Delete"
|
||||||
|
type="button"
|
||||||
|
title="Remove from project"
|
||||||
|
className="btn p-0 dropdown-toggle hide-arrow"
|
||||||
|
onClick={() => setSelectedEmployee(emp)}
|
||||||
|
>
|
||||||
|
<i className="bx bx-trash me-1 text-danger"></i>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<span>Not in project</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
{!employeeLodaing && filteredEmployees.length === 0 && (
|
||||||
|
<div className="text-center text-muted py-3 d-flex justify-content-center align-items-center py-12">
|
||||||
|
<p>
|
||||||
|
{!activeEmployee
|
||||||
|
? "No active employees assigned to the project"
|
||||||
|
: "No inactive employees assigned to the project"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Teams;
|
||||||
@ -1,420 +0,0 @@
|
|||||||
import React, { useState, useEffect, useCallback } from "react";
|
|
||||||
import MapUsers from "./MapUsers";
|
|
||||||
import { Link, NavLink, useNavigate, useParams } from "react-router-dom";
|
|
||||||
|
|
||||||
import showToast from "../../services/toastService";
|
|
||||||
import Avatar from "../common/Avatar";
|
|
||||||
import moment from "moment";
|
|
||||||
|
|
||||||
import ProjectRepository from "../../repositories/ProjectRepository";
|
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
|
||||||
import { changeMaster } from "../../slices/localVariablesSlice";
|
|
||||||
import useMaster from "../../hooks/masterHook/useMaster";
|
|
||||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
|
||||||
import { ASSIGN_TO_PROJECT } from "../../utils/constants";
|
|
||||||
import ConfirmModal from "../common/ConfirmModal";
|
|
||||||
import eventBus from "../../services/eventBus";
|
|
||||||
import {
|
|
||||||
useEmployeesByProjectAllocated,
|
|
||||||
useManageProjectAllocation,
|
|
||||||
} from "../../hooks/useProjects";
|
|
||||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
|
||||||
|
|
||||||
const Teams = () => {
|
|
||||||
// const {projectId} = useParams()
|
|
||||||
// const projectId = useSelector((store)=>store.localVariables.projectId)
|
|
||||||
const projectId = useSelectedProject();
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const { data, loading } = useMaster();
|
|
||||||
const [isModalOpen, setIsModelOpen] = useState(false);
|
|
||||||
const [error, setError] = useState("");
|
|
||||||
const [empJobRoles, setEmpJobRoles] = useState(null);
|
|
||||||
const [employees, setEmployees] = useState([]);
|
|
||||||
const [filteredEmployees, setFilteredEmployees] = useState([]);
|
|
||||||
const [removingEmployeeId, setRemovingEmployeeId] = useState(null);
|
|
||||||
const [assignedLoading, setAssignedLoading] = useState(false);
|
|
||||||
const [activeEmployee, setActiveEmployee] = useState(true);
|
|
||||||
const [deleteEmployee, setDeleteEmplyee] = useState(null);
|
|
||||||
const [searchTerm, setSearchTerm] = useState(""); // State for search term
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const HasAssignUserPermission = useHasUserPermission(ASSIGN_TO_PROJECT);
|
|
||||||
const [IsDeleteModal, setIsDeleteModal] = useState(false);
|
|
||||||
const {
|
|
||||||
projectEmployees,
|
|
||||||
loading: employeeLodaing,
|
|
||||||
refetch,
|
|
||||||
} = useEmployeesByProjectAllocated(projectId);
|
|
||||||
const {
|
|
||||||
mutate: submitAllocations,
|
|
||||||
isPending,
|
|
||||||
isSuccess,
|
|
||||||
isError,
|
|
||||||
} = useManageProjectAllocation({
|
|
||||||
onSuccessCallback: () => {
|
|
||||||
setRemovingEmployeeId(null);
|
|
||||||
setAssignedLoading(false);
|
|
||||||
setDeleteEmplyee(null);
|
|
||||||
closeDeleteModal();
|
|
||||||
},
|
|
||||||
onErrorCallback: () => {
|
|
||||||
closeDeleteModal();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const removeAllocation = (item) => {
|
|
||||||
setRemovingEmployeeId(item.id);
|
|
||||||
|
|
||||||
submitAllocations({
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
empID: item.employeeId,
|
|
||||||
jobRoleId: item.jobRoleId,
|
|
||||||
projectId: projectId,
|
|
||||||
status: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
added: false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEmpAlicationFormSubmit = (allocaionObj) => {
|
|
||||||
let items = allocaionObj.map((item) => {
|
|
||||||
return {
|
|
||||||
empID: item.empID,
|
|
||||||
jobRoleId: item.jobRoleId,
|
|
||||||
projectId: projectId,
|
|
||||||
status: true,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
submitAllocations({ items, added: true });
|
|
||||||
|
|
||||||
setActiveEmployee(true);
|
|
||||||
setFilteredEmployees(employees.filter((emp) => emp.isActive));
|
|
||||||
|
|
||||||
const dropdown = document.querySelector(
|
|
||||||
'select[name="DataTables_Table_0_length"]'
|
|
||||||
);
|
|
||||||
if (dropdown) dropdown.value = "true";
|
|
||||||
};
|
|
||||||
|
|
||||||
const getRole = (jobRoleId) => {
|
|
||||||
if (loading) return "Loading...";
|
|
||||||
if (!Array.isArray(empJobRoles)) return "Unassigned";
|
|
||||||
if (!jobRoleId) return "Unassigned";
|
|
||||||
|
|
||||||
const role = empJobRoles.find((b) => b.id == jobRoleId);
|
|
||||||
return role ? role.name : "Unassigned";
|
|
||||||
};
|
|
||||||
const openModel = () => {
|
|
||||||
setIsModelOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onModelClose = () => {
|
|
||||||
setIsModelOpen(false);
|
|
||||||
const modalElement = document.getElementById("user-model");
|
|
||||||
if (modalElement) {
|
|
||||||
modalElement.classList.remove("show");
|
|
||||||
modalElement.style.display = "none";
|
|
||||||
document.body.classList.remove("modal-open");
|
|
||||||
document.querySelector(".modal-backdrop").remove();
|
|
||||||
}
|
|
||||||
const modalBackdropElement = document.querySelector(".modal-backdrop");
|
|
||||||
if (modalBackdropElement) {
|
|
||||||
modalBackdropElement.remove();
|
|
||||||
}
|
|
||||||
document.body.style.overflow = "auto";
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(changeMaster("Job Role"));
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (projectEmployees) {
|
|
||||||
setEmployees(projectEmployees);
|
|
||||||
//setFilteredEmployees(projectEmployees?.filter((emp) => emp.isActive));
|
|
||||||
const filtered = projectEmployees.filter((emp) => emp.isActive);
|
|
||||||
setFilteredEmployees(filtered);
|
|
||||||
}
|
|
||||||
}, [projectEmployees, employeeLodaing]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
setEmpJobRoles(data);
|
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const filterAndSearchEmployees = useCallback(() => {
|
|
||||||
const statusFiltered = employees.filter((emp) =>
|
|
||||||
activeEmployee ? emp.isActive : !emp.isActive
|
|
||||||
);
|
|
||||||
|
|
||||||
if (searchTerm === "") {
|
|
||||||
setFilteredEmployees(statusFiltered);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lowercasedSearchTerm = searchTerm.toLowerCase();
|
|
||||||
const searchedAndFiltered = statusFiltered.filter((item) => {
|
|
||||||
const fullName =
|
|
||||||
`${item.firstName} ${item.middleName} ${item.lastName}`.toLowerCase();
|
|
||||||
const roleName = getRole(item.jobRoleId).toLowerCase();
|
|
||||||
|
|
||||||
return (
|
|
||||||
fullName.includes(lowercasedSearchTerm) ||
|
|
||||||
roleName.includes(lowercasedSearchTerm)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
setFilteredEmployees(searchedAndFiltered);
|
|
||||||
}, [employees, activeEmployee, searchTerm, getRole]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
filterAndSearchEmployees();
|
|
||||||
}, [employees, activeEmployee, searchTerm, filterAndSearchEmployees]);
|
|
||||||
|
|
||||||
const handleFilterEmployee = (e) => {
|
|
||||||
const filterValue = e.target.value;
|
|
||||||
// if (filterValue === "true") {
|
|
||||||
// setActiveEmployee(true);
|
|
||||||
// setFilteredEmployees(employees.filter((emp) => emp.isActive));
|
|
||||||
// } else {
|
|
||||||
// setFilteredEmployees(employees.filter((emp) => !emp.isActive));
|
|
||||||
// setActiveEmployee(false);
|
|
||||||
// }
|
|
||||||
setActiveEmployee(filterValue === "true");
|
|
||||||
setSearchTerm("");
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSearch = (e) => {
|
|
||||||
setSearchTerm(e.target.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteModalOpen = (item) => {
|
|
||||||
setDeleteEmplyee(item);
|
|
||||||
setIsDeleteModal(true);
|
|
||||||
};
|
|
||||||
const closeDeleteModal = () => setIsDeleteModal(false);
|
|
||||||
|
|
||||||
const handler = useCallback(
|
|
||||||
(msg) => {
|
|
||||||
if (msg.projectIds.some((item) => item === projectId)) {
|
|
||||||
refetch();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[projectId, refetch]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
eventBus.on("assign_project_all", handler);
|
|
||||||
return () => eventBus.off("assign_project_all", handler);
|
|
||||||
}, [handler]);
|
|
||||||
|
|
||||||
const employeeHandler = useCallback(
|
|
||||||
(msg) => {
|
|
||||||
if (filteredEmployees.some((item) => item.employeeId == msg.employeeId)) {
|
|
||||||
refetch();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[filteredEmployees, refetch]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
eventBus.on("employee", employeeHandler);
|
|
||||||
return () => eventBus.off("employee", employeeHandler);
|
|
||||||
}, [employeeHandler]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className="modal fade"
|
|
||||||
id="user-model"
|
|
||||||
tabIndex="-1"
|
|
||||||
aria-labelledby="userModalLabel"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<MapUsers
|
|
||||||
projectId={projectId}
|
|
||||||
onClose={onModelClose}
|
|
||||||
empJobRoles={empJobRoles}
|
|
||||||
onSubmit={handleEmpAlicationFormSubmit}
|
|
||||||
allocation={employees}
|
|
||||||
assignedLoading={assignedLoading}
|
|
||||||
setAssignedLoading={setAssignedLoading}
|
|
||||||
></MapUsers>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{IsDeleteModal && (
|
|
||||||
<ConfirmModal
|
|
||||||
isOpen={IsDeleteModal}
|
|
||||||
type="delete"
|
|
||||||
header="Removed Employee"
|
|
||||||
message="Are you sure you want delete?"
|
|
||||||
onSubmit={() => removeAllocation(deleteEmployee)}
|
|
||||||
onClose={closeDeleteModal}
|
|
||||||
loading={isPending}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="card card-action mb-6">
|
|
||||||
<div className="card-body">
|
|
||||||
<div className="row d-flex justify-content-between mb-4">
|
|
||||||
<div className="col-md-6 col-12 d-flex align-items-center">
|
|
||||||
<div className="dataTables_filter d-inline-flex align-items-center ms-2">
|
|
||||||
<input
|
|
||||||
type="search"
|
|
||||||
className="form-control form-control-sm me-4"
|
|
||||||
placeholder="Search by Name or Role"
|
|
||||||
aria-controls="DataTables_Table_0"
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={handleSearch}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="col-md-6 col-12 d-flex justify-content-end align-items-center">
|
|
||||||
<div
|
|
||||||
className="dataTables_length text-start py-2 px-2"
|
|
||||||
id="DataTables_Table_0_length"
|
|
||||||
>
|
|
||||||
<label>
|
|
||||||
<select
|
|
||||||
name="DataTables_Table_0_length"
|
|
||||||
aria-controls="DataTables_Table_0"
|
|
||||||
className="form-select form-select-sm"
|
|
||||||
onChange={handleFilterEmployee}
|
|
||||||
// value={false}
|
|
||||||
aria-label=""
|
|
||||||
defaultValue="true"
|
|
||||||
>
|
|
||||||
<option value="true">Active Employee</option>
|
|
||||||
<option value="false">In-Active Employee</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`link-button btn-primary btn-sm ${
|
|
||||||
HasAssignUserPermission ? "" : "d-none"
|
|
||||||
}`}
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#user-model"
|
|
||||||
>
|
|
||||||
<i className="bx bx-plus-circle me-2"></i>
|
|
||||||
Assign Employee
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="table-responsive text-nowrap">
|
|
||||||
{employeeLodaing && <p>Loading..</p>}
|
|
||||||
{!employeeLodaing &&
|
|
||||||
filteredEmployees &&
|
|
||||||
filteredEmployees.length > 0 && (
|
|
||||||
<table className="table ">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>
|
|
||||||
<div className="text-start ms-5">Name</div>
|
|
||||||
</th>
|
|
||||||
<th>Assigned Date</th>
|
|
||||||
{!activeEmployee && <th>Release Date</th>}
|
|
||||||
<th>Project Role</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="table-border-bottom-0">
|
|
||||||
{filteredEmployees &&
|
|
||||||
filteredEmployees.map((item) => (
|
|
||||||
<tr key={item.id}>
|
|
||||||
<td>
|
|
||||||
<div className="d-flex justify-content-start align-items-center">
|
|
||||||
<Avatar
|
|
||||||
firstName={item.firstName}
|
|
||||||
lastName={item.lastName}
|
|
||||||
></Avatar>
|
|
||||||
<div className="d-flex flex-column">
|
|
||||||
<a
|
|
||||||
onClick={() =>
|
|
||||||
navigate(
|
|
||||||
`/employee/${item.employeeId}?for=attendance`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="text-heading text-truncate cursor-pointer"
|
|
||||||
>
|
|
||||||
<span className="fw-normal">
|
|
||||||
{item.firstName} {item.middleName}{" "}
|
|
||||||
{item.lastName}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{" "}
|
|
||||||
{moment(item.allocationDate).format(
|
|
||||||
"DD-MMM-YYYY"
|
|
||||||
)}{" "}
|
|
||||||
</td>
|
|
||||||
{!activeEmployee && (
|
|
||||||
<td>
|
|
||||||
{item.reAllocationDate
|
|
||||||
? moment(item.reAllocationDate).format(
|
|
||||||
"DD-MMM-YYYY"
|
|
||||||
)
|
|
||||||
: "Present"}
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
<td>
|
|
||||||
<span className="badge bg-label-primary me-1">
|
|
||||||
{getRole(item.jobRoleId)}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{item.isActive && (
|
|
||||||
<button
|
|
||||||
aria-label="Delete"
|
|
||||||
type="button"
|
|
||||||
title="Remove from project"
|
|
||||||
className="btn p-0 dropdown-toggle hide-arrow"
|
|
||||||
onClick={() => deleteModalOpen(item)}
|
|
||||||
>
|
|
||||||
{" "}
|
|
||||||
{removingEmployeeId === item.id ? (
|
|
||||||
<div
|
|
||||||
className="spinner-border spinner-border-sm text-primary"
|
|
||||||
role="status"
|
|
||||||
>
|
|
||||||
<span className="visually-hidden">
|
|
||||||
Loading...
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<i className="bx bx-trash me-1 text-danger"></i>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{!item.isActive && <span>Not in project</span>}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)}
|
|
||||||
{!employeeLodaing && filteredEmployees.length === 0 && (
|
|
||||||
<div className="text-center text-muted py-3">
|
|
||||||
{activeEmployee
|
|
||||||
? "No active employees assigned to the project"
|
|
||||||
: "No inactive employees assigned to the project"}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Teams;
|
|
||||||
@ -72,15 +72,15 @@ const EditProfile = ({ TenantId, onClose }) => {
|
|||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form className="row g-6" onSubmit={handleSubmit(onSubmit)}>
|
<form className="row g-6" onSubmit={handleSubmit(onSubmit)}>
|
||||||
<h6>Edit Tenant</h6>
|
<h5>Edit Tenant</h5>
|
||||||
|
|
||||||
<div className="col-sm-6 mt-1">
|
<div className="col-sm-6 mt-1 text-start">
|
||||||
<Label htmlFor="firstName" required>First Name</Label>
|
<Label htmlFor="firstName" required>First Name</Label>
|
||||||
<input id="firstName" type="text" className="form-control form-control-sm" {...register("firstName")} inputMode='text' />
|
<input id="firstName" type="text" className="form-control form-control-sm" {...register("firstName")} inputMode='text' />
|
||||||
{errors.firstName && <div className="danger-text">{errors.firstName.message}</div>}
|
{errors.firstName && <div className="danger-text">{errors.firstName.message}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-sm-6 mt-1">
|
<div className="col-sm-6 mt-1 text-start">
|
||||||
<Label htmlFor="lastName" required>Last Name</Label>
|
<Label htmlFor="lastName" required>Last Name</Label>
|
||||||
<input id="lastName" type="text" className="form-control form-control-sm" {...register("lastName")} />
|
<input id="lastName" type="text" className="form-control form-control-sm" {...register("lastName")} />
|
||||||
{errors.lastName && <div className="danger-text">{errors.lastName.message}</div>}
|
{errors.lastName && <div className="danger-text">{errors.lastName.message}</div>}
|
||||||
@ -88,32 +88,32 @@ const EditProfile = ({ TenantId, onClose }) => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className="col-sm-6 mt-1">
|
<div className="col-sm-6 mt-1 text-start">
|
||||||
<Label htmlFor="contactNumber" required>Contact Number</Label>
|
<Label htmlFor="contactNumber" required>Contact Number</Label>
|
||||||
<input id="contactNumber" type="text" className="form-control form-control-sm" {...register("contactNumber")} inputMode="tel"
|
<input id="contactNumber" type="text" className="form-control form-control-sm" {...register("contactNumber")} inputMode="tel"
|
||||||
placeholder="+91 9876543210" />
|
placeholder="+91 9876543210" />
|
||||||
{errors.contactNumber && <div className="danger-text">{errors.contactNumber.message}</div>}
|
{errors.contactNumber && <div className="danger-text">{errors.contactNumber.message}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-sm-6 mt-1">
|
<div className="col-sm-6 mt-1 text-start">
|
||||||
<Label htmlFor="domainName" >Domain Name</Label>
|
<Label htmlFor="domainName" >Domain Name</Label>
|
||||||
<input id="domainName" type="text" className="form-control form-control-sm" {...register("domainName")} />
|
<input id="domainName" type="text" className="form-control form-control-sm" {...register("domainName")} />
|
||||||
{errors.domainName && <div className="danger-text">{errors.domainName.message}</div>}
|
{errors.domainName && <div className="danger-text">{errors.domainName.message}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-sm-6 mt-1">
|
<div className="col-sm-6 mt-1 text-start">
|
||||||
<Label htmlFor="taxId" >Tax ID</Label>
|
<Label htmlFor="taxId" >Tax ID</Label>
|
||||||
<input id="taxId" type="text" className="form-control form-control-sm" {...register("taxId")} />
|
<input id="taxId" type="text" className="form-control form-control-sm" {...register("taxId")} />
|
||||||
{errors.taxId && <div className="danger-text">{errors.taxId.message}</div>}
|
{errors.taxId && <div className="danger-text">{errors.taxId.message}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-sm-6 mt-1">
|
<div className="col-sm-6 mt-1 text-start">
|
||||||
<Label htmlFor="officeNumber" >Office Number</Label>
|
<Label htmlFor="officeNumber" >Office Number</Label>
|
||||||
<input id="officeNumber" type="text" className="form-control form-control-sm" {...register("officeNumber")} />
|
<input id="officeNumber" type="text" className="form-control form-control-sm" {...register("officeNumber")} />
|
||||||
{errors.officeNumber && <div className="danger-text">{errors.officeNumber.message}</div>}
|
{errors.officeNumber && <div className="danger-text">{errors.officeNumber.message}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-sm-6 mt-1">
|
<div className="col-sm-6 mt-1 text-start">
|
||||||
<Label htmlFor="industryId" required>Industry</Label>
|
<Label htmlFor="industryId" required>Industry</Label>
|
||||||
<select className="form-select form-select-sm" {...register("industryId")}>
|
<select className="form-select form-select-sm" {...register("industryId")}>
|
||||||
{industryLoading ? <option value="">Loading...</option> :
|
{industryLoading ? <option value="">Loading...</option> :
|
||||||
@ -125,7 +125,7 @@ const EditProfile = ({ TenantId, onClose }) => {
|
|||||||
{errors.industryId && <div className="danger-text">{errors.industryId.message}</div>}
|
{errors.industryId && <div className="danger-text">{errors.industryId.message}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-sm-6 mt-1">
|
<div className="col-sm-6 mt-1 text-start">
|
||||||
<Label htmlFor="reference">Reference</Label>
|
<Label htmlFor="reference">Reference</Label>
|
||||||
<select className="form-select form-select-sm" {...register("reference")}>
|
<select className="form-select form-select-sm" {...register("reference")}>
|
||||||
{reference.map((org) => (
|
{reference.map((org) => (
|
||||||
@ -134,7 +134,7 @@ const EditProfile = ({ TenantId, onClose }) => {
|
|||||||
</select>
|
</select>
|
||||||
{errors.reference && <div className="danger-text">{errors.reference.message}</div>}
|
{errors.reference && <div className="danger-text">{errors.reference.message}</div>}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-sm-6">
|
<div className="col-sm-6 text-start">
|
||||||
<Label htmlFor="organizationSize" required>
|
<Label htmlFor="organizationSize" required>
|
||||||
Organization Size
|
Organization Size
|
||||||
</Label>
|
</Label>
|
||||||
@ -154,19 +154,19 @@ const EditProfile = ({ TenantId, onClose }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 mt-1">
|
<div className="col-12 mt-1 text-start">
|
||||||
<Label htmlFor="billingAddress" required>Billing Address</Label>
|
<Label htmlFor="billingAddress" required>Billing Address</Label>
|
||||||
<textarea id="billingAddress" className="form-control" {...register("billingAddress")} rows={2} />
|
<textarea id="billingAddress" className="form-control" {...register("billingAddress")} rows={2} />
|
||||||
{errors.billingAddress && <div className="danger-text">{errors.billingAddress.message}</div>}
|
{errors.billingAddress && <div className="danger-text">{errors.billingAddress.message}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 mt-1">
|
<div className="col-12 mt-1 text-start">
|
||||||
<Label htmlFor="description">Description</Label>
|
<Label htmlFor="description">Description</Label>
|
||||||
<textarea id="description" className="form-control" {...register("description")} rows={2} />
|
<textarea id="description" className="form-control" {...register("description")} rows={2} />
|
||||||
{errors.description && <div className="danger-text">{errors.description.message}</div>}
|
{errors.description && <div className="danger-text">{errors.description.message}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-sm-12">
|
<div className="col-sm-12 text-start">
|
||||||
<Label htmlFor="logImage">Logo Image</Label>
|
<Label htmlFor="logImage">Logo Image</Label>
|
||||||
<LogoUpload
|
<LogoUpload
|
||||||
preview={logoPreview}
|
preview={logoPreview}
|
||||||
@ -176,9 +176,9 @@ const EditProfile = ({ TenantId, onClose }) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="d-flex justify-content-center gap-2 mt-3">
|
<div className="d-flex justify-content-end gap-2 mt-3">
|
||||||
|
<button type="button" disabled={isPending} className="btn btn-sm btn-label-secondary" onClick={onClose}>Cancel</button>
|
||||||
<button type="submit" disabled={isPending} className="btn btn-sm btn-primary">{isPending ? "Please Wait..." : "Submit"}</button>
|
<button type="submit" disabled={isPending} className="btn btn-sm btn-primary">{isPending ? "Please Wait..." : "Submit"}</button>
|
||||||
<button type="button" disabled={isPending} className="btn btn-sm btn-secondary" onClick={onClose}>Cancel</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
const toBase64 = (file) =>
|
const toBase64 = (file) =>
|
||||||
@ -10,11 +10,15 @@ const toBase64 = (file) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const LogoUpload = ({ preview, setPreview, fileName, setFileName }) => {
|
export const LogoUpload = ({ preview, setPreview, fileName, setFileName }) => {
|
||||||
const {
|
const { register, setValue, watch, formState: { errors } } = useFormContext();
|
||||||
register,
|
const logoImage = watch("logoImage");
|
||||||
setValue,
|
|
||||||
formState: { errors },
|
// Sync preview when the form value changes
|
||||||
} = useFormContext();
|
useEffect(() => {
|
||||||
|
if (logoImage && !preview) {
|
||||||
|
setPreview(logoImage); // Use base64 as preview
|
||||||
|
}
|
||||||
|
}, [logoImage, preview, setPreview]);
|
||||||
|
|
||||||
const handleUpload = async (e) => {
|
const handleUpload = async (e) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useFormContext, Controller } from "react-hook-form";
|
import { useFormContext, Controller } from "react-hook-form";
|
||||||
import Label from "../common/Label";
|
import Label from "../common/Label";
|
||||||
import DatePicker from "../common/DatePicker";
|
import DatePicker from "../common/DatePicker";
|
||||||
@ -6,11 +6,14 @@ import { useCreateTenant, useIndustries } from "../../hooks/useTenant";
|
|||||||
import { LogoUpload } from "./LogoUpload";
|
import { LogoUpload } from "./LogoUpload";
|
||||||
import { orgSize, reference } from "../../utils/constants";
|
import { orgSize, reference } from "../../utils/constants";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import { useGlobalServices } from "../../hooks/masterHook/useMaster";
|
||||||
|
import SelectMultiple from "../common/SelectMultiple";
|
||||||
|
|
||||||
const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
|
const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
|
||||||
const { data, isError, isLoading: industryLoading } = useIndustries();
|
const { data, isError, isLoading: industryLoading } = useIndustries();
|
||||||
const [logoPreview, setLogoPreview] = useState(null);
|
const [logoPreview, setLogoPreview] = useState(null);
|
||||||
const [logoName, setLogoName] = useState("");
|
const [logoName, setLogoName] = useState("");
|
||||||
|
const { data: services, isLoading: serviceLoading } = useGlobalServices();
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
control,
|
control,
|
||||||
@ -42,6 +45,7 @@ const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
|
|||||||
"industryId",
|
"industryId",
|
||||||
"reference",
|
"reference",
|
||||||
"logoImage",
|
"logoImage",
|
||||||
|
"serviceIds",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (valid) {
|
if (valid) {
|
||||||
@ -53,6 +57,13 @@ const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const logoImage = getValues("logoImage");
|
||||||
|
if (logoImage) {
|
||||||
|
setLogoPreview(logoImage);
|
||||||
|
setLogoName("Uploaded Logo");
|
||||||
|
}
|
||||||
|
}, [getValues]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -181,7 +192,7 @@ const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-sm-6">
|
<div className="col-sm-6">
|
||||||
<Label htmlFor="reference">Reference</Label>
|
<Label htmlFor="reference" required>Reference</Label>
|
||||||
<select
|
<select
|
||||||
id="reference"
|
id="reference"
|
||||||
className="form-select shadow-none border py-1 px-2 small"
|
className="form-select shadow-none border py-1 px-2 small"
|
||||||
@ -198,6 +209,20 @@ const OrganizationInfo = ({ onNext, onPrev, onSubmitTenant }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="col-sm-6">
|
||||||
|
<SelectMultiple
|
||||||
|
name="serviceIds"
|
||||||
|
label="Services"
|
||||||
|
options={services?.data}
|
||||||
|
isLoading={serviceLoading}
|
||||||
|
labelKey="name"
|
||||||
|
valueKey="id"
|
||||||
|
/>
|
||||||
|
{errors.serviceIds && (
|
||||||
|
<div className="danger-text">{errors.serviceIds.message}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div className="col-sm-12">
|
<div className="col-sm-12">
|
||||||
<Label htmlFor="description">Description</Label>
|
<Label htmlFor="description">Description</Label>
|
||||||
|
|||||||
@ -5,12 +5,12 @@ import GlobalModel from "../common/GlobalModel";
|
|||||||
import { useTenantContext } from "../../pages/Tenant/TenantPage";
|
import { useTenantContext } from "../../pages/Tenant/TenantPage";
|
||||||
import { useTenantDetailsContext } from "../../pages/Tenant/TenantDetails";
|
import { useTenantDetailsContext } from "../../pages/Tenant/TenantDetails";
|
||||||
import IconButton from "../common/IconButton";
|
import IconButton from "../common/IconButton";
|
||||||
import { hasUserPermission } from "../../utils/authUtils";
|
|
||||||
import { MANAGE_TENANTS } from "../../utils/constants";
|
import { MANAGE_TENANTS } from "../../utils/constants";
|
||||||
|
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||||
|
|
||||||
const Profile = ({ data }) => {
|
const Profile = ({ data }) => {
|
||||||
const {setEditTenant} = useTenantDetailsContext()
|
const {setEditTenant} = useTenantDetailsContext()
|
||||||
const canUpdateTenant = hasUserPermission(MANAGE_TENANTS)
|
const canUpdateTenant = useHasUserPermission(MANAGE_TENANTS)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="container-fuid">
|
<div className="container-fuid">
|
||||||
|
|||||||
@ -224,7 +224,7 @@ const SubScription = ({ onSubmitSubScription, onNext }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{Object.keys(errors).length > 0 && (
|
{Object.keys(errors).length > 0 && (
|
||||||
<div class="alert alert-danger" role="alert">
|
<div role="alert">
|
||||||
{Object.entries(errors).map(([key, error]) => (
|
{Object.entries(errors).map(([key, error]) => (
|
||||||
<div key={key} className="danger-text">
|
<div key={key} className="danger-text">
|
||||||
{error?.message}
|
{error?.message}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ const TenantFilterPanel = ({ onApply }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
// ✅ Close popup when navigating to another component
|
// Close popup when navigating to another component
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleClosePanel();
|
handleClosePanel();
|
||||||
@ -106,13 +106,13 @@ const TenantFilterPanel = ({ onApply }) => {
|
|||||||
<div className="d-flex justify-content-end py-3 gap-2">
|
<div className="d-flex justify-content-end py-3 gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-label-secondary btn-xs"
|
className="btn btn-label-secondary btn-sm"
|
||||||
onClick={onClear}
|
onClick={onClear}
|
||||||
|
|
||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" className="btn btn-primary btn-xs" >
|
<button type="submit" className="btn btn-primary btn-sm" >
|
||||||
Apply
|
Apply
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -33,6 +33,7 @@ export const newTenantSchema = z.object({
|
|||||||
organizationSize: z.string().nonempty("Organization size is required"),
|
organizationSize: z.string().nonempty("Organization size is required"),
|
||||||
industryId: z.string().uuid("Invalid industry ID"),
|
industryId: z.string().uuid("Invalid industry ID"),
|
||||||
reference: z.string().nonempty("Reference is required"),
|
reference: z.string().nonempty("Reference is required"),
|
||||||
|
serviceIds: z.array(z.string()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const tenantDefaultValues = {
|
export const tenantDefaultValues = {
|
||||||
@ -51,13 +52,14 @@ export const tenantDefaultValues = {
|
|||||||
organizationSize: "",
|
organizationSize: "",
|
||||||
industryId: "", // should be a valid UUID if pre-filled
|
industryId: "", // should be a valid UUID if pre-filled
|
||||||
reference: "",
|
reference: "",
|
||||||
|
serviceIds:[]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const getSubscriptionSchema = (minUsers) =>
|
export const getSubscriptionSchema = (minUsers) =>
|
||||||
z.object({
|
z.object({
|
||||||
planId: z.string().min(1, { message: "Please select Plan" }),
|
planId: z.string().min(1, { message: "Please select a plan to continue" }),
|
||||||
currencyId: z.string().uuid("Invalid currency"),
|
currencyId: z.string().uuid("Invalid currency"),
|
||||||
maxUsers: z
|
maxUsers: z
|
||||||
.number({ invalid_type_error: "Must be a number" })
|
.number({ invalid_type_error: "Must be a number" })
|
||||||
|
|||||||
@ -1,14 +1,13 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useController } from "react-hook-form";
|
import { useController } from "react-hook-form";
|
||||||
|
|
||||||
|
|
||||||
const DatePicker = ({
|
const DatePicker = ({
|
||||||
name,
|
name,
|
||||||
control,
|
control,
|
||||||
placeholder = "DD-MM-YYYY",
|
placeholder = "DD-MM-YYYY",
|
||||||
className = "",
|
className = "",
|
||||||
allowText = false,
|
allowText = false,
|
||||||
maxDate, // removed default new Date()
|
maxDate,
|
||||||
minDate,
|
minDate,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
@ -22,43 +21,43 @@ const DatePicker = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inputRef.current) {
|
if (!inputRef.current) return;
|
||||||
flatpickr(inputRef.current, {
|
|
||||||
|
const fp = flatpickr(inputRef.current, {
|
||||||
dateFormat: "d-m-Y",
|
dateFormat: "d-m-Y",
|
||||||
allowInput: allowText,
|
allowInput: allowText,
|
||||||
defaultDate: value
|
defaultDate: value ? new Date(value) : null, // safely convert to Date
|
||||||
? flatpickr.parseDate(value, "Y-m-d")
|
maxDate: maxDate ? new Date(maxDate) : undefined,
|
||||||
: null,
|
minDate: minDate ? new Date(minDate) : undefined,
|
||||||
maxDate: maxDate ?? undefined, // only applied if passed
|
onChange: (selectedDates) => {
|
||||||
minDate: minDate ? new Date(minDate.split("T")[0]) : undefined,
|
|
||||||
onChange: function (selectedDates) {
|
|
||||||
if (selectedDates.length > 0) {
|
if (selectedDates.length > 0) {
|
||||||
// store in YYYY-MM-DD
|
onChange(flatpickr.formatDate(selectedDates[0], "Y-m-d"));
|
||||||
const formatted = flatpickr.formatDate(selectedDates[0], "Y-m-d");
|
|
||||||
onChange(formatted);
|
|
||||||
} else {
|
} else {
|
||||||
onChange("");
|
onChange("");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
...rest
|
...rest
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
return () => {
|
||||||
|
fp.destroy(); // clean up on unmount
|
||||||
|
};
|
||||||
}, [inputRef, value, allowText, maxDate, minDate, rest, onChange]);
|
}, [inputRef, value, allowText, maxDate, minDate, rest, onChange]);
|
||||||
|
|
||||||
|
const displayValue = value ? flatpickr.formatDate(new Date(value), "d-m-Y") : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={` position-relative ${className}`}>
|
<div className={`position-relative ${className}`}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control form-control-sm "
|
className="form-control form-control-sm"
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
defaultValue={
|
value={displayValue}
|
||||||
value
|
onChange={(e) => {
|
||||||
? flatpickr.formatDate(
|
if (allowText) {
|
||||||
flatpickr.parseDate(value, "Y-m-d"),
|
onChange(e.target.value); // allow manual typing if enabled
|
||||||
"d-m-Y"
|
|
||||||
)
|
|
||||||
: ""
|
|
||||||
}
|
}
|
||||||
|
}}
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
inputRef.current = el;
|
inputRef.current = el;
|
||||||
ref(el);
|
ref(el);
|
||||||
@ -70,7 +69,7 @@ const DatePicker = ({
|
|||||||
<span
|
<span
|
||||||
className="position-absolute top-50 end-0 pe-1 translate-middle-y cursor-pointer"
|
className="position-absolute top-50 end-0 pe-1 translate-middle-y cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (inputRef.current && inputRef.current._flatpickr) {
|
if (inputRef.current?._flatpickr) {
|
||||||
inputRef.current._flatpickr.open();
|
inputRef.current._flatpickr.open();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -148,10 +148,20 @@ export const DateRangePicker1 = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (defaultRange && resetSignal !== undefined) {
|
if (resetSignal !== undefined) {
|
||||||
|
if (defaultRange) {
|
||||||
applyDefaultDates();
|
applyDefaultDates();
|
||||||
|
} else {
|
||||||
|
setValue(startField, "", { shouldValidate: true });
|
||||||
|
setValue(endField, "", { shouldValidate: true });
|
||||||
|
|
||||||
|
if (inputRef.current?._flatpickr) {
|
||||||
|
inputRef.current._flatpickr.clear();
|
||||||
}
|
}
|
||||||
}, [resetSignal, defaultRange]);
|
}
|
||||||
|
}
|
||||||
|
}, [resetSignal, defaultRange, setValue, startField, endField]);
|
||||||
|
|
||||||
|
|
||||||
const start = getValues(startField);
|
const start = getValues(startField);
|
||||||
const end = getValues(endField);
|
const end = getValues(endField);
|
||||||
|
|||||||
@ -203,7 +203,7 @@ const FilterIcon = ({
|
|||||||
<>
|
<>
|
||||||
<li><hr className="my-1" /></li>
|
<li><hr className="my-1" /></li>
|
||||||
<li>
|
<li>
|
||||||
<div className="fw-bold text-dark mb-1">Floors</div>
|
<div className="fw-bold text-dark mb-2 mt-2">Floors</div>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
{uniqueFloors.length > 0 ? (
|
{uniqueFloors.length > 0 ? (
|
||||||
uniqueFloors.map((floor, idx) => (
|
uniqueFloors.map((floor, idx) => (
|
||||||
@ -235,7 +235,7 @@ const FilterIcon = ({
|
|||||||
<>
|
<>
|
||||||
<li><hr className="my-1" /></li>
|
<li><hr className="my-1" /></li>
|
||||||
<li>
|
<li>
|
||||||
<div className="fw-bold text-dark mb-1">Activities</div>
|
<div className="fw-bold text-dark mb-2 mt-2">Activities</div>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
{uniqueActivities.length > 0 ? (
|
{uniqueActivities.length > 0 ? (
|
||||||
uniqueActivities.map((activity, idx) => (
|
uniqueActivities.map((activity, idx) => (
|
||||||
|
|||||||
65
src/components/common/HoverPopup.jsx
Normal file
65
src/components/common/HoverPopup.jsx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
const HoverPopup = ({ title, content, children }) => {
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const triggerRef = useRef(null);
|
||||||
|
const popupRef = useRef(null);
|
||||||
|
|
||||||
|
// Toggle on hover or click
|
||||||
|
const handleMouseEnter = () => setVisible(true);
|
||||||
|
const handleClick = () => setVisible((prev) => !prev);
|
||||||
|
|
||||||
|
// Hide on outside click
|
||||||
|
useEffect(() => {
|
||||||
|
const handleDocumentClick = (e) => {
|
||||||
|
if (
|
||||||
|
!popupRef.current?.contains(e.target) &&
|
||||||
|
!triggerRef.current?.contains(e.target)
|
||||||
|
) {
|
||||||
|
setVisible(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (visible) {
|
||||||
|
document.addEventListener("click", handleDocumentClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("click", handleDocumentClick);
|
||||||
|
};
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="d-inline-block position-relative"
|
||||||
|
ref={triggerRef}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onClick={handleClick}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
{visible && (
|
||||||
|
<div
|
||||||
|
ref={popupRef}
|
||||||
|
className="bg-white border rounded shadow-sm py-1 px-2 text-start text-lowercase"
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "100%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translateX(-50%)",
|
||||||
|
zIndex: 1050,
|
||||||
|
minWidth: "220px",
|
||||||
|
marginTop: "8px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title && <h6 className="mb-2 text-capitalize">{title}</h6>}
|
||||||
|
<div className="text-capitalize">{content}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HoverPopup;
|
||||||
|
|
||||||
50
src/components/common/Modal.jsx
Normal file
50
src/components/common/Modal.jsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
|
||||||
|
const Modal = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
disabled,
|
||||||
|
size = "md",
|
||||||
|
position = "top",
|
||||||
|
}) => {
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
if (disabled) return;
|
||||||
|
onClose();
|
||||||
|
}, [disabled, onClose]);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="modal fade show"
|
||||||
|
style={{ display: "block", backgroundColor: "rgba(0,0,0,0.6)" }}
|
||||||
|
tabIndex="-1"
|
||||||
|
role="dialog"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`modal-dialog modal-${size} modal-dialog-${position}`}
|
||||||
|
role="document"
|
||||||
|
>
|
||||||
|
<div className="modal-content text-white shadow-lg">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="modal-header justify-content-center pb-2 border-0">
|
||||||
|
<h5 className="modal-title">{title}</h5>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-close btn-close-white"
|
||||||
|
onClick={handleClose}
|
||||||
|
aria-label="Close"
|
||||||
|
></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Body */}
|
||||||
|
<div className="modal-body pt-0">{body}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Modal;
|
||||||
@ -2,18 +2,23 @@ import React, { useState, useEffect, useRef } from "react";
|
|||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import "./MultiSelectDropdown.css";
|
import "./MultiSelectDropdown.css";
|
||||||
|
import Label from "./Label";
|
||||||
|
|
||||||
const SelectMultiple = ({
|
const SelectMultiple = ({
|
||||||
name,
|
name,
|
||||||
options = [],
|
options = [],
|
||||||
label = "Select options",
|
label = "Select options",
|
||||||
labelKey = "name", // Can now be a function or a string
|
labelKey = "name",
|
||||||
valueKey = "id",
|
valueKey = "id",
|
||||||
placeholder = "Please select...",
|
placeholder = "Please select...",
|
||||||
IsLoading = false,
|
IsLoading = false,required = false
|
||||||
}) => {
|
}) => {
|
||||||
const { setValue, watch } = useFormContext();
|
const { setValue, watch,register } = useFormContext();
|
||||||
const selectedValues = watch(name) || [];
|
useEffect(() => {
|
||||||
|
register(name, { value: [] });
|
||||||
|
}, [register, name]);
|
||||||
|
|
||||||
|
const selectedValues = watch(name) || [];
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [searchText, setSearchText] = useState("");
|
const [searchText, setSearchText] = useState("");
|
||||||
@ -59,10 +64,14 @@ const SelectMultiple = ({
|
|||||||
setValue(name, updated, { shouldValidate: true });
|
setValue(name, updated, { shouldValidate: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const filteredOptions = options.filter((item) => {
|
const filteredOptions = (options || []).filter((item) => {
|
||||||
const label = getLabel(item);
|
const label = getLabel(item);
|
||||||
return label?.toLowerCase().includes(searchText.toLowerCase());
|
return (
|
||||||
});
|
typeof label === "string" &&
|
||||||
|
label.toLowerCase().includes(searchText.toLowerCase())
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
const dropdownElement = (
|
const dropdownElement = (
|
||||||
<div
|
<div
|
||||||
@ -132,6 +141,7 @@ const SelectMultiple = ({
|
|||||||
<>
|
<>
|
||||||
<div ref={containerRef} className="multi-select-dropdown-container" style={{ position: "relative" }}>
|
<div ref={containerRef} className="multi-select-dropdown-container" style={{ position: "relative" }}>
|
||||||
<label className="form-label mb-1">{label}</label>
|
<label className="form-label mb-1">{label}</label>
|
||||||
|
<Label className={name} required={required}></Label>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="multi-select-dropdown-header"
|
className="multi-select-dropdown-header"
|
||||||
|
|||||||
@ -1,258 +0,0 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { useForm, useFieldArray } from "react-hook-form";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { MasterRespository } from "../../repositories/MastersRepository";
|
|
||||||
import showToast from "../../services/toastService";
|
|
||||||
import { getCachedData, cacheData } from "../../slices/apiDataManager";
|
|
||||||
import { useUpdateActivity } from "../../hooks/masterHook/useMaster";
|
|
||||||
import Label from "../common/Label";
|
|
||||||
|
|
||||||
|
|
||||||
const schema = z.object({
|
|
||||||
activityName: z.string().min(1, { message: "Activity name is required" }),
|
|
||||||
unitOfMeasurement: z.string().min(1, { message: "Measurement is required" }),
|
|
||||||
checkList: z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
id: z.any().default(null),
|
|
||||||
description: z.string().min(1, { message: "Checklist item cannot be empty" }),
|
|
||||||
isMandatory: z.boolean().default(false),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const UpdateActivity = ({ activityData, onClose }) => {
|
|
||||||
const { mutate: updateActivity, isPending: isLoading } = useUpdateActivity(() => onClose?.());
|
|
||||||
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
control,
|
|
||||||
setValue,
|
|
||||||
reset,
|
|
||||||
setError,
|
|
||||||
clearErrors,
|
|
||||||
getValues,
|
|
||||||
formState: { errors },
|
|
||||||
} = useForm({
|
|
||||||
resolver: zodResolver(schema),
|
|
||||||
defaultValues: {
|
|
||||||
id: activityData?.id,
|
|
||||||
activityName: activityData?.activityName,
|
|
||||||
unitOfMeasurement: activityData?.unitOfMeasurement,
|
|
||||||
checkList: activityData?.checkLists || [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { fields: checkListItems, append, remove } = useFieldArray({
|
|
||||||
control,
|
|
||||||
name: "checkList",
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (activityData) {
|
|
||||||
reset({
|
|
||||||
id: activityData.id,
|
|
||||||
activityName: activityData.activityName,
|
|
||||||
unitOfMeasurement: activityData.unitOfMeasurement,
|
|
||||||
checkList: activityData.checkLists || [],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [activityData, reset]);
|
|
||||||
|
|
||||||
const addChecklistItem = () => {
|
|
||||||
const values = getValues("checkList");
|
|
||||||
const lastIndex = checkListItems.length - 1;
|
|
||||||
|
|
||||||
if (
|
|
||||||
checkListItems.length > 0 &&
|
|
||||||
(!values?.[lastIndex] || values[lastIndex].description.trim() === "")
|
|
||||||
) {
|
|
||||||
setError(`checkList.${lastIndex}.description`, {
|
|
||||||
type: "manual",
|
|
||||||
message: "Please fill this checklist item before adding another.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearErrors(`checkList.${lastIndex}.description`);
|
|
||||||
append({ id: null, description: "", isMandatory: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeChecklistItem = (index) => {
|
|
||||||
remove(index);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChecklistChange = (index, value) => {
|
|
||||||
setValue(`checkList.${index}`, value);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = (formData) => {
|
|
||||||
const payload = { ...formData, id: activityData.id };
|
|
||||||
updateActivity({ id: activityData.id, payload });
|
|
||||||
};
|
|
||||||
// const onSubmit = async(data) => {
|
|
||||||
// setIsLoading(true);
|
|
||||||
|
|
||||||
// const Activity = {...data, id:activityData.id}
|
|
||||||
// try
|
|
||||||
// {
|
|
||||||
// const response = await MasterRespository.updateActivity( activityData?.id, Activity );
|
|
||||||
// const updatedActivity = response.data;
|
|
||||||
// const cachedData = getCachedData("Activity")
|
|
||||||
|
|
||||||
// if (cachedData) {
|
|
||||||
// const updatedActivities = cachedData.map((activity) =>
|
|
||||||
// activity.id === updatedActivity.id ? { ...activity, ...updatedActivity } : activity
|
|
||||||
// );
|
|
||||||
|
|
||||||
// cacheData( "Activity", updatedActivities );
|
|
||||||
// onClose()
|
|
||||||
// }
|
|
||||||
// setIsLoading( false )
|
|
||||||
// showToast("Activity Successfully Updated", "success");
|
|
||||||
// } catch ( err )
|
|
||||||
// {
|
|
||||||
// setIsLoading( false )
|
|
||||||
|
|
||||||
// showToast("error.message", "error");
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const tooltipTriggerList = Array.from(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
|
||||||
tooltipTriggerList.forEach((el) => new bootstrap.Tooltip(el));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
{/* <h6>Update Activity</h6> */}
|
|
||||||
<div className="row">
|
|
||||||
{/* Activity Name */}
|
|
||||||
<div className="col-md-6 text-start">
|
|
||||||
<Label className="form-label" required>Activity Name</Label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
{...register("activityName")}
|
|
||||||
className={`form-control form-control-sm ${errors.activityName ? "is-invalid" : ""}`}
|
|
||||||
/>
|
|
||||||
{errors.activityName && (
|
|
||||||
<div className="text-danger">{errors.activityName.message}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Unit of Measurement */}
|
|
||||||
<div className="col-md-6 text-start">
|
|
||||||
<Label className="form-label" required>Measurement</Label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
{...register("unitOfMeasurement")}
|
|
||||||
className={`form-control form-control-sm ${errors.unitOfMeasurement ? "is-invalid" : ""}`}
|
|
||||||
/>
|
|
||||||
{errors.unitOfMeasurement && (
|
|
||||||
<div className="text-danger">{errors.unitOfMeasurement.message}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Checklist */}
|
|
||||||
<div className="col-md-12 text-start mt-1">
|
|
||||||
<p className="py-1 my-0">{checkListItems.length > 0 ? "Check List" : "Add Check List"}</p>
|
|
||||||
{checkListItems.length > 0 && (
|
|
||||||
<table className="table mt-1 border-0">
|
|
||||||
<thead className="py-0 my-0 table-border-top-0">
|
|
||||||
<tr className="py-1">
|
|
||||||
<th colSpan={2} className="py-1">
|
|
||||||
<small>Name</small>
|
|
||||||
</th>
|
|
||||||
<th colSpan={2} className="py-1 text-center">
|
|
||||||
<small>Is Mandatory</small>
|
|
||||||
</th>
|
|
||||||
<th className="text-center py-1">Action</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{checkListItems.map((item, index) => (
|
|
||||||
<tr key={item.id} className="border-top-0">
|
|
||||||
<td colSpan={2} className=" border-0">
|
|
||||||
<input
|
|
||||||
className="d-none"
|
|
||||||
{...register(`checkList.${index}.id`)}
|
|
||||||
></input>
|
|
||||||
<input
|
|
||||||
{...register(`checkList.${index}.description`)}
|
|
||||||
className="form-control form-control-sm"
|
|
||||||
placeholder={`Checklist item ${index + 1}`}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleChecklistChange(index, e.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{errors.checkList?.[index]?.description && (
|
|
||||||
<small
|
|
||||||
style={{ fontSize: "10px" }}
|
|
||||||
className="danger-text"
|
|
||||||
>
|
|
||||||
{errors.checkList[index]?.description?.message}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td colSpan={2} className="text-center border-0">
|
|
||||||
<input
|
|
||||||
className="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
{...register(`checkList.${index}.isMandatory`)}
|
|
||||||
defaultChecked={item.isMandatory}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td className="text-center border-0">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => removeChecklistItem(index)}
|
|
||||||
className="btn btn-xs btn-icon btn-text-secondary"
|
|
||||||
|
|
||||||
>
|
|
||||||
<i className="bx bxs-minus-circle text-danger" data-bs-toggle="tooltip"
|
|
||||||
title="Add Check"
|
|
||||||
data-bs-original-title="Add check" ></i>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-xs btn-primary mt-2"
|
|
||||||
onClick={addChecklistItem}
|
|
||||||
>
|
|
||||||
<i className="bx bx-plus-circle" data-bs-toggle="tooltip"
|
|
||||||
title="Add Check"
|
|
||||||
data-bs-original-title="Add check" ></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Submit / Cancel */}
|
|
||||||
<div className="col-12 text-end mt-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-sm btn-label-secondary me-3"
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button type="submit" className="btn btn-sm btn-primary">
|
|
||||||
{isLoading ? "Please Wait" : "Submit"}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default UpdateActivity;
|
|
||||||
@ -3,8 +3,6 @@ import CreateRole from "./CreateRole";
|
|||||||
import EditRole from "./EditRole";
|
import EditRole from "./EditRole";
|
||||||
import CreateJobRole from "./CreateJobRole";
|
import CreateJobRole from "./CreateJobRole";
|
||||||
import EditJobRole from "./EditJobRole";
|
import EditJobRole from "./EditJobRole";
|
||||||
import CreateActivity from "./CreateActivity";
|
|
||||||
import EditActivity from "./EditActivity";
|
|
||||||
import CreateWorkCategory from "./CreateWorkCategory";
|
import CreateWorkCategory from "./CreateWorkCategory";
|
||||||
import EditWorkCategory from "./EditWorkCategory";
|
import EditWorkCategory from "./EditWorkCategory";
|
||||||
import CreateCategory from "./CreateContactCategory";
|
import CreateCategory from "./CreateContactCategory";
|
||||||
@ -16,6 +14,8 @@ import ManagePaymentMode from "./ManagePaymentMode";
|
|||||||
import ManageExpenseStatus from "./ManageExpenseStatus";
|
import ManageExpenseStatus from "./ManageExpenseStatus";
|
||||||
import ManageDocumentCategory from "./ManageDocumentCategory";
|
import ManageDocumentCategory from "./ManageDocumentCategory";
|
||||||
import ManageDocumentType from "./ManageDocumentType";
|
import ManageDocumentType from "./ManageDocumentType";
|
||||||
|
import ManageServices from "./Services/ManageServices";
|
||||||
|
import ServiceGroups from "./Services/ServicesGroups";
|
||||||
|
|
||||||
const MasterModal = ({ modaldata, closeModal }) => {
|
const MasterModal = ({ modaldata, closeModal }) => {
|
||||||
if (!modaldata?.modalType || modaldata.modalType === "delete") {
|
if (!modaldata?.modalType || modaldata.modalType === "delete") {
|
||||||
@ -34,8 +34,6 @@ const MasterModal = ({ modaldata, closeModal }) => {
|
|||||||
),
|
),
|
||||||
"Job Role": <CreateJobRole onClose={closeModal} />,
|
"Job Role": <CreateJobRole onClose={closeModal} />,
|
||||||
"Edit-Job Role": <EditJobRole data={item} onClose={closeModal} />,
|
"Edit-Job Role": <EditJobRole data={item} onClose={closeModal} />,
|
||||||
"Activity": <CreateActivity onClose={closeModal} />,
|
|
||||||
"Edit-Activity": <EditActivity activityData={item} onClose={closeModal} />,
|
|
||||||
"Work Category": <CreateWorkCategory onClose={closeModal} />,
|
"Work Category": <CreateWorkCategory onClose={closeModal} />,
|
||||||
"Edit-Work Category": <EditWorkCategory data={item} onClose={closeModal} />,
|
"Edit-Work Category": <EditWorkCategory data={item} onClose={closeModal} />,
|
||||||
"Contact Category": <CreateCategory data={item} onClose={closeModal} />,
|
"Contact Category": <CreateCategory data={item} onClose={closeModal} />,
|
||||||
@ -60,6 +58,15 @@ const MasterModal = ({ modaldata, closeModal }) => {
|
|||||||
"Edit-Document Type": (
|
"Edit-Document Type": (
|
||||||
<ManageDocumentType data={item} onClose={closeModal} />
|
<ManageDocumentType data={item} onClose={closeModal} />
|
||||||
),
|
),
|
||||||
|
"Services": (
|
||||||
|
<ManageServices onClose={closeModal} />
|
||||||
|
),
|
||||||
|
"Edit-Services": (
|
||||||
|
<ManageServices data={item} onClose={closeModal} />
|
||||||
|
),
|
||||||
|
"Manage-Services": (
|
||||||
|
<ServiceGroups service={item} onClose={closeModal}/>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
return modalComponents[modalType] || null;
|
return modalComponents[modalType] || null;
|
||||||
|
|||||||
@ -3,20 +3,23 @@ import { useFieldArray, useForm } from "react-hook-form";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
|
||||||
import { MasterRespository } from "../../repositories/MastersRepository";
|
import {
|
||||||
import { clearApiCacheKey } from "../../slices/apiCacheSlice";
|
useCreateActivity,
|
||||||
import { getCachedData, cacheData } from "../../slices/apiDataManager";
|
useUpdateActivity,
|
||||||
import showToast from "../../services/toastService";
|
} from "../../../hooks/masterHook/useMaster";
|
||||||
import { useCreateActivity } from "../../hooks/masterHook/useMaster";
|
import Label from "../../common/Label";
|
||||||
import Label from "../common/Label";
|
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
activityName: z.string().min(1, { message: "Activity Name is required" }),
|
activityName: z.string().min(1, { message: "Activity Name is required" }),
|
||||||
unitOfMeasurement: z.string().min(1, { message: "Unit of Measurement is required" }),
|
unitOfMeasurement: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "Unit of Measurement is required" }),
|
||||||
checkList: z
|
checkList: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
description: z.string().min(1, { message: "descriptionlist item cannot be empty" }),
|
description: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "descriptionlist item cannot be empty" }),
|
||||||
isMandatory: z.boolean().default(false),
|
isMandatory: z.boolean().default(false),
|
||||||
id: z.any().default(null),
|
id: z.any().default(null),
|
||||||
})
|
})
|
||||||
@ -24,9 +27,14 @@ const schema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const CreateActivity = ({ onClose }) => {
|
const ManageActivity = ({ activity = null, whichGroup = null, close }) => {
|
||||||
const maxDescriptionLength = 255;
|
const maxDescriptionLength = 255;
|
||||||
const { mutate: createActivity, isPending: isLoading } = useCreateActivity(() => onClose?.());
|
const { mutate: createActivity, isPending: isLoading } = useCreateActivity(
|
||||||
|
() => close?.()
|
||||||
|
);
|
||||||
|
const { mutate: UpdateActivity, isPending: isUpdating } = useUpdateActivity(
|
||||||
|
() => close?.()
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@ -75,56 +83,70 @@ const CreateActivity = ({ onClose }) => {
|
|||||||
append({ id: null, description: "", isMandatory: false });
|
append({ id: null, description: "", isMandatory: false });
|
||||||
}, [checkListItems, getValues, append, setError, clearErrors]);
|
}, [checkListItems, getValues, append, setError, clearErrors]);
|
||||||
|
|
||||||
const removeChecklistItem = useCallback((index) => {
|
const removeChecklistItem = useCallback(
|
||||||
|
(index) => {
|
||||||
remove(index);
|
remove(index);
|
||||||
}, [remove]);
|
},
|
||||||
|
[remove]
|
||||||
|
);
|
||||||
|
|
||||||
const handleChecklistChange = useCallback((index, value) => {
|
const handleChecklistChange = useCallback(
|
||||||
|
(index, value) => {
|
||||||
setValue(`checkList.${index}`, value);
|
setValue(`checkList.${index}`, value);
|
||||||
}, [setValue]);
|
},
|
||||||
|
[setValue]
|
||||||
|
);
|
||||||
|
|
||||||
const onSubmit = (formData) => {
|
const onSubmit = (formData) => {
|
||||||
createActivity(formData);
|
let payload = {
|
||||||
|
...formData,
|
||||||
|
activityGroupId: whichGroup,
|
||||||
|
};
|
||||||
|
if (activity) {
|
||||||
|
UpdateActivity({ id: activity.id, payload: payload });
|
||||||
|
} else {
|
||||||
|
createActivity(payload);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// const onSubmit = (data) => {
|
|
||||||
// setIsLoading(true);
|
|
||||||
|
|
||||||
// MasterRespository.createActivity(data)
|
|
||||||
// .then( ( resp ) =>
|
|
||||||
// {
|
|
||||||
|
|
||||||
// const cachedData = getCachedData("Activity");
|
|
||||||
// const updatedData = [ ...cachedData, resp?.data ];
|
|
||||||
// cacheData("Activity", updatedData);
|
|
||||||
// showToast("Activity Successfully Added.", "success");
|
|
||||||
// setIsLoading(false);
|
|
||||||
// handleClose()
|
|
||||||
// })
|
|
||||||
// .catch((error) => {
|
|
||||||
// showToast(error.message, "error");
|
|
||||||
// setIsLoading(false);
|
|
||||||
// });
|
|
||||||
// };
|
|
||||||
const handleClose = useCallback(() => {
|
|
||||||
reset();
|
|
||||||
onClose();
|
|
||||||
}, [reset, onClose]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tooltipTriggerList = Array.from(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
if (activity) {
|
||||||
|
reset({
|
||||||
|
activityName: activity.activityName || "",
|
||||||
|
unitOfMeasurement: activity.unitOfMeasurement || "",
|
||||||
|
checkList: activity.checkLists?.map((check) => ({
|
||||||
|
id: check.id || null, // Use the ID provided in the checklist
|
||||||
|
description: check.description || "",
|
||||||
|
isMandatory: check.isMandatory || false,
|
||||||
|
})) || [{ description: "", isMandatory: false }], // Default to an empty checklist item
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [activity, reset]);
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
reset();
|
||||||
|
close();
|
||||||
|
}, [reset, close]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const tooltipTriggerList = Array.from(
|
||||||
|
document.querySelectorAll('[data-bs-toggle="tooltip"]')
|
||||||
|
);
|
||||||
tooltipTriggerList.forEach((el) => new bootstrap.Tooltip(el));
|
tooltipTriggerList.forEach((el) => new bootstrap.Tooltip(el));
|
||||||
}, []);
|
}, []);
|
||||||
|
let isPending = isLoading || isUpdating;
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
<form onSubmit={handleSubmit(onSubmit)} className="px-7">
|
||||||
{/* <h6>Create Activity</h6> */}
|
{/* <h6>Create Activity</h6> */}
|
||||||
<div className="row">
|
<div className="row border border-1 border-secondary rounded-3 mt-2">
|
||||||
<div className="col-6 text-start">
|
<div className="col-6 text-start mt-3">
|
||||||
<Label className="form-label" required>Activity</Label>
|
<Label className="form-label" required>
|
||||||
|
Activity
|
||||||
|
</Label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
{...register("activityName")}
|
{...register("activityName")}
|
||||||
className={`form-control form-control-sm ${errors.activityName ? "is-invalid" : ""
|
className={`form-control form-control-sm ${
|
||||||
|
errors.activityName ? "is-invalid" : ""
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
{errors.activityName && (
|
{errors.activityName && (
|
||||||
@ -132,12 +154,15 @@ const CreateActivity = ({ onClose }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-6 text-start">
|
<div className="col-6 text-start mt-3">
|
||||||
<Label className="form-label" required>Measurement</Label>
|
<Label className="form-label" required>
|
||||||
|
Measurement
|
||||||
|
</Label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
{...register("unitOfMeasurement")}
|
{...register("unitOfMeasurement")}
|
||||||
className={`form-control form-control-sm ${errors.unitOfMeasurement ? "is-invalid" : ""
|
className={`form-control form-control-sm ${
|
||||||
|
errors.unitOfMeasurement ? "is-invalid" : ""
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
{errors.unitOfMeasurement && (
|
{errors.unitOfMeasurement && (
|
||||||
@ -145,12 +170,20 @@ const CreateActivity = ({ onClose }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-md-12 text-start mt-1">
|
<div className="col-md-12 text-start mt-3">
|
||||||
<p className="py-1 my-0">{checkListItems.length > 0 ? "Check List" : "Add Check List"}</p>
|
<label className="py-1 form-label my-0">
|
||||||
{checkListItems.length > 0 && (
|
Add Check List <i
|
||||||
<table className="table mt-1 border-0">
|
className="bx bx-plus-circle text-primary cursor-pointer"
|
||||||
<thead className="py-0 my-0 table-border-top-0">
|
data-bs-toggle="tooltip"
|
||||||
<tr className="py-1">
|
title="Add Check"
|
||||||
|
data-bs-original-title="Add check"
|
||||||
|
onClick={addChecklistItem}
|
||||||
|
></i>
|
||||||
|
</label>
|
||||||
|
{checkListItems.length > 0 ? (
|
||||||
|
<table className="table mt-1 border-none">
|
||||||
|
<thead className="py-0 my-0 border-none ">
|
||||||
|
<tr className="py-1 border-secondary ">
|
||||||
<th colSpan={2} className="py-1">
|
<th colSpan={2} className="py-1">
|
||||||
<small>Name</small>
|
<small>Name</small>
|
||||||
</th>
|
</th>
|
||||||
@ -160,10 +193,10 @@ const CreateActivity = ({ onClose }) => {
|
|||||||
<th className="text-center py-1">Action</th>
|
<th className="text-center py-1">Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="table-border-bottom-0 ">
|
<tbody className="table-border-bottom-0 border-secondary ">
|
||||||
{checkListItems.map((item, index) => (
|
{checkListItems.map((item, index) => (
|
||||||
<tr key={index} className="border-top-0">
|
<tr key={index}>
|
||||||
<td colSpan={2} className="border-top-0 border-0">
|
<td colSpan={2} className="border-none" >
|
||||||
<input
|
<input
|
||||||
className="d-none"
|
className="d-none"
|
||||||
{...register(`checkList.${index}.id`)}
|
{...register(`checkList.${index}.id`)}
|
||||||
@ -185,7 +218,7 @@ const CreateActivity = ({ onClose }) => {
|
|||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td colSpan={2} className="text-center border-0">
|
<td colSpan={2} className="text-center border-none">
|
||||||
<input
|
<input
|
||||||
className="form-check-input"
|
className="form-check-input"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@ -193,49 +226,44 @@ const CreateActivity = ({ onClose }) => {
|
|||||||
defaultChecked={item.isMandatory}
|
defaultChecked={item.isMandatory}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-center border-0">
|
<td className="text-center border-none">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => removeChecklistItem(index)}
|
onClick={() => removeChecklistItem(index)}
|
||||||
className="btn btn-xs btn-icon btn-text-secondary"
|
className="btn btn-xs btn-icon btn-text-secondary"
|
||||||
>
|
>
|
||||||
<i className="bx bxs-minus-circle text-danger" data-bs-toggle="tooltip"
|
<i
|
||||||
|
className="bx bxs-minus-circle text-danger"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
title="Remove Check"
|
title="Remove Check"
|
||||||
data-bs-original-title="Remove check"></i>
|
data-bs-original-title="Remove check"
|
||||||
|
></i>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
)}
|
):(<div className="d-flex justify-content-center my-0"><p className="text-secondary m-0">Not Yet Added</p> </div>)}
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-xs btn-primary mt-2"
|
|
||||||
onClick={addChecklistItem}
|
|
||||||
>
|
|
||||||
<i className="bx bx-plus-circle" data-bs-toggle="tooltip"
|
|
||||||
title="Add Check"
|
|
||||||
data-bs-original-title="Add check" ></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="col-12 text-end mt-3">
|
<div className="col-12 text-end mt-3 mb-4">
|
||||||
<button
|
<button
|
||||||
type="reset"
|
type="reset"
|
||||||
className="btn btn-sm btn-label-secondary me-3"
|
className="btn btn-sm btn-label-secondary me-3"
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
|
disabled={isPending}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" className="btn btn-sm btn-primary">
|
<button type="submit" className="btn btn-sm btn-primary">
|
||||||
{isLoading ? "Please Wait" : "Submit"}
|
{isPending ? "Please Wait" : activity ? "Update" : "Submit"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CreateActivity;
|
export default ManageActivity;
|
||||||
110
src/components/master/Services/ManageGroup.jsx
Normal file
110
src/components/master/Services/ManageGroup.jsx
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import {
|
||||||
|
useCreateActivityGroup,
|
||||||
|
useUpdateActivityGroup,
|
||||||
|
} from "../../../hooks/masterHook/useMaster";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { ActivityGroupSchema } from "./ServicesSchema";
|
||||||
|
import Label from "../../common/Label";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
const ManageGroup = ({ group = null, whichService = null, close }) => {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm({
|
||||||
|
resolver: zodResolver(ActivityGroupSchema),
|
||||||
|
defaultValues: { name: "", description: "" },
|
||||||
|
});
|
||||||
|
const { mutate: createGroup, isPending: isCreating } = useCreateActivityGroup(
|
||||||
|
() => close()
|
||||||
|
);
|
||||||
|
const { mutate: UpdateGroup, isPending: isUpdating } = useUpdateActivityGroup(
|
||||||
|
() => close()
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (group) {
|
||||||
|
reset({
|
||||||
|
name: group.name || " ",
|
||||||
|
description: group.description || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [group, reset]);
|
||||||
|
const onSubmit = (formdata) => {
|
||||||
|
if (group) {
|
||||||
|
let payload = {
|
||||||
|
...formdata,
|
||||||
|
serviceId: whichService,
|
||||||
|
id: group.id,
|
||||||
|
};
|
||||||
|
UpdateGroup({ id: group.id, payload: payload });
|
||||||
|
} else {
|
||||||
|
let payload = {
|
||||||
|
...formdata,
|
||||||
|
serviceId: whichService,
|
||||||
|
};
|
||||||
|
|
||||||
|
createGroup(payload);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let isPending = isCreating || isUpdating;
|
||||||
|
return (
|
||||||
|
<form className="row px-12" onSubmit={handleSubmit(onSubmit)} >
|
||||||
|
<div className="border border-1 border-secondary rounded-3 p-2 mt-2">
|
||||||
|
<div className="col-12 col-md-12 text-start">
|
||||||
|
<Label className="form-label" required>
|
||||||
|
Group Name
|
||||||
|
</Label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
{...register("name")}
|
||||||
|
className={`form-control form-control-sm ${errors.name ? "is-invalids" : ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
{errors.name && (
|
||||||
|
<p className="danger-text m-0">{errors.name.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="col-12 col-md-12 text-start mb-2">
|
||||||
|
<Label className="form-label" htmlFor="description" required>
|
||||||
|
Description
|
||||||
|
</Label>
|
||||||
|
<textarea
|
||||||
|
rows="3"
|
||||||
|
{...register("description")}
|
||||||
|
className={`form-control form-control-sm ${errors.description ? "is-invalids" : ""
|
||||||
|
}`}
|
||||||
|
></textarea>
|
||||||
|
|
||||||
|
{errors.description && (
|
||||||
|
<p className="danger-text m-0">{errors.description.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-12 text-end mt-5">
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-label-secondary me-3"
|
||||||
|
aria-label="Close"
|
||||||
|
disabled={isPending}
|
||||||
|
onClick={close}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-sm btn-primary"
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
{isPending ? "Please Wait..." : group ? "Update" : "Submit"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ManageGroup;
|
||||||
107
src/components/master/Services/ManageServices.jsx
Normal file
107
src/components/master/Services/ManageServices.jsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import React, { useEffect } from "react";
|
||||||
|
import Label from "../../common/Label";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { useCreateService, useUpdateService } from "../../../hooks/masterHook/useMaster";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
name: z.string().min(1, { message: "Service Name is required" }),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "Description is required" })
|
||||||
|
.max(255, { message: "Description cannot exceed 255 characters" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ManageServices = ({ data , onClose }) => {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: { name: "", description: "" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutate: CreateServices, isPending: Creating } = useCreateService(() =>
|
||||||
|
onClose?.()
|
||||||
|
);
|
||||||
|
const { mutate: UpdateServices, isPending: Updating } = useUpdateService(() =>
|
||||||
|
onClose?.()
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSubmit = (payload) => {
|
||||||
|
if (data && data.id) {
|
||||||
|
UpdateServices({ id: data.id, payload: { ...payload, id: data.id } });
|
||||||
|
} else {
|
||||||
|
CreateServices(payload);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
reset({
|
||||||
|
name: data.name ?? "",
|
||||||
|
description: data.description ?? "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data, reset]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="col-12 col-md-12 text-start">
|
||||||
|
<Label className="form-label" required>
|
||||||
|
Service Name
|
||||||
|
</Label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
{...register("name")}
|
||||||
|
className={`form-control ${errors.name ? "is-invalid" : ""}`}
|
||||||
|
/>
|
||||||
|
{errors.name && <p className="danger-text">{errors.name.message}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-12 col-md-12 text-start">
|
||||||
|
<Label className="form-label" htmlFor="description" required>
|
||||||
|
Description
|
||||||
|
</Label>
|
||||||
|
<textarea
|
||||||
|
rows="3"
|
||||||
|
{...register("description")}
|
||||||
|
className={`form-control ${errors.description ? "is-invalid" : ""}`}
|
||||||
|
></textarea>
|
||||||
|
{errors.description && (
|
||||||
|
<p className="danger-text">{errors.description.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-12 text-end">
|
||||||
|
<button
|
||||||
|
type="reset"
|
||||||
|
className="btn btn-sm btn-label-secondary me-3"
|
||||||
|
data-bs-dismiss="modal"
|
||||||
|
aria-label="Close"
|
||||||
|
disabled={Creating || Updating}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-sm btn-primary"
|
||||||
|
disabled={Creating || Updating}
|
||||||
|
>
|
||||||
|
{Creating
|
||||||
|
? "Please Wait..."
|
||||||
|
: Updating
|
||||||
|
? "Please Wait..."
|
||||||
|
: data
|
||||||
|
? "Update"
|
||||||
|
: "Submit"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ManageServices;
|
||||||
263
src/components/master/Services/ServicesGroups.jsx
Normal file
263
src/components/master/Services/ServicesGroups.jsx
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
useActivitiesByGroups,
|
||||||
|
useGroups,
|
||||||
|
} from "../../../hooks/masterHook/useMaster";
|
||||||
|
import ManageGroup from "./ManageGroup";
|
||||||
|
import ManageActivity from "./ManageActivity";
|
||||||
|
import { useMasterContext } from "../../../pages/master/MasterPage";
|
||||||
|
const ServiceGroups = ({ service }) => {
|
||||||
|
const [openService, setOpenService] = useState(true);
|
||||||
|
const [activeGroupId, setActiveGroupId] = useState(null); // track selected group
|
||||||
|
const {setDeleletingServiceItem} =useMasterContext()
|
||||||
|
const [isManageGroup, setManageGroup] = useState({
|
||||||
|
isOpen: false,
|
||||||
|
group: null,
|
||||||
|
serviceId: null,
|
||||||
|
});
|
||||||
|
const [isManageActivity, setManageActivity] = useState({
|
||||||
|
isOpen: false,
|
||||||
|
activity: null, // activity is either a single activity for editing or null for creating new activity
|
||||||
|
groupId: null, // groupId for managing activities in specific groups
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch groups and activities data
|
||||||
|
const { data: groups, isLoading } = useGroups(service?.id); // Fetch groups for the service
|
||||||
|
const { data: activities, isLoading: actLoading } =
|
||||||
|
useActivitiesByGroups(activeGroupId); // Fetch activities based on activeGroupId
|
||||||
|
|
||||||
|
if (isLoading) return <div>Loading groups...</div>; // Show loading state while groups are being fetched
|
||||||
|
|
||||||
|
// Handle toggling of group to view activities
|
||||||
|
const toggleGroup = (groupId) => {
|
||||||
|
setActiveGroupId((prev) => (prev === groupId ? null : groupId)); // If the same group is clicked again, close it
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
|
||||||
|
<div className="w-100 my-2">
|
||||||
|
|
||||||
|
<p className="fs-5 fw-semibold">Manage Service</p>
|
||||||
|
<div className="accordion" id="accordionExample">
|
||||||
|
<div className="accordion-item active shadow-none">
|
||||||
|
{/* Service Header */}
|
||||||
|
<div className="d-flex justify-content-between text-start align-items-center accordion-header py-1">
|
||||||
|
<p className="m-0 fw-bold fs-6">{service.name}</p>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-primary"
|
||||||
|
onClick={() =>
|
||||||
|
setManageGroup({
|
||||||
|
isOpen: true,
|
||||||
|
group: null, // No group selected, for creating a new group
|
||||||
|
serviceId: service.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<i className="bx bx-plus-circle me-2"></i>Add Group
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Groups Section */}
|
||||||
|
<div
|
||||||
|
id="accordionOne"
|
||||||
|
className={`accordion-collapse collapse ${
|
||||||
|
openService ? "show" : ""
|
||||||
|
}`}
|
||||||
|
aria-labelledby="headingOne"
|
||||||
|
data-bs-parent="#accordionExample"
|
||||||
|
>
|
||||||
|
{/* Show ManageGroup for creating a new group */}
|
||||||
|
{isManageGroup.isOpen && isManageGroup.group === null ? (
|
||||||
|
<ManageGroup
|
||||||
|
group={null} // Indicating new group creation
|
||||||
|
whichService={isManageGroup.serviceId}
|
||||||
|
close={() =>
|
||||||
|
setManageGroup({
|
||||||
|
isOpen: false,
|
||||||
|
group: null,
|
||||||
|
serviceId: service.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="accordion-body text-start m-0 p-0">
|
||||||
|
<div className="dropdown-divider border"></div>
|
||||||
|
|
||||||
|
{groups?.data?.map((group) => {
|
||||||
|
const isOpen = activeGroupId === group.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="accordion-item shadow-none m-0 py-2 px-2"
|
||||||
|
key={group.id}
|
||||||
|
>
|
||||||
|
<div className="d-flex justify-content-between text-start accordion-header">
|
||||||
|
{/* Show group toggle button only if ManageGroup is not open */}
|
||||||
|
<div className="d-flex gap-1 align-items-center">
|
||||||
|
{!isManageGroup.isOpen && (
|
||||||
|
<span
|
||||||
|
onClick={() => toggleGroup(group.id)}
|
||||||
|
className="text-end cursor-pointer"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target={`#accordionGroup${group.id}`}
|
||||||
|
aria-expanded={isOpen}
|
||||||
|
aria-controls={`accordionGroup${group.id}`}
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className={`bx bx-lg ${
|
||||||
|
isOpen ? "bx-chevron-up" : "bx-chevron-down"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<p className="m-0 fw-bold ">{group.name}</p>
|
||||||
|
</div>
|
||||||
|
<div className="d-flex flex-row gap-3">
|
||||||
|
<div className="d-flex flex-row gap-2">
|
||||||
|
{/* Create New Activity */}
|
||||||
|
<i
|
||||||
|
className="bx bx-plus-circle text-primary cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
setManageActivity({
|
||||||
|
isOpen: true,
|
||||||
|
activity: null, // Indicating new activity creation
|
||||||
|
groupId: group.id, // Set the groupId for the new activity
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* Edit Group */}
|
||||||
|
<i
|
||||||
|
className="bx bx-edit text-secondary cursor-pointer"
|
||||||
|
onClick={() =>
|
||||||
|
setManageGroup({
|
||||||
|
isOpen: true,
|
||||||
|
group: group, // Group selected for Editing
|
||||||
|
serviceId: service.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{/* Delete Group */}
|
||||||
|
<i className="bx bx-trash text-danger cursor-pointer" onClick={()=>setDeleletingServiceItem({isOpen:true,ItemId:group.id,whichItem:"group"})} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Only show ManageGroup for the specific group if it's open */}
|
||||||
|
{isManageGroup.isOpen &&
|
||||||
|
isManageGroup.group?.id === group.id ? (
|
||||||
|
<ManageGroup
|
||||||
|
group={group} // For editing
|
||||||
|
whichService={isManageGroup.serviceId}
|
||||||
|
close={() =>
|
||||||
|
setManageGroup({
|
||||||
|
isOpen: false,
|
||||||
|
group: null,
|
||||||
|
serviceId: null,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : isManageActivity.isOpen &&
|
||||||
|
isManageActivity.groupId === group.id ? (
|
||||||
|
<ManageActivity
|
||||||
|
activity={isManageActivity.activity} // Pass the activity object for editing
|
||||||
|
whichGroup={group.id} // Set groupId for creating/editing activity
|
||||||
|
close={() => {
|
||||||
|
setManageActivity({
|
||||||
|
isOpen: false,
|
||||||
|
activity: null,
|
||||||
|
groupId: null,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
id={`accordionGroup${group.id}`}
|
||||||
|
className={`accordion-collapse collapse ${
|
||||||
|
isOpen ? "show" : ""
|
||||||
|
}`}
|
||||||
|
aria-labelledby={group.id}
|
||||||
|
data-bs-parent="#accordionOne"
|
||||||
|
>
|
||||||
|
<div className="accordion-body ">
|
||||||
|
{isOpen && actLoading && (
|
||||||
|
<p className="text-center m-0">
|
||||||
|
Loading activities...
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isOpen && activities?.data?.length > 0 ? (
|
||||||
|
<div className="row border-top-2">
|
||||||
|
{/* Header Row */}
|
||||||
|
<div className="col-12 d-flex justify-content-between py-2 border-bottom px-4">
|
||||||
|
<span className="fw-semibold text-uppercase">
|
||||||
|
Activity Name
|
||||||
|
</span>
|
||||||
|
<span className="fw-semibold text-uppercase">
|
||||||
|
Unit of Measurement
|
||||||
|
</span>
|
||||||
|
<span className="fw-semibold text-uppercase">
|
||||||
|
Action
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Map through activities */}
|
||||||
|
{activities.data.map((activity) => (
|
||||||
|
<div
|
||||||
|
className="col-12 d-flex justify-content-between py-2 "
|
||||||
|
key={activity.id}
|
||||||
|
>
|
||||||
|
{/* Activity Name Column */}
|
||||||
|
<div className="col d-flex justify-content-start">
|
||||||
|
<span>{activity.activityName}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Unit of Measurement Column */}
|
||||||
|
<div className="col d-flex justify-content-start">
|
||||||
|
<span>{activity.unitOfMeasurement}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Column */}
|
||||||
|
<div className="col-auto d-flex gap-3 justify-content-end">
|
||||||
|
{/* Edit Activity */}
|
||||||
|
<i
|
||||||
|
className="bx bx-sm bx-edit text-secondary cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
setManageActivity({
|
||||||
|
isOpen: true,
|
||||||
|
activity: activity, // Pass the specific activity for editing
|
||||||
|
groupId: group.id, // Set groupId for the specific activity
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* Delete Activity */}
|
||||||
|
<i
|
||||||
|
className="bx bx-sm bx-trash text-danger cursor-pointer"
|
||||||
|
onClick={() => setDeleletingServiceItem({isOpen:true,ItemId:activity.id,whichItem:"activity"})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
isOpen && (
|
||||||
|
<p className="text-center m-0">
|
||||||
|
No activities found
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ServiceGroups;
|
||||||
17
src/components/master/Services/ServicesSchema.js
Normal file
17
src/components/master/Services/ServicesSchema.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
name: z.string().min(1, { message: "Service Name is required" }),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "Description is required" })
|
||||||
|
.max(255, { message: "Description cannot exceed 255 characters" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ActivityGroupSchema = z.object({
|
||||||
|
name: z.string().min(1, { message: "Group Name is required" }),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: "Description is required" })
|
||||||
|
.max(255, { message: "Description cannot exceed 255 characters" }),
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
@ -12,46 +12,76 @@ import { setDefaultDateRange } from "../slices/localVariablesSlice";
|
|||||||
// ----------------------------Query-----------------------------
|
// ----------------------------Query-----------------------------
|
||||||
|
|
||||||
|
|
||||||
export const useAttendance = (projectId) => {
|
// export const useAttendance = (projectId) => {
|
||||||
const dispatch = useDispatch()
|
// const dispatch = useDispatch()
|
||||||
|
// const {
|
||||||
|
// data: attendance = [],
|
||||||
|
// isLoading: loading,
|
||||||
|
// error,
|
||||||
|
// refetch: recall,
|
||||||
|
// isFetching
|
||||||
|
// } = useQuery({
|
||||||
|
// queryKey: ["attendance", projectId],
|
||||||
|
// queryFn: async () => {
|
||||||
|
// const response = await AttendanceRepository.getAttendance(projectId);
|
||||||
|
// return response.data;
|
||||||
|
// },
|
||||||
|
// enabled: !!projectId,
|
||||||
|
// onError: (error) => {
|
||||||
|
// showToast(error.message || "Error while fetching Attendance", "error");
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return {
|
||||||
|
// attendance,
|
||||||
|
// loading,
|
||||||
|
// error,
|
||||||
|
// recall,
|
||||||
|
// isFetching
|
||||||
|
// };
|
||||||
|
// };
|
||||||
|
|
||||||
|
export const useAttendance = (projectId, organizationId, includeInactive = false, date = null) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: attendance = [],
|
data: attendance = [],
|
||||||
isLoading: loading,
|
isLoading: loading,
|
||||||
error,
|
error,
|
||||||
refetch: recall,
|
refetch: recall,
|
||||||
isFetching
|
isFetching,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["attendance", projectId],
|
queryKey: ["attendance", projectId, organizationId, includeInactive, date], // include filters in cache key
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await AttendanceRepository.getAttendance(projectId);
|
const response = await AttendanceRepository.getAttendance(
|
||||||
|
projectId,
|
||||||
|
organizationId,
|
||||||
|
includeInactive,
|
||||||
|
date
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
enabled: !!projectId,
|
enabled: !!projectId, // only run if projectId exists
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
showToast(error.message || "Error while fetching Attendance", "error");
|
showToast(error.message || "Error while fetching Attendance", "error");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return { attendance, loading, error, recall, isFetching };
|
||||||
attendance,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
recall,
|
|
||||||
isFetching
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAttendancesLogs = (projectId, fromDate, toDate) => {
|
export const useAttendancesLogs = (projectId, fromDate, toDate,organizationId) => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const enabled = !!projectId && !!fromDate && !!toDate;
|
const enabled = !!projectId && !!fromDate && !!toDate;
|
||||||
|
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ['attendanceLogs', projectId, fromDate, toDate],
|
queryKey: ['attendanceLogs', projectId, fromDate, toDate,organizationId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await AttendanceRepository.getAttendanceFilteredByDate(
|
const res = await AttendanceRepository.getAttendanceFilteredByDate(
|
||||||
projectId,
|
projectId,
|
||||||
fromDate,
|
fromDate,
|
||||||
toDate
|
toDate,
|
||||||
|
organizationId
|
||||||
);
|
);
|
||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
@ -112,30 +142,58 @@ export const useAttendanceByEmployee = (employeeId, fromDate, toDate) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useRegularizationRequests = (projectId) => {
|
// export const useRegularizationRequests = (projectId) => {
|
||||||
|
// const {
|
||||||
|
// data: regularizes = [],
|
||||||
|
// isLoading: loading,
|
||||||
|
// error,
|
||||||
|
// refetch,
|
||||||
|
// } = useQuery({
|
||||||
|
// queryKey: ["regularizedList", projectId],
|
||||||
|
// queryFn: async () => {
|
||||||
|
// const response = await AttendanceRepository.getRegularizeList(projectId);
|
||||||
|
// return response.data;
|
||||||
|
// },
|
||||||
|
// enabled: !!projectId,
|
||||||
|
// onError: (error) => {
|
||||||
|
// showToast(error.message || "Error while fetching Regularization Requests", "error");
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return {
|
||||||
|
// regularizes,
|
||||||
|
// loading,
|
||||||
|
// error,
|
||||||
|
// refetch,
|
||||||
|
// };
|
||||||
|
// };
|
||||||
|
|
||||||
|
export const useRegularizationRequests = (projectId, organizationId, IncludeInActive = false) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: regularizes = [],
|
data: regularizes = [],
|
||||||
isLoading: loading,
|
isLoading: loading,
|
||||||
error,
|
error,
|
||||||
refetch,
|
refetch: recall,
|
||||||
|
isFetching,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["regularizedList", projectId],
|
queryKey: ["regularizedList", projectId, organizationId, IncludeInActive], // include filters in cache key
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await AttendanceRepository.getRegularizeList(projectId);
|
const response = await AttendanceRepository.getRegularizeList(
|
||||||
|
projectId,
|
||||||
|
organizationId,
|
||||||
|
IncludeInActive,
|
||||||
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
enabled: !!projectId,
|
enabled: !!projectId, // only run if projectId exists
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
showToast(error.message || "Error while fetching Regularization Requests", "error");
|
showToast(error.message || "Error while fetching regularizes", "error");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return { regularizes, loading, error, recall, isFetching };
|
||||||
regularizes,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
refetch,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
87
src/hooks/useAuth.jsx
Normal file
87
src/hooks/useAuth.jsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
import {
|
||||||
|
Mutation,
|
||||||
|
useMutation,
|
||||||
|
useQuery,
|
||||||
|
useQueryClient,
|
||||||
|
} from "@tanstack/react-query";
|
||||||
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
|
import AuthRepository from "../repositories/AuthRepository.jsx";
|
||||||
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import {
|
||||||
|
closeAuthModal,
|
||||||
|
openAuthModal,
|
||||||
|
} from "../slices/localVariablesSlice.jsx";
|
||||||
|
import { removeSession } from "../utils/authUtils.js";
|
||||||
|
|
||||||
|
export const useTenants = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["tenantlist"],
|
||||||
|
queryFn: async () => await AuthRepository.getTenantList(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSelectTenant = (onSuccessCallBack) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (tenantId) => {
|
||||||
|
const res = await AuthRepository.selectTenant(tenantId);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (localStorage.getItem("jwtToken")) {
|
||||||
|
localStorage.setItem("jwtToken", data.token);
|
||||||
|
localStorage.setItem("refreshToken", data.refreshToken);
|
||||||
|
} else {
|
||||||
|
sessionStorage.setItem("jwtToken", data.token);
|
||||||
|
sessionStorage.setItem("refreshToken", data.refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onSuccessCallBack) onSuccessCallBack();
|
||||||
|
},
|
||||||
|
|
||||||
|
onError: (error) => {
|
||||||
|
showToast(error.message || "Error while creating project", "error");
|
||||||
|
localStorage.removeItem("jwtToken");
|
||||||
|
localStorage.removeItem("refreshToken");
|
||||||
|
localStorage.removeItem("ctnt");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAuthModal = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { isOpen } = useSelector((state) => state.localVariables.AuthModal);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isOpen,
|
||||||
|
onOpen: () => dispatch(openAuthModal()),
|
||||||
|
onClose: () => dispatch(closeAuthModal()),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const useLogout = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
let payload = { refreshToken: localStorage.getItem("refreshToken") || sessionStorage.getItem("refreshToken") };
|
||||||
|
return await AuthRepository.logout(payload);
|
||||||
|
},
|
||||||
|
|
||||||
|
onSuccess: (data) => {
|
||||||
|
removeSession()
|
||||||
|
|
||||||
|
window.location.href = "/auth/login";
|
||||||
|
if (onSuccessCallBack) onSuccessCallBack();
|
||||||
|
},
|
||||||
|
|
||||||
|
onError: (error) => {
|
||||||
|
showToast(error.message || "Error while creating project", "error");
|
||||||
|
removeSession()
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -1,7 +1,8 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import GlobalRepository from "../repositories/GlobalRepository";
|
import GlobalRepository from "../repositories/GlobalRepository";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
|
||||||
// 🔹 Dashboard Progression Data Hook
|
|
||||||
export const useDashboard_Data = ({ days, FromDate, projectId }) => {
|
export const useDashboard_Data = ({ days, FromDate, projectId }) => {
|
||||||
const [dashboard_data, setDashboard_Data] = useState([]);
|
const [dashboard_data, setDashboard_Data] = useState([]);
|
||||||
const [isLineChartLoading, setLoading] = useState(false);
|
const [isLineChartLoading, setLoading] = useState(false);
|
||||||
@ -38,120 +39,120 @@ export const useDashboard_Data = ({ days, FromDate, projectId }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const useDashboard_AttendanceData = (date, projectId) => {
|
// export const useDashboard_AttendanceData = (date, projectId) => {
|
||||||
const [dashboard_Attendancedata, setDashboard_AttendanceData] = useState([]);
|
// const [dashboard_Attendancedata, setDashboard_AttendanceData] = useState([]);
|
||||||
const [isLineChartLoading, setLoading] = useState(false);
|
// const [isLineChartLoading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState("");
|
// const [error, setError] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
const fetchData = async () => {
|
// const fetchData = async () => {
|
||||||
setLoading(true);
|
// setLoading(true);
|
||||||
setError("");
|
// setError("");
|
||||||
|
|
||||||
try {
|
// try {
|
||||||
const response = await GlobalRepository.getDashboardAttendanceData(date, projectId); // date in 2nd param
|
// const response = await GlobalRepository.getDashboardAttendanceData(date, projectId); // date in 2nd param
|
||||||
setDashboard_AttendanceData(response.data);
|
// setDashboard_AttendanceData(response.data);
|
||||||
} catch (err) {
|
// } catch (err) {
|
||||||
setError("Failed to fetch dashboard data.");
|
// setError("Failed to fetch dashboard data.");
|
||||||
console.error(err);
|
// console.error(err);
|
||||||
} finally {
|
// } finally {
|
||||||
setLoading(false);
|
// setLoading(false);
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
if (date && projectId !== null) {
|
// if (date && projectId !== null) {
|
||||||
fetchData();
|
// fetchData();
|
||||||
}
|
// }
|
||||||
}, [date, projectId]);
|
// }, [date, projectId]);
|
||||||
|
|
||||||
return { dashboard_Attendancedata, isLineChartLoading: isLineChartLoading, error };
|
// return { dashboard_Attendancedata, isLineChartLoading: isLineChartLoading, error };
|
||||||
};
|
// };
|
||||||
|
|
||||||
|
|
||||||
// 🔹 Dashboard Projects Card Data Hook
|
// 🔹 Dashboard Projects Card Data Hook
|
||||||
export const useDashboardProjectsCardData = () => {
|
// export const useDashboardProjectsCardData = () => {
|
||||||
const [projectsCardData, setProjectsData] = useState([]);
|
// const [projectsCardData, setProjectsData] = useState([]);
|
||||||
const [loading, setLoading] = useState(false);
|
// const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState("");
|
// const [error, setError] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
const fetchProjectsData = async () => {
|
// const fetchProjectsData = async () => {
|
||||||
setLoading(true);
|
// setLoading(true);
|
||||||
setError("");
|
// setError("");
|
||||||
|
|
||||||
try {
|
// try {
|
||||||
const response = await GlobalRepository.getDashboardProjectsCardData();
|
// const response = await GlobalRepository.getDashboardProjectsCardData();
|
||||||
setProjectsData(response.data);
|
// setProjectsData(response.data);
|
||||||
} catch (err) {
|
// } catch (err) {
|
||||||
setError("Failed to fetch projects card data.");
|
// setError("Failed to fetch projects card data.");
|
||||||
console.error(err);
|
// console.error(err);
|
||||||
} finally {
|
// } finally {
|
||||||
setLoading(false);
|
// setLoading(false);
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
fetchProjectsData();
|
// fetchProjectsData();
|
||||||
}, []);
|
// }, []);
|
||||||
|
|
||||||
return { projectsCardData, loading, error };
|
// return { projectsCardData, loading, error };
|
||||||
};
|
// };
|
||||||
|
|
||||||
// 🔹 Dashboard Teams Card Data Hook
|
// 🔹 Dashboard Teams Card Data Hook
|
||||||
export const useDashboardTeamsCardData = (projectId) => {
|
// export const useDashboardTeamsCardData = (projectId) => {
|
||||||
const [teamsCardData, setTeamsData] = useState({});
|
// const [teamsCardData, setTeamsData] = useState({});
|
||||||
const [loading, setLoading] = useState(false);
|
// const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState("");
|
// const [error, setError] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
const fetchTeamsData = async () => {
|
// const fetchTeamsData = async () => {
|
||||||
setLoading(true);
|
// setLoading(true);
|
||||||
setError("");
|
// setError("");
|
||||||
|
|
||||||
try {
|
// try {
|
||||||
const response = await GlobalRepository.getDashboardTeamsCardData(projectId);
|
// const response = await GlobalRepository.getDashboardTeamsCardData(projectId);
|
||||||
setTeamsData(response.data || {});
|
// setTeamsData(response.data || {});
|
||||||
} catch (err) {
|
// } catch (err) {
|
||||||
setError("Failed to fetch teams card data.");
|
// setError("Failed to fetch teams card data.");
|
||||||
console.error("Error fetching teams card data:", err);
|
// console.error("Error fetching teams card data:", err);
|
||||||
setTeamsData({});
|
// setTeamsData({});
|
||||||
} finally {
|
// } finally {
|
||||||
setLoading(false);
|
// setLoading(false);
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
fetchTeamsData();
|
// fetchTeamsData();
|
||||||
}, [projectId]);
|
// }, [projectId]);
|
||||||
|
|
||||||
return { teamsCardData, loading, error };
|
// return { teamsCardData, loading, error };
|
||||||
};
|
// };
|
||||||
|
|
||||||
export const useDashboardTasksCardData = (projectId) => {
|
// export const useDashboardTasksCardData = (projectId) => {
|
||||||
const [tasksCardData, setTasksData] = useState({});
|
// const [tasksCardData, setTasksData] = useState({});
|
||||||
const [loading, setLoading] = useState(false);
|
// const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState("");
|
// const [error, setError] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
const fetchTasksData = async () => {
|
// const fetchTasksData = async () => {
|
||||||
setLoading(true);
|
// setLoading(true);
|
||||||
setError("");
|
// setError("");
|
||||||
|
|
||||||
try {
|
// try {
|
||||||
const response = await GlobalRepository.getDashboardTasksCardData(projectId);
|
// const response = await GlobalRepository.getDashboardTasksCardData(projectId);
|
||||||
setTasksData(response.data);
|
// setTasksData(response.data);
|
||||||
} catch (err) {
|
// } catch (err) {
|
||||||
setError("Failed to fetch tasks card data.");
|
// setError("Failed to fetch tasks card data.");
|
||||||
console.error(err);
|
// console.error(err);
|
||||||
setTasksData({});
|
// setTasksData({});
|
||||||
} finally {
|
// } finally {
|
||||||
setLoading(false);
|
// setLoading(false);
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
fetchTasksData();
|
// fetchTasksData();
|
||||||
}, [projectId]);
|
// }, [projectId]);
|
||||||
|
|
||||||
return { tasksCardData, loading, error };
|
// return { tasksCardData, loading, error };
|
||||||
};
|
// };
|
||||||
|
|
||||||
|
|
||||||
export const useAttendanceOverviewData = (projectId, days) => {
|
export const useAttendanceOverviewData = (projectId, days) => {
|
||||||
@ -180,3 +181,75 @@ export const useAttendanceOverviewData = (projectId, days) => {
|
|||||||
|
|
||||||
return { attendanceOverviewData, loading, error };
|
return { attendanceOverviewData, loading, error };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// -------------------Query----------------------------
|
||||||
|
|
||||||
|
// export const useDashboard_Data = (days, FromDate, projectId)=>{
|
||||||
|
// return useQuery({
|
||||||
|
// queryKey:["dashboardProjectProgress"],
|
||||||
|
// queryFn:async()=> {
|
||||||
|
// const payload = {
|
||||||
|
// days,
|
||||||
|
// FromDate: FromDate || '',
|
||||||
|
// projectId: projectId || null,
|
||||||
|
// };
|
||||||
|
// const resp = await GlobalRepository.getDashboardProgressionData(payload);
|
||||||
|
// return resp.data;
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
export const useDashboard_AttendanceData = (date,projectId)=>{
|
||||||
|
return useQuery({
|
||||||
|
queryKey:["dashboardAttendances",date,projectId],
|
||||||
|
queryFn:async()=> {
|
||||||
|
|
||||||
|
const resp = await await GlobalRepository.getDashboardAttendanceData(date, projectId)
|
||||||
|
return resp.data;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDashboardTeamsCardData =(projectId)=>{
|
||||||
|
return useQuery({
|
||||||
|
queryKey:["dashboardTeams",projectId],
|
||||||
|
queryFn:async()=> {
|
||||||
|
|
||||||
|
const resp = await GlobalRepository.getDashboardTeamsCardData(projectId)
|
||||||
|
return resp.data;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDashboardTasksCardData = (projectId) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey:["dashboardTasks",projectId],
|
||||||
|
queryFn:async()=> {
|
||||||
|
|
||||||
|
const resp = await GlobalRepository.getDashboardTasksCardData(projectId)
|
||||||
|
return resp.data;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// export const useAttendanceOverviewData = (projectId, days) => {
|
||||||
|
// return useQuery({
|
||||||
|
// queryKey:["dashboardAttendanceOverView",projectId],
|
||||||
|
// queryFn:async()=> {
|
||||||
|
|
||||||
|
// const resp = await GlobalRepository.getAttendanceOverview(projectId, days);
|
||||||
|
// return resp.data;
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
export const useDashboardProjectsCardData = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey:["dashboardProjects"],
|
||||||
|
queryFn:async()=> {
|
||||||
|
|
||||||
|
const resp = await GlobalRepository.getDashboardProjectsCardData();
|
||||||
|
return resp.data;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -221,7 +221,7 @@ export const useUpdateEmployee = () => {
|
|||||||
mutationFn: (employeeData) =>
|
mutationFn: (employeeData) =>
|
||||||
EmployeeRepository.manageEmployee(employeeData),
|
EmployeeRepository.manageEmployee(employeeData),
|
||||||
onSuccess: (_, variables) => {
|
onSuccess: (_, variables) => {
|
||||||
const id = variables.id || variables.employeeId;
|
const id = variables?.id || variables?.employeeId;
|
||||||
const isAllEmployee = variables.IsAllEmployee;
|
const isAllEmployee = variables.IsAllEmployee;
|
||||||
|
|
||||||
// Cache invalidation
|
// Cache invalidation
|
||||||
|
|||||||
196
src/hooks/useOrganization.js
Normal file
196
src/hooks/useOrganization.js
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
import { useSelector, useDispatch } from "react-redux";
|
||||||
|
import {
|
||||||
|
toggleOrgModal,
|
||||||
|
openOrgModal,
|
||||||
|
closeOrgModal,
|
||||||
|
} from "../slices/localVariablesSlice";
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import OrganizationRepository from "../repositories/OrganizationRespository";
|
||||||
|
import showToast from "../services/toastService";
|
||||||
|
|
||||||
|
export const useOrganizationModal = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { isOpen, orgData, startStep, prevStep, flowType } = useSelector(
|
||||||
|
(state) => state.localVariables.OrganizationModal
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isOpen,
|
||||||
|
orgData,
|
||||||
|
startStep,
|
||||||
|
prevStep,
|
||||||
|
flowType,
|
||||||
|
onOpen: (options = {}) =>
|
||||||
|
dispatch(
|
||||||
|
openOrgModal({
|
||||||
|
isOpen: true,
|
||||||
|
orgData: options.orgData ?? orgData ?? null,
|
||||||
|
startStep: options.startStep ?? startStep ?? 1,
|
||||||
|
prevStep: options.prevStep ?? prevStep ?? 1,
|
||||||
|
flowType: options.flowType ?? flowType ?? "default",
|
||||||
|
})
|
||||||
|
),
|
||||||
|
onClose: () => dispatch(closeOrgModal()),
|
||||||
|
onToggle: () => dispatch(toggleOrgModal()),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// ================================Query=============================================================
|
||||||
|
|
||||||
|
export const useOrganization=(id)=>{
|
||||||
|
return useQuery({
|
||||||
|
queryKey:["organization",id],
|
||||||
|
queryFn:async()=> {
|
||||||
|
const resp = await await OrganizationRepository.getOrganizaion(id);
|
||||||
|
return resp.data
|
||||||
|
},
|
||||||
|
enabled:!!id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
export const useOrganizationBySPRID = (sprid) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["organization by", sprid],
|
||||||
|
queryFn: async () => {
|
||||||
|
const resp = await OrganizationRepository.getOrganizationBySPRID(sprid);
|
||||||
|
return resp.data;
|
||||||
|
},
|
||||||
|
enabled: !!sprid,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useOrganizationsList = (
|
||||||
|
pageSize,
|
||||||
|
pageNumber,
|
||||||
|
active,
|
||||||
|
sprid,
|
||||||
|
searchString = ""
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: [
|
||||||
|
"organizationList",
|
||||||
|
pageSize,
|
||||||
|
pageNumber,
|
||||||
|
active,
|
||||||
|
sprid,
|
||||||
|
searchString,
|
||||||
|
],
|
||||||
|
queryFn: async () => {
|
||||||
|
const resp = await OrganizationRepository.getOrganizationList(
|
||||||
|
pageSize,
|
||||||
|
pageNumber,
|
||||||
|
active,
|
||||||
|
sprid,
|
||||||
|
searchString
|
||||||
|
);
|
||||||
|
return resp.data;
|
||||||
|
},
|
||||||
|
keepPreviousData: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useOrganizationEmployees = (
|
||||||
|
projectId,
|
||||||
|
organizationId,
|
||||||
|
searchString
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["OrgEmployees", projectId, organizationId, searchString],
|
||||||
|
queryFn: async () =>
|
||||||
|
await OrganizationRepository.getOrganizationEmployees(
|
||||||
|
projectId,
|
||||||
|
organizationId,
|
||||||
|
searchString
|
||||||
|
),
|
||||||
|
enabled: !!projectId ,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// =================================Mutation========================================================
|
||||||
|
|
||||||
|
export const useCreateOrganization = (onSuccessCallback) => {
|
||||||
|
const useClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (OrgPayload) =>
|
||||||
|
await OrganizationRepository.createOrganization(OrgPayload),
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
useClient.invalidateQueries({ queryKey: ["organizationList"] });
|
||||||
|
showToast("Organization created successfully", "success");
|
||||||
|
if (onSuccessCallback) onSuccessCallback();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
showToast(
|
||||||
|
error.response.data.message ||
|
||||||
|
error.message ||
|
||||||
|
"Something went wrong please try again !",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAssignOrgToProject = (onSuccessCallback) => {
|
||||||
|
const useClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (payload) =>
|
||||||
|
await OrganizationRepository.assignOrganizationToProject(payload),
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
const { projectId } = variables;
|
||||||
|
useClient.invalidateQueries({
|
||||||
|
queryKey: ["projectAssignedOrganiztions"],
|
||||||
|
});
|
||||||
|
useClient.invalidateQueries({
|
||||||
|
queryKey: ["projectAssignedOrganization", projectId],
|
||||||
|
});
|
||||||
|
showToast("Organization successfully", "success");
|
||||||
|
if (onSuccessCallback) onSuccessCallback();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
showToast(
|
||||||
|
error.response.data.message ||
|
||||||
|
error.message ||
|
||||||
|
"Something went wrong please try again !",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
export const useAssignOrgToTenant = (onSuccessCallback) => {
|
||||||
|
const useClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (payload) =>
|
||||||
|
await OrganizationRepository.assignOrganizationToTenanat(payload),
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
useClient.invalidateQueries({ queryKey: ["organizationList"] });
|
||||||
|
showToast("Organization added successfully", "success");
|
||||||
|
if (onSuccessCallback) onSuccessCallback();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
showToast(
|
||||||
|
error.response.data.message ||
|
||||||
|
error.message ||
|
||||||
|
"Something went wrong please try again !",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
export const useUpdateOrganization = (onSuccessCallback) => {
|
||||||
|
const useClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({orgId,payload}) =>
|
||||||
|
await OrganizationRepository.updateOrganizaion(orgId,payload),
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
useClient.invalidateQueries({ queryKey: ["organizationList"] });
|
||||||
|
showToast("Organization Updated successfully", "success");
|
||||||
|
if (onSuccessCallback) onSuccessCallback();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
showToast(
|
||||||
|
error.response.data.message ||
|
||||||
|
error.message ||
|
||||||
|
"Something went wrong please try again !",
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -6,21 +6,24 @@ import { VIEW_PROJECTS } from "../utils/constants";
|
|||||||
import showToast from "../services/toastService";
|
import showToast from "../services/toastService";
|
||||||
|
|
||||||
export const useProjectAccess = (projectId) => {
|
export const useProjectAccess = (projectId) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { data: projectPermissions, isLoading, isFetched } =
|
const { data: projectPermissions, isLoading, isFetched } =
|
||||||
useAllProjectLevelPermissions(projectId);
|
useAllProjectLevelPermissions(projectId);
|
||||||
|
|
||||||
const canView = useHasUserPermission(VIEW_PROJECTS);
|
const canView = useHasUserPermission(VIEW_PROJECTS);
|
||||||
const navigate = useNavigate();
|
|
||||||
|
const loading = isLoading || !isFetched;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (projectId && isFetched && !isLoading && !canView) {
|
if (projectId && !loading && !canView) {
|
||||||
showToast("You don't have permission to view project details", "warning");
|
showToast("You don't have permission to view project details", "warning");
|
||||||
navigate("/projects");
|
navigate("/projects");
|
||||||
}
|
}
|
||||||
}, [projectId, isFetched, isLoading, canView, navigate]);
|
}, [projectId, loading, canView, navigate]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
canView,
|
canView,
|
||||||
loading: isLoading || !isFetched,
|
loading,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -14,17 +14,15 @@ import {
|
|||||||
} from "@tanstack/react-query";
|
} from "@tanstack/react-query";
|
||||||
import showToast from "../services/toastService";
|
import showToast from "../services/toastService";
|
||||||
|
|
||||||
|
export const useCurrentService = () => {
|
||||||
|
return useSelector((store) => store.globalVariables.selectedServiceId);
|
||||||
|
};
|
||||||
|
|
||||||
// ------------------------------Query-------------------
|
// ------------------------------Query-------------------
|
||||||
|
|
||||||
export const useProjects = () => {
|
export const useProjects = () => {
|
||||||
const loggedUser = useSelector((store) => store.globalVariables.loginUser);
|
const loggedUser = useSelector((store) => store.globalVariables.loginUser);
|
||||||
|
return useQuery({
|
||||||
const {
|
|
||||||
data: projects = [],
|
|
||||||
isLoading: loading,
|
|
||||||
error,
|
|
||||||
refetch,
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: ["ProjectsList"],
|
queryKey: ["ProjectsList"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await ProjectRepository.getProjectList();
|
const response = await ProjectRepository.getProjectList();
|
||||||
@ -32,31 +30,40 @@ export const useProjects = () => {
|
|||||||
},
|
},
|
||||||
enabled: !!loggedUser,
|
enabled: !!loggedUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
|
||||||
projects,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
refetch,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useEmployeesByProjectAllocated = (selectedProject) => {
|
export const useEmployeesByProjectAllocated = (
|
||||||
|
projectId,
|
||||||
|
serviceId,
|
||||||
|
organizationId,
|
||||||
|
emloyeeeStatus
|
||||||
|
) => {
|
||||||
const {
|
const {
|
||||||
data = [],
|
data = [],
|
||||||
isLoading,
|
isLoading,
|
||||||
refetch,
|
refetch,
|
||||||
error,
|
error,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["empListByProjectAllocated", selectedProject],
|
queryKey: [
|
||||||
|
"empListByProjectAllocated",
|
||||||
|
projectId,
|
||||||
|
serviceId,
|
||||||
|
organizationId,
|
||||||
|
emloyeeeStatus,
|
||||||
|
],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await ProjectRepository.getProjectAllocation(selectedProject);
|
const res = await ProjectRepository.getProjectAllocation(
|
||||||
return res.data || res;
|
projectId,
|
||||||
|
serviceId,
|
||||||
|
organizationId,
|
||||||
|
emloyeeeStatus
|
||||||
|
);
|
||||||
|
return res?.data || res;
|
||||||
},
|
},
|
||||||
enabled: !!selectedProject,
|
enabled: !!projectId,
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
showToast(
|
showToast(
|
||||||
error.message || "Error while Fetching project Allocated Employees",
|
error.message || "Error while fetching project allocated employees",
|
||||||
"error"
|
"error"
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -172,17 +179,20 @@ export const useProjectName = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useProjectInfra = (projectId) => {
|
export const useProjectInfra = (projectId, serviceId) => {
|
||||||
const {
|
const {
|
||||||
data: projectInfra,
|
data: projectInfra,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
isFetched
|
isFetched,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ["ProjectInfra", projectId],
|
queryKey: ["ProjectInfra", projectId, serviceId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (!projectId) return null;
|
if (!projectId) return null;
|
||||||
const res = await ProjectRepository.getProjectInfraByproject(projectId);
|
const res = await ProjectRepository.getProjectInfraByproject(
|
||||||
|
projectId,
|
||||||
|
serviceId
|
||||||
|
);
|
||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
enabled: !!projectId,
|
enabled: !!projectId,
|
||||||
@ -191,26 +201,30 @@ export const useProjectInfra = (projectId) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return { projectInfra, isLoading, error,isFetched };
|
return { projectInfra, isLoading, error, isFetched };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useProjectTasks = (workAreaId, IsExpandedArea = false) => {
|
export const useProjectTasks = (
|
||||||
const {
|
workAreaId,
|
||||||
data: ProjectTaskList,
|
serviceId = null,
|
||||||
isLoading,
|
isExpandedArea = false
|
||||||
error,
|
) => {
|
||||||
} = useQuery({
|
const { data, isLoading, error } = useQuery({
|
||||||
queryKey: ["WorkItems", workAreaId],
|
queryKey: ["WorkItems", workAreaId, serviceId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const res = await ProjectRepository.getProjectTasksByWorkArea(workAreaId);
|
const res = await ProjectRepository.getProjectTasksByWorkArea(
|
||||||
return res.data;
|
workAreaId,
|
||||||
|
serviceId
|
||||||
|
);
|
||||||
|
return res.data; // return actual task list
|
||||||
},
|
},
|
||||||
enabled: !!workAreaId && !!IsExpandedArea,
|
enabled: !!workAreaId && isExpandedArea, // only fetch if workAreaId exists and area is expanded
|
||||||
onError: (error) => {
|
onError: (err) => {
|
||||||
showToast(error.message || "Error while Fetching project Tasks", "error");
|
showToast(err.message || "Error while fetching project tasks", "error");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return { ProjectTaskList, isLoading, error };
|
|
||||||
|
return { ProjectTaskList: data, isLoading, error };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useProjectTasksByEmployee = (employeeId, fromDate, toDate) => {
|
export const useProjectTasksByEmployee = (employeeId, fromDate, toDate) => {
|
||||||
@ -268,14 +282,55 @@ export const useProjectLevelEmployeePermission = (employeeId, projectId) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useProjectAssignedOrganizations = (projectId) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["projectAssignedOrganiztions", projectId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const resp = await ProjectRepository.getProjectAssignedOrganizations(
|
||||||
|
projectId
|
||||||
|
);
|
||||||
|
return resp.data;
|
||||||
|
},
|
||||||
|
enabled: !!projectId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
export const useProjectAssignedServices = (projectId) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["projectAssignedOrganization", projectId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const resp = await ProjectRepository.getProjectAssignedServices(
|
||||||
|
projectId
|
||||||
|
);
|
||||||
|
return resp.data;
|
||||||
|
},
|
||||||
|
enabled: !!projectId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useEmployeeForTaskAssign = (
|
||||||
|
projectId,
|
||||||
|
serviceId,
|
||||||
|
organizationId
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["EmployeeForTaskAssign", projectId, serviceId, organizationId],
|
||||||
|
queryFn: async () =>
|
||||||
|
await ProjectRepository.getEmployeeForTaskAssign(
|
||||||
|
projectId,
|
||||||
|
serviceId,
|
||||||
|
organizationId
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// -- -------------Mutation-------------------------------
|
// -- -------------Mutation-------------------------------
|
||||||
|
|
||||||
export const useCreateProject = ({ onSuccessCallback }) => {
|
export const useCreateProject = ({ onSuccessCallback }) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async (newProject) => {
|
mutationFn: async (payload) => {
|
||||||
const res = await ProjectRepository.manageProject(newProject);
|
const res = await ProjectRepository.manageProject(payload);
|
||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
@ -301,11 +356,11 @@ export const useCreateProject = ({ onSuccessCallback }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUpdateProject = ({ onSuccessCallback }) => {
|
export const useUpdateProject = ( onSuccessCallback ) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { mutate, isPending, isSuccess, isError } = useMutation({
|
const { mutate, isPending, isSuccess, isError } = useMutation({
|
||||||
mutationFn: async ({ projectId, updatedData }) => {
|
mutationFn: async ({ projectId, payload }) => {
|
||||||
return await ProjectRepository.updateProject(projectId, updatedData);
|
return await ProjectRepository.updateProject(projectId, payload);
|
||||||
},
|
},
|
||||||
|
|
||||||
onSuccess: (data, variables) => {
|
onSuccess: (data, variables) => {
|
||||||
@ -365,8 +420,8 @@ export const useManageProjectAllocation = ({
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const { mutate, isPending, isSuccess, isError } = useMutation({
|
const { mutate, isPending, isSuccess, isError } = useMutation({
|
||||||
mutationFn: async ({ items }) => {
|
mutationFn: async ({ payload }) => {
|
||||||
const response = await ProjectRepository.manageProjectAllocation(items);
|
const response = await ProjectRepository.manageProjectAllocation(payload);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
onSuccess: (data, variables, context) => {
|
onSuccess: (data, variables, context) => {
|
||||||
@ -374,7 +429,7 @@ export const useManageProjectAllocation = ({
|
|||||||
queryKey: ["empListByProjectAllocated"],
|
queryKey: ["empListByProjectAllocated"],
|
||||||
});
|
});
|
||||||
queryClient.removeQueries({ queryKey: ["projectEmployees"] });
|
queryClient.removeQueries({ queryKey: ["projectEmployees"] });
|
||||||
if (variables?.added) {
|
if (variables.actionType === "assign") {
|
||||||
showToast("Employee Assigned Successfully", "success");
|
showToast("Employee Assigned Successfully", "success");
|
||||||
} else {
|
} else {
|
||||||
showToast("Removed Employee Successfully", "success");
|
showToast("Removed Employee Successfully", "success");
|
||||||
|
|||||||
@ -8,30 +8,25 @@ import { useSelector } from "react-redux";
|
|||||||
|
|
||||||
// ---------Query---------------------------------
|
// ---------Query---------------------------------
|
||||||
|
|
||||||
export const useTaskList = (projectId, dateFrom, toDate) => {
|
export const useTaskList = (projectId, pageSize, pageNumber, serviceId, filter) => {
|
||||||
const enabled = !!projectId && !!dateFrom && !!toDate;
|
|
||||||
|
|
||||||
const {
|
return useQuery({
|
||||||
data: TaskList = [],
|
queryKey: ["taskList", projectId, pageSize, pageNumber, serviceId, filter],
|
||||||
isLoading: loading,
|
|
||||||
error,
|
|
||||||
refetch,
|
|
||||||
} = useQuery({
|
|
||||||
queryKey: ["taskList", projectId, dateFrom, toDate],
|
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const response = await TasksRepository.getTaskList(
|
const response = await TasksRepository.getTaskList(
|
||||||
projectId,
|
projectId,
|
||||||
dateFrom,
|
pageSize,
|
||||||
toDate
|
pageNumber,
|
||||||
|
serviceId,
|
||||||
|
filter
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
enabled,
|
enabled:!!projectId
|
||||||
});
|
});
|
||||||
|
|
||||||
return { TaskList, loading, error, refetch };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const useTaskById = (TaskId) => {
|
export const useTaskById = (TaskId) => {
|
||||||
const {
|
const {
|
||||||
data: Task = null,
|
data: Task = null,
|
||||||
|
|||||||
@ -2,11 +2,9 @@
|
|||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
// import { MasterDataProvider } from "./provider/MasterDataContext";
|
|
||||||
|
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { store } from './store/store';
|
import { store } from './store/store';
|
||||||
import { ModalProvider } from './ModalContext.jsx';
|
|
||||||
import { ChangePasswordProvider } from './components/Context/ChangePasswordContext.jsx';
|
import { ChangePasswordProvider } from './components/Context/ChangePasswordContext.jsx';
|
||||||
import { ModalProvider1 } from './pages/Gallary/ModalContext.jsx';
|
import { ModalProvider1 } from './pages/Gallary/ModalContext.jsx';
|
||||||
|
|
||||||
@ -15,15 +13,12 @@ createRoot(document.getElementById('root')!).render(
|
|||||||
// <StrictMode>
|
// <StrictMode>
|
||||||
// <MasterDataProvider>
|
// <MasterDataProvider>
|
||||||
<Provider store={ store }>
|
<Provider store={ store }>
|
||||||
<ModalProvider>
|
|
||||||
<ChangePasswordProvider >
|
<ChangePasswordProvider >
|
||||||
<ModalProvider1>
|
<ModalProvider1>
|
||||||
<App />
|
<App />
|
||||||
</ModalProvider1>
|
</ModalProvider1>
|
||||||
</ChangePasswordProvider>
|
</ChangePasswordProvider>
|
||||||
</ModalProvider>
|
|
||||||
</Provider>
|
</Provider>
|
||||||
// </MasterDataProvider>
|
|
||||||
|
|
||||||
// </StrictMode>,
|
// </StrictMode>,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -13,11 +13,10 @@ import Regularization from "../../components/Activities/Regularization";
|
|||||||
import { useAttendance } from "../../hooks/useAttendance";
|
import { useAttendance } from "../../hooks/useAttendance";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { setProjectId } from "../../slices/localVariablesSlice";
|
import { setProjectId } from "../../slices/localVariablesSlice";
|
||||||
import { hasUserPermission } from "../../utils/authUtils";
|
|
||||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||||
import { REGULARIZE_ATTENDANCE } from "../../utils/constants";
|
import { REGULARIZE_ATTENDANCE } from "../../utils/constants";
|
||||||
import eventBus from "../../services/eventBus";
|
import eventBus from "../../services/eventBus";
|
||||||
import { useProjectName } from "../../hooks/useProjects";
|
import { useProjectAssignedOrganizations, useProjectName } from "../../hooks/useProjects";
|
||||||
import GlobalModel from "../../components/common/GlobalModel";
|
import GlobalModel from "../../components/common/GlobalModel";
|
||||||
import CheckCheckOutmodel from "../../components/Activities/CheckCheckOutForm";
|
import CheckCheckOutmodel from "../../components/Activities/CheckCheckOutForm";
|
||||||
import AttendLogs from "../../components/Activities/AttendLogs";
|
import AttendLogs from "../../components/Activities/AttendLogs";
|
||||||
@ -39,6 +38,13 @@ const AttendancePage = () => {
|
|||||||
const [modelConfig, setModelConfig] = useState();
|
const [modelConfig, setModelConfig] = useState();
|
||||||
const DoRegularized = useHasUserPermission(REGULARIZE_ATTENDANCE);
|
const DoRegularized = useHasUserPermission(REGULARIZE_ATTENDANCE);
|
||||||
const { projectNames, loading: projectLoading, fetchData } = useProjectName();
|
const { projectNames, loading: projectLoading, fetchData } = useProjectName();
|
||||||
|
const [appliedFilters, setAppliedFilters] = useState({
|
||||||
|
selectedOrganization: "",
|
||||||
|
selectedServices: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: organizations = [], isLoading: orgLoading } =
|
||||||
|
useProjectAssignedOrganizations(selectedProject);
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
markTime: "",
|
markTime: "",
|
||||||
@ -128,8 +134,7 @@ const AttendancePage = () => {
|
|||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`nav-link ${
|
className={`nav-link ${activeTab === "all" ? "active" : ""
|
||||||
activeTab === "all" ? "active" : ""
|
|
||||||
} fs-6`}
|
} fs-6`}
|
||||||
onClick={() => handleTabChange("all")}
|
onClick={() => handleTabChange("all")}
|
||||||
data-bs-toggle="tab"
|
data-bs-toggle="tab"
|
||||||
@ -141,8 +146,7 @@ const AttendancePage = () => {
|
|||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`nav-link ${
|
className={`nav-link ${activeTab === "logs" ? "active" : ""
|
||||||
activeTab === "logs" ? "active" : ""
|
|
||||||
} fs-6`}
|
} fs-6`}
|
||||||
onClick={() => handleTabChange("logs")}
|
onClick={() => handleTabChange("logs")}
|
||||||
data-bs-toggle="tab"
|
data-bs-toggle="tab"
|
||||||
@ -155,8 +159,7 @@ const AttendancePage = () => {
|
|||||||
<li className={`nav-item ${!DoRegularized ? "d-none" : ""}`}>
|
<li className={`nav-item ${!DoRegularized ? "d-none" : ""}`}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`nav-link ${
|
className={`nav-link ${activeTab === "regularization" ? "active" : ""
|
||||||
activeTab === "regularization" ? "active" : ""
|
|
||||||
} fs-6`}
|
} fs-6`}
|
||||||
onClick={() => handleTabChange("regularization")}
|
onClick={() => handleTabChange("regularization")}
|
||||||
data-bs-toggle="tab"
|
data-bs-toggle="tab"
|
||||||
@ -168,8 +171,30 @@ const AttendancePage = () => {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Single search input that moves */}
|
{/* Search + Organization filter */}
|
||||||
<div className="col-12 col-md-auto mt-2 mt-md-0 ms-md-auto px-2">
|
<div className="col-12 col-md-auto mt-2 mt-md-0 ms-md-auto d-flex gap-2 align-items-center">
|
||||||
|
{/* Organization Dropdown */}
|
||||||
|
<select
|
||||||
|
className="form-select form-select-sm"
|
||||||
|
style={{ minWidth: "180px" }}
|
||||||
|
value={appliedFilters.selectedOrganization}
|
||||||
|
onChange={(e) =>
|
||||||
|
setAppliedFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
selectedOrganization: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
disabled={orgLoading}
|
||||||
|
>
|
||||||
|
<option value="">All Organizations</option>
|
||||||
|
{organizations?.map((org) => (
|
||||||
|
<option key={org.id} value={org.id}>
|
||||||
|
{org.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{/* Search Input */}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control form-control-sm"
|
className="form-control form-control-sm"
|
||||||
@ -179,6 +204,8 @@ const AttendancePage = () => {
|
|||||||
style={{ minWidth: "200px" }}
|
style={{ minWidth: "200px" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -191,6 +218,7 @@ const AttendancePage = () => {
|
|||||||
handleModalData={handleModalData}
|
handleModalData={handleModalData}
|
||||||
getRole={getRole}
|
getRole={getRole}
|
||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
|
organizationId={appliedFilters.selectedOrganization}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -199,12 +227,16 @@ const AttendancePage = () => {
|
|||||||
<AttendanceLog
|
<AttendanceLog
|
||||||
handleModalData={handleModalData}
|
handleModalData={handleModalData}
|
||||||
searchTerm={searchTerm}
|
searchTerm={searchTerm}
|
||||||
|
organizationId={appliedFilters.selectedOrganization}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{activeTab === "regularization" && DoRegularized && (
|
{activeTab === "regularization" && DoRegularized && (
|
||||||
<div className="tab-pane fade show active py-0">
|
<div className="tab-pane fade show active py-0">
|
||||||
<Regularization searchTerm={searchTerm} />
|
<Regularization
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
organizationId={appliedFilters.selectedOrganization}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -1,251 +0,0 @@
|
|||||||
import React, { useEffect, useMemo, useState } from "react";
|
|
||||||
import { useDispatch } from "react-redux";
|
|
||||||
import { useTaskList } from "../../hooks/useTasks";
|
|
||||||
import { useProjectName } from "../../hooks/useProjects";
|
|
||||||
import { setProjectId } from "../../slices/localVariablesSlice";
|
|
||||||
import Breadcrumb from "../../components/common/Breadcrumb";
|
|
||||||
import DateRangePicker from "../../components/common/DateRangePicker";
|
|
||||||
import FilterIcon from "../../components/common/FilterIcon";
|
|
||||||
import GlobalModel from "../../components/common/GlobalModel";
|
|
||||||
import ReportTask from "../../components/Activities/ReportTask";
|
|
||||||
import ReportTaskComments from "../../components/Activities/ReportTaskComments";
|
|
||||||
import SubTask from "../../components/Activities/SubTask";
|
|
||||||
import { formatNumber, formatUTCToLocalTime } from "../../utils/dateUtils";
|
|
||||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
|
||||||
import { APPROVE_TASK, ASSIGN_REPORT_TASK } from "../../utils/constants";
|
|
||||||
import { useSelectedProject } from "../../slices/apiDataManager";
|
|
||||||
import moment from "moment";
|
|
||||||
import Loader from "../../components/common/Loader";
|
|
||||||
|
|
||||||
const DailyTask = () => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const selectedProject = useSelectedProject();
|
|
||||||
const { projectNames } = useProjectName();
|
|
||||||
const ApprovedTaskRights = useHasUserPermission(APPROVE_TASK);
|
|
||||||
const ReportTaskRights = useHasUserPermission(ASSIGN_REPORT_TASK);
|
|
||||||
|
|
||||||
const [filters, setFilters] = useState({
|
|
||||||
selectedBuilding: "",
|
|
||||||
selectedFloors: [],
|
|
||||||
selectedActivities: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
const [dateRange, setDateRange] = useState({ startDate: "", endDate: "" });
|
|
||||||
|
|
||||||
const { TaskList, loading: taskLoading } = useTaskList(
|
|
||||||
selectedProject || null,
|
|
||||||
dateRange?.startDate || null,
|
|
||||||
dateRange?.endDate || null
|
|
||||||
);
|
|
||||||
|
|
||||||
// Ensure project is set
|
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedProject && projectNames.length > 0) {
|
|
||||||
debugger
|
|
||||||
dispatch(setProjectId(projectNames[0].id));
|
|
||||||
}
|
|
||||||
}, [selectedProject, projectNames, dispatch]);
|
|
||||||
|
|
||||||
// 🔹 Reset filters when project changes
|
|
||||||
useEffect(() => {
|
|
||||||
setFilters({
|
|
||||||
selectedBuilding: "",
|
|
||||||
selectedFloors: [],
|
|
||||||
selectedActivities: [],
|
|
||||||
});
|
|
||||||
}, [selectedProject]);
|
|
||||||
|
|
||||||
// Memoized filtering
|
|
||||||
const filteredTasks = useMemo(() => {
|
|
||||||
if (!TaskList) return [];
|
|
||||||
return TaskList.filter((task) => {
|
|
||||||
const { selectedBuilding, selectedFloors, selectedActivities } = filters;
|
|
||||||
|
|
||||||
if (selectedBuilding && task?.workItem?.workArea?.floor?.building?.name !== selectedBuilding) return false;
|
|
||||||
if (selectedFloors.length > 0 && !selectedFloors.includes(task?.workItem?.workArea?.floor?.floorName)) return false;
|
|
||||||
if (selectedActivities.length > 0 && !selectedActivities.includes(task?.workItem?.activityMaster?.activityName)) return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}, [TaskList, filters]);
|
|
||||||
|
|
||||||
// Memoized dates
|
|
||||||
const groupedTasks = useMemo(() => {
|
|
||||||
const groups = {};
|
|
||||||
filteredTasks.forEach((task) => {
|
|
||||||
const date = task.assignmentDate.split("T")[0];
|
|
||||||
if (!groups[date]) groups[date] = [];
|
|
||||||
groups[date].push(task);
|
|
||||||
});
|
|
||||||
return Object.keys(groups)
|
|
||||||
.sort((a, b) => new Date(b) - new Date(a))
|
|
||||||
.map((date) => ({ date, tasks: groups[date] }));
|
|
||||||
}, [filteredTasks]);
|
|
||||||
|
|
||||||
// --- Modal State
|
|
||||||
const [modal, setModal] = useState({ type: null, data: null });
|
|
||||||
|
|
||||||
const openModal = (type, data = null) => setModal({ type, data });
|
|
||||||
const closeModal = () => setModal({ type: null, data: null });
|
|
||||||
|
|
||||||
// --- Render helpers
|
|
||||||
const renderTeamMembers = (task, refIndex) => (
|
|
||||||
<div
|
|
||||||
key={refIndex}
|
|
||||||
tabIndex="0"
|
|
||||||
className="d-flex align-items-center avatar-group justify-content-center"
|
|
||||||
data-bs-toggle="popover"
|
|
||||||
data-bs-trigger="focus"
|
|
||||||
data-bs-placement="left"
|
|
||||||
data-bs-html="true"
|
|
||||||
data-bs-content={`
|
|
||||||
<div class="border border-secondary rounded custom-popover p-2 px-3">
|
|
||||||
${task.teamMembers
|
|
||||||
.map(
|
|
||||||
(m) => `
|
|
||||||
<div class="d-flex align-items-center gap-2 mb-2">
|
|
||||||
<div class="avatar avatar-xs">
|
|
||||||
<span class="avatar-initial rounded-circle bg-label-primary">
|
|
||||||
${m?.firstName?.charAt(0) || ""}${m?.lastName?.charAt(0) || ""}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span>${m.firstName} ${m.lastName}</span>
|
|
||||||
</div>`
|
|
||||||
)
|
|
||||||
.join("")}
|
|
||||||
</div>
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{task.teamMembers.slice(0, 3).map((m) => (
|
|
||||||
<div key={m.id} className="avatar avatar-xs" title={`${m.firstName} ${m.lastName}`}>
|
|
||||||
<span className="avatar-initial rounded-circle bg-label-primary">
|
|
||||||
{m?.firstName.slice(0, 1)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{task.teamMembers.length > 3 && (
|
|
||||||
<div className="avatar avatar-xs" title={`${task.teamMembers.length - 3} more`}>
|
|
||||||
<span className="avatar-initial rounded-circle bg-label-secondary">+{task.teamMembers.length - 3}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{/* --- Modals --- */}
|
|
||||||
{modal.type === "report" && (
|
|
||||||
<GlobalModel isOpen size="md" closeModal={closeModal}>
|
|
||||||
<ReportTask report={modal.data} closeModal={closeModal} />
|
|
||||||
</GlobalModel>
|
|
||||||
)}
|
|
||||||
{modal.type === "comments" && (
|
|
||||||
<GlobalModel isOpen size="lg" closeModal={closeModal}>
|
|
||||||
<ReportTaskComments
|
|
||||||
commentsData={modal.data.task}
|
|
||||||
actionAllow={modal.data.isActionAllow}
|
|
||||||
handleCloseAction={(isSubTask) => {
|
|
||||||
if (isSubTask) openModal("subtask", modal.data.task);
|
|
||||||
else closeModal();
|
|
||||||
}}
|
|
||||||
closeModal={closeModal}
|
|
||||||
/>
|
|
||||||
</GlobalModel>
|
|
||||||
)}
|
|
||||||
{modal.type === "subtask" && (
|
|
||||||
<GlobalModel isOpen size="lg" closeModal={closeModal}>
|
|
||||||
<SubTask activity={modal.data} onClose={closeModal} />
|
|
||||||
</GlobalModel>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="container-fluid">
|
|
||||||
<Breadcrumb data={[{ label: "Home", link: "/dashboard" }, { label: "Daily Progress Report" }]} />
|
|
||||||
|
|
||||||
<div className="card card-action mb-6">
|
|
||||||
<div className="card-body p-1 p-sm-2">
|
|
||||||
{!selectedProject && (<div className="text-center text-muted">Please Select Project</div>)}
|
|
||||||
{/* --- Filters --- */}
|
|
||||||
<div className="d-flex align-items-center mb-2">
|
|
||||||
<DateRangePicker onRangeChange={setDateRange} endDateMode="today" DateDifference="6" dateFormat="DD-MM-YYYY" />
|
|
||||||
<FilterIcon
|
|
||||||
taskListData={TaskList}
|
|
||||||
onApplyFilters={setFilters}
|
|
||||||
currentSelectedBuilding={filters.selectedBuilding}
|
|
||||||
currentSelectedFloors={filters.selectedFloors}
|
|
||||||
currentSelectedActivities={filters.selectedActivities}
|
|
||||||
selectedProject={selectedProject}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* --- Table --- */}
|
|
||||||
<div className="table-responsive text-nowrap mt-3" style={{ minHeight: "200px" }}>
|
|
||||||
<table className="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Activity</th>
|
|
||||||
<th>Assigned</th>
|
|
||||||
<th>Completed</th>
|
|
||||||
<th>Assign On</th>
|
|
||||||
<th>Team</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{taskLoading && (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={6} className="text-center align-middle" style={{ height: "200px" }}>
|
|
||||||
<Loader />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
{!taskLoading && groupedTasks.length === 0 && (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={6} className="text-center align-middle" style={{ height: "200px" }}>
|
|
||||||
No reports available for the selected date range.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
{!taskLoading &&
|
|
||||||
groupedTasks.map(({ date, tasks }) => (
|
|
||||||
<React.Fragment key={date}>
|
|
||||||
<tr className="table-row-header text-start">
|
|
||||||
<td colSpan={6}><strong>{formatUTCToLocalTime(date)}</strong></td>
|
|
||||||
</tr>
|
|
||||||
{tasks.map((task, idx) => (
|
|
||||||
<tr key={task.id || idx}>
|
|
||||||
<td className="flex-wrap text-start">
|
|
||||||
<div>{task.workItem.activityMaster?.activityName || "No Activity Name"}</div>
|
|
||||||
<div className="text-sm">
|
|
||||||
{task.workItem.workArea?.floor?.building?.name} › {task.workItem.workArea?.floor?.floorName} › {task.workItem.workArea?.areaName}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>{formatNumber(task.plannedTask)} / {formatNumber(task.workItem.plannedWork - task.workItem.completedWork)}</td>
|
|
||||||
<td>{task.completedTask}</td>
|
|
||||||
<td>{formatUTCToLocalTime(task.assignmentDate)}</td>
|
|
||||||
<td className="text-center">{renderTeamMembers(task, idx)}</td>
|
|
||||||
<td className="text-center">
|
|
||||||
<div className="d-flex justify-content-end gap-2">
|
|
||||||
{ReportTaskRights && !task.reportedDate && (
|
|
||||||
<button className="btn btn-xs btn-primary" onClick={() => openModal("report", task)}>Report</button>
|
|
||||||
)}
|
|
||||||
{ApprovedTaskRights && task.reportedDate && !task.approvedBy && (
|
|
||||||
<button className="btn btn-xs btn-warning" onClick={() => openModal("comments", { task, isActionAllow: true })}>QC</button>
|
|
||||||
)}
|
|
||||||
<button className="btn btn-xs btn-primary" onClick={() => openModal("comments", { task, isActionAllow: false })}>Comment</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default DailyTask;
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user