Merge branch 'newFeature_Directory'

This commit is contained in:
Vikas Nale 2025-06-10 20:18:25 +05:30
commit 500006c1ef
58 changed files with 5396 additions and 412 deletions

166
package-lock.json generated
View File

@ -27,6 +27,7 @@
"react-apexcharts": "^1.7.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.54.2",
"react-quill": "^2.0.0",
"react-redux": "^9.2.0",
"react-router-dom": "^6.20.1",
"react-toastify": "^11.0.2",
@ -1494,6 +1495,15 @@
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
"devOptional": true
},
"node_modules/@types/quill": {
"version": "1.3.10",
"resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz",
"integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==",
"license": "MIT",
"dependencies": {
"parchment": "^1.1.2"
}
},
"node_modules/@types/react": {
"version": "18.3.16",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.16.tgz",
@ -2100,7 +2110,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
"integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"dev": true,
"dependencies": {
"call-bind-apply-helpers": "^1.0.0",
"es-define-property": "^1.0.0",
@ -2118,7 +2127,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz",
"integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==",
"dev": true,
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
@ -2131,7 +2139,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.2.tgz",
"integrity": "sha512-0lk0PHFe/uz0vl527fG9CgdE9WdafjDbCXvBbs+LUv000TVt2Jjhqbs4Jwm8gz070w8xXyEAxrPOMullsxXeGg==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.8",
"get-intrinsic": "^1.2.5"
@ -2210,6 +2217,15 @@
"node": ">=6.0"
}
},
"node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
"license": "MIT",
"engines": {
"node": ">=0.8"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@ -2373,6 +2389,26 @@
}
}
},
"node_modules/deep-equal": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz",
"integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==",
"license": "MIT",
"dependencies": {
"is-arguments": "^1.1.1",
"is-date-object": "^1.0.5",
"is-regex": "^1.1.4",
"object-is": "^1.1.5",
"object-keys": "^1.1.1",
"regexp.prototype.flags": "^1.5.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@ -2383,7 +2419,6 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
"integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
"dev": true,
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
@ -2400,7 +2435,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
"integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
"dev": true,
"dependencies": {
"define-data-property": "^1.0.1",
"has-property-descriptors": "^1.0.0",
@ -2482,7 +2516,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz",
"integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==",
"dev": true,
"dependencies": {
"call-bind-apply-helpers": "^1.0.0",
"es-errors": "^1.3.0",
@ -2575,7 +2608,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"engines": {
"node": ">= 0.4"
}
@ -2584,7 +2616,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"engines": {
"node": ">= 0.4"
}
@ -2949,6 +2980,12 @@
"node": ">=0.10.0"
}
},
"node_modules/eventemitter3": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
"integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==",
"license": "MIT"
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
@ -2959,11 +2996,23 @@
"node": ">=0.8.x"
}
},
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT"
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
},
"node_modules/fast-diff": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz",
"integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==",
"license": "Apache-2.0"
},
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@ -3123,7 +3172,6 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@ -3150,7 +3198,6 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
"integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@ -3167,7 +3214,6 @@
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.5.tgz",
"integrity": "sha512-Y4+pKa7XeRUPWFNvOOYHkRYrfzW07oraURSvjDmRVOJ748OrVmeXtpE4+GCEHncjCjkTxPNRt8kEbxDhsn6VTg==",
"dev": true,
"dependencies": {
"call-bind-apply-helpers": "^1.0.0",
"dunder-proto": "^1.0.0",
@ -3277,7 +3323,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
@ -3319,7 +3364,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
"integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
"dev": true,
"dependencies": {
"es-define-property": "^1.0.0"
},
@ -3346,7 +3390,6 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
@ -3358,7 +3401,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"dependencies": {
"has-symbols": "^1.0.3"
},
@ -3373,7 +3415,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.2"
},
@ -3460,6 +3501,22 @@
"node": ">= 0.4"
}
},
"node_modules/is-arguments": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
"integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"has-tostringtag": "^1.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-array-buffer": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
@ -3568,7 +3625,6 @@
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
"integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
"dev": true,
"dependencies": {
"has-tostringtag": "^1.0.0"
},
@ -3683,7 +3739,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.0.tgz",
"integrity": "sha512-B6ohK4ZmoftlUe+uvenXSbPJFo6U37BH7oO1B3nQH8f/7h27N56s85MhUtbFJAziz5dcmuR3i8ovUl35zp8pFA==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.7",
"gopd": "^1.1.0",
@ -4026,6 +4081,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@ -4166,11 +4227,26 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/object-is": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
"integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
"license": "MIT",
"dependencies": {
"call-bind": "^1.0.7",
"define-properties": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
"dev": true,
"engines": {
"node": ">= 0.4"
}
@ -4312,6 +4388,12 @@
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/parchment": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz",
"integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==",
"license": "BSD-3-Clause"
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -4468,6 +4550,34 @@
}
]
},
"node_modules/quill": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz",
"integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==",
"license": "BSD-3-Clause",
"dependencies": {
"clone": "^2.1.1",
"deep-equal": "^1.0.1",
"eventemitter3": "^2.0.3",
"extend": "^3.0.2",
"parchment": "^1.1.4",
"quill-delta": "^3.6.2"
}
},
"node_modules/quill-delta": {
"version": "3.6.3",
"resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz",
"integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==",
"license": "MIT",
"dependencies": {
"deep-equal": "^1.0.1",
"extend": "^3.0.2",
"fast-diff": "1.1.2"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -4535,6 +4645,21 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react-quill": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0.tgz",
"integrity": "sha512-4qQtv1FtCfLgoD3PXAur5RyxuUbPXQGOHgTlFie3jtxp43mXDtzCKaOgQ3mLyZfi1PUlyjycfivKelFhy13QUg==",
"license": "MIT",
"dependencies": {
"@types/quill": "^1.3.10",
"lodash": "^4.17.4",
"quill": "^1.3.7"
},
"peerDependencies": {
"react": "^16 || ^17 || ^18",
"react-dom": "^16 || ^17 || ^18"
}
},
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
@ -4651,7 +4776,6 @@
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz",
"integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.7",
"define-properties": "^1.2.1",
@ -4946,7 +5070,6 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
"integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
"dev": true,
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",
@ -4963,7 +5086,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
"integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
"dev": true,
"dependencies": {
"define-data-property": "^1.1.4",
"es-errors": "^1.3.0",

View File

@ -30,6 +30,7 @@
"react-apexcharts": "^1.7.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.54.2",
"react-quill": "^2.0.0",
"react-redux": "^9.2.0",
"react-router-dom": "^6.20.1",
"react-toastify": "^11.0.2",

View File

@ -5069,6 +5069,9 @@ fieldset:disabled .btn {
.card-group > .card {
margin-bottom: var(--bs-card-group-margin);
}
.card-minHeight{
min-height: 430px;
}
@media (min-width: 576px) {
.card-group {
display: flex;
@ -16889,7 +16892,8 @@ li:not(:first-child) .dropdown-item,
box-shadow: var(--bs-box-shadow-xs);
filter: none;
opacity: 1;
transform: translate(23px, -25px);
transform: translate(6px, -9px);
z-index: 1056;
border-radius: 0.25rem;
transition: all 0.23s ease 0.1s;
/* For hover effect of close btn */
@ -16899,18 +16903,18 @@ li:not(:first-child) .dropdown-item,
transition: none;
}
}
.modal .btn-close:hover,
/* .modal .btn-close:hover,
.modal .btn-close:focus,
.modal .btn-close:active {
opacity: 1;
outline: 0;
transform: translate(20px, -20px);
}
:dir(rtl) .modal .btn-close:hover,
} */
/* :dir(rtl) .modal .btn-close:hover,
:dir(rtl) .modal .btn-close:focus,
:dir(rtl) .modal .btn-close:active {
transform: translate(26px, -20px);
}
} */
.modal .btn-close::before {
display: block;
background-color: var(--bs-secondary-color);

View File

@ -1,3 +1,4 @@
import { DireProvider } from "./Context/DireContext";
import AppRoutes from "./router/AppRoutes";
import { ToastContainer } from "react-toastify";
@ -5,8 +6,13 @@ import { ToastContainer } from "react-toastify";
const App = () => {
return (
<div className="app">
<AppRoutes />
<ToastContainer></ToastContainer>
<DireProvider>
<AppRoutes />
</DireProvider>
<ToastContainer>
</ToastContainer>
</div>
);

View File

@ -0,0 +1,21 @@
import React, { createContext, useContext, useState } from "react";
const DireContext = createContext(undefined);
export const DireProvider = ({ children }) => {
const [dirActions, setDirActions] = useState([]);
return (
<DireContext.Provider value={{ dirActions, setDirActions }}>
{children}
</DireContext.Provider>
);
};
export const useDir = () => {
const context = useContext(DireContext);
if (!context) {
throw new Error("useDir must be used within a <DireProvider>");
}
return context;
};

View File

@ -0,0 +1,15 @@
import React, { createContext, useContext, useState } from "react";
const FabContext = createContext();
export const FabProvider = ({ children }) => {
const [actions, setActions] = useState([]);
return (
<FabContext.Provider value={{ actions, setActions }}>
{children}
</FabContext.Provider>
);
};
export const useFab = () => useContext(FabContext);

View File

@ -5,6 +5,7 @@ import { convertShortTime } from "../../utils/dateUtils";
import RenderAttendanceStatus from "./RenderAttendanceStatus";
import usePagination from "../../hooks/usePagination";
import { useNavigate } from "react-router-dom";
import {ITEMS_PER_PAGE} from "../../utils/constants";
const Attendance = ({ attendance, getRole, handleModalData }) => {
const [loading, setLoading] = useState(false);
@ -33,7 +34,7 @@ const Attendance = ({ attendance, getRole, handleModalData }) => {
const { currentPage, totalPages, currentItems, paginate } = usePagination(
filteredData,
20
ITEMS_PER_PAGE
);
return (
<>

View File

@ -0,0 +1,192 @@
import React from "react";
import Avatar from "../common/Avatar";
import { getBucketNameById } from "./DirectoryUtils";
import { useBuckets } from "../../hooks/useDirectory";
import { getPhoneIcon } from "./DirectoryUtils";
import { useDir } from "../../Context/DireContext";
const CardViewDirectory = ({
IsActive,
contact,
setSelectedContact,
setIsOpenModal,
setOpen_contact,
setIsOpenModalNote,
IsDeleted,
restore,
}) => {
const { buckets } = useBuckets();
const { dirActions, setDirActions } = useDir();
return (
<div
className="card text-start border-1"
style={{ background: `${!IsActive ? "#f8f6f6" : ""}` }}
>
<div className="card-body px-1 py-2 pb-0">
<div className="d-flex justify-content-between">
<div
className={`d-flex align-items-center ${
IsActive && "cursor-pointer"
}`}
onClick={() => {
if (IsActive) {
setIsOpenModalNote(true);
setOpen_contact(contact);
}
}}
>
<Avatar
size="xs"
firstName={
(contact?.name || "").trim().split(" ")[0]?.charAt(0) || ""
}
lastName={
(contact?.name || "").trim().split(" ")[1]?.charAt(0) || ""
}
/>{" "}
<span className="text-heading fs-6"> {contact?.name}</span>
</div>
<div>
{IsActive && (
<div className="dropdown z-2">
<button
type="button"
className="btn btn-xs btn-icon btn-text-secondary rounded-pill dropdown-toggle hide-arrow p-0 m-0"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i
className="bx bx-dots-vertical-rounded text-muted p-0"
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 w-auto">
<li
onClick={() => {
setSelectedContact(contact);
setIsOpenModal(true);
}}
>
<a className="dropdown-item px-2 cursor-pointer py-1">
<i className="bx bx-edit bx-xs text-primary me-2"></i>
<span className="align-left ">Modify</span>
</a>
</li>
<li>
<a
className="dropdown-item px-2 cursor-pointer py-1"
onClick={() => IsDeleted(contact?.id)}
>
<i className="bx bx-trash text-danger bx-xs me-2"></i>
<span className="align-left">Delete</span>
</a>
</li>
</ul>
</div>
)}
{!IsActive && (
<i
className={`bx ${
dirActions.action && dirActions.id === contact.id
? "bx-loader-alt bx-spin"
: "bx-recycle"
} me-1 text-primary cursor-pointer`}
title="Restore"
onClick={() => {
setDirActions({ action: false, id: contact.id });
restore(contact.id);
}}
></i>
)}
</div>
</div>
<ul className="list-inline m-0 ps-4 d-flex align-items-start">
{/* <li className="list-inline-item me-1 small">
<i className="fa-solid fa-briefcase me-2"></i>
</li> */}
<li className="list-inline-item text-break small ms-5">
{contact.organization}
</li>
</ul>
</div>
<div
className={`card-footer text-start px-1 py-1 ${
IsActive && "cursor-pointer"
}`}
onClick={() => {
if (IsActive) {
setIsOpenModalNote(true);
setOpen_contact(contact);
}
}}
>
<hr className="my-0" />
{contact.contactEmails[0] && (
<ul className="list-unstyled my-1 d-flex align-items-start ms-2">
<li className="me-2">
<i className="bx bx-envelope bx-xs mt-1"></i>
</li>
<li className="flex-grow-1 text-break small">
{contact.contactEmails[0].emailAddress}
</li>
</ul>
)}
{contact.contactPhones[0] && (
<ul className="list-inline m-0 ms-2">
<li className="list-inline-item me-1">
<i
className={` ${getPhoneIcon(
contact.contactPhones[0].label
)} bx-xs`}
></i>
</li>
<li className="list-inline-item text-small">
{contact.contactPhones[0]?.phoneNumber}
</li>
</ul>
)}
{contact?.contactCategory?.name ? (
<ul className="list-inline m-0 ms-2">
<li className="list-inline-item me-2 my-1">
<i className="fa-solid fa-tag fs-6 ms-1"></i>
</li>
<li className="list-inline-item text-small active">
{contact?.contactCategory?.name}
</li>
</ul>
) : (
<ul className="list-inline m-0 ms-2">
<li className="list-inline-item me-2 my-1">
<i className="fa-solid fa-tag fs-6 ms-1"></i>
</li>
<li className="list-inline-item text-small active">Other</li>
</ul>
)}
<ul className="list-inline m-0 ms-2">
{contact?.bucketIds?.map((bucketId) => (
<li key={bucketId} className="list-inline-item me-1">
<span
className="badge bg-label-primary rounded-pill d-flex align-items-center gap-1"
style={{ padding: "0.1rem 0.3rem" }}
>
<i className="bx bx-pin bx-xs"></i>
<span className="small-text">
{getBucketNameById(buckets, bucketId)}
</span>
</span>
</li>
))}
</ul>
</div>
</div>
);
};
export default CardViewDirectory;

View File

@ -0,0 +1,65 @@
import { z } from "zod";
export const ContactSchema = z
.object({
name: z.string().min(1, "Name is required"),
organization: z.string().min(1, "Organization name is required"),
contactCategoryId: z.string().nullable().optional(),
address: z.string().optional(),
description: z.string().min(1, { message: "Description is required" }),
projectIds: z.array(z.string()).nullable().optional(), // min(1, "Project is required")
contactEmails: z
.array(
z.object({
label: z.string(),
emailAddress: z.string().email("Invalid email").or(z.literal("")),
})
)
.optional()
.default([]),
contactPhones: z
.array(
z.object({
label: z.string(),
phoneNumber: z
.string()
.min(6, "Invalid Number")
.max(13, "Invalid Number")
.regex(/^[\d\s+()-]+$/, "Invalid phone number format").or(z.literal("")),
})
)
.optional()
.default([]),
tags: z
.array(
z.object({
id: z.string().nullable(),
name: z.string(),
})
)
.min(1, { message: "At least one tag is required" }),
bucketIds: z.array(z.string()).nonempty({ message: "At least one label is required" })
})
// .refine((data) => {
// const hasValidEmail = (data.contactEmails || []).some(
// (e) => e.emailAddress?.trim() !== ""
// );
// const hasValidPhone = (data.contactPhones || []).some(
// (p) => p.phoneNumber?.trim() !== ""
// );
// return hasValidEmail || hasValidPhone;
// }, {
// message: "At least one contact (email or phone) is required",
// path: ["contactPhone"],
// });
// Buckets
export const bucketScheam = z.object( {
name: z.string().min( 1, {message: "Name is required"} ),
description:z.string().min(1,{message:"Description is required"})
})

View File

@ -0,0 +1,26 @@
import {useBuckets} from "../../hooks/useDirectory";
export const getEmailIcon = (type) => {
switch (type) {
case 'Work': return "bx bx-briefcase me-1 " ;
case 'Personal': return "bx bx-user me-1";
case 'support': return "bx headphone-mic me-1";
case 'billing': return "bx bx-receipt me-1";
default: return "bx bx-envelope me-1";
}
};
export const getPhoneIcon = (type) => {
switch (type) {
case 'Business': return "bx bx-phone me-1 ";
case 'Personal': return "bx bx-mobile me-1 ";
case 'Office': return "bx bx-phone me-1 ";
default: return "bx bx-phone me-1";
}
};
export const getBucketNameById = (buckets, id) => {
const bucket = buckets.find(b => b.id === id);
return bucket ? bucket.name : 'Unknown';
};

View File

@ -0,0 +1,158 @@
import React, { useState, useEffect } from "react";
import { useSortableData } from "../../hooks/useSortableData";
import Avatar from "../common/Avatar";
const EmployeeList = ({ employees, onChange, bucket }) => {
const [employeefiltered, setEmployeeFilter] = useState([]);
const [employeeStatusList, setEmployeeStatusList] = useState([]);
const [searchTerm, setSearchTerm] = useState("");
useEffect(() => {
setEmployeeFilter(employees?.filter((emp) => emp.email != null) || []);
}, [employees]);
// Initialize checked employees based on assignedEmployee prop
useEffect(() => {
if (Array.isArray(bucket?.employeeIds)) {
const initialStatus = bucket?.employeeIds?.map((id) => ({
employeeId: id,
isActive: true,
}));
setEmployeeStatusList(initialStatus);
}
}, [bucket]);
// Send updated list to parent
useEffect(() => {
if (onChange) {
onChange(employeeStatusList);
}
}, [employeeStatusList]);
const handleCheckboxChange = (id) => {
setEmployeeStatusList((prev) => {
const exists = prev.find((emp) => emp.employeeId === id);
if (exists) {
return prev.map((emp) =>
emp.employeeId === id ? { ...emp, isActive: !emp.isActive } : emp
);
} else {
return [...prev, { employeeId: id, isActive: true }];
}
});
};
const isChecked = (id) => {
const found = employeeStatusList.find((emp) => emp.employeeId === id);
return found?.isActive || false;
};
// Sorting
const {
items: sortedEmployees,
requestSort,
sortConfig,
} = useSortableData(employeefiltered, {
key: (e) => `${e?.firstName} ${e?.lastName}`,
direction: "asc",
});
const getSortIcon = () => {
if (!sortConfig) return null;
return sortConfig.direction === "asc" ? (
<i className="bx bx-caret-up text-secondary"></i>
) : (
<i className="bx bx-caret-down text-secondary"></i>
);
};
const filteredEmployees = sortedEmployees?.filter((employee) => {
const fullName =
`${employee?.firstName} ${employee?.lastName}`?.toLowerCase();
return fullName.includes(searchTerm.toLowerCase());
});
return (
<>
<div className="d-flex justify-content-between align-items-center mt-2">
<p className="m-0 fw-normal">Add Employee</p>
<div className="px-1">
<input
type="search"
className="form-control form-control-sm"
placeholder="Search Employee..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
</div>
<div className="table-responsive px-1 my-1 px-sm-0">
<table className="table align-middle mb-0">
<thead className="table-light">
<tr>
<th
onClick={() =>
requestSort((e) => `${e.firstName} ${e.lastName}`)
}
className="text-start cursor-pointer"
>
<span className="ps-2">Name {getSortIcon()}</span>
</th>
<th className="text-start">Role</th>
</tr>
</thead>
<tbody>
{employees.length === 0 ? (
<tr>
<td colSpan={4}>
<div className="d-flex justify-content-center align-items-center py-5">
No Employee Available
</div>
</td>
</tr>
) : filteredEmployees.length === 0 ? (
<tr className="my-4">
<td colSpan={4}>
<div className="d-flex justify-content-center align-items-center py-5">
No Matching Employee Found.
</div>
</td>
</tr>
) : (
filteredEmployees?.map((employee) => (
<tr key={employee.id}>
<td>
<div className="d-flex align-items-center text-start">
<input
className="form-check-input me-3 mt-1"
type="checkbox"
checked={isChecked(employee.id)}
onChange={() => handleCheckboxChange(employee?.id)}
disabled={bucket?.createdBy?.id === employee?.id}
/>
<Avatar
size="xs"
classAvatar="m-0"
firstName={employee.firstName}
lastName={employee.lastName}
/>
<span
className="text-truncate mx-0"
style={{ maxWidth: "150px" }}
>{`${employee.firstName} ${employee.lastName}`}</span>
</div>
</td>
<td className="text-start">
<small className="text-muted">{employee.jobRole}</small>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</>
);
};
export default EmployeeList;

View File

@ -0,0 +1,131 @@
import React, { useEffect } from "react";
import Avatar from "../common/Avatar";
import { getEmailIcon, getPhoneIcon } from "./DirectoryUtils";
import { useDir } from "../../Context/DireContext";
const ListViewDirectory = ({
IsActive,
contact,
setSelectedContact,
setIsOpenModal,
setOpen_contact,
setIsOpenModalNote,
IsDeleted,
restore,
}) => {
const { dirActions, setDirActions } = useDir();
return (
<tr className={!IsActive ? "bg-light" : ""}>
<td
className="text-start cursor-pointer"
style={{ width: "18%" }}
colSpan={2}
onClick={() => {
if (IsActive) {
setIsOpenModalNote(true);
setOpen_contact(contact);
}
}}
>
<div className="d-flex align-items-center">
<Avatar
size="xs"
classAvatar="m-0"
firstName={
(contact?.name || "").trim().split(" ")[0]?.charAt(0) || ""
}
lastName={
(contact?.name || "").trim().split(" ")[1]?.charAt(0) || ""
}
/>
<span className="text-truncate mx-0" style={{ maxWidth: "150px" }}>
{contact?.name || ""}
</span>
</div>
</td>
<td className="px-2" style={{ width: "20%" }}>
<div className="d-flex flex-column align-items-start text-truncate">
{contact.contactEmails.length > 0 ? (contact.contactEmails?.map((email, index) => (
<span key={email.id} className="text-truncate">
<i
className={getEmailIcon(email.label)}
style={{ fontSize: "12px" }}
></i>
<a
href={`mailto:${email.emailAddress}`}
className="text-decoration-none ms-1"
>
{email.emailAddress}
</a>
</span>
))):(<span className="small-text m-0 px-2">NA</span>)}
</div>
</td>
<td className="px-2" style={{ width: "20%" }}>
<div className="d-flex flex-column align-items-start text-truncate">
{contact.contactPhones?.length > 0 ? (
contact.contactPhones?.map((phone, index) => (
<span key={phone.id}>
<i
className={getPhoneIcon(phone.label)}
style={{ fontSize: "12px" }}
></i>
<span className="ms-1">{phone.phoneNumber}</span>
</span>
))
):(<span className="text-small m-0 px-2">NA</span>)}
</div>
</td>
<td
colSpan={2}
className="text-start text-truncate px-2"
style={{ width: "20%", maxWidth: "200px" }}
>
{contact.organization}
</td>
<td className="px-2" style={{ width: "10%" }}>
<span className="badge badge-outline-secondary">
{contact?.contactCategory?.name || "Other"}
</span>
</td>
<td className="align-middle text-center" style={{ width: "12%" }}>
{IsActive && (
<>
<i
className="bx bx-edit bx-sm text-primary cursor-pointer me-2"
onClick={() => {
setSelectedContact(contact);
setIsOpenModal(true);
}}
></i>
<i
className="bx bx-trash bx-sm text-danger cursor-pointer"
onClick={() => IsDeleted(contact.id)}
></i>
</>
)}
{!IsActive && (
<i
className={`bx ${
dirActions.action && dirActions.id === contact.id ? "bx-loader-alt bx-spin"
: "bx-recycle"
} me-1 text-primary cursor-pointer`}
title="Restore"
onClick={() => {
setDirActions({ action: false, id: contact.id });
restore(contact.id);
}}
></i>
)}
</td>
</tr>
);
};
export default ListViewDirectory;

View File

@ -0,0 +1,429 @@
import React, { useEffect, useState } from "react";
import IconButton from "../common/IconButton";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { bucketScheam } from "./DirectorySchema";
import showToast from "../../services/toastService";
import Directory from "../../pages/Directory/Directory";
import { DirectoryRepository } from "../../repositories/DirectoryRepository";
import { cacheData, getCachedData } from "../../slices/apiDataManager";
import { useBuckets } from "../../hooks/useDirectory";
import EmployeeList from "./EmployeeList";
import { useAllEmployees, useEmployees } from "../../hooks/useEmployees";
import { useSortableData } from "../../hooks/useSortableData";
import ConfirmModal from "../common/ConfirmModal";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { DIRECTORY_ADMIN, DIRECTORY_MANAGER } from "../../utils/constants";
import { useProfile } from "../../hooks/useProfile";
const ManageBucket = () => {
const { profile } = useProfile();
const [bucketList, setBucketList] = useState([]);
const { employeesList } = useAllEmployees(false);
const [selectedEmployee, setSelectEmployee] = useState([]);
const { buckets, loading, refetch } = useBuckets();
const [action_bucket, setAction_bucket] = useState(false);
const [isSubmitting, setSubmitting] = useState(false);
const [selected_bucket, select_bucket] = useState(null);
const [deleteBucket, setDeleteBucket] = useState(null);
const [searchTerm, setSearchTerm] = useState("");
const DirManager = useHasUserPermission(DIRECTORY_MANAGER);
const DirAdmin = useHasUserPermission(DIRECTORY_ADMIN);
const {
items: sortedBuckteList,
requestSort,
sortConfig,
} = useSortableData(bucketList, {
key: (e) => `${e.name}`,
direction: "asc",
});
const getSortIcon = () => {
if (!sortConfig) return null;
return sortConfig.direction === "asc" ? (
<i className="bx bx-caret-up text-secondary"></i>
) : (
<i className="bx bx-caret-down text-secondary"></i>
);
};
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm({
resolver: zodResolver(bucketScheam),
defaultValues: {
name: "",
description: "",
},
});
const onSubmit = async (data) => {
setSubmitting(true);
try {
const cache_buckets = getCachedData("buckets") || [];
let response;
// Utility: Compare arrays regardless of order
const arraysAreEqual = (a, b) => {
if (a.length !== b.length) return false;
const setA = new Set(a);
const setB = new Set(b);
return [...setA].every((id) => setB.has(id));
};
// UPDATE existing bucket
if (selected_bucket) {
const payload = { ...data, id: selected_bucket.id };
// 1. Update bucket details
response = await DirectoryRepository.UpdateBuckets(
selected_bucket.id,
payload
);
const updatedBuckets = cache_buckets.map((bucket) =>
bucket.id === selected_bucket.id ? response?.data : bucket
);
cacheData("buckets", updatedBuckets);
setBucketList(updatedBuckets);
// 2. Update employee assignments if they changed
const existingEmployeeIds = selected_bucket?.employeeIds || [];
const employeesToUpdate = selectedEmployee.filter((emp) => {
const isExisting = existingEmployeeIds.includes(emp.employeeId);
return (!isExisting && emp.isActive) || (isExisting && !emp.isActive);
});
// Create a filtered list of active employee IDs to compare
const newActiveEmployeeIds = selectedEmployee
.filter((emp) => {
const isExisting = existingEmployeeIds.includes(emp.employeeId);
return (
(!isExisting && emp.isActive) || (isExisting && !emp.isActive)
);
})
.map((emp) => emp.employeeId);
if (
!arraysAreEqual(newActiveEmployeeIds, existingEmployeeIds) &&
employeesToUpdate.length != 0
) {
try {
response = await DirectoryRepository.AssignedBuckets(
selected_bucket.id,
employeesToUpdate
);
} catch (assignError) {
const assignMessage =
assignError?.response?.data?.message ||
assignError?.message ||
"Error assigning employees.";
showToast(assignMessage, "error");
}
}
const updatedData = cache_buckets?.map((bucket) =>
bucket.id === response?.data?.id ? response.data : bucket
);
cacheData("buckets", updatedData);
setBucketList(updatedData);
showToast("Bucket Updated Successfully", "success");
}
// CREATE new bucket
else {
response = await DirectoryRepository.CreateBuckets(data);
const updatedBuckets = [...cache_buckets, response?.data];
cacheData("buckets", updatedBuckets);
setBucketList(updatedBuckets);
showToast("Bucket Created Successfully", "success");
}
handleBack();
} catch (error) {
const message =
error?.response?.data?.message ||
error?.message ||
"Error occurred during API call";
showToast(message, "error");
} finally {
setSubmitting(false);
}
};
const handleDeleteContact = async () => {
try {
const resp = await DirectoryRepository.DeleteBucket(deleteBucket);
const cache_buckets = getCachedData("buckets") || [];
const updatedBuckets = cache_buckets.filter(
(bucket) => bucket.id != deleteBucket
);
cacheData("buckets", updatedBuckets);
setBucketList(updatedBuckets);
showToast("Bucket deleted successfully", "success");
setDeleteBucket(null);
} catch (error) {
const message =
error?.response?.data?.message ||
error?.message ||
"Error occurred during API call.";
showToast(message, "error");
}
};
useEffect(() => {
reset({
name: selected_bucket?.name || "",
description: selected_bucket?.description || "",
});
}, [selected_bucket]);
useEffect(() => {
setBucketList(buckets);
}, [buckets]);
const handleBack = () => {
select_bucket(null);
setAction_bucket(false);
setSubmitting(false);
};
const sortedBucktesList = sortedBuckteList?.filter((bucket) => {
const term = searchTerm?.toLowerCase();
const name = bucket.name?.toLowerCase();
return name?.includes(term);
});
return (
<>
{deleteBucket && (
<div
className={`modal fade ${deleteBucket ? "show" : ""}`}
tabIndex="-1"
role="dialog"
style={{
display: deleteBucket ? "block" : "none",
backgroundColor: deleteBucket ? "rgba(0,0,0,0.5)" : "transparent",
}}
>
<ConfirmModal
type={"delete"}
header={"Delete Bucket"}
message={"Are you sure you want delete?"}
onSubmit={handleDeleteContact}
onClose={() => setDeleteBucket(null)}
// loading={IsDeleting}
/>
</div>
)}
<div className="container m-0 p-0" style={{ minHeight: "200px" }}>
<div className="d-flex justify-content-center">
<p className="fs-6 fw-semibold m-0">Manage Buckets</p>
</div>
<div className="d-flex justify-content-between px-2 px-sm-0 mt-5 mt-3 align-items-center ">
{action_bucket ? (
<i
className={`fa-solid fa-arrow-left fs-5 cursor-pointer`}
onClick={handleBack}
></i>
) : (
<div className="d-flex align-items-center gap-2">
<input
type="search"
className="form-control form-control-sm"
placeholder="Search Bucket ..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<i
className={`bx bx-refresh cursor-pointer fs-4 ${
loading ? "spin" : ""
}`}
title="Refresh"
onClick={() => refetch()}
/>
</div>
)}
<button
type="button"
className={`btn btn-sm btn-primary ms-auto ${
action_bucket ? "d-none" : ""
}`}
onClick={() => setAction_bucket(true)}
>
<i className="bx bx-plus-circle me-2"></i>
Add Bucket
</button>
</div>
<div>
{!action_bucket ? (
<div className="table-responsive text-nowrap pt-1 px-2 px-sm-0 mt-3">
<table className="table px-2">
<thead className="p-0">
<tr className="p-0">
<th
colSpan={2}
className="cursor-pointer"
onClick={() => requestSort((e) => `${e.name} `)}
>
<div className="d-flex justify-content-start align-items-center gap-1 mx-2">
<span>Name {getSortIcon()}</span>
</div>
</th>
<th className="text-start d-none d-sm-table-cell">
<div className="d-flex align-items-center justify-content-center gap-1">
<span>Description</span>
</div>
</th>
<th>Contacts</th>
<th>
<div className="d-flex align-items-center justify-content-center gap-1">
<span>Action</span>
</div>
</th>
</tr>
</thead>
<tbody className="table-border-bottom-0 overflow-auto">
{loading && (
<tr className="mt-10">
<td colSpan={5}>
{" "}
<div className="d-flex justify-content-center align-items-center py-5">
Loading...
</div>
</td>
</tr>
)}
{!loading && buckets.length == 0 && (
<tr>
<td colSpan={5}>
<div className="d-flex justify-content-center align-items-center py-5">
Bucket Not Available.
</div>
</td>
</tr>
)}
{!loading && sortedBucktesList.length == 0 && (
<tr>
<td className="text-center py-4 h-25" colSpan={5}>
<div className="d-flex justify-content-center align-items-center py-5">
No Matching Bucket Found.
</div>
</td>
</tr>
)}
{!loading &&
sortedBucktesList.map((bucket) => (
<tr key={bucket.id}>
<td colSpan={2} className="text-start text-wrap">
<i className="bx bx-right-arrow-alt me-1"></i>{" "}
{bucket.name}
</td>
<td
className="text-start d-none d-sm-table-cell text-truncate"
style={{
maxWidth: "300px",
whiteSpace: "wrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
title={bucket.description}
>
{bucket.description}
</td>
<td>{bucket.numberOfContacts}</td>
<td className="justify-content-center">
{(DirManager ||
DirAdmin ||
bucket?.createdBy?.id ===
profile?.employeeInfo?.id) && (
<div className="d-flex justify-content-center align-items-center gap-2">
<i
className="bx bx-edit bx-sm text-primary cursor-pointer "
onClick={() => {
select_bucket(bucket);
setAction_bucket(true);
}}
></i>
<i
className="bx bx-trash bx-sm text-danger cursor-pointer"
onClick={() => setDeleteBucket(bucket?.id)}
></i>
</div>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<>
<form onSubmit={handleSubmit(onSubmit)} className="px-2 px-sm-0">
<div className="">
<label className="form-label">Bucket Name</label>
<input
className="form-control form-control-sm"
{...register("name")}
/>
{errors.name && (
<small className="danger-text">{errors.name.message}</small>
)}
</div>
<div className="">
<label className="form-label">Bucket Discription</label>
<textarea
className="form-control form-control-sm"
rows="3"
{...register("description")}
/>
{errors.description && (
<small className="danger-text">
{errors.description.message}
</small>
)}
</div>
{selected_bucket && (
<EmployeeList
employees={employeesList}
onChange={(data) => setSelectEmployee(data)}
bucket={selected_bucket}
/>
)}
<div className="mt-2 d-flex justify-content-center gap-3">
<button
onClick={() => handleBack()}
className="btn btn-sm btn-secondary"
disabled={isSubmitting}
>
Cancel
</button>
<button
type="submit"
className="btn btn-sm btn-primary"
disabled={isSubmitting}
>
{isSubmitting ? "Please wait..." : "Submit"}
</button>
</div>
</form>
</>
)}
</div>
</div>
</>
);
};
export default ManageBucket;

View File

@ -1,40 +1,55 @@
import React, { useEffect } from "react";
import { useForm, useFieldArray, FormProvider } from "react-hook-form";
import React, { useEffect, useState } from "react";
import {
useForm,
useFieldArray,
FormProvider,
useFormContext,
} from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import TagInput from "../common/TagInput";
import { z } from "zod";
import IconButton from "../common/IconButton";
import useMaster, {
useContactCategory,
useContactTags,
} from "../../hooks/masterHook/useMaster";
import { useDispatch, useSelector } from "react-redux";
import { changeMaster } from "../../slices/localVariablesSlice";
import { useBuckets, useOrganization } from "../../hooks/useDirectory";
import { useProjects } from "../../hooks/useProjects";
import SelectMultiple from "../common/SelectMultiple";
import { ContactSchema } from "./DirectorySchema";
import InputSuggestions from "../common/InputSuggestion";
export const directorySchema = z.object({
firstName: z.string().min(1, "First Name is required"),
lastName: z.string().min(1, "Last Name is required"),
organization: z.string().min(1, "Organization name is required"),
type: z.string().min(1, "Type is required"),
address: z.string().optional(),
description: z.string().min(1, { message: "Description is required" }),
email: z
.array(z.string().email("Invalid email"))
.nonempty("At least one email required"),
phone: z
.array(z.string().regex(/^\d{10}$/, "Phone must be 10 digits"))
.nonempty("At least one phone number is required"),
tags: z.array(z.string()).optional(),
});
const ManageDirectory = ({ submitContact, onCLosed }) => {
const selectedMaster = useSelector(
(store) => store.localVariables.selectedMaster
);
const [categoryData, setCategoryData] = useState([]);
const [TagsData, setTagsData] = useState([]);
const { data, loading } = useMaster();
const { buckets, loading: bucketsLoaging } = useBuckets();
const { projects, loading: projectLoading } = useProjects();
const { contactCategory, loading: contactCategoryLoading } =
useContactCategory();
const { organizationList, loading: orgLoading } = useOrganization();
const { contactTags, loading: Tagloading } = useContactTags();
const [IsSubmitting, setSubmitting] = useState(false);
const dispatch = useDispatch();
const ManageDirectory = () => {
const methods = useForm({
resolver: zodResolver(directorySchema),
resolver: zodResolver(ContactSchema),
defaultValues: {
firstName: "",
lastName: "",
name: "",
organization: "",
type: "",
contactCategoryId: null,
address: "",
description: "",
email: [""],
phone: [""],
projectIds: [],
contactEmails: [],
contactPhones: [],
tags: [],
bucketIds: [],
},
});
@ -44,6 +59,9 @@ const ManageDirectory = () => {
control,
getValues,
trigger,
setValue,
watch,
reset,
formState: { errors },
} = methods;
@ -51,160 +69,359 @@ const ManageDirectory = () => {
fields: emailFields,
append: appendEmail,
remove: removeEmail,
} = useFieldArray({ control, name: "email" });
} = useFieldArray({ control, name: "contactEmails" });
const {
fields: phoneFields,
append: appendPhone,
remove: removePhone,
} = useFieldArray({ control, name: "phone" });
} = useFieldArray({ control, name: "contactPhones" });
useEffect(() => {
if (emailFields.length === 0) appendEmail("");
if (phoneFields.length === 0) appendPhone("");
if (emailFields.length === 0) {
appendEmail({ label: "Work", emailAddress: "" });
}
if (phoneFields.length === 0) {
appendPhone({ label: "Office", phoneNumber: "" });
}
}, [emailFields.length, phoneFields.length]);
const onSubmit = (data) => {
// console.log("Submitted:\n" + JSON.stringify(data, null, 2));
};
const handleAddEmail = async () => {
const emails = getValues("email");
const emails = getValues("contactEmails");
const lastIndex = emails.length - 1;
const valid = await trigger(`email.${lastIndex}`);
if (valid) appendEmail("");
const valid = await trigger(`contactEmails.${lastIndex}.emailAddress`);
if (valid) {
appendEmail({ label: "Work", emailAddress: "" });
}
};
const handleAddPhone = async () => {
const phones = getValues("phone");
const phones = getValues("contactPhones");
const lastIndex = phones.length - 1;
const valid = await trigger(`phone.${lastIndex}`);
if (valid) appendPhone("");
const valid = await trigger(`contactPhones.${lastIndex}.phoneNumber`);
if (valid) {
appendPhone({ label: "Office", phoneNumber: "" });
}
};
const watchBucketIds = watch("bucketIds");
const toggleBucketId = (id) => {
const updated = watchBucketIds?.includes(id)
? watchBucketIds.filter((val) => val !== id)
: [...watchBucketIds, id];
setValue("bucketIds", updated, { shouldValidate: true });
};
const handleCheckboxChange = (id) => {
const updated = watchBucketIds.includes(id)
? watchBucketIds.filter((i) => i !== id)
: [...watchBucketIds, id];
setValue("bucketIds", updated, { shouldValidate: true });
};
const onSubmit = (data) => {
const cleaned = {
...data,
contactEmails: (data.contactEmails || []).filter(
(e) => e.emailAddress?.trim() !== ""
),
contactPhones: (data.contactPhones || []).filter(
(p) => p.phoneNumber?.trim() !== ""
),
};
setSubmitting(true);
submitContact(cleaned, reset, setSubmitting);
};
const orgValue = watch("organization");
const handleClosed = () => {
onCLosed();
};
return (
<FormProvider {...methods}>
<form className="p-2 p-sm-0" onSubmit={handleSubmit(onSubmit)}>
<div className="d-flex justify-content-center align-items-center">
<h6 className="m-0 fw-18"> Create New Contact</h6>
</div>
<div className="row">
<div className="col-md-6">
<label className="form-label">First Name</label>
<input className="form-control form-control-sm" {...register("firstName")} />
{errors.firstName && <small className="danger-text">{errors.firstName.message}</small>}
<div className="col-md-6 text-start">
<label className="form-label">Name</label>
<input
className="form-control form-control-sm"
{...register("name")}
/>
{errors.name && (
<small className="danger-text">{errors.name.message}</small>
)}
</div>
<div className="col-md-6">
<label className="form-label">Last Name</label>
<input className="form-control form-control-sm" {...register("lastName")} />
{errors.lastName && <small className="danger-text">{errors.lastName.message}</small>}
<div className="col-md-6 text-start">
<label className="form-label">Organization</label>
<InputSuggestions
organizationList={organizationList}
value={getValues("organization") || ""}
onChange={(val) => setValue("organization", val)}
error={errors.organization?.message}
/>
</div>
</div>
<div className="col-12">
<label className="form-label">Organization</label>
<input className="form-control form-control-sm" {...register("organization")} />
{errors.organization && <small className="danger-text">{errors.organization.message}</small>}
</div>
<div className="row">
<div className="row mt-1">
<div className="col-md-6">
<label className="form-label">Email</label>
{emailFields.map((field, index) => (<>
<div key={field.id} className="d-flex align-items-center mb-1">
<input
type="email"
className="form-control form-control-sm"
{...register(`email.${index}`)}
placeholder="email@example.com"
/>
{index === emailFields.length - 1 ? (
<button
type="button"
className="btn btn-xs btn-primary ms-1"
onClick={handleAddEmail}
{emailFields.map((field, index) => (
<div
key={field.id}
className="row d-flex align-items-center mb-1"
>
<div className="col-5 text-start">
<label className="form-label">Label</label>
<select
className="form-select form-select-sm"
{...register(`contactEmails.${index}.label`)}
>
<i className="bx bx-plus bx-xs" />
</button>
) : (
<button
type="button"
className="btn btn-xs btn-danger ms-1"
onClick={() => removeEmail(index)}
>
<i className="bx bx-x bx-xs" />
</button>
)}
<option value="Work">Work</option>
<option value="Personal">Personal</option>
<option value="Other">Other</option>
</select>
{errors.contactEmails?.[index]?.label && (
<small className="danger-text">
{errors.contactEmails[index].label.message}
</small>
)}
</div>
<div className="col-7 text-start">
<label className="form-label">Email</label>
<div className="d-flex align-items-center">
<input
type="email"
className="form-control form-control-sm"
{...register(`contactEmails.${index}.emailAddress`)}
placeholder="email@example.com"
/>
{index === emailFields.length - 1 ? (
// <button
// type="button"
// className="btn btn-xs btn-primary ms-1"
// onClick={handleAddEmail}
// style={{ width: "24px", height: "24px" }}
// >
<i
className="bx bx-plus-circle bx-xs ms-1 cursor-pointer text-primary"
onClick={handleAddEmail}
/>
) : (
// <button
// type="button"
// className="btn btn-xs btn-danger ms-1 p-0"
// onClick={() => removeEmail(index)}
// style={{ width: "24px", height: "24px" }}
// >
<i
className="bx bx-minus-circle bx-xs ms-1 cursor-pointer text-primary"
onClick={() => removeEmail(index)}
/>
)}
</div>
{errors.contactEmails?.[index]?.emailAddress && (
<small className="danger-text">
{errors.contactEmails[index].emailAddress.message}
</small>
)}
</div>
</div>
{errors.email?.[index] && (
<small className="danger-text ms-2">
{errors.email[index]?.message}
</small>
)}
</>
))}
</div>
<div className="col-md-6">
<label className="form-label">Phone</label>
{phoneFields.map((field, index) => (<>
<div key={field.id} className="d-flex align-items-center mb-1">
<input
type="text"
className="form-control form-control-sm"
{...register(`phone.${index}`)}
placeholder="9876543210"
/>
{index === phoneFields.length - 1 ? (
<button
type="button"
className="btn btn-xs btn-primary ms-1"
onClick={handleAddPhone}
{phoneFields.map((field, index) => (
<div
key={field.id}
className="row d-flex align-items-center mb-2"
>
<div className="col-5 text-start">
<label className="form-label">Label</label>
<select
className="form-select form-select-sm"
{...register(`contactPhones.${index}.label`)}
>
<i className="bx bx-plus bx-xs" />
</button>
) : (
<button
type="button"
className="btn btn-xs btn-danger ms-1"
onClick={() => removePhone(index)} // Remove the phone field
>
<i className="bx bx-x bx-xs" />
</button>
)}
<option value="Office">Office</option>
<option value="Personal">Personal</option>
<option value="Business">Business</option>
</select>
{errors.phone?.[index]?.label && (
<small className="danger-text">
{errors.ContactPhones[index].label.message}
</small>
)}
</div>
<div className="col-7 text-start">
<label className="form-label">Phone</label>
<div className="d-flex align-items-center">
<input
type="text"
className="form-control form-control-sm"
{...register(`contactPhones.${index}.phoneNumber`)}
placeholder="9876543210"
/>
{index === phoneFields.length - 1 ? (
// <button
// type="button"
// className="btn btn-xs btn-primary ms-1"
// onClick={handleAddPhone}
// style={{ width: "24px", height: "24px" }}
// >
<i
className="bx bx-plus-circle bx-xs ms-1 cursor-pointer text-primary"
onClick={handleAddPhone}
/>
) : (
// <button
// type="button"
// className="btn btn-xs btn-danger ms-1"
// onClick={() => removePhone(index)}
// style={{ width: "24px", height: "24px" }}
// >
<i
className="bx bx-minus-circle bx-xs ms-1 cursor-pointer text-danager"
onClick={() => removePhone(index)}
/>
)}
</div>
{errors.contactPhones?.[index]?.phoneNumber && (
<small className="danger-text">
{errors.contactPhones[index].phoneNumber.message}
</small>
)}
</div>
</div>
{errors.phone?.[ index ] && <small className="danger-text ms-2">{errors.phone[ index ]?.message}</small>}
</>
))}
</div>
{errors.contactPhone?.message && (
<div className="danger-text">{errors.contactPhone.message}</div>
)}
</div>
<div className="row my-1">
<div className="col-md-6">
<label className="form-label">Type</label>
<input className="form-control form-control-sm" {...register("type")} />
{errors.type && <small className="danger-text">{errors.type.message}</small>}
<div className="col-md-6 text-start">
<label className="form-label">Category</label>
<select
className="form-select form-select-sm"
{...register("contactCategoryId")}
>
{contactCategoryLoading && !contactCategory ? (
<option disabled value="">
Loading...
</option>
) : (
<>
<option disabled value="">
Select Category
</option>
{contactCategory?.map((cate) => (
<option key={cate.id} value={cate.id}>
{cate.name}
</option>
))}
</>
)}
</select>
{errors.contactCategoryId && (
<small className="danger-text">
{errors.contactCategoryId.message}
</small>
)}
</div>
<div className="col-md-6">
<TagInput name="tags" label="Tags" />
<div className="col-12 col-md-6 text-start">
<SelectMultiple
name="projectIds"
label="Select Projects"
options={projects}
labelKey="name"
valueKey="id"
IsLoading={projectLoading}
/>
{errors.projectIds && (
<small className="danger-text">{errors.projectIds.message}</small>
)}
</div>
</div>
<div className="col-12">
<div className="col-12 text-start">
<TagInput name="tags" label="Tags" options={contactTags} />
{errors.tags && (
<small className="danger-text">{errors.tags.message}</small>
)}
</div>
<div className="row">
<div className="col-md-12 mt-1 text-start">
<label className="form-label ">Select Bucket</label>
<ul className="d-flex flex-wrap px-1 list-unstyled mb-0">
{bucketsLoaging && <p>Loading...</p>}
{buckets?.map((item) => (
<li
key={item.id}
className="list-inline-item flex-shrink-0 me-6 mb-2"
>
<div className="form-check ">
<input
type="checkbox"
className="form-check-input"
id={`item-${item.id}`}
checked={watchBucketIds.includes(item.id)}
onChange={() => handleCheckboxChange(item.id)}
/>
<label
className="form-check-label"
htmlFor={`item-${item.id}`}
>
{item.name}
</label>
</div>
</li>
))}
{errors.bucketIds && (
<small className="danger-text mt-0">
{errors.bucketIds.message}
</small>
)}
</ul>
</div>
</div>
<div className="col-12 text-start">
<label className="form-label">Address</label>
<textarea className="form-control form-control-sm" rows="2" {...register("address")} />
<textarea
className="form-control form-control-sm"
rows="2"
{...register("address")}
/>
</div>
<div className="col-12">
<div className="col-12 text-start">
<label className="form-label">Description</label>
<textarea className="form-control form-control-sm" rows="2" {...register("description")} />
{errors.description && <small className="danger-text">{errors.description.message}</small>}
<textarea
className="form-control form-control-sm"
rows="2"
{...register("description")}
/>
{errors.description && (
<small className="danger-text">{errors.description.message}</small>
)}
</div>
<div className="d-flex justify-content-evenly py-2">
<button className="btn btn-sm btn-primary" type="submit">Submit</button>
<button className="btn btn-sm btn-secondary" type="button">Cancel</button>
<div className="d-flex justify-content-center gap-1 py-2">
<button className="btn btn-sm btn-primary" type="submit">
{IsSubmitting ? "Please Wait..." : "Submit"}
</button>
<button
className="btn btn-sm btn-secondary"
type="button"
onClick={handleClosed}
>
Cancel
</button>
</div>
</form>
</FormProvider>

View File

@ -0,0 +1,220 @@
import React, { useState } from "react";
import ReactQuill from "react-quill";
import moment from "moment";
import Avatar from "../common/Avatar";
import { DirectoryRepository } from "../../repositories/DirectoryRepository";
import showToast from "../../services/toastService";
import { cacheData, getCachedData } from "../../slices/apiDataManager";
import "../common/TextEditor/Editor.css";
const NoteCardDirectory = ({
refetchProfile,
refetchNotes,
noteItem,
contactId,
setProfileContact,
}) => {
const [editing, setEditing] = useState(false);
const [editorValue, setEditorValue] = useState(noteItem.note);
const [isLoading, setIsLoading] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [isActivProcess, setActiveProcessing] = useState(false);
const handleUpdateNote = async () => {
try {
setIsLoading(true);
const payload = {
id: noteItem.id,
note: editorValue,
contactId: contactId,
};
const response = await DirectoryRepository.UpdateNote(
noteItem.id,
payload
);
setProfileContact((prev) => ({
...prev,
notes: prev.notes.map((note) =>
note.id === noteItem.id ? response?.data : note
),
}));
const cached_contactProfile = getCachedData("Contact Profile");
if (
cached_contactProfile &&
cached_contactProfile.contactId === contactId
) {
const updatedProfile = {
...cached_contactProfile,
data: {
...cached_contactProfile?.data,
notes: cached_contactProfile?.data?.notes.map((note) =>
note.id === noteItem.id ? response?.data : note
),
},
};
cacheData("Contact Profile", updatedProfile);
}
setEditing(false);
setIsLoading(false);
showToast("Note Updated successfully", "success");
} catch (error) {
setIsLoading(false);
const msg =
error.reponse.data.message ||
error.message ||
"Error occured during API calling.";
showToast("Failed to update note", "error");
}
};
const handleDeleteNote = async (activeStatue) => {
try {
activeStatue ? setActiveProcessing(true) : setIsDeleting(true);
const resp = await DirectoryRepository.DeleteNote(
noteItem.id,
activeStatue
);
setProfileContact((prev) => ({
...prev,
notes: prev.notes.filter((note) => note.id !== noteItem.id),
}));
const cachedContactProfile = getCachedData("Contact Profile");
if (
cachedContactProfile &&
cachedContactProfile.contactId === contactId
) {
const updatedCache = {
...cachedContactProfile,
data: {
...cachedContactProfile?.data,
notes: (cachedContactProfile?.data?.notes || []).filter(
(note) => note.id !== noteItem.id
),
},
};
cacheData("Contact Profile", updatedCache);
}
setIsDeleting(false);
setActiveProcessing(false);
refetchNotes(contactId, false);
refetchProfile(contactId);
showToast(
`Note ${activeStatue ? "Restored" : "Deleted"} Successfully`,
"success"
);
} catch (error) {
setIsDeleting(false);
const msg =
error.response?.data?.message ||
error.message ||
"Error occured during API calling";
showToast(msg, "error");
}
};
return (
<div
className="card p-1 shadow-sm border-1 mb-5 conntactNote rounded"
style={{
width: "100%",
minWidth: "300px",
borderRadius: "0px",
background: `${noteItem.isActive ? "" : "#f8f6f6"}`,
}}
key={noteItem.id}
>
<div className="d-flex justify-content-between align-items-center mb-1">
<div className="d-flex align-items-center">
<Avatar
size="xs"
firstName={noteItem.createdBy.firstName}
lastName={noteItem.createdBy.lastName}
className="m-0"
/>
<div className="d-flex flex-column ms-2">
<span className="fw-semibold small">
{noteItem.createdBy.firstName} {noteItem.createdBy.lastName}
</span>
<span className="text-muted" style={{ fontSize: "10px" }}>
{moment
.utc(noteItem.createdAt)
.add(5, "hours")
.add(30, "minutes")
.format("MMMM DD, YYYY [at] hh:mm A")}
</span>
</div>
</div>
<div>
{noteItem.isActive ? (
<>
<i
className="bx bxs-edit bx-sm me-1 text-primary cursor-pointer"
onClick={() => setEditing(true)}
></i>
{!isDeleting ? (
<i
className="bx bx-trash bx-sm me-1 text-secondary cursor-pointer"
onClick={() => handleDeleteNote(!noteItem.isActive)}
></i>
) : (
<div
className="spinner-border spinner-border-sm text-secondary"
role="status"
>
<span className="visually-hidden">Loading...</span>
</div>
)}
</>
) : isActivProcess ? (
<i className="bx bx-loader-alt bx-spin text-primary"></i>
) : (
<i
className="bx bx-recycle me-1 text-primary cursor-pointer"
onClick={() => handleDeleteNote(!noteItem.isActive)}
title="Restore"
></i>
)}
</div>
</div>
<hr className="mt-0" />
{editing ? (
<>
<ReactQuill
value={editorValue}
onChange={setEditorValue}
theme="snow"
className="compact-editor"
/>
<div className="d-flex justify-content-end gap-2">
<span
className="text-secondary cursor-pointer"
aria-disabled={isLoading}
onClick={() => setEditing(false)}
>
Cancel
</span>
<span
className="text-primary cursor-pointer"
aria-disabled={isLoading}
onClick={handleUpdateNote}
>
{isLoading ? "Please Wait..." : "Submit"}
</span>
</div>
</>
) : (
<div dangerouslySetInnerHTML={{ __html: noteItem.note }} />
)}
</div>
);
};
export default NoteCardDirectory;

View File

@ -0,0 +1,188 @@
import React, { useEffect, useState } from "react";
import Editor from "../common/TextEditor/Editor";
import Avatar from "../common/Avatar";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { showText } from "pdf-lib";
import { DirectoryRepository } from "../../repositories/DirectoryRepository";
import moment from "moment";
import { cacheData, getCachedData } from "../../slices/apiDataManager";
import NoteCardDirectory from "./NoteCardDirectory";
import showToast from "../../services/toastService";
import { useContactNotes } from "../../hooks/useDirectory";
const schema = z.object({
note: z.string().min(1, { message: "Note is required" }),
});
const NotesDirectory = ({
refetchProfile,
isLoading,
contactProfile,
setProfileContact,
}) => {
const [IsActive, setIsActive] = useState(true);
const { contactNotes, refetch } = useContactNotes(contactProfile?.id, true);
const [NotesData, setNotesData] = useState();
const [IsSubmitting, setIsSubmitting] = useState(false);
const [addNote, setAddNote] = useState(true);
const {
register,
handleSubmit,
setValue,
watch,
formState: { errors },
} = useForm({
resolver: zodResolver(schema),
defaultValues: {
note: "",
},
});
const noteValue = watch("note");
const handleEditorChange = (value) => {
setValue("note", value, { shouldValidate: true });
};
const onSubmit = async (data) => {
const newNote = { ...data, contactId: contactProfile?.id };
try {
setIsSubmitting(true);
const response = await DirectoryRepository.CreateNote(newNote);
const createdNote = response.data;
setProfileContact((prev) => ({
...prev,
notes: [...(prev.notes || []), createdNote],
}));
const cached_contactProfile = getCachedData("Contact Profile");
if (
cached_contactProfile &&
cached_contactProfile.contactId === contactProfile?.id
) {
const updatedProfile = {
...cached_contactProfile.data,
notes: [...(cached_contactProfile.notes || []), createdNote],
};
cacheData("Contact Profile", updatedProfile);
}
setValue("note", "");
setIsSubmitting(false);
showToast("Note added successfully!", "success");
setAddNote(true);
setIsActive(true);
} catch (error) {
setIsSubmitting(false);
const msg =
error.response.data.message ||
error.message ||
"Error occured during API calling";
showToast(msg, "error");
}
};
const onCancel = () => {
setValue("note", "");
};
const handleSwitch = () => {
setIsActive(!IsActive);
if (IsActive) {
refetch(contactProfile?.id, false);
}
};
return (
<div className="text-start">
<div className="d-flex align-items-center justify-content-between">
<p className="fw-semibold m-0">Notes :</p>
<div className="m-0 d-flex aligin-items-center">
<label className="switch switch-primary">
<input
type="checkbox"
className="switch-input"
onChange={() => handleSwitch(!IsActive)}
value={IsActive}
/>
<span className="switch-toggle-slider">
<span className="switch-on">
{/* <i class="icon-base bx bx-check"></i> */}
</span>
<span className="switch-off">
{/* <i class="icon-base bx bx-x"></i> */}
</span>
</span>
<span className="switch-label ">Show Including Inactive Notes</span>
</label>
</div>
</div>
{addNote && (
<form onSubmit={handleSubmit(onSubmit)}>
<Editor
value={noteValue}
loading={IsSubmitting}
onChange={handleEditorChange}
onCancel={onCancel}
onSubmit={handleSubmit(onSubmit)}
/>
{errors.notes && (
<p className="text-danger small mt-1">{errors.note.message}</p>
)}
</form>
)}
<div className="d-flex justify-content-end px-2">
<span
className={`btn btn-sm ${addNote ? "btn-danger" : "btn-primary"}`}
onClick={() => setAddNote(!addNote)}
>
{addNote ? "Hide Editor" : "Add Note"}
</span>
</div>
<div className=" justify-content-start px-1 mt-1">
{isLoading && (
<div className="text-center">
{" "}
<p>Loading...</p>{" "}
</div>
)}
{!isLoading &&
[...(IsActive ? contactProfile?.notes || [] : contactNotes || [])]
.reverse()
.map((noteItem) => (
<NoteCardDirectory
refetchProfile={refetchProfile}
refetchNotes={refetch}
refetchContact={refetch}
noteItem={noteItem}
contactId={contactProfile?.id}
setProfileContact={setProfileContact}
key={noteItem.id}
/>
))}
{IsActive && (
<div>
{!isLoading && contactProfile?.notes.length == 0 && !addNote && (
<div className="text-center mt-5">No Notes Found</div>
)}
</div>
)}
{!IsActive && (
<div>
{!isLoading && contactNotes.length == 0 && !addNote && (
<div className="text-center mt-5">No Notes Found</div>
)}
</div>
)}
</div>
</div>
);
};
export default NotesDirectory;

View File

@ -0,0 +1,234 @@
import React, { useEffect, useState } from "react";
import { useContactProfile } from "../../hooks/useDirectory";
import Avatar from "../common/Avatar";
import moment from "moment";
import NotesDirectory from "./NotesDirectory";
const ProfileContactDirectory = ({ contact, setOpen_contact, closeModal }) => {
const { contactProfile, loading, refetch } = useContactProfile(contact?.id);
const [copiedIndex, setCopiedIndex] = useState(null);
const [profileContact, setProfileContact] = useState();
const [expanded, setExpanded] = useState(false);
const description = contactProfile?.description || "";
const limit = 500;
const toggleReadMore = () => setExpanded(!expanded);
const isLong = description.length > limit;
const displayText = expanded
? description
: description.slice(0, limit) + (isLong ? "..." : "");
useEffect(() => {
setProfileContact(contactProfile);
}, [contactProfile]);
const handleCopy = (email, index) => {
navigator.clipboard.writeText(email);
setCopiedIndex(index);
setTimeout(() => setCopiedIndex(null), 2000); // Reset after 2 seconds
};
return (
<div className="p-1">
<div className="text-center m-0 p-0">
<p className="fw-semibold fs-6 m-0">Contact Profile</p>
</div>
<div>
<div className="d-flex align-items-center mb-2">
<Avatar
size="sm"
classAvatar="m-0"
firstName={
(contact?.name || "").trim().split(" ")[0]?.charAt(0) || ""
}
lastName={
(contact?.name || "").trim().split(" ")[1]?.charAt(0) || ""
}
/>
<div className="d-flex flex-column text-start ms-1">
<span className="m-0 fw-semibold">{contact?.name}</span>
<small className="text-secondary small-text">
{contactProfile?.tags?.map((tag) => tag.name).join(" | ")}
</small>
</div>
</div>
<div className="row">
<div className="col-12 col-md-6 d-flex flex-column text-start">
{contactProfile?.contactEmails?.length > 0 && (
<div className="d-flex mb-2">
<div style={{ width: "100px", minWidth: "100px" }}>
<p className="m-0">Email:</p>
</div>
<div style={{ flex: 1 }}>
<ul className="list-unstyled mb-0">
{contactProfile.contactEmails.map((email, idx) => (
<li className="d-flex align-items-center mb-1" key={idx}>
<i className="bx bx-envelope bx-xs me-1 mt-1"></i>
<span className="me-1 flex-grow text-break overflow-wrap">
{email.emailAddress}
</span>
<i
className={`bx bx-copy-alt cursor-pointer bx-xs text-start ${
copiedIndex === idx
? "text-secondary"
: "text-primary"
}`}
title={copiedIndex === idx ? "Copied!" : "Copy Email"}
style={{ flexShrink: 0 }}
onClick={() => handleCopy(email.emailAddress, idx)}
></i>
</li>
))}
</ul>
</div>
</div>
)}
{contactProfile?.contactPhones?.length > 0 && (
<div className="d-flex mb-2">
<div style={{ width: "100px", minWidth: "100px" }}>
<p className="m-0">Phone : </p>
</div>
<div>
<ul className="list-inline mb-0">
{contactProfile?.contactPhones.map((phone, idx) => (
<li className="list-inline-item me-3" key={idx}>
<i className="bx bx-phone bx-xs me-1"></i>
{phone.phoneNumber}
</li>
))}
</ul>
</div>
</div>
)}
{contactProfile?.createdAt && (
<div className="d-flex mb-2">
<div style={{ width: "100px", minWidth: "100px" }}>
<p className="m-0">Created : </p>
</div>
<div className="d-flex align-items-center">
<li className="list-inline-item">
<i className="bx bx-calendar-week bx-xs me-1"></i>
{moment(contactProfile.createdAt).format("MMMM, DD YYYY")}
</li>
</div>
</div>
)}
{contactProfile?.address && (
<div className="d-flex mb-2">
<div style={{ width: "100px", minWidth: "100px" }}>
<p className="m-0">Location:</p>
</div>
<div className="d-flex align-items-center">
<i className="bx bx-map bx-xs me-1 "></i>
<span className="text-break small">
{contactProfile.address}
</span>
</div>
</div>
)}
</div>
<div className="col-12 col-md-6 d-flex flex-column text-start">
{contactProfile?.organization && (
<div className="d-flex mb-2">
<div style={{ width: "100px", minWidth: "100px" }}>
<p className="m-0">Orgnization : </p>
</div>
<div className="d-flex align-items-center">
<i className="fa-solid fa-briefcase me-2"></i>
<span style={{ wordBreak: "break-word" }}>
{contactProfile.organization}
</span>
</div>
</div>
)}
{contactProfile?.contactCategory && (
<div className="d-flex mb-2">
<div style={{ width: "100px", minWidth: "100px" }}>
<p className="m-0">Category : </p>
</div>
<div>
<ul className="list-inline mb-0">
<li className="list-inline-item">
<i className="bx bx-user bx-xs me-1"></i>
{contactProfile.contactCategory.name}
</li>
</ul>
</div>
</div>
)}
{contactProfile?.buckets?.length > 0 && (
<div className="d-flex ">
{contactProfile?.contactEmails?.length > 0 && (
<div className="d-flex mb-2 align-items-center">
<div style={{ width: "100px", minWidth: "100px" }}>
<p className="m-0">Buckets : </p>
</div>
<div>
<ul className="list-inline mb-0">
{contactProfile.buckets.map((bucket) => (
<li className="list-inline-item me-2" key={bucket.id}>
<span className="badge bg-label-primary my-1">
{bucket.name}
</span>
</li>
))}
</ul>
</div>
</div>
)}
</div>
)}
</div>
</div>
{contactProfile?.projects?.length > 0 && (
<div className="d-flex mb-2 align-items-start">
<div style={{ minWidth: "100px" }}>
<p className="m-0 text-start">Projects :</p>
</div>
<div className="text-start">
<ul className="list-inline mb-0">
{contactProfile.projects.map((project, index) => (
<li className="list-inline-item me-2" key={project.id}>
{project.name}
{index < contactProfile.projects.length - 1 && ","}
</li>
))}
</ul>
</div>
</div>
)}
<div className="d-flex mb-2 align-items-start">
<div style={{ minWidth: "100px" }}>
<p className="m-0 text-start">Description :</p>
</div>
<div className="text-start">
{displayText}
{isLong && (
<span
onClick={toggleReadMore}
className="text-primary mx-1 cursor-pointer"
>
{expanded ? "Read less" : "Read more"}
</span>
)}
</div>
</div>
<hr className="my-1" />
<NotesDirectory
refetchProfile={refetch}
isLoading={loading}
contactProfile={profileContact}
setProfileContact={setProfileContact}
/>
</div>
</div>
);
};
export default ProfileContactDirectory;

View File

@ -0,0 +1,465 @@
import React, { useEffect, useState } from "react";
import {
useForm,
useFieldArray,
FormProvider,
useFormContext,
} from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import TagInput from "../common/TagInput";
import IconButton from "../common/IconButton";
import useMaster, {
useContactCategory,
useContactTags,
} from "../../hooks/masterHook/useMaster";
import { useDispatch, useSelector } from "react-redux";
import { changeMaster } from "../../slices/localVariablesSlice";
import { useBuckets, useOrganization } from "../../hooks/useDirectory";
import { useProjects } from "../../hooks/useProjects";
import SelectMultiple from "../common/SelectMultiple";
import { ContactSchema } from "./DirectorySchema";
import InputSuggestions from "../common/InputSuggestion";
const UpdateContact = ({ submitContact, existingContact, onCLosed }) => {
const selectedMaster = useSelector(
(store) => store.localVariables.selectedMaster
);
const [categoryData, setCategoryData] = useState([]);
const [TagsData, setTagsData] = useState([]);
const { data, loading } = useMaster();
const { buckets, loading: bucketsLoaging } = useBuckets();
const { projects, loading: projectLoading } = useProjects();
const { contactCategory, loading: contactCategoryLoading } =
useContactCategory();
const { contactTags, loading: Tagloading } = useContactTags();
const [ IsSubmitting, setSubmitting ] = useState( false );
const [isInitialized, setIsInitialized] = useState(false);
const dispatch = useDispatch();
const {organizationList} = useOrganization()
const methods = useForm({
resolver: zodResolver(ContactSchema),
defaultValues: {
name: "",
organization: "",
contactCategoryId: null,
address: "",
description: "",
projectIds: [],
contactEmails: [],
contactPhones: [],
tags: [],
bucketIds: [],
},
});
const {
register,
handleSubmit,
control,
getValues,
trigger,
setValue,
watch,
reset,
formState: { errors },
} = methods;
const {
fields: emailFields,
append: appendEmail,
remove: removeEmail,
} = useFieldArray({ control, name: "contactEmails" });
const {
fields: phoneFields,
append: appendPhone,
remove: removePhone,
} = useFieldArray({ control, name: "contactPhones" });
const handleAddEmail = async () => {
const emails = getValues("contactEmails");
const lastIndex = emails.length - 1;
const valid = await trigger(`contactEmails.${lastIndex}.emailAddress`);
if (valid) {
appendEmail({ label: "Work", emailAddress: "" });
}
};
const handleAddPhone = async () => {
const phones = getValues("contactPhones");
const lastIndex = phones.length - 1;
const valid = await trigger(`contactPhones.${lastIndex}.phoneNumber`);
if (valid) {
appendPhone({ label: "Office", phoneNumber: "" });
}
};
const watchBucketIds = watch("bucketIds");
const toggleBucketId = (id) => {
const updated = watchBucketIds?.includes(id)
? watchBucketIds.filter((val) => val !== id)
: [...watchBucketIds, id];
setValue("bucketIds", updated, { shouldValidate: true });
};
const handleCheckboxChange = (id) => {
const updated = watchBucketIds.includes(id)
? watchBucketIds.filter((i) => i !== id)
: [...watchBucketIds, id];
setValue("bucketIds", updated, { shouldValidate: true });
};
const onSubmit = async (data) => {
const cleaned = {
...data,
contactEmails: (data.contactEmails || [])
.filter((e) => e.emailAddress?.trim() !== "")
.map((email, index) => {
const existingEmail = existingContact.contactEmails?.[index];
return existingEmail
? { ...email, id: existingEmail.id }
: email;
}),
contactPhones: (data.contactPhones || [])
.filter((p) => p.phoneNumber?.trim() !== "")
.map((phone, index) => {
const existingPhone = existingContact.contactPhones?.[index];
return existingPhone
? { ...phone, id: existingPhone.id }
: phone;
}),
};
setSubmitting(true);
await submitContact({ ...cleaned, id: existingContact.id });
setSubmitting(false);
};
const orgValue = watch("organization")
const handleClosed = () => {
onCLosed();
};
useEffect(() => {
const isValidContact =
existingContact &&
typeof existingContact === "object" &&
!Array.isArray(existingContact);
if (!isInitialized &&isValidContact && TagsData) {
reset({
name: existingContact.name || "",
organization: existingContact.organization || "",
contactEmails: existingContact.contactEmails || [],
contactPhones: existingContact.contactPhones || [],
contactCategoryId: existingContact.contactCategory?.id || null,
address: existingContact.address || "",
description: existingContact.description || "",
projectIds: existingContact.projectIds || null,
tags: existingContact.tags || [],
bucketIds: existingContact.bucketIds || [],
} );
if (!existingContact.contactPhones || existingContact.contactPhones.length === 0) {
appendPhone({ label: "Office", phoneNumber: "" });
}
if (!existingContact.contactEmails || existingContact.contactEmails.length === 0) {
appendEmail({ label: "Work", emailAddress: "" });
}
setIsInitialized(true)
}
// return()=> reset()
}, [ existingContact, buckets, projects ] );
return (
<FormProvider {...methods}>
<form className="p-2 p-sm-0" onSubmit={handleSubmit(onSubmit)}>
<div className="d-flex justify-content-center align-items-center">
<h6 className="m-0 fw-18"> Update Contact</h6>
</div>
<div className="row">
<div className="col-md-6 text-start">
<label className="form-label">Name</label>
<input
className="form-control form-control-sm"
{...register("name")}
/>
{errors.name && (
<small className="danger-text">{errors.name.message}</small>
)}
</div>
<div className="col-md-6 text-start">
<label className="form-label">Organization</label>
<InputSuggestions
organizationList={organizationList}
value={getValues("organization") || ""}
onChange={(val) => setValue("organization", val)}
error={errors.organization?.message}
/>
{errors.organization && (
<small className="danger-text">
{errors.organization.message}
</small>
)}
</div>
</div>
<div className="row mt-1">
<div className="col-md-6">
{emailFields.map((field, index) => (
<div
key={field.id}
className="row d-flex align-items-center mb-1"
>
<div className="col-5 text-start">
<label className="form-label">Label</label>
<select
className="form-select form-select-sm"
{...register(`contactEmails.${index}.label`)}
>
<option value="Work">Work</option>
<option value="Personal">Personal</option>
<option value="Other">Other</option>
</select>
{errors.contactEmails?.[index]?.label && (
<small className="danger-text">
{errors.contactEmails[index].label.message}
</small>
)}
</div>
<div className="col-7 text-start">
<label className="form-label">Email</label>
<div className="d-flex align-items-center">
<input
type="email"
className="form-control form-control-sm"
{...register(`contactEmails.${index}.emailAddress`)}
placeholder="email@example.com"
/>
{index === emailFields.length - 1 ? (
// <button
// type="button"
// className="btn btn-xs btn-primary ms-1"
// style={{ width: "24px", height: "24px" }}
// >
<i className="bx bx-plus-circle bx-xs ms-1 cursor-pointer text-primary" onClick={handleAddEmail}/>
) : (
// <button
// type="button"
// className="btn btn-xs btn-danger ms-1 p-0"
// onClick={() => removeEmail(index)}
// style={{ width: "24px", height: "24px" }}
// >
<i className="bx bx-minus-circle bx-xs ms-1 cursor-pointer text-danger" onClick={() => removeEmail(index)}/>
)}
</div>
{errors.contactEmails?.[index]?.emailAddress && (
<small className="danger-text">
{errors.contactEmails[index].emailAddress.message}
</small>
)}
</div>
</div>
))}
</div>
<div className="col-md-6">
{phoneFields.map((field, index) => (
<div
key={field.id}
className="row d-flex align-items-center mb-2"
>
<div className="col-5 text-start">
<label className="form-label">Label</label>
<select
className="form-select form-select-sm"
{...register(`contactPhones.${index}.label`)}
>
<option value="Office">Office</option>
<option value="Personal">Personal</option>
<option value="Business">Business</option>
</select>
{errors.phone?.[index]?.label && (
<small className="danger-text">
{errors.ContactPhones[index].label.message}
</small>
)}
</div>
<div className="col-7 text-start">
<label className="form-label">Phone</label>
<div className="d-flex align-items-center">
<input
type="text"
className="form-control form-control-sm"
{...register(`contactPhones.${index}.phoneNumber`)}
placeholder="9876543210"
/>
{index === phoneFields.length - 1 ? (
// <button
// type="button"
// className="btn btn-xs btn-primary ms-1"
// onClick={handleAddPhone}
// style={{ width: "24px", height: "24px" }}
// >
<i className="bx bx-plus-circle bx-xs ms-1 cursor-pointer text-primary" onClick={handleAddPhone} />
) : (
// <button
// type="button"
// className="btn btn-xs btn-danger ms-1"
// onClick={() => removePhone(index)}
// style={{ width: "24px", height: "24px" }}
// >
<i className="bx bx-minus-circle bx-xs ms-1 cursor-pointer text-danger" onClick={() => removePhone(index)} />
)}
</div>
{errors.contactPhones?.[index]?.phoneNumber && (
<small className="danger-text">
{errors.contactPhones[index].phoneNumber.message}
</small>
)}
</div>
</div>
))}
</div>
{errors.contactPhone?.message && (
<div className="danger-text">{errors.contactPhone.message}</div>
)}
</div>
<div className="row my-1">
<div className="col-md-6 text-start">
<label className="form-label">Category</label>
<select
className="form-select form-select-sm"
{...register("contactCategoryId")}
>
{contactCategoryLoading && !contactCategory ? (
<option disabled value="">
Loading...
</option>
) : (
<>
<option disabled value="">
Select Category
</option>
{contactCategory?.map((cate) => (
<option key={cate.id} value={cate.id}>
{cate.name}
</option>
))}
</>
)}
</select>
{errors.contactCategoryId && (
<small className="danger-text">
{errors.contactCategoryId.message}
</small>
)}
</div>
<div className="col-12 col-md-6 text-start">
<SelectMultiple
name="projectIds"
label="Select Projects"
options={projects}
labelKey="name"
valueKey="id"
IsLoading={projectLoading}
/>
{errors.projectIds && (
<small className="danger-text">{errors.projectIds.message}</small>
)}
</div>
</div>
<div className="col-12 text-start">
<TagInput name="tags" label="Tags" options={contactTags} />
{errors.tags && (
<small className="danger-text">{errors.tags.message}</small>
)}
</div>
<div className="row">
<div className="col-md-12 mt-1 text-start">
<label className="form-label ">Select Label</label>
<ul className="d-flex flex-wrap px-1 list-unstyled mb-0">
{bucketsLoaging && <p>Loading...</p>}
{buckets?.map((item) => (
<li
key={item.id}
className="list-inline-item flex-shrink-0 me-6 mb-2"
>
<div className="form-check ">
<input
type="checkbox"
className="form-check-input"
id={`item-${item.id}`}
checked={watchBucketIds.includes(item.id)}
onChange={() => handleCheckboxChange(item.id)}
/>
<label
className="form-check-label"
htmlFor={`item-${item.id}`}
>
{item.name}
</label>
</div>
</li>
))}
{errors.bucketIds && (
<small className="danger-text mt-0">
{errors.bucketIds.message}
</small>
)}
</ul>
</div>
</div>
<div className="col-12 text-start">
<label className="form-label">Address</label>
<textarea
className="form-control form-control-sm"
rows="2"
{...register("address")}
/>
</div>
<div className="col-12 text-start">
<label className="form-label">Description</label>
<textarea
className="form-control form-control-sm"
rows="2"
{...register("description")}
/>
{errors.description && (
<small className="danger-text">{errors.description.message}</small>
)}
</div>
<div className="d-flex justify-content-center gap-1 py-2">
<button className="btn btn-sm btn-primary" type="submit" disabled={IsSubmitting}>
{IsSubmitting ? "Please Wait..." : "Update"}
</button>
<button
className="btn btn-sm btn-secondary"
type="button"
onClick={handleClosed}
disabled={IsSubmitting}
>
Cancel
</button>
</div>
</form>
</FormProvider>
);
};
export default UpdateContact;

View File

@ -637,6 +637,7 @@ const ManageEmployee = ({ employeeId, onClosed }) => {
aria-label="manage employee"
type="reset"
className="btn btn-sm btn-primary ms-2"
disabled={isloading}
>
Clear
</button>

View File

@ -3,7 +3,7 @@ const Footer = () => {
<>
<footer className="content-footer footer bg-footer-theme">
<div className="container-xxl d-flex flex-wrap justify-content-between py-2 flex-md-row flex-column">
<div className="mb-2 mb-md-0" style={{width: "100%", textAlign: "right"}}>
<div className="mb-2 mb-md-0 small-text" style={{width: "100%", textAlign: "right"}}>
© {new Date().getFullYear()}
, by <a href="https://marcosolutions.co.in/" target="_blank" className="font-weight-light footer-link">MARCO AIoT Technologies Pvt. Ltd.</a>
</div>

View File

@ -1,10 +1,13 @@
import React from "react";
import { hasUserPermission } from "../../utils/authUtils";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { VIEW_PROJECT_INFRA } from "../../utils/constants";
import { DIRECTORY_ADMIN, DIRECTORY_MANAGER, DIRECTORY_USER, VIEW_PROJECT_INFRA } from "../../utils/constants";
const ProjectNav = ({ onPillClick, activePill }) => {
const HasViewInfraStructure = useHasUserPermission(VIEW_PROJECT_INFRA);
const HasViewInfraStructure = useHasUserPermission( VIEW_PROJECT_INFRA );
const DirAdmin = useHasUserPermission(DIRECTORY_ADMIN);
const DireManager = useHasUserPermission(DIRECTORY_MANAGER)
const DirUser = useHasUserPermission(DIRECTORY_USER)
return (
<div className="nav-align-top">
@ -73,7 +76,8 @@ const ProjectNav = ({ onPillClick, activePill }) => {
<i className="bx bxs-file-image bx-sm me-1_5"></i> <span className="d-none d-md-inline">Image Gallary</span>
</a>
</li>
<li className="nav-item">
{(DirAdmin || DireManager || DirUser) && (
<li className="nav-item">
<a
className={`nav-link ${activePill === "directory" ? "active" : ""}`}
href="#"
@ -85,6 +89,8 @@ const ProjectNav = ({ onPillClick, activePill }) => {
<i className='bx bxs-contact bx-sm me-1_5'></i> <span className="d-none d-md-inline">Directory</span>
</a>
</li>
)}
</ul>
</div>
);

View File

@ -10,7 +10,7 @@ function hashString(str) {
return hash;
}
const Avatar = ({ firstName, lastName, size = "sm" }) => {
const Avatar = ({ firstName, lastName, size = "sm", classAvatar }) => {
// Combine firstName and lastName to create a unique string for hashing
const fullName = `${firstName} ${lastName}`;
@ -51,7 +51,7 @@ const Avatar = ({ firstName, lastName, size = "sm" }) => {
return (
<div className="avatar-wrapper p-1">
<div className={`avatar avatar-${size} me-2`}>
<div className={`avatar avatar-${size} me-2 ${classAvatar}`}>
<span className={`avatar-initial rounded-circle ${bgClass}`}>
{generateAvatarText(firstName, lastName)}
</span>

View File

@ -3,30 +3,32 @@ import { useNavigate } from "react-router-dom";
const Breadcrumb = ({ data }) => {
const navigate = useNavigate();
return (
<nav aria-label="breadcrumb">
<ol className="breadcrumb breadcrumb-custom-icon">
{data.map((item, index) =>
{data.map((item, index) => (
item.link ? (
<li className="breadcrumb-item cursor-pointer" key={index}>
<a
aria-label="breadcrumb link link-underline-primary "
role="button"
tabIndex={0}
aria-label="breadcrumb link"
onClick={() => navigate(item.link)}
onKeyDown={(e) => {
if (e.key === 'Enter') navigate(item.link);
}}
>
{item.label}
</a>
<i className="breadcrumb-icon icon-base bx bx-chevron-right align-middle"></i>
</li>
) : (
<li
className="breadcrumb-item active "
key={new Date().getMilliseconds()}
>
{" "}
<li className="breadcrumb-item active" key={index}>
{item.label}
</li>
)
)}
))}
</ol>
</nav>
);

View File

@ -0,0 +1,35 @@
.fab-container {
position: fixed;
bottom: 35px;
right: 30px;
z-index: 1050;
display: flex;
flex-direction: column;
align-items: end;
}
.fab-main {
/* width: 45px;
height: 45px;
border-radius: 100%;
background-color: #0d6efd;
color: white;
border: none; */
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
/* font-size: 24px; */
cursor: pointer;
pointer-events: auto;
}
.fab-option {
pointer-events: auto;
margin-bottom: 10px;
}
@media (max-width: 768px) {
.fab-container {
right: 20px;
left: 50%;
bottom: 20px;
}
}

View File

@ -0,0 +1,33 @@
import React, { useState } from "react";
import {useFab} from "../../Context/FabContext";
import './FloatingMenu.css'
const FloatingMenu = () => {
const { actions } = useFab();
const [open, setOpen] = useState(false);
if (actions.length === 0) return null;
return (
<div className="fab-container">
{open &&
actions.map((action, index) => (
<button
key={index}
className={`badge bg-${action.color} rounded-pill mb-2 d-inline-flex align-items-center gap-2 px-3 py-1 cursor-pointer fab-option`}
onClick={action.onClick}
title={action.label}
>
<i className={action.icon}></i>
<span>{action.label}</span>
</button>
))}
<button type="button" className="btn btn-lg btn-icon rounded-pill me-2 btn-primary fab-main " onClick={() => setOpen(!open)}>
<span className={`bx ${open ? "bx-x" : "bx-plus"}`}></span>
</button>
</div>
);
};
export default FloatingMenu;

View File

@ -8,32 +8,45 @@ const GlobalModel = ({
dialogClass = '', // For additional custom classes on modal dialog
role = 'dialog', // Accessibility role for the modal
size = '', // Dynamically set the size (sm, lg, xl)
dataAttributes = {} // Additional dynamic data-bs-* attributes
dataAttributes = {}, // Additional dynamic data-bs-* attributes
IsCloseBtn=true
}) => {
const modalRef = useRef(null); // Reference to the modal element
useEffect(() => {
const modalElement = modalRef.current;
const modalInstance = new window.bootstrap.Modal(modalElement);
useEffect(() => {
const modalElement = modalRef.current;
const modalInstance = new window.bootstrap.Modal(modalElement, {
backdrop: false,
});
// Show modal if isOpen is true
if (isOpen) {
modalInstance.show();
} else {
modalInstance.hide();
}
if (isOpen) {
modalInstance.show();
} else {
modalInstance.hide();
}
// Handle modal hide event to invoke the closeModal function
const handleHideModal = () => {
closeModal(); // Close the modal via React state
};
const handleHideModal = () => {
closeModal();
// FIX: Remove any lingering body classes/styles
document.body.classList.remove('modal-open');
document.body.style.overflow = '';
document.body.style.paddingRight = '';
};
modalElement.addEventListener('hidden.bs.modal', handleHideModal);
return () => {
modalElement.removeEventListener('hidden.bs.modal', handleHideModal);
// Also clean up just in case component unmounts
document.body.classList.remove('modal-open');
document.body.style.overflow = '';
document.body.style.paddingRight = '';
};
}, [isOpen, closeModal]);
modalElement.addEventListener('hidden.bs.modal', handleHideModal);
return () => {
modalElement.removeEventListener('hidden.bs.modal', handleHideModal);
};
}, [isOpen, closeModal]);
// Dynamically set the modal size classes (modal-sm, modal-lg, modal-xl)
const modalSizeClass = size ? `modal-${size}` : ''; // Default is empty if no size is specified
@ -41,7 +54,20 @@ const GlobalModel = ({
// Dynamically generate data-bs attributes
const dataAttributesProps = Object.keys(dataAttributes).map(key => ({
[key]: dataAttributes[key],
}));
} ) );
// The gray background
const backdropStyle = {
position: 'fixed',
top: 0,
bottom: 0,
left: 0,
right: 0,
backgroundColor: 'rgba(0,0,0,0.3)',
};
return (
<div
@ -52,18 +78,19 @@ const GlobalModel = ({
aria-hidden="true"
ref={modalRef} // Assign the ref to the modal element
{...dataAttributesProps}
style={backdropStyle}
>
<div className={`modal-dialog ${dialogClass} ${modalSizeClass } mx-sm-auto mx-1`} role={role}>
<div className={`modal-dialog ${dialogClass} ${modalSizeClass } mx-sm-auto mx-1`} role={role} >
<div className="modal-content">
<div className="modal-header p-0">
{/* Close button inside the modal header */}
<button
{IsCloseBtn && <button
type="button"
className="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
onClick={closeModal} // Trigger the React closeModal function
></button>
></button>}
</div>
<div className="modal-body p-sm-4 p-0">
{children} {/* Render children here, which can be the ReportTask component */}

View File

@ -0,0 +1,79 @@
import React, { useState } from "react";
const InputSuggestions = ({
organizationList = [],
value,
onChange,
error,
}) => {
const [filteredList, setFilteredList] = useState([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const handleInputChange = (e) => {
const val = e.target.value;
onChange(val);
const matches = organizationList.filter((org) =>
org.toLowerCase().includes(val.toLowerCase())
);
setFilteredList(matches);
setShowSuggestions(true);
};
const handleSelectSuggestion = (val) => {
onChange(val);
setShowSuggestions(false);
};
return (
<div className="position-relative">
<input
className="form-control form-control-sm"
value={value}
onChange={handleInputChange}
onBlur={() => setTimeout(() => setShowSuggestions(false), 150)}
onFocus={() => {
if (value) setShowSuggestions(true);
}}
/>
{showSuggestions && filteredList.length > 0 && (
<ul
className="list-group shadow-sm position-absolute w-100 bg-white border zindex-tooltip"
style={{
maxHeight: "180px",
overflowY: "auto",
marginTop: "2px",
zIndex: 1000,
borderRadius:"0px"
}}
>
{filteredList.map((org) => (
<li
key={org}
className="list-group-item list-group-item-action border-none "
style={{
cursor: "pointer",
padding: "5px 12px",
fontSize: "14px",
transition: "background-color 0.2s",
}}
onMouseDown={() => handleSelectSuggestion(org)}
onMouseEnter={(e) =>
(e.currentTarget.style.backgroundColor = "#f8f9fa")
}
onMouseLeave={(e) =>
(e.currentTarget.style.backgroundColor = "transparent")
}
>
{org}
</li>
))}
</ul>
)}
{error && <small className="danger-text">{error}</small>}
</div>
);
};
export default InputSuggestions;

View File

@ -0,0 +1,158 @@
/* Container for the multi-select dropdown */
.multi-select-dropdown-container {
position: relative;
}
/* Header of the dropdown */
.multi-select-dropdown-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px;
border: 1px solid #ddd;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s ease;
}
.multi-select-dropdown-header .placeholder-style {
color: #6c757d;
font-size: 14px;
}
.multi-select-dropdown-header .placeholder-style-selected {
/* color: #0d6efd; */
font-size: 12px;
}
/* Arrow icon */
.multi-select-dropdown-arrow {
width: 14px;
height: 14px;
margin-left: 10px;
}
/* Dropdown options */
.multi-select-dropdown-options {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 10;
border: 1px solid #ddd;
border-radius: 3px;
background-color: white;
max-height: 250px;
overflow-y: auto;
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2);
margin-top: 5px;
}
/* Search input */
.multi-select-dropdown-search {
padding: 4px;
border-bottom: 1px solid #f1f3f5;
}
.multi-select-dropdown-search-input {
width: 100%;
padding: 4px;
border: none;
outline: none;
background-color: #f8f9fa;
border-radius: 6px;
font-size: 14px;
}
/* Select All checkbox */
.multi-select-dropdown-select-all {
display: flex;
align-items: center;
padding: 4px;
}
.multi-select-dropdown-select-all .custom-checkbox {
margin-right: 8px;
}
.multi-select-dropdown-select-all-label {
font-size: 14px;
color: #333;
}
/* Options in dropdown */
.multi-select-dropdown-option {
display: flex;
align-items: center;
padding: 4px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.multi-select-dropdown-option:hover {
background-color: #f1f3f5;
}
.multi-select-dropdown-option.selected {
background-color: #dbe7ff;
color: #0d6efd;
}
.multi-select-dropdown-option input[type="checkbox"] {
margin-right: 10px;
}
/* Custom checkbox */
.custom-checkbox {
width: 18px;
height: 18px;
border-radius: 4px;
border: 1px solid #ddd;
background-color: white;
cursor: pointer;
position: relative;
margin-right: 10px;
}
.custom-checkbox:checked {
background-color: #696cff;
border-color: #696cff;
}
.custom-checkbox:checked::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 10px;
height: 10px;
border-radius: 2px;
}
.multi-select-dropdown-Not-found{
text-align: center;
padding: 1px 3px;
}
.multi-select-dropdown-Not-found:hover {
background: #e2dfdf;
}
/* Responsive styles */
@media (max-width: 767px) {
.multi-select-dropdown-container {
width: 100%;
}
.multi-select-dropdown-header {
font-size: 12px;
}
.multi-select-dropdown-options {
width: 100%;
max-height: 200px;
}
}

View File

@ -0,0 +1,131 @@
import React, { useState, useEffect, useRef } from "react";
import { useFormContext } from "react-hook-form";
import "./MultiSelectDropdown.css";
const SelectMultiple = ({
name,
options = [],
label = "Select options",
labelKey = "name",
valueKey = "id",
placeholder = "Please select...",
IsLoading = false,
}) => {
const { setValue, watch } = useFormContext();
const selectedValues = watch(name) || [];
const [isOpen, setIsOpen] = useState(false);
const [searchText, setSearchText] = useState("");
const dropdownRef = useRef(null);
useEffect(() => {
const handleClickOutside = (e) => {
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const handleCheckboxChange = (value) => {
const updated = selectedValues.includes(value)
? selectedValues.filter((v) => v !== value)
: [...selectedValues, value];
setValue(name, updated, { shouldValidate: true });
};
const filteredOptions = options.filter((item) =>
item[labelKey]?.toLowerCase().includes(searchText.toLowerCase())
);
return (
<div ref={dropdownRef} className="multi-select-dropdown-container">
<label className="form-label mb-1">{label}</label>
<div
className="multi-select-dropdown-header"
onClick={() => setIsOpen((prev) => !prev)}
>
<span
className={
selectedValues.length > 0
? "placeholder-style-selected"
: "placeholder-style"
}
>
<div className="selected-badges-container">
{selectedValues.length > 0 ? (
selectedValues.map((val) => {
const found = options.find((opt) => opt[valueKey] === val);
return (
<span
key={val}
className="badge badge-selected-item mx-1 mb-1"
>
{found ? found[labelKey] : ""}
</span>
);
})
) : (
<span className="placeholder-text">{placeholder}</span>
)}
</div>
</span>
<i className="bx bx-chevron-down"></i>
</div>
{isOpen && (
<div className="multi-select-dropdown-options">
<div className="multi-select-dropdown-search">
<input
type="text"
placeholder="Search..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
className="multi-select-dropdown-search-input"
/>
</div>
{filteredOptions.map((item) => {
const labelVal = item[labelKey];
const valueVal = item[valueKey];
const isChecked = selectedValues.includes(valueVal);
return (
<div
key={valueVal}
className={`multi-select-dropdown-option ${
isChecked ? "selected" : ""
}`}
>
<input
type="checkbox"
className="custom-checkbox form-check-input"
checked={isChecked}
onChange={() => handleCheckboxChange(valueVal)}
/>
<label className="text-secondary">{labelVal}</label>
</div>
);
})}
{!IsLoading && filteredOptions.length === 0 && (
<div className="multi-select-dropdown-Not-found">
<label className="text-muted">
Not Found {`'${searchText}'`}
</label>
</div>
)}
{IsLoading && filteredOptions.length === 0 && (
<div className="multi-select-dropdown-Not-found">
<label className="text-muted">Loading...</label>
</div>
)}
</div>
)}
</div>
);
};
export default SelectMultiple;

View File

@ -1,32 +1,106 @@
import React, { useState, useEffect } from "react";
import { useFormContext } from "react-hook-form";
import { useFormContext, useWatch } from "react-hook-form";
import React, { useEffect, useState } from "react";
const TagInput = ({
label = "Tags",
name = "tags",
placeholder = "Enter ... ",
placeholder = "Start typing to add... like employee, manager",
color = "#e9ecef",
options = [],
}) => {
const [tags, setTags] = useState([]);
const [input, setInput] = useState("");
const { setValue } = useFormContext();
const [suggestions, setSuggestions] = useState([]);
const { setValue, trigger, control } = useFormContext();
const watchedTags = useWatch({ control, name });
useEffect(() => {
if (
Array.isArray(watchedTags) &&
JSON.stringify(tags) !== JSON.stringify(watchedTags)
) {
setTags(watchedTags);
}
}, [JSON.stringify(watchedTags)]);
useEffect(() => {
setValue(name, tags); // sync to form when tags change
}, [tags, name, setValue]);
if (input.trim() === "") {
setSuggestions([]);
} else {
const filtered = options?.filter(
(opt) =>
opt?.name?.toLowerCase()?.includes(input.toLowerCase()) &&
!tags?.some((tag) => tag.name === opt.name)
);
setSuggestions(filtered);
}
}, [input, options, tags]);
const addTag = (e) => {
e.preventDefault();
const trimmed = input.trim();
if (trimmed !== "" && !tags.includes(trimmed)) {
setTags([...tags, trimmed]);
const addTag = async (tagObj) => {
if (!tags.some((tag) => tag.name === tagObj.name)) {
const cleanedTag = {
id: tagObj.id ?? null,
name: tagObj.name,
};
const newTags = [...tags, cleanedTag];
setTags(newTags);
setValue(name, newTags, { shouldValidate: true });
await trigger(name);
setInput("");
setSuggestions([]);
}
};
const removeTag = (removeIndex) => {
const updated = tags.filter((_, i) => i !== removeIndex);
setTags(updated);
const removeTag = (indexToRemove) => {
const newTags = tags.filter((_, i) => i !== indexToRemove);
setTags(newTags);
setValue(name, newTags, { shouldValidate: true });
trigger(name);
};
const handleInputKeyDown = (e) => {
if ((e.key === "Enter" || e.key === " ")&& input.trim() !== "") {
e.preventDefault();
const existing = options.find(
(opt) => opt.name.toLowerCase() === input.trim().toLowerCase()
);
const newTag = existing
? existing
: {
id: null,
name: input.trim(),
description: input.trim(),
};
addTag(newTag);
} else if (e.key === "Backspace" && input === "") {
setTags((prev) => prev.slice(0, -1));
}
};
const handleInputKey = (e) => {
const key = e.key?.toLowerCase();
if ((key === "enter" || key === " " || e.code === "Space") && input.trim() !== "") {
e.preventDefault();
const existing = options.find(
(opt) => opt.name.toLowerCase() === input.trim().toLowerCase()
);
const newTag = existing
? existing
: {
id: null,
name: input.trim(),
description: input.trim(),
};
addTag(newTag);
} else if ((key === "backspace" || e.code === "Backspace") && input === "") {
setTags((prev) => prev.slice(0, -1));
}
};
const handleSuggestionClick = (suggestion) => {
addTag(suggestion);
};
const backgroundColor = color || "#f8f9fa";
@ -34,35 +108,77 @@ const TagInput = ({
return (
<>
<label htmlFor={name} className="form-label">{label}</label>
<div className="form-control form-control-sm d-flex justify-content-start flex-wrap gap-1" style={{ minHeight: "12px" }}>
{tags.map((tag, i) => (
<span
key={i}
className="d-flex align-items-center"
<label htmlFor={name} className="form-label">
{label}
</label>
<div
className="form-control form-control-sm p-1"
style={{ minHeight: "38px", position: "relative" }}
>
<div className="d-flex flex-wrap align-items-center gap-1">
{tags.map((tag, index) => (
<span
key={index}
className="d-flex align-items-center"
style={{
color: iconColor,
backgroundColor,
padding: "2px 6px",
borderRadius: "2px",
fontSize: "0.85rem",
}}
>
{tag.name}
<i
className="bx bx-x bx-xs ms-1"
onClick={() => removeTag(index)}
style={{ cursor: "pointer" }}
/>
</span>
))}
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleInputKeyDown}
onKeyUp={handleInputKey}
placeholder={placeholder}
style={{
color: iconColor,
backgroundColor,
padding: "2px 3px",
borderRadius: "2px"
border: "none",
outline: "none",
flex: 1,
minWidth: "120px",
}}
/>
</div>
{suggestions.length > 0 && (
<ul
className="list-group position-absolute mt-1 bg-white w-50 shadow-sm "
style={{
zIndex: 1000,
maxHeight: "150px",
overflowY: "auto",
boxShadow:"0px 4px 10px rgba(0, 0, 0, 0.2)",borderRadius:"3px",border:"1px solid #ddd"
}}
>
{tag}
<i className="bx bx-x bx-xs ms-1" onClick={() => removeTag(i)}></i>
</span>
))}
<input
type="text"
className="border-0 flex-grow-1"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => (e.key === "Enter" ? addTag(e) : null)}
placeholder={placeholder}
style={{ outline: "none", minWidth: "120px" }}
/>
{suggestions.map((sugg, i) => (
<li
key={i}
className="dropdown-item p-1 hoverBox"
onClick={() => handleSuggestionClick(sugg)}
style={{cursor: "pointer", fontSize: "0.875rem"}}
>
{sugg.name}
</li>
))}
</ul>
)}
</div>
</>
);
};
export default TagInput;

View File

@ -0,0 +1,132 @@
.editor-wrapper {
max-width: 100%;
margin: 1px auto;
background: #fff;
overflow: hidden;
}
.ql-container {
border: 1px solid #ccc;
border-bottom: none;
min-height: 80px;
}
.custom-toolbar {
/* text-align: left; */
background-color: transparent;
border: 1px solid #ccc;
border-top: none;
}
/* Target the dropdown in the toolbar */
.ql-toolbar .ql-picker.ql-header {
position: relative;
}
/* Open the dropdown upwards */
.ql-toolbar .ql-picker.ql-header .ql-picker-options {
bottom: 100%; /* Move it above the picker */
top: auto; /* Cancel default dropdown positioning */
margin-bottom: 5px; /* Optional spacing */
}
.ql-toolbar .ql-picker.ql-header {
font-family: Arial, sans-serif;
font-size: 10px;
}
.ql-toolbar .ql-picker-label {
background-color: #eee;
/* padding: 6px 10px; */
border-radius: 4px;
cursor: pointer;
color: #333;
}
.ql-toolbar .ql-picker-options {
background-color: white;
border: 1px solid #ccc;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
border-radius: 4px;
overflow: hidden;
}
.ql-toolbar .ql-picker-options span {
padding: 2px 1px;
display: block;
cursor: pointer;
}
.ql-toolbar .ql-picker-options span:hover {
background-color: #f0f0f0;
}
.ql-toolbar .ql-picker-item{
padding: 0px;
}
.ql-snow.ql-toolbar button, .ql-snow .ql-toolbar button {
background: none;
border: none;
cursor: pointer;
display: inline-block;
float: left;
font-size: 15px;
padding: 2px 2px;
width: 28px;
}
.ql-toolbar.ql-snow{
padding: 4px;
}
@media (max-width: 768px) {
.ql-toolbar.ql-snow .ql-formats {
margin-right: 1px;
align-items: center;
}
}
.ql-snow.ql-toolbar button,
.ql-snow .ql-toolbar button {
background: none;
border: none;
cursor: pointer;
display: inline-block;
height: 18px;
padding: 2px 2px;
width: 22px;
font-size: 14px;
/* REMOVE THIS to fix side-alignment */
/* float: left; */
vertical-align: middle;
}
.ql-snow .ql-picker-label {
font-size: 10px; /* Smaller text */
padding: 0 6px; /* Horizontal padding */
height: 20px; /* Height of the label */
line-height: 20px; /* Match height to vertically center single-line text */
background-color: #eee;
border-radius: 0px;
cursor: pointer;
color: #333;
text-align: center;
width: 100%;
display: flex; /* Enable flexbox */
align-items: center; /* Vertical centering */
justify-content: center; /* Horizontal centering */
}
/* Remove custom upward-opening styles */
.ql-toolbar .ql-picker.ql-header .ql-picker-options {
top: 100%; /* Position it below the label */
bottom: auto; /* Cancel the upward positioning */
margin-top: 5px; /* Optional spacing */
}
.ql-editor {
padding: 4px 15px;
}

View File

@ -0,0 +1,96 @@
import React, { useRef } from "react";
import ReactQuill from "react-quill";
import "quill/dist/quill.snow.css";
import "./Editor.css";
const Editor = ({
value,
loading,
onChange,
onCancel,
onSubmit,
placeholder = "Start writing...",
}) => {
const modules = {
toolbar: {
container: "#custom-toolbar",
},
};
const formats = [
"header",
"bold",
"italic",
"underline",
"strike",
"list",
"bullet",
"blockquote",
"code-block",
"link",
"align",
"image",
];
return (
<div className="editor-wrapper m-5">
<div id="custom-toolbar" className="ql-toolbar ql-snow custom-toolbar">
<div className="d-flex justify-content-between align-items-center w-100">
{/* Left: Quill Format Buttons */}
<span className="d-flex">
<span className="ql-formats">
<select className="ql-header" defaultValue="">
<option value="1" />
<option value="2" />
<option value="" />
</select>
<button className="ql-bold" />
<button className="ql-italic" />
<button className="ql-underline" />
<button className="ql-strike" />
</span>
<span className="ql-formats">
<button className="ql-list" value="ordered" />
<button className="ql-list" value="bullet" />
{/* <button className="ql-image" value="file" /> */}
</span>
<span className="ql-formats">
<button className="ql-link" />
</span>
</span>
</div>
</div>
<ReactQuill
value={value}
onChange={onChange}
modules={modules}
formats={formats}
theme="snow"
placeholder={placeholder}
/>
{/* Right: Submit + Cancel Buttons */}
<div className="d-flex justify-content-end gap-2 p-1">
<span
className="btn btn-xs btn-secondary"
aria-disabled={loading}
onClick={onCancel}
>
Cancel
</span>
<span
type="submit"
className="btn btn-xs btn-primary"
onClick={onSubmit}
aria-disabled={loading}
>
{loading ? "Please Wait..." : "Submit"}
</span>
</div>
</div>
);
};
export default Editor;

View File

@ -0,0 +1,113 @@
import React, { useEffect,useState } from 'react'
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { MasterRespository } from '../../repositories/MastersRepository';
import { clearApiCacheKey } from '../../slices/apiCacheSlice';
import { getCachedData,cacheData } from '../../slices/apiDataManager';
import showToast from '../../services/toastService';
const schema = z.object({
name: z.string().min(1, { message: "Category name is required" }),
description: z.string().min(1, { message: "Description is required" })
.max(255, { message: "Description cannot exceed 255 characters" }),
});
const CreateContactCategory = ({onClose}) => {
const[isLoading,setIsLoading] = useState(false)
const {
register,
handleSubmit,
formState: { errors },reset
} = useForm({
resolver: zodResolver(schema),
defaultValues: {
name: "",
description: "",
},
});
const onSubmit = (data) => {
setIsLoading(true)
MasterRespository.createContactCategory(data).then((resp)=>{
setIsLoading(false)
resetForm()
const cachedData = getCachedData("Contact Category");
const updatedData = [...cachedData, resp?.data];
cacheData("Contact Category", updatedData);
showToast("Contact Category Added successfully.", "success");
onClose()
}).catch((error)=>{
showToast(error?.response?.data?.message, "error");
setIsLoading(false)
})
};
const resetForm = () => {
reset({
name: "",
description: ""
});
setDescriptionLength(0);
}
useEffect(()=>{
return ()=>resetForm()
},[])
const [descriptionLength, setDescriptionLength] = useState(0);
const maxDescriptionLength = 255;
return (<>
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 col-md-12">
<label className="form-label">Category Name</label>
<input type="text"
{...register("name")}
className={`form-control ${errors.name ? 'is-invalids' : ''}`}
/>
{errors.name && <p className="text-danger">{errors.name.message}</p>}
</div>
<div className="col-12 col-md-12">
<label className="form-label" htmlFor="description">Description</label>
<textarea
rows="3"
{...register("description")}
className={`form-control ${errors.description ? 'is-invalids' : ''}`}
onChange={(e) => {
setDescriptionLength(e.target.value.length);
register("description").onChange(e);
}}
></textarea>
<div className="text-end small text-muted">
{maxDescriptionLength - descriptionLength} characters left
</div>
{errors.description && (
<p className="text-danger">{errors.description.message}</p>
)}
</div>
<div className="col-12 text-center">
<button type="submit" className="btn btn-sm btn-primary me-3">
{isLoading? "Please Wait...":"Submit"}
</button>
<button
type="reset"
className="btn btn-sm btn-label-secondary "
data-bs-dismiss="modal"
aria-label="Close"
>
Cancel
</button>
</div>
</form>
</>
)
}
export default CreateContactCategory;

View File

@ -0,0 +1,114 @@
import React, { useEffect,useState } from 'react'
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { MasterRespository } from '../../repositories/MastersRepository';
import { clearApiCacheKey } from '../../slices/apiCacheSlice';
import { getCachedData,cacheData } from '../../slices/apiDataManager';
import showToast from '../../services/toastService';
const schema = z.object({
name: z.string().min(1, { message: "Tag name is required" }),
description: z.string().min(1, { message: "Description is required" })
.max(255, { message: "Description cannot exceed 255 characters" }),
});
const CreateContactTag = ({onClose}) => {
const[isLoading,setIsLoading] = useState(false)
const {
register,
handleSubmit,
formState: { errors },reset
} = useForm({
resolver: zodResolver(schema),
defaultValues: {
name: "",
description: "",
},
});
const onSubmit = (data) => {
setIsLoading(true)
MasterRespository.createContactTag(data).then((resp)=>{
setIsLoading(false)
resetForm()
debugger
const cachedData = getCachedData("Contact Tag");
const updatedData = [...cachedData, resp?.data];
cacheData("Contact Tag", updatedData);
showToast("Contact Tag Added successfully.", "success");
console.log(getCachedData("Contact Tag"))
onClose()
}).catch((error)=>{
showToast(error?.response?.data?.message, "error");
setIsLoading(false)
})
};
const resetForm = () => {
reset({
name: "",
description: ""
});
setDescriptionLength(0);
}
useEffect(()=>{
return ()=>resetForm()
},[])
const [descriptionLength, setDescriptionLength] = useState(0);
const maxDescriptionLength = 255;
return (<>
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 col-md-12">
<label className="form-label">Tag Name</label>
<input type="text"
{...register("name")}
className={`form-control ${errors.name ? 'is-invalids' : ''}`}
/>
{errors.name && <p className="text-danger">{errors.name.message}</p>}
</div>
<div className="col-12 col-md-12">
<label className="form-label" htmlFor="description">Description</label>
<textarea
rows="3"
{...register("description")}
className={`form-control ${errors.description ? 'is-invalids' : ''}`}
onChange={(e) => {
setDescriptionLength(e.target.value.length);
register("description").onChange(e);
}}
></textarea>
<div className="text-end small text-muted">
{maxDescriptionLength - descriptionLength} characters left
</div>
{errors.description && (
<p className="text-danger">{errors.description.message}</p>
)}
</div>
<div className="col-12 text-center">
<button type="submit" className="btn btn-sm btn-primary me-3">
{isLoading? "Please Wait...":"Submit"}
</button>
<button
type="reset"
className="btn btn-sm btn-label-secondary "
data-bs-dismiss="modal"
aria-label="Close"
>
Cancel
</button>
</div>
</form>
</>
)
}
export default CreateContactTag;

View File

@ -0,0 +1,126 @@
import React, { useEffect,useState } from 'react'
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { MasterRespository } from '../../repositories/MastersRepository';
import { clearApiCacheKey } from '../../slices/apiCacheSlice';
import { getCachedData,cacheData } from '../../slices/apiDataManager';
import showToast from '../../services/toastService';
const schema = z.object({
name: z.string().min(1, { message: "Category name is required" }),
description: z.string().min(1, { message: "Description is required" })
.max(255, { message: "Description cannot exceed 255 characters" }),
});
const EditContactCategory= ({data,onClose}) => {
const[isLoading,setIsLoading] = useState(false)
const {
register,
handleSubmit,
formState: { errors },reset
} = useForm({
resolver: zodResolver(schema),
defaultValues: {
name: data?.name || "",
description:data?.description || "",
},
});
const onSubmit = (formdata) => {
setIsLoading(true)
const result = {
id:data?.id,
name: formdata?.name,
description: formdata.description,
};
MasterRespository.updateContactCategory(data?.id,result).then((resp)=>{
setIsLoading(false)
showToast("Contact Category Updated successfully.", "success");
const cachedData = getCachedData("Contact Category");
if (cachedData) {
const updatedData = cachedData.map((category) =>
category.id === data?.id ? { ...category, ...resp.data } : category
);
cacheData("Contact Category", updatedData);
}
onClose()
}).catch((error)=>{
showToast(error?.response?.data?.message, "error")
setIsLoading(false)
})
};
const resetForm = () => {
reset({
name: "",
description: ""
});
setDescriptionLength(0);
}
useEffect(()=>{
return ()=>resetForm()
},[])
const [descriptionLength, setDescriptionLength] = useState(0);
const maxDescriptionLength = 255;
return (<>
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 col-md-12">
<label className="form-label">Category Name</label>
<input type="text"
{...register("name")}
className={`form-control ${errors.name ? 'is-invalids' : ''}`}
/>
{errors.name && <p className="text-danger">{errors.name.message}</p>}
</div>
<div className="col-12 col-md-12">
<label className="form-label" htmlFor="description">Description</label>
<textarea
rows="3"
{...register("description")}
className={`form-control ${errors.description ? 'is-invalids' : ''}`}
onChange={(e) => {
setDescriptionLength(e.target.value.length);
register("description").onChange(e);
}}
></textarea>
<div className="text-end small text-muted">
{maxDescriptionLength - descriptionLength} characters left
</div>
{errors.description && (
<p className="text-danger">{errors.description.message}</p>
)}
</div>
<div className="col-12 text-center">
<button type="submit" className="btn btn-sm btn-primary me-3">
{isLoading? "Please Wait...":"Submit"}
</button>
<button
type="reset"
className="btn btn-sm btn-label-secondary "
data-bs-dismiss="modal"
aria-label="Close"
>
Cancel
</button>
</div>
</form>
</>
)
}
export default EditContactCategory;

View File

@ -0,0 +1,126 @@
import React,{useState,useEffect} from 'react'
import {useForm} from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { MasterRespository } from '../../repositories/MastersRepository';
import { clearApiCacheKey } from '../../slices/apiCacheSlice';
import { getCachedData,cacheData } from '../../slices/apiDataManager';
import showToast from '../../services/toastService';
const schema = z.object({
name: z.string().min(1, { message: "Tag name is required" }),
description: z.string().min(1, { message: "Description is required" })
.max(255, { message: "Description cannot exceed 255 characters" }),
});
const EditContactTag= ({data,onClose}) => {
const[isLoading,setIsLoading] = useState(false)
const {
register,
handleSubmit,
formState: { errors },reset
} = useForm({
resolver: zodResolver(schema),
defaultValues: {
name: data?.name || "",
description:data?.description || "",
},
});
const onSubmit = (formdata) => {
setIsLoading(true)
const result = {
id:data?.id,
name: formdata?.name,
description: formdata.description,
};
MasterRespository.updateContactTag(data?.id,result).then((resp)=>{
setIsLoading(false)
showToast("Contact Tag Updated successfully.", "success");
const cachedData = getCachedData("Contact Tag");
if (cachedData) {
const updatedData = cachedData.map((category) =>
category.id === data?.id ? { ...category, ...resp.data } : category
);
cacheData("Contact Tag", updatedData);
}
onClose()
}).catch((error)=>{
showToast(error?.response?.data?.message, "error")
setIsLoading(false)
})
};
const resetForm = () => {
reset({
name: "",
description: ""
});
setDescriptionLength(0);
}
useEffect(()=>{
return ()=>resetForm()
},[])
const [descriptionLength, setDescriptionLength] = useState(0);
const maxDescriptionLength = 255;
return (<>
<form className="row g-2" onSubmit={handleSubmit(onSubmit)}>
<div className="col-12 col-md-12">
<label className="form-label">Tag Name</label>
<input type="text"
{...register("name")}
className={`form-control ${errors.name ? 'is-invalids' : ''}`}
/>
{errors.name && <p className="text-danger">{errors.name.message}</p>}
</div>
<div className="col-12 col-md-12">
<label className="form-label" htmlFor="description">Description</label>
<textarea
rows="3"
{...register("description")}
className={`form-control ${errors.description ? 'is-invalids' : ''}`}
onChange={(e) => {
setDescriptionLength(e.target.value.length);
register("description").onChange(e);
}}
></textarea>
<div className="text-end small text-muted">
{maxDescriptionLength - descriptionLength} characters left
</div>
{errors.description && (
<p className="text-danger">{errors.description.message}</p>
)}
</div>
<div className="col-12 text-center">
<button type="submit" className="btn btn-sm btn-primary me-3">
{isLoading? "Please Wait...":"Submit"}
</button>
<button
type="reset"
className="btn btn-sm btn-label-secondary "
data-bs-dismiss="modal"
aria-label="Close"
>
Cancel
</button>
</div>
</form>
</>
)
}
export default EditContactTag;

View File

@ -13,6 +13,10 @@ import {cacheData, getCachedData} from "../../slices/apiDataManager";
import showToast from "../../services/toastService";
import CreateWorkCategory from "./CreateWorkCategory";
import EditWorkCategory from "./EditWorkCategory";
import CreateCategory from "./CreateContactCategory";
import CreateContactTag from "./CreateContactTag";
import EditContactCategory from "./EditContactCategory";
import EditContactTag from "./EditContactTag";
const MasterModal = ({ modaldata, closeModal }) => {
@ -21,7 +25,6 @@ const MasterModal = ({ modaldata, closeModal }) => {
const handleSelectedMasterDeleted = async () =>
{
const deleteFn = MasterRespository[modaldata.masterType];
if (!deleteFn) {
showToast(`No delete strategy defined for master type`,"error");
return false;
@ -74,7 +77,6 @@ const MasterModal = ({ modaldata, closeModal }) => {
</div>
);
}
return (
<div
className="modal fade"
@ -93,13 +95,16 @@ const MasterModal = ({ modaldata, closeModal }) => {
>
<div className="modal-content">
<div className="modal-body p-sm-4 p-0">
<button
<div className="d-flex justify-content-between">
<h6>{ `${modaldata?.modalType} `}</h6>
<button
type="button"
className="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
onClick={closeModal}
></button>
</div>
{modaldata.modalType === "Application Role" && (
<CreateRole masmodalType={modaldata.masterType} onClose={closeModal} />
@ -125,6 +130,18 @@ const MasterModal = ({ modaldata, closeModal }) => {
{modaldata.modalType === "Edit-Work Category" && (
<EditWorkCategory data={modaldata.item} onClose={closeModal} />
)}
{modaldata.modalType === "Contact Category" && (
<CreateCategory data={modaldata.item} onClose={closeModal} />
)}
{modaldata.modalType === "Edit-Contact Category" && (
<EditContactCategory data={modaldata.item} onClose={closeModal} />
)}
{modaldata.modalType === "Contact Tag" && (
<CreateContactTag data={modaldata.item} onClose={closeModal} />
)}
{modaldata.modalType === "Edit-Contact Tag" && (
<EditContactTag data={modaldata.item} onClose={closeModal} />
)}
</div>
</div>
</div>

View File

@ -1,5 +1,5 @@
// it important ------
export const mastersList = [ {id: 1, name: "Application Role"}, {id: 2, name: "Job Role"}, {id: 3, name: "Activity"},{id: 4, name:"Work Category"} ]
export const mastersList = [ {id: 1, name: "Application Role"}, {id: 2, name: "Job Role"}, {id: 3, name: "Activity"},{id: 4, name:"Work Category"},{id:5,name:"Contact Category"},{id:6,name:"Contact Tag"}]
// -------------------
export const dailyTask = [

View File

@ -23,11 +23,6 @@
"text": "Employees",
"available": true,
"link": "/employees"
},
{
"text": "Directory",
"available": true,
"link": "/directory"
}
]
},
@ -68,6 +63,12 @@
"link": "/activities/gallary"
}
]
},
{
"text": "Directory",
"icon": "bx bx-group",
"available": true,
"link": "/directory"
},
{
"text": "Administration",

View File

@ -47,10 +47,18 @@ const useMaster = (isMa) => {
response = await MasterRespository.getActivites();
response = response.data
break;
case "Work Category":
case "Work Category":
response = await MasterRespository.getWorkCategory();
response = response.data
break;
case "Contact Category":
response = await MasterRespository.getContactCategory();
response = response.data
break;
case "Contact Tag":
response = await MasterRespository.getContactTag();
response = response.data
break;
case "Status":
response = [{description: null,featurePermission: null,id: "02dd4761-363c-49ed-8851-3d2489a3e98d",status:"status 1"},{description: null,featurePermission: null,id: "03dy9761-363c-49ed-8851-3d2489a3e98d",status:"status 2"},{description: null,featurePermission: null,id: "03dy7761-263c-49ed-8851-3d2489a3e98d",status:"Status 3"}];
break;
@ -149,4 +157,67 @@ export const useActivitiesMaster = () =>
}, [] )
return {categories,categoryLoading,categoryError}
}
}
export const useContactCategory = () =>
{
const [ contactCategory, setContactCategory ] = useState( [] )
const [ loading, setLoading ] = useState( false )
const [ Error, setError ] = useState()
const fetchConatctCategory = async() =>
{
const cache_Category = getCachedData( "Contact Category" );
if ( !cache_Category )
{
try
{
let resp = await MasterRespository.getContactCategory();
setContactCategory( resp.data );
cacheData("Contact Category",resp.data)
} catch ( error )
{
setError(error)
}
} else
{
setContactCategory(cache_Category)
}
}
useEffect( () =>
{
fetchConatctCategory()
}, [] )
return { contactCategory,loading,Error}
}
export const useContactTags = () => {
const [contactTags, setContactTags] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
const fetchContactTag = async () => {
const cache_Tags = getCachedData("Contact Tag");
if (!cache_Tags) {
setLoading(true);
try {
const resp = await MasterRespository.getContactTag();
setContactTags(resp.data);
cacheData("Contact Tag", resp.data);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
} else {
setContactTags(cache_Tags);
}
};
fetchContactTag();
}, []);
return { contactTags, loading, error };
};

179
src/hooks/useDirectory.js Normal file
View File

@ -0,0 +1,179 @@
import { useEffect, useState } from "react";
import { DirectoryRepository } from "../repositories/DirectoryRepository";
import { cacheData, getCachedData } from "../slices/apiDataManager";
export const useDirectory = (isActive,prefernceContacts) => {
const [contacts, setContacts] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const fetch = async (activeParam = isActive) => {
setLoading(true);
try {
const response = await DirectoryRepository.GetContacts(activeParam,prefernceContacts);
setContacts(response.data);
cacheData("contacts", { data: response.data, isActive: activeParam });
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
useEffect(() => {
const cachedContacts = getCachedData("contacts");
if (!cachedContacts?.data || cachedContacts.isActive !== isActive || prefernceContacts) {
fetch(isActive,prefernceContacts);
} else {
setContacts(cachedContacts.data);
}
}, [isActive,prefernceContacts]);
return {
contacts,
loading,
error,
refetch: fetch,
};
};
export const useBuckets = () => {
const [buckets, setBuckets] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const fetchBuckets = async () => {
setLoading(true);
try {
const resp = await DirectoryRepository.GetBucktes();
setBuckets(resp.data);
cacheData("buckets", resp.data);
setLoading(false);
} catch (error) {
const msg =
error?.response?.data?.message ||
error?.message ||
"Something went wrong";
setError( msg );
setLoading(false);
}
};
useEffect(() => {
const cacheBuckets = getCachedData("buckets");
if (!cacheBuckets) {
fetchBuckets();
} else {
setBuckets(cacheBuckets);
}
}, []);
return { buckets, loading, error, refetch: fetchBuckets };
};
export const useContactProfile = (id) => {
const [contactProfile, setContactProfile] = useState(null);
const [loading, setLoading] = useState(false);
const [Error, setError] = useState("");
const fetchContactProfile = async () => {
setLoading(true);
try {
const resp = await DirectoryRepository.GetContactProfile(id);
setContactProfile(resp.data);
cacheData("Contact Profile", { data: resp.data, contactId: id });
} catch (err) {
const msg =
err?.response?.data?.message ||
err?.message ||
"Something went wrong";
setError(msg);
} finally {
setLoading(false);
}
};
useEffect( () =>
{
const cached = getCachedData("Contact Profile");
if (!cached || cached.contactId !== id) {
fetchContactProfile(id);
} else {
setContactProfile(cached.data);
}
}, [id]);
return { contactProfile, loading, Error ,refetch:fetchContactProfile};
};
export const useContactNotes = (id, IsActive) => {
const [contactNotes, setContactNotes] = useState([]);
const [loading, setLoading] = useState(false);
const [Error, setError] = useState("");
const fetchContactNotes = async (id,IsActive) => {
setLoading(true);
try {
const resp = await DirectoryRepository.GetNote(id, IsActive);
setContactNotes(resp.data);
cacheData("Contact Notes", { data: resp.data, contactId: id });
} catch (err) {
const msg =
err?.response?.data?.message ||
err?.message ||
"Something went wrong";
setError(msg);
} finally {
setLoading(false);
}
};
useEffect(() => {
const cached = getCachedData("Contact Notes");
if (!cached || cached.contactId !== id) {
id && fetchContactNotes(id,IsActive);
} else {
setContactNotes(cached.data);
}
}, [id,IsActive]);
return { contactNotes, loading, Error,refetch:fetchContactNotes };
};
export const useOrganization = () => {
const [organizationList, setOrganizationList] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const fetchOrg = async () => {
const cacheOrg = getCachedData("organizations");
if (cacheOrg?.length != 0) {
setLoading(true);
try {
const resp = await DirectoryRepository.GetOrganizations();
cacheData("organizations", resp.data);
setOrganizationList(resp.data);
setLoading(false);
} catch (error) {
const msg =
error?.response?.data?.message ||
error?.message ||
"Something went wrong";
setError(msg);
}
} else {
setOrganizationList(cacheOrg);
}
};
useEffect(() => {
fetchOrg();
}, []);
return { organizationList, loading, error };
};

View File

@ -4,6 +4,7 @@ import ProjectRepository from "../repositories/ProjectRepository";
import { useProfile } from "./useProfile";
import { useDispatch, useSelector } from "react-redux";
import { setProjectId } from "../slices/localVariablesSlice";
import EmployeeList from "../components/Directory/EmployeeList";
export const useProjects = () => {
@ -130,3 +131,48 @@ export const useProjectDetails = (projectId) => {
return { projects_Details, loading, error, refetch: fetchData }
}
export const useProjectsByEmployee = ( employeeId ) =>
{
const [projectList, setProjectList] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const fetchProjects = async (id) => {
try {
setLoading(true);
setError(''); // clear previous error
const res = await ProjectRepository.getProjectsByEmployee(id);
setProjectList(res.data);
cacheData( 'ProjectsByEmployee', {data: res.data, employeeId: id} );
setLoading(false)
} catch (err) {
setError( err?.message || 'Failed to fetch projects' );
setLoading(false)
}
};
useEffect(() => {
if (!employeeId) return;
const cache_project = getCachedData('ProjectsByEmployee');
if (
!cache_project?.data ||
cache_project?.employeeId !== employeeId
) {
fetchProjects(employeeId);
} else {
setProjectList(cache_project.data);
}
}, [employeeId]);
return {
projectList,
loading,
error,
refetch : fetchProjects
}
};

View File

@ -0,0 +1,35 @@
import { useState, useMemo } from 'react';
export const useSortableData = (items, config = null) => {
const [sortConfig, setSortConfig] = useState(config);
const sortedItems = useMemo(() => {
let sortableItems = [...items];
if (sortConfig !== null) {
sortableItems.sort((a, b) => {
const aValue = sortConfig.key(a).toLowerCase();
const bValue = sortConfig.key(b).toLowerCase();
if (aValue < bValue) return sortConfig.direction === 'asc' ? -1 : 1;
if (aValue > bValue) return sortConfig.direction === 'asc' ? 1 : -1;
return 0;
});
}
return sortableItems;
}, [items, sortConfig]);
const requestSort = (keyFn) => {
let direction = 'asc';
if (
sortConfig &&
sortConfig.key.toString() === keyFn.toString() &&
sortConfig.direction === 'asc'
) {
direction = 'desc';
}
setSortConfig({ key: keyFn, direction });
};
return { items: sortedItems, requestSort, sortConfig };
};

View File

@ -164,4 +164,7 @@ padding: 1px !important;
.accordion-button:not(.collapsed) .toggle-icon {
content: "\f146"; /* minus-circle */
}
.hoverBox:hover{
background-color: #f1f3f5;
}

View File

@ -4,25 +4,30 @@ import Header from "../components/Layout/Header";
import Sidebar from "../components/Layout/Sidebar";
import Footer from "../components/Layout/Footer";
import FloatingMenu from "../components/common/FloatingMenu";
import { FabProvider } from "../Context/FabContext";
const HomeLayout = () => {
useEffect(() => {
Main();
}, []);
return (
<div className="layout-wrapper layout-content-navbar" >
<div className="layout-container" >
<Sidebar />
<div className="layout-page ">
<Header />
<div className="content-wrapper" >
<Outlet />
<Footer />
<FabProvider>
<div className="layout-wrapper layout-content-navbar">
<div className="layout-container">
<Sidebar />
<div className="layout-page ">
<Header />
<div className="content-wrapper">
<Outlet />
<Footer />
</div>
</div>
<FloatingMenu />
<div className="layout-overlay layout-menu-toggle"></div>
</div>
<div className="layout-overlay layout-menu-toggle"></div>
</div>
</div>
</FabProvider>
);
};

View File

@ -141,7 +141,7 @@ const AttendancePage = () => {
]}
></Breadcrumb>
<div className="nav-align-top nav-tabs-shadow">
<ul className="nav nav-tabs" role="tablist">
<ul className="nav nav-tabs align-items-center" role="tablist">
<div
className="dataTables_length text-start py-2 px-2 d-flex "
id="DataTables_Table_0_length"
@ -175,6 +175,25 @@ const AttendancePage = () => {
</label>
)}
</div>
<li
className={`nav-item ms-auto ${
activeTab === "regularization" ? "d-none" : ""
}`}
>
<label className="switch switch-primary">
<input
type="checkbox"
className="switch-input"
checked={showOnlyCheckout}
onChange={handleToggle}
/>
<span className="switch-toggle-slider">
<span className="switch-on"></span>
<span className="switch-off"></span>
</span>
<span className="switch-label m-2">Pending Actions</span>
</label>
</li>
</ul>
<ul className="nav nav-tabs" role="tablist">
@ -214,27 +233,9 @@ const AttendancePage = () => {
</button>
</li>
<li
className={`nav-item ms-auto ${
activeTab === "regularization" ? "d-none" : ""
}`}
>
<label className="switch switch-primary">
<input
type="checkbox"
className="switch-input"
checked={showOnlyCheckout}
onChange={handleToggle}
/>
<span className="switch-toggle-slider">
<span className="switch-on"></span>
<span className="switch-off"></span>
</span>
<span className="switch-label m-2">Pending Actions</span>
</label>
</li>
</ul>
<div className="tab-content attedanceTabs py-2">
<div className="tab-content attedanceTabs py-2 px-1 px-sm-3">
{projectLoading && <span>Loading..</span>}
{!projectLoading && !attendances && <span>Not Found</span>}

View File

@ -1,137 +1,478 @@
import React, { useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import Breadcrumb from "../../components/common/Breadcrumb";
import IconButton from "../../components/common/IconButton";
import GlobalModel from "../../components/common/GlobalModel";
import ManageDirectory from "../../components/Directory/ManageDirectory";
import ListViewDirectory from "../../components/Directory/ListViewDirectory";
import { useBuckets, useDirectory } from "../../hooks/useDirectory";
import { DirectoryRepository } from "../../repositories/DirectoryRepository";
import { cacheData, getCachedData } from "../../slices/apiDataManager";
import showToast from "../../services/toastService";
import UpdateContact from "../../components/Directory/UpdateContact";
import CardViewDirectory from "../../components/Directory/CardViewDirectory";
import { useContactCategory } from "../../hooks/masterHook/useMaster";
import usePagination from "../../hooks/usePagination";
import { ITEMS_PER_PAGE } from "../../utils/constants";
import ProfileContactDirectory from "../../components/Directory/ProfileContactDirectory";
import ConfirmModal from "../../components/common/ConfirmModal";
import DirectoryListTableHeader from "./DirectoryListTableHeader";
import DirectoryPageHeader from "./DirectoryPageHeader";
import ManageBucket from "../../components/Directory/ManageBucket";
import { useFab } from "../../Context/FabContext";
import { DireProvider, useDir } from "../../Context/DireContext";
const Directory = () => {
const Directory = ({ IsPage = true, prefernceContacts }) => {
const [projectPrefernce, setPerfence] = useState(null);
const [IsActive, setIsActive] = useState(true);
const [isOpenModal, setIsOpenModal] = useState(false);
const closedModel = () => setIsOpenModal(false);
const [isOpenModalNote, setIsOpenModalNote] = useState(false);
const [selectedContact, setSelectedContact] = useState(null);
const [open_contact, setOpen_contact] = useState(null);
const [ContactList, setContactList] = useState([]);
const [contactCategories, setContactCategories] = useState([]);
const [searchText, setSearchText] = useState("");
const [listView, setListView] = useState(false);
const [selectedBucketIds, setSelectedBucketIds] = useState([]);
const [deleteContact, setDeleteContact] = useState(null);
const [IsDeleting, setDeleting] = useState(false);
const [openBucketModal, setOpenBucketModal] = useState(false);
const [tempSelectedBucketIds, setTempSelectedBucketIds] = useState([]);
const [tempSelectedCategoryIds, setTempSelectedCategoryIds] = useState([]);
const { setActions } = useFab();
const { dirActions, setDirActions } = useDir();
const { contacts, loading, refetch } = useDirectory(
IsActive,
projectPrefernce
);
const { contactCategory, loading: contactCategoryLoading } =
useContactCategory();
const { buckets, refetch: refetchBucket } = useBuckets();
const submitContact = async (data) => {
try {
let response;
let updatedContacts;
const contacts_cache = getCachedData("contacts")?.data || [];
if (selectedContact) {
response = await DirectoryRepository.UpdateContact(data.id, data);
updatedContacts = contacts_cache.map((contact) =>
contact.id === data.id ? response.data : contact
);
showToast("Contact updated successfully", "success");
setIsOpenModal(false);
setSelectedContact(null);
} else {
response = await DirectoryRepository.CreateContact(data);
updatedContacts = [...contacts_cache, response.data];
showToast("Contact created successfully", "success");
setIsOpenModal(false);
}
// cacheData("Contacts", {data:updatedContacts,isActive:IsActive});
// setContactList(updatedContacts);
refetch(IsActive, prefernceContacts);
refetchBucket();
} catch (error) {
const msg =
error.response?.data?.message ||
error.message ||
"Error occurred during API call!";
showToast(msg, "error");
}
};
const handleDeleteContact = async (overrideId = null) => {
try {
if (!IsActive) {
setDirActions((prev) => ({ ...prev, action: true }));
} else {
setDeleting(true);
}
const id = overrideId || (!IsActive ? dirActions.id : deleteContact);
if (!id) {
showToast("No contact selected for deletion", "error");
return;
}
await DirectoryRepository.DeleteContact(id, !IsActive);
const updatedContacts = ContactList.filter((c) => c.id !== id);
setContactList(updatedContacts);
cacheData("Contacts", { data: updatedContacts, isActive: IsActive });
showToast(
`Contact ${IsActive ? "Deleted" : "Restored"} successfully`,
"success"
);
setDeleteContact(null);
refetchBucket();
setDirActions({ action: false, id: null });
setDeleting(false);
} catch (error) {
const msg =
error?.response?.data?.message ||
error.message ||
"Error occurred during API call";
showToast(msg, "error");
setDeleting(false);
setDirActions({ action: false, id: null });
}
};
const closedModel = () => {
setIsOpenModal(false);
setSelectedContact(null);
setOpen_contact(null);
};
const [selectedCategoryIds, setSelectedCategoryIds] = useState(
contactCategory.map((category) => category.id)
);
useEffect(() => {
setContactList(contacts);
setTempSelectedCategoryIds([]);
setTempSelectedBucketIds([]);
}, [contacts]);
const usedCategoryIds = [
...new Set(contacts.map((c) => c.contactCategory?.id)),
];
const filteredCategories = contactCategory.filter((category) =>
usedCategoryIds.includes(category.id)
);
const handleTempBucketChange = (id) => {
setTempSelectedBucketIds((prev) =>
prev.includes(id) ? prev.filter((bid) => bid !== id) : [...prev, id]
);
};
const handleTempCategoryChange = (id) => {
setTempSelectedCategoryIds((prev) =>
prev.includes(id) ? prev.filter((cid) => cid !== id) : [...prev, id]
);
};
const usedBucketIds = [
...new Set(contacts.flatMap((c) => c.bucketIds || [])),
];
const filteredBuckets = buckets.filter((bucket) =>
usedBucketIds.includes(bucket.id)
);
const filteredContacts = useMemo(() => {
return ContactList.filter((c) => {
const matchesSearch =
c.name.toLowerCase().includes(searchText.toLowerCase()) ||
c.organization.toLowerCase().includes(searchText.toLowerCase());
const matchesCategory =
selectedCategoryIds.length === 0 ||
selectedCategoryIds.includes(c.contactCategory?.id);
const matchesBucket =
selectedBucketIds.length === 0 ||
(c.bucketIds || []).some((id) => selectedBucketIds.includes(id));
return matchesSearch && matchesCategory && matchesBucket;
}).sort((a, b) => a.name.localeCompare(b.name));
}, [
ContactList,
searchText,
selectedCategoryIds,
selectedBucketIds,
selectedContact,
]);
const applyFilter = () => {
setSelectedBucketIds(tempSelectedBucketIds);
setSelectedCategoryIds(tempSelectedCategoryIds);
};
const clearFilter = () => {
setTempSelectedBucketIds([]);
setTempSelectedCategoryIds([]);
setSelectedBucketIds([]);
setSelectedCategoryIds([]);
};
const { currentPage, totalPages, currentItems, paginate } = usePagination(
filteredContacts,
ITEMS_PER_PAGE
);
const renderModalContent = () => {
if (selectedContact) {
return (
<UpdateContact
existingContact={selectedContact}
submitContact={submitContact}
onCLosed={closedModel}
/>
);
}
if (!open_contact) {
return (
<ManageDirectory submitContact={submitContact} onCLosed={closedModel} />
);
}
};
useEffect(() => {
const actions = [];
if (IsPage) {
actions.push({
label: "Manage Bucket",
icon: "fa-solid fa-bucket fs-5",
color: "primary",
onClick: () => setOpenBucketModal(true),
});
}
if (buckets?.length > 0) {
actions.push({
label: "New Contact",
icon: "bx bx-plus-circle",
color: "warning",
onClick: () => setIsOpenModal(true),
});
}
setActions(actions);
return () => setActions([]);
}, [IsPage, buckets]);
useEffect(() => {
setPerfence(prefernceContacts);
}, [prefernceContacts]);
return (
<div className="container-xxl flex-grow-1 container-p-y">
<Breadcrumb
data={[
{ label: "Home", link: "/dashboard" },
{ label: "Directory (Comming Soon)", link: null },
]}
></Breadcrumb>
{IsPage && (
<Breadcrumb
data={[
{ label: "Home", link: "/dashboard" },
{ label: "Directory", link: null },
]}
></Breadcrumb>
)}
<GlobalModel isOpen={isOpenModal} closeModal={closedModel}>
<ManageDirectory />
</GlobalModel>
<div className="row">
<div className="row mx-0 px-0">
<div className="col-md-4 col-6 flex-grow-1 mb-2 px-1">
<input
type="search"
className="form-control form-control-sm"
placeholder="Search projects..."
{isOpenModal && (
<GlobalModel
isOpen={isOpenModal}
closeModal={() => {
setSelectedContact(null);
setIsOpenModal(false);
}}
size="xl"
>
{renderModalContent()}
</GlobalModel>
)}
{isOpenModalNote && (
<GlobalModel
isOpen={isOpenModalNote}
closeModal={() => {
setOpen_contact(null);
setIsOpenModalNote(false);
}}
size="xl"
>
{open_contact && (
<ProfileContactDirectory
contact={open_contact}
setOpen_contact={setOpen_contact}
closeModal={() => setIsOpenModalNote(false)}
/>
</div>
<div className="col-md-8 col-6 text-end flex-grow-1 mb-2 px-1">
<button
type="button"
className={`btn btn-sm btn-primary `}
onClick={() => setIsOpenModal(true)}
>
<i className="bx bx-plus-circle me-2"></i>
New Contact
</button>
</div>
)}
</GlobalModel>
)}
{deleteContact && (
<div
className={`modal fade ${deleteContact ? "show" : ""}`}
tabIndex="-1"
role="dialog"
style={{
display: deleteContact ? "block" : "none",
backgroundColor: deleteContact ? "rgba(0,0,0,0.5)" : "transparent",
}}
aria-hidden="false"
>
<ConfirmModal
type={"delete"}
header={"Delete Contact"}
message={"Are you sure you want delete?"}
onSubmit={handleDeleteContact}
onClose={() => setDeleteContact(null)}
loading={IsDeleting}
/>
</div>
<div className="table-responsive text-nowrap py-2 ">
<table className="table px-2">
<thead>
)}
{openBucketModal && (
<GlobalModel
isOpen={openBucketModal}
closeModal={() => setOpenBucketModal(false)}
size="lg"
>
<ManageBucket buckets={buckets} />
</GlobalModel>
)}
<div className="card p-2 card-minHeight">
<DirectoryPageHeader
searchText={searchText}
setSearchText={setSearchText}
setIsActive={setIsActive}
listView={listView}
setListView={setListView}
filteredBuckets={filteredBuckets}
tempSelectedBucketIds={tempSelectedBucketIds}
handleTempBucketChange={handleTempBucketChange}
filteredCategories={filteredCategories}
tempSelectedCategoryIds={tempSelectedCategoryIds}
handleTempCategoryChange={handleTempCategoryChange}
clearFilter={clearFilter}
applyFilter={applyFilter}
loading={loading}
IsActive={IsActive}
setOpenBucketModal={setOpenBucketModal}
/>
{/* Messages when listView is false */}
{!listView && (
<div className="d-flex flex-column justify-content-center align-items-center text-center ">
{loading && <p className="mt-10">Loading...</p>}
{!loading && contacts?.length === 0 && (
<p className="mt-10">No contact found</p>
)}
{!loading && contacts?.length > 0 && currentItems.length === 0 && (
<p className="mt-10">No matching contact found</p>
)}
</div>
)}
{/* Table view (listView === true) */}
{listView ? (
<DirectoryListTableHeader>
{loading && (
<tr>
<th className="text-start" colSpan="2">
Name
</th>
<th className="px-2">
<div className="d-flex align-items-center gap-1">
<IconButton
size={12}
iconClass="bx bx-envelope"
color="primary"
onClick={() => alert("User icon clicked")}
/>
<span>Email</span>
</div>
</th>
<th className="mx-2">
<div className="d-flex align-items-center m-0 p-0 gap-1">
<IconButton
size={12}
iconClass="bx bx-phone"
color="warning"
onClick={() => alert("User icon clicked")}
/>
<span>Phone</span>
</div>
</th>
<th className="mx-2">
<div className="d-flex align-items-center gap-1">
<IconButton
size={12}
iconClass="bx bxs-grid-alt"
color="info"
/>
<span>Organization</span>
</div>
</th>
<th className="mx-2">
<div className="dropdown">
<a
className="dropdown-toggle hide-arrow cursor-pointer align-items-center"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Type <i className="bx bx-filter bx-sm"></i>
</a>
{/* <ul className="dropdown-menu p-2 text-capitalize">
{[
{ id: 1, label: "Active" },
{ id: 2, label: "On Hold" },
{ id: 3, label: "Inactive" },
{ id: 4, label: "Completed" },
].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>
</th>
<th
// className={`mx-2 ${
// HasManageProject ? "d-sm-table-cell" : "d-none"
// }`}
>
Action
</th>
</tr>
</thead>
<tbody className="table-border-bottom-0 overflow-auto ">
<tr>
<td colSpan="12" className="text-center py-4">
comming soon....
<td colSpan={10}>
{" "}
<p className="mt-10">Loading...</p>{" "}
</td>
</tr>
</tbody>
</table>
</div>
)}
{!loading && contacts?.length === 0 && (
<tr>
<td colSpan={10}>
<p className="mt-10">No contact found</p>
</td>
</tr>
)}
{!loading && currentItems.length === 0 && contacts?.length > 0 && (
<tr>
<td colSpan={10}>
<p className="mt-10">No matching contact found</p>
</td>
</tr>
)}
{!loading &&
currentItems.map((contact) => (
<ListViewDirectory
key={contact.id}
IsActive={IsActive}
contact={contact}
setSelectedContact={setSelectedContact}
setIsOpenModal={setIsOpenModal}
setOpen_contact={setOpen_contact}
setIsOpenModalNote={setIsOpenModalNote}
IsDeleted={setDeleteContact}
restore={handleDeleteContact}
/>
))}
</DirectoryListTableHeader>
) : (
<div className="row mt-5">
{!loading &&
currentItems.map((contact) => (
<div
key={contact.id}
className="col-12 col-sm-6 col-md-4 col-lg-4 mb-4"
>
<CardViewDirectory
IsActive={IsActive}
contact={contact}
setSelectedContact={setSelectedContact}
setIsOpenModal={setIsOpenModal}
setOpen_contact={setOpen_contact}
setIsOpenModalNote={setIsOpenModalNote}
IsDeleted={setDeleteContact}
restore={handleDeleteContact}
/>
</div>
))}
</div>
)}
{/* Pagination */}
{!loading &&
contacts?.length > 0 &&
currentItems.length > ITEMS_PER_PAGE && (
<nav aria-label="Page navigation">
<ul className="pagination pagination-sm justify-content-end py-1">
<li
className={`page-item ${currentPage === 1 ? "disabled" : ""}`}
>
<button
className="page-link btn-xs"
onClick={() => paginate(currentPage - 1)}
>
&laquo;
</button>
</li>
{[...Array(totalPages)].map((_, index) => (
<li
key={index}
className={`page-item ${
currentPage === index + 1 ? "active" : ""
}`}
>
<button
className="page-link"
onClick={() => paginate(index + 1)}
>
{index + 1}
</button>
</li>
))}
<li
className={`page-item ${
currentPage === totalPages ? "disabled" : ""
}`}
>
<button
className="page-link"
onClick={() => paginate(currentPage + 1)}
>
&raquo;
</button>
</li>
</ul>
</nav>
)}
</div>
</div>
);

View File

@ -0,0 +1,39 @@
import React from "react";
import IconButton from "../../components/common/IconButton";
const DirectoryListTableHeader = ({ children }) => {
return (
<div className="table-responsive text-nowrap py-2">
<table className="table px-2">
<thead>
<tr>
<th colSpan={2}>
<div className="d-flex align-items-center gap-1">
<span>Name</span>
</div>
</th>
<th className="px-2 text-start">
<div className="d-flex text-center align-items-center gap-1 justify-content-start">
<span>Email</span>
</div>
</th>
<th className="mx-2">
<div className="d-flex align-items-center m-0 p-0 gap-1">
<span>Phone</span>
</div>
</th>
<th colSpan={2} className="mx-2 ps-20">
Organization
</th>
<th className="mx-2">Category</th>
<th>Action</th>
</tr>
</thead>
<tbody className="table-border-bottom-0 overflow-auto">
{children}
</tbody>
</table>
</div>
);
};
export default DirectoryListTableHeader;

View File

@ -0,0 +1,196 @@
import React, { useEffect, useState } from "react";
const DirectoryPageHeader = ({
searchText,
setSearchText,
setIsActive,
listView,
setListView,
filteredBuckets,
tempSelectedBucketIds,
handleTempBucketChange,
filteredCategories,
tempSelectedCategoryIds,
handleTempCategoryChange,
clearFilter,
applyFilter,
loading,
IsActive,
setOpenBucketModal,
}) => {
const [filtered, setFiltered] = useState();
useEffect(() => {
setFiltered(
tempSelectedBucketIds?.length + tempSelectedCategoryIds?.length
);
}, [tempSelectedBucketIds, tempSelectedCategoryIds]);
return (
<>
{/* <div className="row">vikas</div> */}
<div className="row mx-0 px-0 align-items-center mt-2">
<div className="col-12 col-md-6 mb-2 px-1 d-flex align-items-center gap-4 ">
<input
type="search"
className="form-control me-2"
placeholder="Search Contact..."
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
style={{ width: "200px" }}
/>
<div className="d-flex gap-2 ">
<button
type="button"
className={`btn btn-xs ${
!listView ? "btn-primary" : "btn-outline-primary"
}`}
onClick={() => setListView(false)}
data-bs-toggle="tooltip"
data-bs-offset="0,8"
data-bs-placement="top"
data-bs-custom-class="tooltip"
title="Card View"
>
<i className="bx bx-grid-alt"></i>
</button>
<button
type="button"
className={`btn btn-xs ${
listView ? "btn-primary" : "btn-outline-primary"
}`}
onClick={() => setListView(true)}
data-bs-toggle="tooltip"
data-bs-offset="0,8"
data-bs-placement="top"
data-bs-custom-class="tooltip"
title="List View"
>
<i className="bx bx-list-ul "></i>
</button>
</div>
<div className="dropdown" style={{ width: "fit-content" }}>
<div className="dropdown" style={{ width: "fit-content" }}>
<a
className="dropdown-toggle hide-arrow cursor-pointer d-flex align-items-center position-relative"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i
className={`fa-solid fa-filter ms-1 fs-5 ${
filtered > 0 ? "text-primary" : "text-muted"
}`}
></i>
{filtered > 0 && (
<span
className="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-warning"
style={{ fontSize: "0.4rem" }}
>
{filtered}
</span>
)}
</a>
<ul className="dropdown-menu p-3" style={{ width: "320px" }}>
<div>
<p className="text-muted m-0 h6 ">Filter by</p>
{/* Bucket Filter */}
<div className="mt-1">
<p className="text-small mb-1 ">Buckets</p>
<div className="d-flex flex-wrap">
{filteredBuckets.map(({ id, name }) => (
<div
className="form-check me-3 mb-1"
style={{ minWidth: "33.33%" }}
key={id}
>
<input
className="form-check-input"
type="checkbox"
id={`bucket-${id}`}
checked={tempSelectedBucketIds.includes(id)}
onChange={() => handleTempBucketChange(id)}
/>
<label
className="form-check-label text-nowrap text-small "
htmlFor={`bucket-${id}`}
>
{name}
</label>
</div>
))}
</div>
</div>
<hr className="m-0" />
{/* Category Filter */}
<div className="mt-1">
<p className="text-small mb-1 ">Categories</p>
<div className="d-flex flex-wrap">
{filteredCategories.map(({ id, name }) => (
<div
className="form-check me-3 mb-1"
style={{ minWidth: "33.33%" }}
key={id}
>
<input
className="form-check-input"
type="checkbox"
id={`cat-${id}`}
checked={tempSelectedCategoryIds.includes(id)}
onChange={() => handleTempCategoryChange(id)}
/>
<label
className="form-check-label text-nowrap text-small"
htmlFor={`cat-${id}`}
>
{name}
</label>
</div>
))}
</div>
</div>
<div className="d-flex justify-content-end gap-2 mt-1">
<button
className="btn btn-xs btn-secondary"
onClick={clearFilter}
>
Clear
</button>
<button
className="btn btn-xs btn-primary"
onClick={applyFilter}
>
Apply Filter
</button>
</div>
</div>
</ul>
</div>
</div>
</div>
<div className="col-12 col-md-6 mb-2 px-1 d-flex justify-content-end gap-2 align-items-center text-end">
<label className="switch switch-primary align-self-start mb-2">
<input
type="checkbox"
className="switch-input me-3"
onChange={() => setIsActive(!IsActive)}
value={IsActive}
disabled={loading}
/>
<span className="switch-toggle-slider">
<span className="switch-on"></span>
<span className="switch-off"></span>
</span>
<span className=" list-inline-item ms-12 ">
Show Inactive Contacts
</span>
</label>
</div>
</div>
</>
);
};
export default DirectoryPageHeader;

View File

@ -0,0 +1,182 @@
import React, { useState, useEffect } from "react";
import { useProjects, useProjectsByEmployee } from "../../hooks/useProjects";
import EmployeeList from "./EmployeeList";
import showToast from "../../services/toastService";
import ProjectRepository from "../../repositories/ProjectRepository";
const AssignToProject = ({ employee, onClose }) => {
const { projects, loading } = useProjects();
const { projectList,loading:selectedProjectLoding ,refetch} = useProjectsByEmployee(employee?.id);
const [isSubmitting,setSubmitting] = useState(false)
const [searchTerm, setSearchTerm] = useState("");
const [checkedProjects, setCheckedProjects] = useState({});
const [selectedEmployees, setSelectedEmployees] = useState([]);
useEffect(() => {
if (projectList && projectList.length > 0) {
const initialChecked = {};
const initialSelected = [];
projectList.forEach((project) => {
initialChecked[project.id] = true;
initialSelected.push({
jobRoleId: employee.jobRoleId,
projectId: project.id,
status: true,
});
});
setCheckedProjects(initialChecked);
setSelectedEmployees(initialSelected);
} else {
setCheckedProjects({});
setSelectedEmployees([]);
}
}, [projectList, employee?.id]);
const handleSearchChange = (e) => {
setSearchTerm(e.target.value.toLowerCase());
};
const handleCheckboxChange = (projectId) => {
const isChecked = !checkedProjects[projectId];
setCheckedProjects((prev) => ({
...prev,
[projectId]: isChecked,
}));
};
const handleSubmit = async () => {
const initiallyAssigned = new Set(projectList.map((p) => p.id.toString()));
const changes = [];
Object.entries(checkedProjects).forEach(([projectId, isChecked]) => {
const wasAssigned = initiallyAssigned.has(projectId);
if (wasAssigned && !isChecked) {
changes.push({
projectId: projectId,
jobRoleId: employee.jobRoleId,
status: false,
});
}
if (!wasAssigned && isChecked) {
changes.push({
projectId: projectId,
jobRoleId: employee.jobRoleId,
status: true,
});
}
});
if (changes.length === 0) {
showToast("Make change before.", "info");
return;
}
try {
setSubmitting(true)
await ProjectRepository.updateProjectsByEmployee(employee.id, changes)
showToast( "Project assignments updated.", "success" );
setSubmitting(false)
onClose();
refetch(employee.id)
} catch (error) {
const msg = error.response?.data?.message || error.message || "Error during API call.";
showToast( msg, "error" );
setSubmitting(false)
}
};
const handleClosedModal = () => {
onClose();
};
const filteredProjects = projects.filter((project) =>
project.name.toLowerCase().includes(searchTerm)
);
return (
<div className="p-2 p-md-0">
<p className="fw-semibold fs-6 m-0">Assign to Project</p>
<div className="row my-1">
<div className="col-12 col-sm-6 col-md-6 mt-2">
<input
type="text"
className="form-control form-control-sm"
placeholder="Search projects..."
value={searchTerm}
onChange={handleSearchChange}
/>
</div>
</div>
{loading ? (
<div className="text-center py-4">
<div className="spinner-border text-primary" role="status" />
<p className="mt-2">Loading projects...</p>
</div>
) : (
<>
<table className="table mt-2 mb-2">
<thead>
<tr className="text-start">
<th>Select Project</th>
</tr>
</thead>
<tbody>
{filteredProjects.length > 0 ? (
filteredProjects.map((project) => (
<tr key={project.id}>
<td className="d-flex align-items-center">
<div className="form-check d-flex justify-content-start align-items-center">
<input
className="form-check-input"
type="checkbox"
id={`project-${project.id}`}
checked={checkedProjects[project.id] || false}
onChange={() => handleCheckboxChange( project.id )}
disabled={selectedProjectLoding}
/>
<label
className="form-check-label ms-2"
htmlFor={`project-${project.id}`}
>
{project.name}
</label>
</div>
</td>
</tr>
))
) : (
<tr>
<td className="text-center text-muted py-3">No projects found.</td>
</tr>
)}
</tbody>
</table>
<div className="d-flex justify-content-center gap-2 mt-2">
<button onClick={handleSubmit} className="btn btn-primary btn-sm" disabled={selectedProjectLoding || loading || isSubmitting }>
{isSubmitting ? "Please Wait...":"Submit"}
</button>
<button onClick={handleClosedModal} className="btn btn-secondary btn-sm" disabled={isSubmitting}>
Cancel
</button>
</div>
</>
)}
</div>
);
};
export default AssignToProject;

View File

@ -22,6 +22,8 @@ import {
import EmployeeRepository from "../../repositories/EmployeeRepository";
import ManageEmployee from "../../components/Employee/ManageEmployee";
import ConfirmModal from "../../components/common/ConfirmModal";
import GlobalModel from "../../components/common/GlobalModel";
import AssignToProject from "./AssignToProject";
const EmployeeList = () => {
const { profile: loginUser } = useProfile();
@ -47,7 +49,8 @@ const EmployeeList = () => {
const [IsDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const [selectedEmpFordelete, setSelectedEmpFordelete] = useState(null);
const [employeeLodaing, setemployeeLodaing] = useState(false);
const [ selectedEmployee, setSelectEmployee ] = useState( null )
const [IsOpenAsssingModal,setOpenAssignModal] = useState(false)
const navigate = useNavigate();
const handleSearch = (e) => {
@ -189,7 +192,11 @@ const EmployeeList = () => {
setSelectedEmpFordelete(employee);
setIsDeleteModalOpen(true);
};
const handleCloseAssignModal = () =>
{
setOpenAssignModal( false )
setSelectEmployee(null)
}
return (
<>
{isCreateModalOpen && (
@ -213,7 +220,8 @@ const EmployeeList = () => {
/>
</div>
</div>
</div>)}
</div> )}
{IsDeleteModalOpen && (
<div
@ -240,6 +248,11 @@ const EmployeeList = () => {
</div>
)}
{IsOpenAsssingModal && ( <GlobalModel isOpen={IsOpenAsssingModal} closeModal={()=>setOpenAssignModal(false)}>
<AssignToProject employee={selectedEmployee} onClose={() => setOpenAssignModal( false )} />
</GlobalModel>)}
<div className="container-xxl flex-grow-1 container-p-y">
<Breadcrumb
data={[
@ -642,6 +655,19 @@ const EmployeeList = () => {
<i className="bx bx-cog bx-sm"></i>{" "}
Manage Role
</button>
<button
className="dropdown-item py-1"
onClick={() =>
{
setSelectEmployee( item ),
setOpenAssignModal(true)
}
}
>
<i className='bx bx-select-multiple'></i>{" "}
Assign Project
</button>
</>
)}
</div>

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { MANAGE_MASTER } from "../../utils/constants";
import { ITEMS_PER_PAGE, MANAGE_MASTER } from "../../utils/constants";
import showToast from "../../services/toastService";
const MasterTable = ({ data, columns, loading, handleModalData }) => {
@ -21,7 +21,7 @@ const MasterTable = ({ data, columns, loading, handleModalData }) => {
const safeData = Array.isArray(data) ? data : [];
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage] = useState(20);
const [itemsPerPage] = useState(ITEMS_PER_PAGE);
const sortKeys = {
"Application Role": "role",

View File

@ -22,6 +22,7 @@ import {
import { useDispatch } from "react-redux";
import { setProjectId } from "../../slices/localVariablesSlice";
import { ComingSoonPage } from "../Misc/ComingSoonPage";
import Directory from "../Directory/Directory";
const ProjectDetails = () => {
let { projectId } = useParams();
@ -117,12 +118,10 @@ const ProjectDetails = () => {
);
break;
}
case "activities": {
case "directory": {
return (
<div className="row">
<div className="col-lg-12 col-xl-12">
<ActivityTimeline></ActivityTimeline>
</div>
<Directory IsPage={ false} prefernceContacts={projectDetails.id} />
</div>
);
}

View File

@ -9,7 +9,7 @@ import showToast from "../../services/toastService";
import { getCachedData, cacheData } from "../../slices/apiDataManager";
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
import { useProfile } from "../../hooks/useProfile";
import { MANAGE_PROJECT } from "../../utils/constants";
import { ITEMS_PER_PAGE, MANAGE_PROJECT } from "../../utils/constants";
import ProjectListView from "./ProjectListView";
const ProjectList = () => {
@ -25,7 +25,7 @@ const ProjectList = () => {
const dispatch = useDispatch();
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage] = useState(10);
const [itemsPerPage] = useState(ITEMS_PER_PAGE);
const [searchTerm, setSearchTerm] = useState("");
const [selectedStatuses, setSelectedStatuses] = useState([
"b74da4c2-d07e-46f2-9919-e75e49b12731",

View File

@ -0,0 +1,35 @@
import { api } from "../utils/axiosClient";
export const DirectoryRepository = {
GetOrganizations: () => api.get("/api/directory/organization"),
GetContacts: (isActive, projectId) => {
const params = new URLSearchParams();
params.append("active", isActive);
if (projectId) {
params.append("projectId", projectId);
}
return api.get(`/api/Directory?${params.toString()}`);
},
CreateContact: (data) => api.post("/api/directory", data),
UpdateContact: (id, data) => api.put(`/api/directory/${id}`, data),
DeleteContact: (id, isActive) =>
api.delete(`/api/directory/${id}/?active=${isActive}`),
AssignedBuckets: (id, data) =>
api.post(`/api/directory/assign-bucket/${id}`, data),
GetBucktes: () => api.get(`/api/directory/buckets`),
CreateBuckets: (data) => api.post(`/api/Directory/bucket`, data),
UpdateBuckets: (id, data) => api.put(`/api/Directory/bucket/${id}`, data),
DeleteBucket: (id) => api.delete(`/api/directory/bucket/${id}`),
GetContactProfile: (id) => api.get(`/api/directory/profile/${id}`),
CreateNote: (data) => api.post("/api/directory/note", data),
GetNote: (id, isActive) =>
api.get(`/api/directory/notes/${id}?active=${isActive}`),
UpdateNote: (id, data) => api.put(`/api/directory/note/${id}`, data),
DeleteNote: (id, isActive) =>
api.delete(`/api/directory/note/${id}?active=${isActive}`),
};

View File

@ -40,10 +40,20 @@ export const MasterRespository = {
"Job Role": ( id ) => api.delete( `/api/roles/jobrole/${ id }` ),
"Activity": ( id ) => api.delete( `/api/master/activity/delete/${ id }` ),
"Application Role":(id)=>api.delete(`/api/roles/${id}`),
"Work Category": (id) => api.delete(`api/master/work-category/${id}`),
"Work Category": ( id ) => api.delete( `api/master/work-category/${ id }` ),
"Contact Category": ( id ) => api.delete( `/api/master/contact-category` ),
"Contact Tag" :(id)=>api.delete(`/api/master/contact-tag/${id}`),
getWorkCategory:() => api.get(`/api/master/work-categories`),
createWorkCategory: (data) => api.post(`/api/master/work-category`,data),
updateWorkCategory: (id,data) => api.post(`/api/master/work-category/edit/${id}`,data),
updateWorkCategory: ( id, data ) => api.post( `/api/master/work-category/edit/${ id }`, data ),
getContactCategory: () => api.get( `/api/master/contact-categories` ),
createContactCategory: (data ) => api.post( `/api/master/contact-category`, data ),
updateContactCategory: ( id, data ) => api.post( `/api/master/contact-category/edit/${ id }`, data ),
getContactTag: () => api.get( `/api/master/contact-tags` ),
createContactTag: (data ) => api.post( `/api/master/contact-tag`, data ),
updateContactTag: ( id, data ) => api.post( `/api/master/contact-tag/edit/${ id }`, data )
}

View File

@ -20,7 +20,9 @@ const ProjectRepository = {
deleteProjectTask:(id)=> api.delete(`/api/project/task/${id}`),
updateProject: (id, data) => api.put(`/api/project/update/${id}`, data),
deleteProject: (id) => api.delete(`/projects/${id}`),
deleteProject: ( id ) => api.delete( `/projects/${ id }` ),
getProjectsByEmployee: ( id ) => api.get( `/api/project/assigned-projects/${ id }` ),
updateProjectsByEmployee:(id,data)=>api.post(`/api/project/assign-projects/${id}`,data)
};
export const TasksRepository = {

View File

@ -1,5 +1,6 @@
export const THRESH_HOLD = 48; // hours
export const DURATION_TIME = 10; // minutes
export const ITEMS_PER_PAGE = 20;
export const OTP_EXPIRY_SECONDS = 600 // OTP time
export const MANAGE_MASTER = "588a8824-f924-4955-82d8-fc51956cf323";
@ -27,3 +28,8 @@ export const VIEW_TASK = "9fcc5f87-25e3-4846-90ac-67a71ab92e3c"
export const ASSIGN_REPORT_TASK = "6a32379b-8b3f-49a6-8c48-4b7ac1b55dc2"
export const DIRECTORY_ADMIN = "4286a13b-bb40-4879-8c6d-18e9e393beda"
export const DIRECTORY_MANAGER = "62668630-13ce-4f52-a0f0-db38af2230c5"
export const DIRECTORY_USER = "0f919170-92d4-4337-abd3-49b66fc871bb"