pramod_Task-#251: A "Notes" section appears in the contact view modal, allowing users to add a note for the selected contact. #124

Merged
pramod.mahajan merged 26 commits from pramod_Task-#251 into Feature_Directory 2025-05-21 11:00:04 +00:00
17 changed files with 1199 additions and 295 deletions

166
package-lock.json generated
View File

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

View File

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

View File

@ -16889,7 +16889,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 +16900,18 @@ li:not(:first-child) .dropdown-item,
transition: none;
}
}
.modal .btn-close:hover,
/* .modal .btn-close:hover,
.modal .btn-close:focus,
.modal .btn-close:active {
opacity: 1;
outline: 0;
transform: translate(20px, -20px);
}
:dir(rtl) .modal .btn-close:hover,
} */
/* :dir(rtl) .modal .btn-close:hover,
:dir(rtl) .modal .btn-close:focus,
:dir(rtl) .modal .btn-close:active {
transform: translate(26px, -20px);
}
} */
.modal .btn-close::before {
display: block;
background-color: var(--bs-secondary-color);

View File

@ -1,61 +1,77 @@
import React from "react";
import Avatar from "../common/Avatar";
const CardViewDirectory = ({ contact,setSelectedContact , setIsOpenModal}) => {
const CardViewDirectory = ({ contact, setSelectedContact, setIsOpenModal,setOpen_contact,setIsOpenModalNote }) => {
return (
<div class="card text-start border-1">
<div class="card-body d-flex justify-content-between px-1 py-2">
<div className="d-flex align-items-center">
<Avatar
size="xs"
firstName={
(contact?.name || "").trim().split(" ")[0]?.charAt(0) || ""
}
lastName={
(contact?.name || "").trim().split(" ")[1]?.charAt(0) || ""
}
/>{" "}
<p className="m-0">{contact.name}</p>
</div>
<div>
<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 bx-sm 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 py-0">
<i className="bx bx-pencil bx-xs me-2"></i>
<span className="align-left small-text">Modify</span>
</a>
</li>
<li>
<a className="dropdown-item px-2 py-0">
<i className="bx bx-trash bx-xs me-2"></i>
<span className="align-left small-text">Delete</span>
</a>
</li>
</ul>
<div className="card text-start border-1">
<div className="card-body px-1 py-2 pb-0">
<div className="d-flex justify-content-between">
<div className="d-flex align-items-center">
<Avatar
size="xs"
firstName={
(contact?.name || "").trim().split(" ")[0]?.charAt(0) || ""
}
lastName={
(contact?.name || "").trim().split(" ")[1]?.charAt(0) || ""
}
/>{" "}
<p className="m-0">{contact.name}</p>
</div>
<div>
<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 bx-sm 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 py-0">
<i className="bx bx-pencil bx-xs me-2"></i>
<span className="align-left small-text">Modify</span>
</a>
</li>
<li>
<a className="dropdown-item px-2 py-0">
<i className="bx bx-trash bx-xs me-2"></i>
<span className="align-left small-text">Delete</span>
</a>
</li>
</ul>
</div>
</div>
</div>
<ul className="list-inline m-0 ps-4">
<li className="list-inline-item me-1" style={{fontSize:"10px"}}>
<i className="bx bx-building bx-xs"></i>
</li>
<li className="list-inline-item" style={{fontSize:"10px"}}>
{contact.organization}
</li>
</ul>
</div>
<div class="card-footer text-start px-1 py-1">
<div className="card-footer text-start px-1 py-1" onClick={() =>
{
setIsOpenModalNote(true)
setOpen_contact(contact)
}}>
<hr className="my-0" />
{contact.contactEmails[0] && (
<ul className="list-inline my-1 ">
@ -80,12 +96,6 @@ const CardViewDirectory = ({ contact,setSelectedContact , setIsOpenModal}) => {
)}
<ul className="list-inline m-0">
<li className="list-inline-item me-2">
<i className="bx bx-building bx-xs"></i>
</li>
<li className="list-inline-item small-text">
{contact.organization}
</li>
<li className="list-inline-item me-2">
<i className="bx bx-merge bx-xs"></i>
</li>

View File

@ -42,16 +42,16 @@ export const ContactSchema = z
bucketIds: z.array(z.string()).optional(),
})
.refine((data) => {
const hasValidEmail = (data.contactEmails || []).some(
(e) => e.emailAddress?.trim() !== ""
);
const hasValidPhone = (data.contactPhones || []).some(
(p) => p.phoneNumber?.trim() !== ""
);
// .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"],
});
// return hasValidEmail || hasValidPhone;
// }, {
// message: "At least one contact (email or phone) is required",
// path: ["contactPhone"],
// });

View File

@ -204,23 +204,22 @@ useEffect(() => {
placeholder="email@example.com"
/>
{index === emailFields.length - 1 ? (
<button
type="button"
className="btn btn-xs btn-primary ms-1"
onClick={handleAddEmail}
style={{ width: "24px", height: "24px" }}
>
<i className="bx bx-plus-circle bx-xs" />
</button>
// <button
// type="button"
// className="btn btn-xs btn-primary ms-1"
// onClick={handleAddEmail}
// style={{ width: "24px", height: "24px" }}
// >
<i className="bx bx-plus-circle bx-xs ms-1 cursor-pointer text-primary" onClick={handleAddEmail} />
) : (
<button
type="button"
className="btn btn-xs btn-danger ms-1 p-0"
onClick={() => removeEmail(index)}
style={{ width: "24px", height: "24px" }}
>
<i className="bx bx-minus-circle bx-xs" />
</button>
// <button
// type="button"
// className="btn btn-xs btn-danger ms-1 p-0"
// onClick={() => removeEmail(index)}
// style={{ width: "24px", height: "24px" }}
// >
<i className="bx bx-minus-circle bx-xs ms-1 cursor-pointer text-primary" onClick={() => removeEmail(index)} />
)}
</div>
{errors.contactEmails?.[index]?.emailAddress && (
@ -264,23 +263,23 @@ useEffect(() => {
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" />
</button>
// <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" />
</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.contactPhones?.[index]?.phoneNumber && (

View File

@ -0,0 +1,183 @@
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 = ({ 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 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 () => {
try {
setIsDeleting(true);
const resp = await DirectoryRepository.DeleteNote(noteItem.id);
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);
showToast("Note 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-2 conntactNote"
style={{ width: "100%", minWidth: "300px", borderRadius: "0px" }}
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(noteItem.createdAt).format("MMMM DD, YYYY [at] hh:mm A")}
</span>
</div>
</div>
<div>
<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}
></i>
)}
{isDeleting && (
<div
class="spinner-border spinner-border-sm text-secondary"
role="status"
>
<span class="visually-hidden">Loading...</span>
</div>
)}
</div>
</div>
<hr className="mt-0" />
{editing ? (
<>
<ReactQuill
value={editorValue}
onChange={setEditorValue}
theme="snow"
className="compact-editor"
/>
<div className="d-flex justify-content-end gap-2">
<span
className="text-secondary cursor-pointer"
aria-disabled={isLoading}
onClick={() => setEditing(false)}
>
Cancel
</span>
<span
className="text-primary cursor-pointer"
aria-disabled={isLoading}
onClick={handleUpdateNote}
>
{isLoading ? "Please Wait..." : "Submit"}
</span>
</div>
</>
) : (
<div dangerouslySetInnerHTML={{ __html: noteItem.note }} />
)}
</div>
);
};
export default NoteCardDirectory;

View File

@ -0,0 +1,148 @@
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";
const schema = z.object({
note: z.string().min(1, { message: "Note is required" }),
});
const NotesDirectory = ({ isLoading, contactProfile, setProfileContact }) => {
const [NotesData, setNotesData] = useState();
const [IsSubmitting, setIsSubmitting] = useState(false);
const [addNote, setAddNote] = useState(false);
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(false);
} catch (error) {
setIsSubmitting(false);
const msg =
error.response.data.message ||
error.message ||
"Error occured during API calling";
showToast(msg, "error");
}
};
const onCancel = () => {
setValue("note", "");
};
return (
<div className="text-start">
<div
className={`${
contactProfile?.notes?.length > 0
? "d-flex justify-content-between"
: "d-flex justify-content-end"
}`}
>
{contactProfile?.notes.length > 0 && (
<p className="fw-semibold m-0">Notes :</p>
)}
<a
className="small-text m-0 cursor-pointer"
onClick={() => setAddNote(!addNote)}
>
<u>
{addNote ? "" : "Add Note"}
<i
className={`bx ${addNote ? "bx-x" : "bx-pencil"} bx-xs`}
></i>{" "}
</u>{" "}
</a>
</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=" justify-content-start overflow-auto px-1"
style={{ maxHeight: "300px" }}
>
{isLoading && (
<div className="text-center">
{" "}
<p>Loading...</p>{" "}
</div>
)}
{!isLoading &&
[...(contactProfile?.notes || [])]
.reverse()
.map((noteItem) => (
<NoteCardDirectory
noteItem={noteItem}
contactId={contactProfile?.id}
setProfileContact={setProfileContact}
key={noteItem.id}
/>
))}
</div>
</div>
);
};
export default NotesDirectory;

View File

@ -0,0 +1,103 @@
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 { conatProfile, loading } = useContactProfile(contact?.id);
const [activeTab, setActiveTab] = useState("profile");
const [profileContact, setProfileContact] = useState();
useEffect(() => {
setProfileContact(conatProfile);
}, [conatProfile]);
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"
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-2">
<span className="m-0 fw-semibold">{contact?.name}</span>
<span className="small">
<i className="bx bx-building bx-xs"></i>{" "}
{conatProfile?.organization}
</span>
</div>
</div>
<div className="d-flex flex-column text-start">
{conatProfile?.contactEmails?.length > 0 && (
<div className="d-flex mb-2">
<div style={{ width: "100px", minWidth: "100px" }}>
<p className="m-0">Email</p>
</div>
<div>
<ul className="list-inline mb-0">
{conatProfile.contactEmails.map((email, idx) => (
<li className="list-inline-item me-3" key={idx}>
<i className="bx bx-envelope bx-xs me-1"></i>
{email.emailAddress}
</li>
))}
</ul>
</div>
</div>
)}
{conatProfile?.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">
{conatProfile.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>
)}
{conatProfile?.createdAt && (
<div className="d-flex mb-2">
<div style={{ width: "100px", minWidth: "100px" }}>
<p className="m-0">Created</p>
</div>
<div>
<ul className="list-inline mb-0">
<li className="list-inline-item">
<i className="bx bx-calendar-week bx-xs me-1"></i>
{moment(conatProfile.createdAt).format("MMMM, DD YYYY")}
</li>
</ul>
</div>
</div>
)}
</div>
<hr className="my-1" />
<NotesDirectory
isLoading={loading}
contactProfile={profileContact}
setProfileContact={setProfileContact}
/>
</div>
</div>
);
};
export default ProfileContactDirectory;

View File

@ -239,23 +239,23 @@ await submitContact({ ...cleaned, id: existingContact.id });
placeholder="email@example.com"
/>
{index === emailFields.length - 1 ? (
<button
type="button"
className="btn btn-xs btn-primary ms-1"
onClick={handleAddEmail}
style={{ width: "24px", height: "24px" }}
>
<i className="bx bx-plus-circle bx-xs" />
</button>
// <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" />
</button>
// <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 && (
@ -299,23 +299,21 @@ await submitContact({ ...cleaned, id: existingContact.id });
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" />
</button>
// <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" />
</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-danger" onClick={() => removePhone(index)} />
)}
</div>
{errors.contactPhones?.[index]?.phoneNumber && (

View File

@ -8,7 +8,8 @@ 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
@ -69,13 +70,13 @@ useEffect(() => {
<div className="modal-content">
<div className="modal-header p-0">
{/* Close button inside the modal header */}
<button
{IsCloseBtn && <button
type="button"
className="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
onClick={closeModal} // Trigger the React closeModal function
></button>
></button>}
</div>
<div className="modal-body p-sm-4 p-0">
{children} {/* Render children here, which can be the ReportTask component */}

View File

@ -118,8 +118,8 @@
}
.custom-checkbox:checked {
background-color: #0d6efd;
border-color: #0d6efd;
background-color: #696cff;
border-color: #696cff;
}
.custom-checkbox:checked::after {

View File

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

View File

@ -0,0 +1,94 @@
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">
<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">
<option value="1" />
<option value="2" />
<option selected />
</select>
<button className="ql-bold" />
<button className="ql-italic" />
<button className="ql-underline" />
<button className="ql-strike" />
</span>
<span className="ql-formats">
<button className="ql-list" value="ordered" />
<button className="ql-list" value="bullet" />
{/* <button className="ql-image" value="file" /> */}
</span>
<span className="ql-formats">
<button className="ql-link" />
</span>
</span>
</div>
</div>
<ReactQuill
value={value}
onChange={onChange}
modules={modules}
formats={formats}
theme="snow"
placeholder={placeholder}
/>
{/* Right: Submit + Cancel Buttons */}
<div className="d-flex justify-content-end gap-2 p-1">
<span className="btn btn-xs btn-secondary" aria-disabled={loading} onClick={onCancel}>
Cancel
</span>
<span
type="submit"
className="btn btn-xs btn-primary"
onClick={onSubmit}
aria-disabled={loading}
>
{loading ? "Please Wait..." : "Submit"}
</span>
</div>
</div>
);
};
export default Editor;

View File

@ -61,3 +61,80 @@ export const useBuckets = () => {
return { buckets, loading, error };
};
export const useContactProfile = (id) =>
{
const [ conatProfile, setContactProfile ] = useState( null );
const [ loading, setLoading ] = useState( false );
const [ Error, setError ] = useState( "" );
const fetchContactProfile = async () => {
const cached = getCachedData("Contact Profile");
if (!cached || cached.contactId !== id) {
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);
}
} else {
setContactProfile(cached.data);
}
};
useEffect(() => {
if ( id )
{
fetchContactProfile(id);
}
}, [id]);
return { conatProfile, loading, Error };
}
export const useContactNotes = (id) =>
{
const [ conatNotes, setContactNotes ] = useState( [] );
const [ loading, setLoading ] = useState( false );
const [ Error, setError ] = useState( "" );
const fetchContactNotes = async () => {
const cached = getCachedData("Contact Notes");
if (!cached || cached.contactId !== id) {
setLoading(true);
try {
const resp = await DirectoryRepository.GetNote(id);
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);
}
} else {
setContactNotes(cached.data);
}
};
useEffect(() => {
if ( id )
{
fetchContactNotes(id);
}
}, [id]);
return { conatProfile, loading, Error };
}

View File

@ -12,15 +12,18 @@ 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 { ITEMS_PER_PAGE } from "../../utils/constants";
import ProfileContactDirectory from "../../components/Directory/ProfileContactDirectory";
const Directory = () => {
const [isOpenModal, setIsOpenModal] = useState(false);
const [isOpenModalNote, setIsOpenModalNote] = useState(false);
const [selectedContact, setSelectedContact] = useState(null);
const [open_contact, setOpen_contact] = useState(null);
const [ContatList, setContactList] = useState([]);
const [contactCategories, setContactCategories] = useState([]);
const [ searchText, setSearchText ] = useState( "" );
const [listView, setListView] = useState(true);
const [searchText, setSearchText] = useState("");
const [listView, setListView] = useState(false);
const { contacts, loading } = useDirectory();
const { contactCategory, loading: contactCategoryLoading } =
@ -59,6 +62,7 @@ const Directory = () => {
const closedModel = () => {
setIsOpenModal(false);
setSelectedContact(null);
setOpen_contact(null);
};
useEffect(() => {
setContactList(contacts);
@ -80,23 +84,39 @@ const Directory = () => {
);
};
const filteredContacts = useMemo(() => {
return ContatList
.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);
return matchesSearch && matchesCategory;
})
.sort((a, b) => a.name.localeCompare(b.name));
return ContatList.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);
return matchesSearch && matchesCategory;
}).sort((a, b) => a.name.localeCompare(b.name));
}, [ContatList, searchText, selectedCategoryIds]);
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} />
);
}
};
return (
<div className="container-xxl flex-grow-1 container-p-y">
<Breadcrumb
@ -109,26 +129,32 @@ const Directory = () => {
{isOpenModal && (
<GlobalModel
isOpen={isOpenModal}
closeModal={() => setIsOpenModal(false)}
closeModal={() =>
{
setSelectedContact(null)
setIsOpenModal(false)
}}
size="lg"
>
{selectedContact ? (
<UpdateContact
existingContact={selectedContact}
submitContact={submitContact}
onCLosed={closedModel}
/>
) : (
<ManageDirectory
submitContact={submitContact}
onCLosed={closedModel}
/>
)}
{renderModalContent()}
</GlobalModel>
)}
{isOpenModalNote && (
<GlobalModel
isOpen={isOpenModalNote}
closeModal={() =>
{
setOpen_contact(null)
setIsOpenModalNote(false)
}}
size="lg"
>
{open_contact && <ProfileContactDirectory contact={open_contact} setOpen_contact={setOpen_contact} closeModal={ () => setIsOpenModalNote(false)} />}
</GlobalModel>
)}
<div className="card p-2">
<div className="row mx-0 px-0 align-items-center">
<div className="col-7 col-md-4 mb-2 px-1 d-flex align-items-center ">
<div className="col-12 col-md-4 mb-2 px-1 d-flex align-items-center ">
<input
type="search"
className="form-control form-control-sm me-2"
@ -166,7 +192,7 @@ const Directory = () => {
<i className="bx bx-list-ul bx-sm"></i>
</button>
</div>
<div className="dropdown">
<div className="dropdown">
<a
className="dropdown-toggle hide-arrow cursor-pointer d-flex align-items-center"
data-bs-toggle="dropdown"
@ -194,117 +220,119 @@ const Directory = () => {
</div>
</div>
<div className="col-5 col-md-8 mb-2 px-1 text-md-end text-end">
<div className="col-12 col-md-8 mb-2 px-1 text-md-end text-end">
<button
type="button"
className="btn btn-xs btn-primary"
onClick={() => setIsOpenModal(true)}
>
<i className="bx bx-plus-circle me-2"></i>
<span className="d-sm-block d-none"> New Contact</span>
New Contact
</button>
</div>
</div>
{
listView ? (
<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">
<IconButton
size={12}
iconClass="bx bx-user"
color="secondary"
onClick={() => alert("User icon clicked")}
/>
<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">
<IconButton
size={12}
iconClass="bx bx-envelope"
color="primary"
/>
<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">Category</th>
<th
// className={`mx-2 ${
// HasManageProject ? "d-sm-table-cell" : "d-none"
// }`}
>
Action
</th>
</tr>
</thead>
<tbody className="table-border-bottom-0 overflow-auto ">
{loading && ContatList.length === 0 && (
{!listView && loading && <p>Loading...</p>}
{listView ? (
<div className="table-responsive text-nowrap py-2 ">
<table className="table px-2">
<thead>
<tr>
<td colSpan={10}>Loading...</td>
</tr>
)}
{!loading && contacts.length == 0 && ContatList.length === 0 && (
<tr>
<td colSpan={10}>No Contact Found</td>
</tr>
)}
{!loading &&
currentItems.map((contact) => (
<ListViewDirectory
key={contact.id}
contact={contact}
setSelectedContact={setSelectedContact}
setIsOpenModal={setIsOpenModal}
/>
))}
</tbody>
</table>
</div>
) : (
<div className="row">
{currentItems.map((contact, index) => (
<div key={contact.id} className="col-12 col-sm-6 col-md-4 col-lg-4 mb-4">
<CardViewDirectory contact={contact}
setSelectedContact={setSelectedContact}
setIsOpenModal={setIsOpenModal}
/>
</div>
))}
</div>
)
}
<th colSpan={2}>
<div className="d-flex align-items-center gap-1">
<IconButton
size={12}
iconClass="bx bx-user"
color="secondary"
onClick={() => alert("User icon clicked")}
/>
<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">
<IconButton
size={12}
iconClass="bx bx-envelope"
color="primary"
/>
<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">Category</th>
<th
// className={`mx-2 ${
// HasManageProject ? "d-sm-table-cell" : "d-none"
// }`}
>
Action
</th>
</tr>
</thead>
<tbody className="table-border-bottom-0 overflow-auto ">
{loading && ContatList.length === 0 && (
<tr>
<td colSpan={10}>Loading...</td>
</tr>
)}
{!loading &&
contacts.length == 0 &&
ContatList.length === 0 && (
<tr>
<td colSpan={10}>No Contact Found</td>
</tr>
)}
{!loading &&
currentItems.map((contact) => (
<ListViewDirectory
key={contact.id}
contact={contact}
setSelectedContact={setSelectedContact}
setIsOpenModal={setIsOpenModal}
/>
))}
</tbody>
</table>
</div>
) : (
<div className="row">
{currentItems.map((contact, index) => (
<div
key={contact.id}
className="col-12 col-sm-6 col-md-4 col-lg-4 mb-4"
>
<CardViewDirectory
contact={contact}
setSelectedContact={setSelectedContact}
setIsOpenModal={setIsOpenModal}
setOpen_contact={setOpen_contact}
setIsOpenModalNote={setIsOpenModalNote}
/>
</div>
))}
</div>
)}
{!loading && (
<nav aria-label="Page ">

View File

@ -5,5 +5,12 @@ export const DirectoryRepository = {
CreateContact: ( data ) => api.post( '/api/directory', data ),
UpdateContact:(id,data)=>api.put(`/api/directory/${id}`,data),
GetBucktes:()=>api.get(`/api/Directory/buckets`)
GetBucktes: () => api.get( `/api/directory/buckets` ),
GetContactProfile: ( id ) => api.get( `/api/directory/profile/${ id }` ),
CreateNote: ( data ) => api.post( '/api/directory/note', data ),
GetNote: ( id ) => api.get( `/api/directory/note/${ id }` ),
UpdateNote: ( id, data ) => api.put( `/api/directory/note/${ id }`, data ),
DeleteNote:(id)=> api.delete(`/api/directory/note/${ id }`)
}