Merge branch 'newFeature_Directory'
This commit is contained in:
commit
500006c1ef
166
package-lock.json
generated
166
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
14
public/assets/vendor/css/core.css
vendored
14
public/assets/vendor/css/core.css
vendored
@ -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);
|
||||
|
@ -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">
|
||||
<DireProvider>
|
||||
<AppRoutes />
|
||||
<ToastContainer></ToastContainer>
|
||||
</DireProvider>
|
||||
|
||||
<ToastContainer>
|
||||
|
||||
</ToastContainer>
|
||||
|
||||
</div>
|
||||
);
|
||||
|
21
src/Context/DireContext.jsx
Normal file
21
src/Context/DireContext.jsx
Normal 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;
|
||||
};
|
15
src/Context/FabContext.jsx
Normal file
15
src/Context/FabContext.jsx
Normal 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);
|
@ -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 (
|
||||
<>
|
||||
|
192
src/components/Directory/CardViewDirectory.jsx
Normal file
192
src/components/Directory/CardViewDirectory.jsx
Normal 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;
|
65
src/components/Directory/DirectorySchema.js
Normal file
65
src/components/Directory/DirectorySchema.js
Normal 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"})
|
||||
})
|
26
src/components/Directory/DirectoryUtils.js
Normal file
26
src/components/Directory/DirectoryUtils.js
Normal 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';
|
||||
};
|
158
src/components/Directory/EmployeeList.jsx
Normal file
158
src/components/Directory/EmployeeList.jsx
Normal 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;
|
131
src/components/Directory/ListViewDirectory.jsx
Normal file
131
src/components/Directory/ListViewDirectory.jsx
Normal 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;
|
429
src/components/Directory/ManageBucket.jsx
Normal file
429
src/components/Directory/ManageBucket.jsx
Normal 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;
|
@ -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>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<div className="col-md-6 text-start">
|
||||
<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>}
|
||||
<InputSuggestions
|
||||
organizationList={organizationList}
|
||||
value={getValues("organization") || ""}
|
||||
onChange={(val) => setValue("organization", val)}
|
||||
error={errors.organization?.message}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
</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>
|
||||
{emailFields.map((field, index) => (<>
|
||||
<div key={field.id} className="d-flex align-items-center mb-1">
|
||||
<div className="d-flex align-items-center">
|
||||
<input
|
||||
type="email"
|
||||
className="form-control form-control-sm"
|
||||
{...register(`email.${index}`)}
|
||||
{...register(`contactEmails.${index}.emailAddress`)}
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
{index === emailFields.length - 1 ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-xs btn-primary ms-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}
|
||||
>
|
||||
<i className="bx bx-plus bx-xs" />
|
||||
</button>
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-xs btn-danger ms-1"
|
||||
// <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)}
|
||||
>
|
||||
<i className="bx bx-x bx-xs" />
|
||||
</button>
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{errors.email?.[index] && (
|
||||
<small className="danger-text ms-2">
|
||||
{errors.email[index]?.message}
|
||||
{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>
|
||||
{phoneFields.map((field, index) => (<>
|
||||
<div key={field.id} className="d-flex align-items-center mb-1">
|
||||
<div className="d-flex align-items-center">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
{...register(`phone.${index}`)}
|
||||
{...register(`contactPhones.${index}.phoneNumber`)}
|
||||
placeholder="9876543210"
|
||||
/>
|
||||
{index === phoneFields.length - 1 ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-xs btn-primary ms-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}
|
||||
>
|
||||
<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>
|
||||
// <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.phone?.[ index ] && <small className="danger-text ms-2">{errors.phone[ index ]?.message}</small>}
|
||||
</>
|
||||
))}
|
||||
|
||||
{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">
|
||||
<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>
|
||||
|
220
src/components/Directory/NoteCardDirectory.jsx
Normal file
220
src/components/Directory/NoteCardDirectory.jsx
Normal 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;
|
188
src/components/Directory/NotesDirectory.jsx
Normal file
188
src/components/Directory/NotesDirectory.jsx
Normal 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;
|
234
src/components/Directory/ProfileContactDirectory.jsx
Normal file
234
src/components/Directory/ProfileContactDirectory.jsx
Normal 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;
|
465
src/components/Directory/UpdateContact.jsx
Normal file
465
src/components/Directory/UpdateContact.jsx
Normal 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;
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 DirAdmin = useHasUserPermission(DIRECTORY_ADMIN);
|
||||
const DireManager = useHasUserPermission(DIRECTORY_MANAGER)
|
||||
const DirUser = useHasUserPermission(DIRECTORY_USER)
|
||||
|
||||
return (
|
||||
<div className="nav-align-top">
|
||||
@ -73,6 +76,7 @@ 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>
|
||||
{(DirAdmin || DireManager || DirUser) && (
|
||||
<li className="nav-item">
|
||||
<a
|
||||
className={`nav-link ${activePill === "directory" ? "active" : ""}`}
|
||||
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
35
src/components/common/FloatingMenu.css
Normal file
35
src/components/common/FloatingMenu.css
Normal 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;
|
||||
}
|
||||
}
|
33
src/components/common/FloatingMenu.jsx
Normal file
33
src/components/common/FloatingMenu.jsx
Normal 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;
|
||||
|
@ -8,33 +8,46 @@ 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);
|
||||
const modalInstance = new window.bootstrap.Modal(modalElement, {
|
||||
backdrop: false,
|
||||
});
|
||||
|
||||
// Show modal if isOpen is true
|
||||
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
|
||||
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]);
|
||||
|
||||
|
||||
|
||||
// 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
|
||||
|
||||
@ -43,6 +56,19 @@ const GlobalModel = ({
|
||||
[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
|
||||
className={`modal fade ${ modalType }`}
|
||||
@ -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-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 */}
|
||||
|
79
src/components/common/InputSuggestion.jsx
Normal file
79
src/components/common/InputSuggestion.jsx
Normal 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;
|
158
src/components/common/MultiSelectDropdown.css
Normal file
158
src/components/common/MultiSelectDropdown.css
Normal 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;
|
||||
}
|
||||
}
|
131
src/components/common/SelectMultiple.jsx
Normal file
131
src/components/common/SelectMultiple.jsx
Normal 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;
|
@ -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(() => {
|
||||
setValue(name, tags); // sync to form when tags change
|
||||
}, [tags, name, setValue]);
|
||||
if (
|
||||
Array.isArray(watchedTags) &&
|
||||
JSON.stringify(tags) !== JSON.stringify(watchedTags)
|
||||
) {
|
||||
setTags(watchedTags);
|
||||
}
|
||||
}, [JSON.stringify(watchedTags)]);
|
||||
|
||||
const addTag = (e) => {
|
||||
e.preventDefault();
|
||||
const trimmed = input.trim();
|
||||
if (trimmed !== "" && !tags.includes(trimmed)) {
|
||||
setTags([...tags, trimmed]);
|
||||
useEffect(() => {
|
||||
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 = 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) => (
|
||||
<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={i}
|
||||
key={index}
|
||||
className="d-flex align-items-center"
|
||||
style={{
|
||||
color: iconColor,
|
||||
backgroundColor,
|
||||
padding: "2px 3px",
|
||||
borderRadius: "2px"
|
||||
padding: "2px 6px",
|
||||
borderRadius: "2px",
|
||||
fontSize: "0.85rem",
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
<i className="bx bx-x bx-xs ms-1" onClick={() => removeTag(i)}></i>
|
||||
{tag.name}
|
||||
<i
|
||||
className="bx bx-x bx-xs ms-1"
|
||||
onClick={() => removeTag(index)}
|
||||
style={{ cursor: "pointer" }}
|
||||
/>
|
||||
</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)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
onKeyUp={handleInputKey}
|
||||
placeholder={placeholder}
|
||||
style={{ outline: "none", minWidth: "120px" }}
|
||||
style={{
|
||||
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"
|
||||
}}
|
||||
>
|
||||
{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;
|
||||
|
132
src/components/common/TextEditor/Editor.css
Normal file
132
src/components/common/TextEditor/Editor.css
Normal 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;
|
||||
}
|
96
src/components/common/TextEditor/Editor.jsx
Normal file
96
src/components/common/TextEditor/Editor.jsx
Normal 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;
|
113
src/components/master/CreateContactCategory.jsx
Normal file
113
src/components/master/CreateContactCategory.jsx
Normal 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;
|
114
src/components/master/CreateContactTag.jsx
Normal file
114
src/components/master/CreateContactTag.jsx
Normal 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;
|
126
src/components/master/EditContactCategory.jsx
Normal file
126
src/components/master/EditContactCategory.jsx
Normal 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;
|
126
src/components/master/EditContactTag.jsx
Normal file
126
src/components/master/EditContactTag.jsx
Normal 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;
|
@ -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,6 +95,8 @@ const MasterModal = ({ modaldata, closeModal }) => {
|
||||
>
|
||||
<div className="modal-content">
|
||||
<div className="modal-body p-sm-4 p-0">
|
||||
<div className="d-flex justify-content-between">
|
||||
<h6>{ `${modaldata?.modalType} `}</h6>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close"
|
||||
@ -100,6 +104,7 @@ const MasterModal = ({ modaldata, closeModal }) => {
|
||||
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>
|
||||
|
@ -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 = [
|
||||
|
@ -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",
|
||||
|
@ -51,6 +51,14 @@ const useMaster = (isMa) => {
|
||||
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;
|
||||
@ -150,3 +158,66 @@ 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
179
src/hooks/useDirectory.js
Normal 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 };
|
||||
};
|
@ -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
|
||||
}
|
||||
};
|
||||
|
||||
|
35
src/hooks/useSortableData.js
Normal file
35
src/hooks/useSortableData.js
Normal 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 };
|
||||
};
|
@ -165,3 +165,6 @@ padding: 1px !important;
|
||||
.accordion-button:not(.collapsed) .toggle-icon {
|
||||
content: "\f146"; /* minus-circle */
|
||||
}
|
||||
.hoverBox:hover{
|
||||
background-color: #f1f3f5;
|
||||
}
|
@ -4,12 +4,15 @@ 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 (
|
||||
<FabProvider>
|
||||
<div className="layout-wrapper layout-content-navbar">
|
||||
<div className="layout-container">
|
||||
<Sidebar />
|
||||
@ -20,9 +23,11 @@ const HomeLayout = () => {
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
<FloatingMenu />
|
||||
<div className="layout-overlay layout-menu-toggle"></div>
|
||||
</div>
|
||||
</div>
|
||||
</FabProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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>}
|
||||
|
||||
|
@ -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">
|
||||
{IsPage && (
|
||||
<Breadcrumb
|
||||
data={[
|
||||
{ label: "Home", link: "/dashboard" },
|
||||
{ label: "Directory (Comming Soon)", link: null },
|
||||
{ label: "Directory", link: null },
|
||||
]}
|
||||
></Breadcrumb>
|
||||
)}
|
||||
|
||||
<GlobalModel isOpen={isOpenModal} closeModal={closedModel}>
|
||||
<ManageDirectory />
|
||||
{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)}
|
||||
/>
|
||||
)}
|
||||
</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="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..."
|
||||
/>
|
||||
</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)}
|
||||
{openBucketModal && (
|
||||
<GlobalModel
|
||||
isOpen={openBucketModal}
|
||||
closeModal={() => setOpenBucketModal(false)}
|
||||
size="lg"
|
||||
>
|
||||
<i className="bx bx-plus-circle me-2"></i>
|
||||
New Contact
|
||||
</button>
|
||||
<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>
|
||||
</div>
|
||||
<div className="table-responsive text-nowrap py-2 ">
|
||||
<table className="table px-2">
|
||||
<thead>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
{!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)}
|
||||
>
|
||||
«
|
||||
</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)}
|
||||
>
|
||||
»
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
39
src/pages/Directory/DirectoryListTableHeader.jsx
Normal file
39
src/pages/Directory/DirectoryListTableHeader.jsx
Normal 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;
|
196
src/pages/Directory/DirectoryPageHeader.jsx
Normal file
196
src/pages/Directory/DirectoryPageHeader.jsx
Normal 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;
|
182
src/pages/employee/AssignToProject.jsx
Normal file
182
src/pages/employee/AssignToProject.jsx
Normal 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;
|
@ -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 && (
|
||||
@ -215,6 +222,7 @@ const EmployeeList = () => {
|
||||
</div>
|
||||
</div> )}
|
||||
|
||||
|
||||
{IsDeleteModalOpen && (
|
||||
<div
|
||||
className={`modal fade ${IsDeleteModalOpen ? "show" : ""}`}
|
||||
@ -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>
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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",
|
||||
|
35
src/repositories/DirectoryRepository.jsx
Normal file
35
src/repositories/DirectoryRepository.jsx
Normal 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}`),
|
||||
};
|
@ -41,9 +41,19 @@ export const MasterRespository = {
|
||||
"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 }` ),
|
||||
"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 ),
|
||||
|
||||
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 )
|
||||
|
||||
}
|
@ -21,6 +21,8 @@ const ProjectRepository = {
|
||||
|
||||
updateProject: (id, data) => api.put(`/api/project/update/${id}`, data),
|
||||
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 = {
|
||||
|
@ -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"
|
Loading…
x
Reference in New Issue
Block a user