Compare commits
477 Commits
bab312b9ee
...
500006c1ef
| Author | SHA1 | Date | |
|---|---|---|---|
| 500006c1ef | |||
| 93d807cca8 | |||
| a77f7cecbb | |||
| 9b40317eb2 | |||
| a480c3b111 | |||
| c2c9fd01fc | |||
| d3524c40f3 | |||
| 7eff9d8c3a | |||
| ad1ad667b1 | |||
| 435fa730b6 | |||
| ff8fca72fa | |||
| c3a52c46eb | |||
|
|
9573daa8b0 | ||
|
|
117ac31d12 | ||
|
|
7dea047536 | ||
|
|
3256a68d1c | ||
|
|
d4f0662f57 | ||
|
|
93c1fb1844 | ||
|
|
0886990f9d | ||
|
|
c7b93006d8 | ||
|
|
b71ca1c7fb | ||
|
|
1d43470783 | ||
|
|
33f5bb3517 | ||
|
|
e3731cab5f | ||
|
|
71631fa68d | ||
|
|
0b99238fa8 | ||
|
|
376ead7967 | ||
|
|
51c02c7156 | ||
|
|
977397d31f | ||
|
|
b77389284c | ||
|
|
29ef1e4016 | ||
|
|
8d966fc5c0 | ||
|
|
901f633177 | ||
|
|
d23a090cd5 | ||
|
|
456fa6d385 | ||
|
|
7bf3d69174 | ||
|
|
d5d5ecafa9 | ||
|
|
49dfc084c0 | ||
|
|
0026de943b | ||
|
|
3e61a9959e | ||
|
|
bc657eebca | ||
| c4288fe35a | |||
| f6634eb527 | |||
|
|
763d81e3b1 | ||
|
|
c78c136467 | ||
|
|
360f3b2352 | ||
|
|
0eaea6a232 | ||
|
|
08ace9b5ba | ||
|
|
7de4de9b32 | ||
|
|
cd7422c991 | ||
|
|
e739a380f7 | ||
|
|
2eee4a1f6a | ||
|
|
b0e3b767d3 | ||
|
|
49a3bec527 | ||
|
|
289f4063f9 | ||
|
|
ffd23dc11c | ||
|
|
941e74fcbb | ||
|
|
94daa0d4c2 | ||
|
|
755ea1b0ee | ||
|
|
0082a60e02 | ||
|
|
7d3a7adf95 | ||
|
|
6d02511847 | ||
|
|
1f8ba16447 | ||
|
|
bae8b130de | ||
|
|
a35e8b645b | ||
|
|
875ba214dd | ||
|
|
bc220180c4 | ||
|
|
8a57d3e978 | ||
|
|
c9960a1273 | ||
|
|
ddaba97c8f | ||
|
|
14698e92fe | ||
|
|
ec32d9160c | ||
|
|
6346f132bd | ||
|
|
3dd901a81f | ||
|
|
f9ad47ef54 | ||
|
|
62a044261a | ||
|
|
3c75222dd7 | ||
|
|
77e3082000 | ||
|
|
db75cec8e7 | ||
|
|
a9f3b27cb3 | ||
|
|
813032ead2 | ||
|
|
eb6d4e413d | ||
|
|
16389bf102 | ||
|
|
a87d2c9143 | ||
|
|
2d1d32ee16 | ||
|
|
c77dc76079 | ||
|
|
04d64e57c9 | ||
|
|
39ef265b56 | ||
|
|
cd487dee49 | ||
|
|
fba983f41d | ||
|
|
5ef8bed2b8 | ||
|
|
201a8881a5 | ||
|
|
ed489842c6 | ||
|
|
24ffb82616 | ||
|
|
0695fb6737 | ||
|
|
45a300e540 | ||
|
|
b4157354f7 | ||
|
|
8f9bfcae6b | ||
|
|
47ecbcdafd | ||
|
|
41d9db6fcc | ||
|
|
330ac0ff62 | ||
|
|
1b399c1ebf | ||
|
|
360bfc5650 | ||
|
|
1d1f82feb6 | ||
|
|
195adbeb72 | ||
|
|
d32b954d94 | ||
|
|
c7e308586a | ||
|
|
df1838dbba | ||
|
|
276c97a734 | ||
|
|
449debc0cf | ||
|
|
560ee8fe27 | ||
|
|
dd0ec9d018 | ||
|
|
30b27ed2bb | ||
|
|
ee9c80749f | ||
|
|
2fc4db1f87 | ||
|
|
7a8bbaf561 | ||
|
|
3ff8066803 | ||
|
|
a965116aa5 | ||
|
|
5a701ca9d7 | ||
|
|
ded8ab7752 | ||
|
|
6a8332b606 | ||
|
|
51e48e632a | ||
|
|
8999d7bb47 | ||
|
|
5e3c833ca6 | ||
|
|
3f5b79dc5c | ||
|
|
f748eecf14 | ||
|
|
df5c2e4397 | ||
|
|
0d09bfda04 | ||
|
|
1b575a7d5f | ||
|
|
17cceeb97c | ||
|
|
eb8140ebac | ||
|
|
c089ee47cf | ||
|
|
6010e630e8 | ||
|
|
0d249c7cdb | ||
|
|
5fd6f26653 | ||
|
|
2643bbbe59 | ||
|
|
d40d4c59f2 | ||
|
|
bfba4133f4 | ||
|
|
31ca02fcd8 | ||
|
|
105b5c6dcc | ||
|
|
2607f852fb | ||
|
|
54de8df956 | ||
|
|
4e19c2a547 | ||
|
|
43b1b1ffe2 | ||
|
|
c784d18427 | ||
|
|
0e38709435 | ||
|
|
4f70d8cb17 | ||
|
|
24d8688069 | ||
|
|
5a3dbe466f | ||
|
|
76bbfee4fc | ||
|
|
3e251b3d47 | ||
|
|
dcc17dea0c | ||
|
|
37066904e3 | ||
|
|
0c44ae76f8 | ||
|
|
2688c95f47 | ||
|
|
8f6c4b0814 | ||
|
|
7d489d177d | ||
|
|
d9053837a6 | ||
|
|
7feaac1a94 | ||
|
|
e3cfc1c073 | ||
|
|
1a7636ad82 | ||
|
|
96d1fd648f | ||
|
|
f9c3c136b3 | ||
|
|
93c0961751 | ||
|
|
0fc460b38f | ||
|
|
7514e73d0c | ||
|
|
038ff416e1 | ||
|
|
69abe1bd42 | ||
|
|
3a6be65a48 | ||
|
|
597fa279ea | ||
|
|
61f0703096 | ||
|
|
a9a5206062 | ||
|
|
75897a2ab4 | ||
|
|
1300ea33b6 | ||
|
|
6cad6ac6e8 | ||
|
|
2d84b489f5 | ||
|
|
7b2a94b5a4 | ||
|
|
a6a4435a77 | ||
|
|
bc50e58150 | ||
|
|
f8f43b62c6 | ||
|
|
dbc1ae2c63 | ||
|
|
76ce07d4d3 | ||
|
|
65319cac7d | ||
|
|
6fec5b68e3 | ||
|
|
41494a0816 | ||
|
|
4753117d7b | ||
|
|
bb6aaeee18 | ||
|
|
bef8a28db2 | ||
|
|
f11f358f49 | ||
|
|
65f1d32bb8 | ||
|
|
7c67af41c3 | ||
|
|
b082aa370b | ||
|
|
15967ce2fc | ||
|
|
c83960c040 | ||
|
|
d10cbdd4bd | ||
|
|
42b769c1b5 | ||
|
|
ada8856b3b | ||
|
|
86e9854071 | ||
|
|
ba04e041b8 | ||
|
|
3d43facdae | ||
|
|
3e1f93d4f1 | ||
|
|
09b1664ac7 | ||
|
|
f62129e3ac | ||
|
|
cdec17735f | ||
|
|
8ece56da52 | ||
|
|
22dc355f7d | ||
|
|
f7c9c7c7ff | ||
|
|
02492b0405 | ||
|
|
283e13985d | ||
|
|
a319171674 | ||
|
|
07a402d28d | ||
|
|
354c653240 | ||
|
|
33f6220622 | ||
|
|
e8504c6627 | ||
|
|
5b3b6904bf | ||
|
|
1e3a1f0365 | ||
|
|
8d7fcc62a7 | ||
|
|
5836a24aed | ||
|
|
8c0ab1102b | ||
|
|
b4081e73bf | ||
|
|
a3428fed85 | ||
|
|
bf0af60c47 | ||
|
|
cecff08ba2 | ||
|
|
8c15cacc04 | ||
|
|
3393eaf48d | ||
|
|
c672e44c76 | ||
|
|
a1e75bf7dd | ||
|
|
379d9ecb8b | ||
|
|
662d464271 | ||
|
|
99863442a6 | ||
| 2386bd0e74 | |||
|
|
b1253bed88 | ||
|
|
f129645386 | ||
|
|
0b46dc0f1f | ||
|
|
fe87e0dbd4 | ||
|
|
74a5c05481 | ||
|
|
7baffac11a | ||
|
|
a3dcd9bee3 | ||
|
|
d19652a98e | ||
| 2b015947ef | |||
| fed6711ece | |||
|
|
6b5aa5fb1a | ||
| 1ca50afc74 | |||
|
|
e83eafddd2 | ||
|
|
1bd050f919 | ||
|
|
1298994f58 | ||
|
|
f92ef4e52a | ||
|
|
34c04f31ec | ||
|
|
0d649fbc78 | ||
| 6217941b0a | |||
|
|
0843123505 | ||
|
|
9c46bffc4b | ||
| da6b56ac8c | |||
| 7f45dbab2a | |||
|
|
426a885629 | ||
|
|
e03d1b4e99 | ||
| 1c8461e72a | |||
|
|
6819bfbb52 | ||
|
|
dbf7178db0 | ||
|
|
d826acbb45 | ||
|
|
4799e8e91c | ||
|
|
07809d6635 | ||
| 99d8be1e4b | |||
| 30ce981a24 | |||
| f20a7cf4bc | |||
| 9c3bcaea9e | |||
| 252bf0904e | |||
| 4b64e7450e | |||
| 5572b80f4a | |||
| 0674fcf5bb | |||
|
|
05f4a9da4d | ||
|
|
e5bcf7111b | ||
|
|
6774957558 | ||
|
|
3594b78253 | ||
|
|
e93062bdf4 | ||
|
|
445a5e0064 | ||
|
|
5666616b6b | ||
|
|
15d313baf1 | ||
| d74854b847 | |||
|
|
eb678585fa | ||
|
|
9ff79b5854 | ||
|
|
616df5e869 | ||
|
|
a88a8d3df9 | ||
|
|
36e7755b7d | ||
|
|
73f57bb59f | ||
|
|
41cbf69550 | ||
|
|
8b5f7e69ab | ||
|
|
85a957c204 | ||
|
|
6fe62e7c0e | ||
|
|
b5d3b0fd08 | ||
|
|
fd4d4f0386 | ||
|
|
acbe3f44b5 | ||
|
|
b268763283 | ||
|
|
767d8abd19 | ||
|
|
5531d408f9 | ||
|
|
3330565dcb | ||
|
|
4628dea561 | ||
| 225f22153d | |||
|
|
8c66a9a083 | ||
|
|
2a5e075db5 | ||
|
|
80a213595d | ||
|
|
0f323a7594 | ||
| 8c43d07488 | |||
|
|
6b5bc8c729 | ||
|
|
ba5b07b608 | ||
|
|
875656dbf5 | ||
|
|
e22ab1ae05 | ||
|
|
416f8e6621 | ||
|
|
51f7e473f4 | ||
|
|
e6cdff1ed6 | ||
|
|
cb0800b103 | ||
|
|
b02c599130 | ||
| 16301def0c | |||
|
|
61e8c78245 | ||
|
|
a0930197d9 | ||
|
|
c6e9d3fcbe | ||
|
|
b478e7e4a7 | ||
|
|
054a7780db | ||
|
|
1f864ac08c | ||
|
|
cea094cd89 | ||
|
|
cae2f2fffd | ||
|
|
fc30db8532 | ||
|
|
0ec3d0c3e3 | ||
|
|
a553ac2fe2 | ||
|
|
e316858c0a | ||
|
|
ed72f29f01 | ||
|
|
f8c3a6e767 | ||
|
|
fc82989577 | ||
|
|
6605293c47 | ||
|
|
c382d9238d | ||
|
|
45e88c07a9 | ||
|
|
eb0e424a18 | ||
|
|
3f7456e5e7 | ||
|
|
478aedf2ae | ||
|
|
866049e385 | ||
|
|
8786f01b40 | ||
|
|
d3f7c44922 | ||
|
|
466f11382a | ||
|
|
3072759ba8 | ||
|
|
a17a32a7cf | ||
|
|
991a833dbf | ||
|
|
5287b40cd0 | ||
|
|
dd35de14a7 | ||
|
|
8fba237173 | ||
|
|
c3643683c8 | ||
|
|
ad145db3c7 | ||
| 2224b10842 | |||
|
|
8abb8a3aa5 | ||
|
|
51c3857675 | ||
|
|
8f94cf6e2d | ||
|
|
199cf4867f | ||
|
|
06c0c5ebb3 | ||
| 16f40f7566 | |||
| d11174984d | |||
| ff56245119 | |||
|
|
1cdaefbec4 | ||
|
|
36b09747b0 | ||
|
|
695369cbf8 | ||
|
|
dcaa9ae7ec | ||
|
|
79e38f6cd5 | ||
|
|
3593e7d46c | ||
|
|
144817aef6 | ||
| 889b5ab069 | |||
|
|
bcc09851d3 | ||
|
|
45cd17c808 | ||
|
|
d9ce90e20e | ||
|
|
385f08e752 | ||
|
|
52ec9408d2 | ||
|
|
64973029cc | ||
|
|
7e4f4c8973 | ||
|
|
6943f4e9ca | ||
|
|
af996196a1 | ||
| b4d3e6453f | |||
|
|
ab47a44c60 | ||
|
|
10e38353aa | ||
|
|
e372405ac0 | ||
|
|
0cefb98ac1 | ||
|
|
c78f94b17f | ||
|
|
55f17daf0f | ||
|
|
a69dd50425 | ||
|
|
3b82653f23 | ||
|
|
7147116d8e | ||
| 2643f1b2d5 | |||
|
|
228c178acf | ||
|
|
cb5cacaee0 | ||
| 1d90c631ef | |||
|
|
ad0be79943 | ||
|
|
a076f1810f | ||
|
|
c60409e108 | ||
|
|
b4df17b7e0 | ||
|
|
eb41a66a79 | ||
|
|
83ec0ec5dd | ||
|
|
e331208b3e | ||
|
|
5b155a3329 | ||
|
|
89221dec38 | ||
|
|
7f8784bf6a | ||
|
|
7942f37dd6 | ||
|
|
c59daf0ffe | ||
|
|
8afef0d81a | ||
|
|
af294a56d9 | ||
|
|
75ab8f677c | ||
|
|
7a30c74594 | ||
|
|
9c9a69bc52 | ||
|
|
c4de655fdf | ||
|
|
6e8bd7eef8 | ||
|
|
3738730ecf | ||
|
|
3572361c67 | ||
|
|
c454b3e884 | ||
|
|
ea2a587fad | ||
|
|
39ea50115f | ||
|
|
8a53e2d8de | ||
|
|
35b036398f | ||
| b71dd7f0f5 | |||
|
|
99e0cd94d4 | ||
|
|
27fafb4f05 | ||
|
|
978165d3de | ||
|
|
b9a7d4977c | ||
|
|
65aa8990a3 | ||
|
|
9509b0b8da | ||
|
|
7e16701b0d | ||
|
|
1e079a89eb | ||
|
|
aba02f64c0 | ||
|
|
c43c668fb8 | ||
|
|
353238fd60 | ||
|
|
b2e1fbcf95 | ||
|
|
b296dec542 | ||
|
|
3efeea55e0 | ||
|
|
126a84b5cc | ||
|
|
7613f36624 | ||
|
|
1cac05ddcb | ||
|
|
49d38f553a | ||
|
|
b65be72809 | ||
|
|
e9903ee2f6 | ||
| b489c094b3 | |||
|
|
28e409ff99 | ||
|
|
b6a0fb54ac | ||
|
|
61ce554e05 | ||
|
|
d956bfd3cc | ||
|
|
71b9a3602b | ||
|
|
68999401e5 | ||
|
|
a837b16a7d | ||
|
|
d716a5ffac | ||
|
|
5f33826e61 | ||
|
|
8e06991bd3 | ||
|
|
79ad15d572 | ||
|
|
4d6171e1ed | ||
|
|
28a4f63d10 | ||
|
|
c2ead3cd0f | ||
|
|
b33fad1b17 | ||
|
|
5ccdf33c35 | ||
| 431a541a89 | |||
|
|
745673b3f3 | ||
|
|
cfa8571079 | ||
|
|
c7b5a0ba7d | ||
|
|
a1ee9dac38 | ||
|
|
085f45210e | ||
|
|
7cfe04de2e | ||
|
|
1c1a1da8a0 | ||
| 9a0482071a | |||
|
|
2922cca22d | ||
|
|
a3d21ba098 | ||
| cdc52ae947 | |||
|
|
04297346fa | ||
| 70cfa36ba8 | |||
|
|
d8df891370 | ||
|
|
dcc611b768 | ||
|
|
413dba4f5c | ||
| 9a4c5df14f | |||
|
|
cd21bc14ff | ||
|
|
40df12de86 | ||
|
|
74ed8d3c4f | ||
|
|
784eebafea | ||
| 88d23fc765 | |||
|
|
50ef045479 | ||
|
|
cc98f34b44 | ||
|
|
5415210d70 | ||
|
|
3670409977 |
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",
|
||||
|
||||
@ -190,6 +190,12 @@
|
||||
padding-left: 50px;
|
||||
} */
|
||||
|
||||
.small-text{
|
||||
.small-text {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.layout-wrapper:not(.layout-horizontal)
|
||||
.layout-navbar
|
||||
.dropdown-menu[data-bs-popper] {
|
||||
inset-block-start: 100%;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
10
src/App.tsx
10
src/App.tsx
@ -1,3 +1,4 @@
|
||||
import { DireProvider } from "./Context/DireContext";
|
||||
import AppRoutes from "./router/AppRoutes";
|
||||
import { ToastContainer } from "react-toastify";
|
||||
|
||||
@ -5,8 +6,13 @@ import { ToastContainer } from "react-toastify";
|
||||
const App = () => {
|
||||
return (
|
||||
<div className="app">
|
||||
<AppRoutes />
|
||||
<ToastContainer></ToastContainer>
|
||||
<DireProvider>
|
||||
<AppRoutes />
|
||||
</DireProvider>
|
||||
|
||||
<ToastContainer>
|
||||
|
||||
</ToastContainer>
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
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 className="col-md-6 text-start">
|
||||
<label className="form-label">Organization</label>
|
||||
<InputSuggestions
|
||||
organizationList={organizationList}
|
||||
value={getValues("organization") || ""}
|
||||
onChange={(val) => setValue("organization", val)}
|
||||
error={errors.organization?.message}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<label className="form-label">Organization</label>
|
||||
<input className="form-control form-control-sm" {...register("organization")} />
|
||||
{errors.organization && <small className="danger-text">{errors.organization.message}</small>}
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<div className="row mt-1">
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Email</label>
|
||||
{emailFields.map((field, index) => (<>
|
||||
<div key={field.id} className="d-flex align-items-center mb-1">
|
||||
<input
|
||||
type="email"
|
||||
className="form-control form-control-sm"
|
||||
{...register(`email.${index}`)}
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
{index === emailFields.length - 1 ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-xs btn-primary ms-1"
|
||||
onClick={handleAddEmail}
|
||||
{emailFields.map((field, index) => (
|
||||
<div
|
||||
key={field.id}
|
||||
className="row d-flex align-items-center mb-1"
|
||||
>
|
||||
<div className="col-5 text-start">
|
||||
<label className="form-label">Label</label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
{...register(`contactEmails.${index}.label`)}
|
||||
>
|
||||
<i className="bx bx-plus bx-xs" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-xs btn-danger ms-1"
|
||||
onClick={() => removeEmail(index)}
|
||||
>
|
||||
<i className="bx bx-x bx-xs" />
|
||||
</button>
|
||||
)}
|
||||
<option value="Work">Work</option>
|
||||
<option value="Personal">Personal</option>
|
||||
<option value="Other">Other</option>
|
||||
</select>
|
||||
{errors.contactEmails?.[index]?.label && (
|
||||
<small className="danger-text">
|
||||
{errors.contactEmails[index].label.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-7 text-start">
|
||||
<label className="form-label">Email</label>
|
||||
<div className="d-flex align-items-center">
|
||||
<input
|
||||
type="email"
|
||||
className="form-control form-control-sm"
|
||||
{...register(`contactEmails.${index}.emailAddress`)}
|
||||
placeholder="email@example.com"
|
||||
/>
|
||||
{index === emailFields.length - 1 ? (
|
||||
// <button
|
||||
// type="button"
|
||||
// className="btn btn-xs btn-primary ms-1"
|
||||
// onClick={handleAddEmail}
|
||||
// style={{ width: "24px", height: "24px" }}
|
||||
// >
|
||||
<i
|
||||
className="bx bx-plus-circle bx-xs ms-1 cursor-pointer text-primary"
|
||||
onClick={handleAddEmail}
|
||||
/>
|
||||
) : (
|
||||
// <button
|
||||
// type="button"
|
||||
// className="btn btn-xs btn-danger ms-1 p-0"
|
||||
// onClick={() => removeEmail(index)}
|
||||
// style={{ width: "24px", height: "24px" }}
|
||||
// >
|
||||
<i
|
||||
className="bx bx-minus-circle bx-xs ms-1 cursor-pointer text-primary"
|
||||
onClick={() => removeEmail(index)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{errors.contactEmails?.[index]?.emailAddress && (
|
||||
<small className="danger-text">
|
||||
{errors.contactEmails[index].emailAddress.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{errors.email?.[index] && (
|
||||
<small className="danger-text ms-2">
|
||||
{errors.email[index]?.message}
|
||||
</small>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Phone</label>
|
||||
{phoneFields.map((field, index) => (<>
|
||||
<div key={field.id} className="d-flex align-items-center mb-1">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
{...register(`phone.${index}`)}
|
||||
placeholder="9876543210"
|
||||
/>
|
||||
{index === phoneFields.length - 1 ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-xs btn-primary ms-1"
|
||||
onClick={handleAddPhone}
|
||||
{phoneFields.map((field, index) => (
|
||||
<div
|
||||
key={field.id}
|
||||
className="row d-flex align-items-center mb-2"
|
||||
>
|
||||
<div className="col-5 text-start">
|
||||
<label className="form-label">Label</label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
{...register(`contactPhones.${index}.label`)}
|
||||
>
|
||||
<i className="bx bx-plus bx-xs" />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-xs btn-danger ms-1"
|
||||
onClick={() => removePhone(index)} // Remove the phone field
|
||||
>
|
||||
<i className="bx bx-x bx-xs" />
|
||||
</button>
|
||||
)}
|
||||
<option value="Office">Office</option>
|
||||
<option value="Personal">Personal</option>
|
||||
<option value="Business">Business</option>
|
||||
</select>
|
||||
{errors.phone?.[index]?.label && (
|
||||
<small className="danger-text">
|
||||
{errors.ContactPhones[index].label.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-7 text-start">
|
||||
<label className="form-label">Phone</label>
|
||||
<div className="d-flex align-items-center">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control form-control-sm"
|
||||
{...register(`contactPhones.${index}.phoneNumber`)}
|
||||
placeholder="9876543210"
|
||||
/>
|
||||
{index === phoneFields.length - 1 ? (
|
||||
// <button
|
||||
// type="button"
|
||||
// className="btn btn-xs btn-primary ms-1"
|
||||
// onClick={handleAddPhone}
|
||||
// style={{ width: "24px", height: "24px" }}
|
||||
// >
|
||||
<i
|
||||
className="bx bx-plus-circle bx-xs ms-1 cursor-pointer text-primary"
|
||||
onClick={handleAddPhone}
|
||||
/>
|
||||
) : (
|
||||
// <button
|
||||
// type="button"
|
||||
// className="btn btn-xs btn-danger ms-1"
|
||||
// onClick={() => removePhone(index)}
|
||||
// style={{ width: "24px", height: "24px" }}
|
||||
// >
|
||||
<i
|
||||
className="bx bx-minus-circle bx-xs ms-1 cursor-pointer text-danager"
|
||||
onClick={() => removePhone(index)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{errors.contactPhones?.[index]?.phoneNumber && (
|
||||
<small className="danger-text">
|
||||
{errors.contactPhones[index].phoneNumber.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{errors.phone?.[ index ] && <small className="danger-text ms-2">{errors.phone[ index ]?.message}</small>}
|
||||
</>
|
||||
))}
|
||||
|
||||
</div>
|
||||
{errors.contactPhone?.message && (
|
||||
<div className="danger-text">{errors.contactPhone.message}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="row my-1">
|
||||
<div className="col-md-6">
|
||||
<label className="form-label">Type</label>
|
||||
<input className="form-control form-control-sm" {...register("type")} />
|
||||
{errors.type && <small className="danger-text">{errors.type.message}</small>}
|
||||
<div className="col-md-6 text-start">
|
||||
<label className="form-label">Category</label>
|
||||
<select
|
||||
className="form-select form-select-sm"
|
||||
{...register("contactCategoryId")}
|
||||
>
|
||||
{contactCategoryLoading && !contactCategory ? (
|
||||
<option disabled value="">
|
||||
Loading...
|
||||
</option>
|
||||
) : (
|
||||
<>
|
||||
<option disabled value="">
|
||||
Select Category
|
||||
</option>
|
||||
{contactCategory?.map((cate) => (
|
||||
<option key={cate.id} value={cate.id}>
|
||||
{cate.name}
|
||||
</option>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</select>
|
||||
{errors.contactCategoryId && (
|
||||
<small className="danger-text">
|
||||
{errors.contactCategoryId.message}
|
||||
</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-md-6">
|
||||
<TagInput name="tags" label="Tags" />
|
||||
<div className="col-12 col-md-6 text-start">
|
||||
<SelectMultiple
|
||||
name="projectIds"
|
||||
label="Select Projects"
|
||||
options={projects}
|
||||
labelKey="name"
|
||||
valueKey="id"
|
||||
IsLoading={projectLoading}
|
||||
/>
|
||||
{errors.projectIds && (
|
||||
<small className="danger-text">{errors.projectIds.message}</small>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<div className="col-12 text-start">
|
||||
<TagInput name="tags" label="Tags" options={contactTags} />
|
||||
{errors.tags && (
|
||||
<small className="danger-text">{errors.tags.message}</small>
|
||||
)}
|
||||
</div>
|
||||
<div className="row">
|
||||
<div className="col-md-12 mt-1 text-start">
|
||||
<label className="form-label ">Select Bucket</label>
|
||||
|
||||
<ul className="d-flex flex-wrap px-1 list-unstyled mb-0">
|
||||
{bucketsLoaging && <p>Loading...</p>}
|
||||
{buckets?.map((item) => (
|
||||
<li
|
||||
key={item.id}
|
||||
className="list-inline-item flex-shrink-0 me-6 mb-2"
|
||||
>
|
||||
<div className="form-check ">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="form-check-input"
|
||||
id={`item-${item.id}`}
|
||||
checked={watchBucketIds.includes(item.id)}
|
||||
onChange={() => handleCheckboxChange(item.id)}
|
||||
/>
|
||||
<label
|
||||
className="form-check-label"
|
||||
htmlFor={`item-${item.id}`}
|
||||
>
|
||||
{item.name}
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
{errors.bucketIds && (
|
||||
<small className="danger-text mt-0">
|
||||
{errors.bucketIds.message}
|
||||
</small>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 text-start">
|
||||
<label className="form-label">Address</label>
|
||||
<textarea className="form-control form-control-sm" rows="2" {...register("address")} />
|
||||
<textarea
|
||||
className="form-control form-control-sm"
|
||||
rows="2"
|
||||
{...register("address")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-12">
|
||||
<div className="col-12 text-start">
|
||||
<label className="form-label">Description</label>
|
||||
<textarea className="form-control form-control-sm" rows="2" {...register("description")} />
|
||||
{errors.description && <small className="danger-text">{errors.description.message}</small>}
|
||||
<textarea
|
||||
className="form-control form-control-sm"
|
||||
rows="2"
|
||||
{...register("description")}
|
||||
/>
|
||||
{errors.description && (
|
||||
<small className="danger-text">{errors.description.message}</small>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="d-flex justify-content-evenly py-2">
|
||||
<button className="btn btn-sm btn-primary" type="submit">Submit</button>
|
||||
<button className="btn btn-sm btn-secondary" type="button">Cancel</button>
|
||||
<div className="d-flex justify-content-center gap-1 py-2">
|
||||
<button className="btn btn-sm btn-primary" type="submit">
|
||||
{IsSubmitting ? "Please Wait..." : "Submit"}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-sm btn-secondary"
|
||||
type="button"
|
||||
onClick={handleClosed}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
|
||||
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>
|
||||
|
||||
@ -602,7 +602,7 @@ const Header = () => {
|
||||
<span className="align-middle">My Profile</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<li onClick={handleProfilePage}>
|
||||
<a
|
||||
aria-label="go to setting "
|
||||
className="dropdown-item cusor-pointer"
|
||||
@ -611,7 +611,7 @@ const Header = () => {
|
||||
<span className="align-middle">Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
{/* <li>
|
||||
<a
|
||||
aria-label="go to billing "
|
||||
className="dropdown-item cusor-pointer"
|
||||
@ -626,8 +626,10 @@ const Header = () => {
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li onClick={openChangePassword}> {/* Use the function from the context */}
|
||||
</li> */}
|
||||
<li onClick={openChangePassword}>
|
||||
{" "}
|
||||
{/* Use the function from the context */}
|
||||
<a
|
||||
aria-label="go to profile"
|
||||
className="dropdown-item cusor-pointer"
|
||||
@ -657,4 +659,4 @@ const Header = () => {
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
export default Header;
|
||||
export default Header;
|
||||
|
||||
@ -26,6 +26,7 @@ const ManageProjectInfo = ({ project, handleSubmitForm, onClose }) => {
|
||||
.object({
|
||||
...(project?.id ? { id: z.string().optional() } : {}),
|
||||
name: z.string().min(1, { message: "Project Name is required" }),
|
||||
shortName: z.string().optional(),
|
||||
contactPerson: z
|
||||
.string()
|
||||
.min( 1, {message: "Contact Person Name is required"} )
|
||||
@ -72,6 +73,7 @@ const ManageProjectInfo = ({ project, handleSubmitForm, onClose }) => {
|
||||
defaultValues: {
|
||||
id: project?.id || "",
|
||||
name: project?.name || "",
|
||||
shortName: project?.shortName || "",
|
||||
contactPerson: project?.contactPerson || "",
|
||||
projectAddress: project?.projectAddress || "",
|
||||
startDate: formatDate(project?.startDate) || currentDate,
|
||||
@ -88,6 +90,7 @@ const ManageProjectInfo = ({ project, handleSubmitForm, onClose }) => {
|
||||
? {
|
||||
id: project?.id || "",
|
||||
name: project?.name || "",
|
||||
shortName: project?.shortName || "",
|
||||
contactPerson: project?.contactPerson || "",
|
||||
projectAddress: project?.projectAddress || "",
|
||||
startDate: formatDate(project?.startDate) || "",
|
||||
@ -108,6 +111,7 @@ const ManageProjectInfo = ({ project, handleSubmitForm, onClose }) => {
|
||||
reset({
|
||||
id: project?.id || "",
|
||||
name: project?.name || "",
|
||||
shortName: project?.shortName || "",
|
||||
contactPerson: project?.contactPerson || "",
|
||||
projectAddress: project?.projectAddress || "",
|
||||
startDate: formatDate(project?.startDate) || currentDate,
|
||||
@ -157,6 +161,27 @@ const ManageProjectInfo = ({ project, handleSubmitForm, onClose }) => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label" htmlFor="shortName">
|
||||
Short Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="shortName"
|
||||
name="shortName"
|
||||
className="form-control"
|
||||
placeholder="Short Name"
|
||||
{...register("shortName")}
|
||||
/>
|
||||
{errors.shortName && (
|
||||
<div
|
||||
className="danger-text text-start"
|
||||
style={{ fontSize: "12px" }}
|
||||
>
|
||||
{errors.shortName.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-12 col-md-12">
|
||||
<label className="form-label" htmlFor="contactPerson">
|
||||
Contact Person
|
||||
|
||||
@ -1,80 +1,75 @@
|
||||
import React, { useState,useEffect } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import ManageProjectInfo from "./ManageProjectInfo";
|
||||
import showToast from "../../services/toastService";
|
||||
import ProjectRepository from "../../repositories/ProjectRepository";
|
||||
import { cacheData,getCachedData } from "../../slices/apiDataManager";
|
||||
import {hasUserPermission} from "../../utils/authUtils";
|
||||
import { cacheData, getCachedData } from "../../slices/apiDataManager";
|
||||
import { hasUserPermission } from "../../utils/authUtils";
|
||||
import moment from "moment";
|
||||
import {useHasUserPermission} from "../../hooks/useHasUserPermission";
|
||||
import {MANAGE_PROJECT} from "../../utils/constants";
|
||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||
import { MANAGE_PROJECT } from "../../utils/constants";
|
||||
|
||||
|
||||
const ProjectBanner = ( {project_data} ) =>
|
||||
{
|
||||
const ProjectBanner = ({ project_data }) => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const manageProject = useHasUserPermission(MANAGE_PROJECT)
|
||||
const [ CurrentProject, setCurrentProject ] = useState( project_data )
|
||||
const manageProject = useHasUserPermission(MANAGE_PROJECT);
|
||||
const [CurrentProject, setCurrentProject] = useState(project_data);
|
||||
if (project_data == null) {
|
||||
return <span>incomplete project information</span>;
|
||||
}
|
||||
|
||||
|
||||
const handleShow = () => setShowModal(true);
|
||||
const handleClose = () => setShowModal(false);
|
||||
|
||||
const handleFormSubmit = ( updatedProject,setLoading ) =>
|
||||
{
|
||||
|
||||
if ( CurrentProject?.id )
|
||||
{
|
||||
ProjectRepository.updateProject(CurrentProject.id,updatedProject).then( ( response ) =>
|
||||
{
|
||||
const updatedProjectData = {
|
||||
...CurrentProject,
|
||||
...response.data,
|
||||
building: CurrentProject.building,
|
||||
};
|
||||
setCurrentProject( updatedProject )
|
||||
|
||||
cacheData( `projectinfo-${ CurrentProject.id }`, updatedProjectData );
|
||||
const projects_list = getCachedData("projectslist");
|
||||
if ( projects_list )
|
||||
{
|
||||
const updatedProjectsList = projects_list.map(project =>
|
||||
project.id === CurrentProject.id ? {
|
||||
...project,
|
||||
...response.data,
|
||||
// tenant:project.tenant
|
||||
} : project
|
||||
);
|
||||
|
||||
cacheData("projectslist",updatedProjectsList)
|
||||
}
|
||||
|
||||
showToast( "Project updated successfully.", "success" );
|
||||
setLoading(false)
|
||||
setShowModal(false)
|
||||
})
|
||||
.catch((error) => {
|
||||
showToast( error.message, "error" );
|
||||
|
||||
});
|
||||
}
|
||||
const handleFormSubmit = (updatedProject, setLoading) => {
|
||||
if (CurrentProject?.id) {
|
||||
ProjectRepository.updateProject(CurrentProject.id, updatedProject)
|
||||
.then((response) => {
|
||||
const updatedProjectData = {
|
||||
...CurrentProject,
|
||||
...response.data,
|
||||
building: CurrentProject.building,
|
||||
};
|
||||
setCurrentProject(updatedProject);
|
||||
|
||||
cacheData(`projectinfo-${CurrentProject.id}`, updatedProjectData);
|
||||
const projects_list = getCachedData("projectslist");
|
||||
if (projects_list) {
|
||||
const updatedProjectsList = projects_list.map((project) =>
|
||||
project.id === CurrentProject.id
|
||||
? {
|
||||
...project,
|
||||
...response.data,
|
||||
// tenant:project.tenant
|
||||
}
|
||||
: project
|
||||
);
|
||||
|
||||
cacheData("projectslist", updatedProjectsList);
|
||||
}
|
||||
|
||||
showToast("Project updated successfully.", "success");
|
||||
setLoading(false);
|
||||
setShowModal(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
showToast(error.message, "error");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`modal fade ${showModal ? 'show' : ''}`}
|
||||
className={`modal fade ${showModal ? "show" : ""}`}
|
||||
tabIndex="-1"
|
||||
role="dialog"
|
||||
style={{ display: showModal ? 'block' : 'none' }}
|
||||
style={{ display: showModal ? "block" : "none" }}
|
||||
aria-hidden={!showModal}
|
||||
>
|
||||
<ManageProjectInfo
|
||||
project={CurrentProject}
|
||||
handleSubmitForm={handleFormSubmit}
|
||||
onClose={handleClose}
|
||||
></ManageProjectInfo>
|
||||
<ManageProjectInfo
|
||||
project={CurrentProject}
|
||||
handleSubmitForm={handleFormSubmit}
|
||||
onClose={handleClose}
|
||||
></ManageProjectInfo>
|
||||
</div>
|
||||
{/* Project Banner */}
|
||||
<div className="col-12">
|
||||
@ -89,17 +84,23 @@ const ProjectBanner = ( {project_data} ) =>
|
||||
style={{ width: "40px", height: "40px" }}
|
||||
/>
|
||||
<h5 className="mb-0">
|
||||
{CurrentProject.name ? CurrentProject.name : "N/A"}
|
||||
{CurrentProject.name
|
||||
? CurrentProject.shortName
|
||||
? `${CurrentProject.name} (${CurrentProject.shortName})`
|
||||
: CurrentProject.name
|
||||
: "N/A"}
|
||||
</h5>
|
||||
</div>
|
||||
{manageProject && (
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm btn-primary ${!manageProject && 'd-none'}`}
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#edit-project-modal"
|
||||
onClick={handleShow}
|
||||
>
|
||||
type="button"
|
||||
className={`btn btn-sm btn-primary ${
|
||||
!manageProject && "d-none"
|
||||
}`}
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#edit-project-modal"
|
||||
onClick={handleShow}
|
||||
>
|
||||
Modify
|
||||
</button>
|
||||
)}
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
getProjectStatusName,
|
||||
} from "../../utils/projectStatus";
|
||||
|
||||
const ProjectCard = ({ projectData }) => {
|
||||
const ProjectCard = ({ projectData, recall }) => {
|
||||
const [projectInfo, setProjectInfo] = useState(projectData);
|
||||
const [projectDetails, setProjectDetails] = useState(null);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
@ -78,7 +78,7 @@ const ProjectCard = ({ projectData }) => {
|
||||
);
|
||||
cacheData("projectslist", updatedProjectsList);
|
||||
}
|
||||
|
||||
recall(getCachedData("projectslist"));
|
||||
showToast("Project updated successfully.", "success");
|
||||
setShowModal(false);
|
||||
})
|
||||
@ -88,7 +88,6 @@ const ProjectCard = ({ projectData }) => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{showModal && projectDetails && (
|
||||
@ -119,17 +118,16 @@ const ProjectCard = ({ projectData }) => {
|
||||
></i>
|
||||
</div>
|
||||
<div className="me-2">
|
||||
<h5 className="mb-0">
|
||||
<a
|
||||
className="stretched-link text-heading"
|
||||
onClick={handleViewProject}
|
||||
>
|
||||
{projectInfo.name}
|
||||
</a>
|
||||
<h5
|
||||
className="mb-0 stretched-link text-heading text-start"
|
||||
onClick={handleViewProject}
|
||||
>
|
||||
{projectInfo.shortName
|
||||
? projectInfo.shortName
|
||||
: projectInfo.name}
|
||||
</h5>
|
||||
<div className="client-info text-body">
|
||||
<span className="fw-medium">Client: </span>
|
||||
<span>{projectInfo.contactPerson}</span>
|
||||
<span>{projectInfo.shortName ? projectInfo.name : ""}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -141,9 +139,14 @@ const ProjectCard = ({ projectData }) => {
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
{modifyProjectLoading? <div class="spinner-border spinner-border-sm text-secondary" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div> :
|
||||
{modifyProjectLoading ? (
|
||||
<div
|
||||
className="spinner-border spinner-border-sm text-secondary"
|
||||
role="status"
|
||||
>
|
||||
<span className="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
) : (
|
||||
<i
|
||||
className="bx bx-dots-vertical-rounded bx-sm text-muted"
|
||||
data-bs-toggle="tooltip"
|
||||
@ -151,7 +154,8 @@ const ProjectCard = ({ projectData }) => {
|
||||
data-bs-placement="top"
|
||||
data-bs-custom-class="tooltip-dark"
|
||||
title="More Action"
|
||||
></i>}
|
||||
></i>
|
||||
)}
|
||||
</button>
|
||||
<ul className="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
@ -191,6 +195,12 @@ const ProjectCard = ({ projectData }) => {
|
||||
<div className="card-body pb-1">
|
||||
<div className="d-flex align-items-center flex-wrap">
|
||||
<div className="text-start mb-4">
|
||||
<p className="mb-1">
|
||||
<span className="text-heading fw-medium">
|
||||
Contact Person:{" "}
|
||||
</span>
|
||||
{projectInfo.contactPerson ? projectInfo.contactPerson : "NA"}
|
||||
</p>
|
||||
<p className="mb-1">
|
||||
<span className="text-heading fw-medium">Start Date: </span>
|
||||
{projectInfo.startDate
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import React from "react";
|
||||
import { hasUserPermission } from "../../utils/authUtils";
|
||||
import { useHasUserPermission } from "../../hooks/useHasUserPermission";
|
||||
import { VIEW_PROJECT_INFRA } from "../../utils/constants";
|
||||
import { DIRECTORY_ADMIN, DIRECTORY_MANAGER, DIRECTORY_USER, VIEW_PROJECT_INFRA } from "../../utils/constants";
|
||||
|
||||
const ProjectNav = ({ onPillClick, activePill }) => {
|
||||
const HasViewInfraStructure = useHasUserPermission(VIEW_PROJECT_INFRA);
|
||||
const HasViewInfraStructure = useHasUserPermission( VIEW_PROJECT_INFRA );
|
||||
const DirAdmin = useHasUserPermission(DIRECTORY_ADMIN);
|
||||
const DireManager = useHasUserPermission(DIRECTORY_MANAGER)
|
||||
const DirUser = useHasUserPermission(DIRECTORY_USER)
|
||||
|
||||
return (
|
||||
<div className="nav-align-top">
|
||||
@ -73,7 +76,8 @@ const ProjectNav = ({ onPillClick, activePill }) => {
|
||||
<i className="bx bxs-file-image bx-sm me-1_5"></i> <span className="d-none d-md-inline">Image Gallary</span>
|
||||
</a>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
{(DirAdmin || DireManager || DirUser) && (
|
||||
<li className="nav-item">
|
||||
<a
|
||||
className={`nav-link ${activePill === "directory" ? "active" : ""}`}
|
||||
href="#"
|
||||
@ -85,6 +89,8 @@ const ProjectNav = ({ onPillClick, activePill }) => {
|
||||
<i className='bx bxs-contact bx-sm me-1_5'></i> <span className="d-none d-md-inline">Directory</span>
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -1,90 +1,98 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
const FilterIcon = ({ taskListData, onApplyFilters, currentSelectedBuilding, currentSelectedFloors, currentSelectedActivities }) => {
|
||||
// State for filters, now managed within FilterIcon
|
||||
const [selectedBuilding, setSelectedBuilding] = useState(currentSelectedBuilding);
|
||||
const [selectedFloors, setSelectedFloors] = useState(currentSelectedFloors);
|
||||
const [selectedActivities, setSelectedActivities] = useState(currentSelectedActivities);
|
||||
const FilterIcon = ({
|
||||
taskListData,
|
||||
onApplyFilters,
|
||||
currentSelectedBuilding,
|
||||
currentSelectedFloors,
|
||||
currentSelectedActivities,
|
||||
}) => {
|
||||
const [selectedBuilding, setSelectedBuilding] = useState(currentSelectedBuilding || "");
|
||||
const [selectedFloors, setSelectedFloors] = useState(currentSelectedFloors || []);
|
||||
const [selectedActivities, setSelectedActivities] = useState(currentSelectedActivities || []);
|
||||
|
||||
// Update internal state when props change (e.g., project selection in DailyTask clears filters)
|
||||
useEffect(() => {
|
||||
setSelectedBuilding(currentSelectedBuilding);
|
||||
setSelectedFloors(currentSelectedFloors);
|
||||
setSelectedActivities(currentSelectedActivities);
|
||||
setSelectedBuilding(currentSelectedBuilding || "");
|
||||
setSelectedFloors(currentSelectedFloors || []);
|
||||
setSelectedActivities(currentSelectedActivities || []);
|
||||
}, [currentSelectedBuilding, currentSelectedFloors, currentSelectedActivities]);
|
||||
|
||||
// Helper to get unique values for filters based on current selections
|
||||
const getUniqueFilterValues = (key) => {
|
||||
const getUniqueFilterValues = (key, overrideBuilding, overrideFloors) => {
|
||||
if (!taskListData) return [];
|
||||
let relevantTasks = taskListData;
|
||||
|
||||
// Filter tasks based on selected building for floors and activities
|
||||
if (selectedBuilding) {
|
||||
relevantTasks = relevantTasks.filter(task =>
|
||||
task?.workItem?.workArea?.floor?.building?.name === selectedBuilding
|
||||
let filteredTasks = [...taskListData];
|
||||
|
||||
if (overrideBuilding) {
|
||||
filteredTasks = filteredTasks.filter(
|
||||
(task) =>
|
||||
task?.workItem?.workArea?.floor?.building?.name === overrideBuilding
|
||||
);
|
||||
}
|
||||
|
||||
// Filter tasks based on selected floors for activities
|
||||
if (selectedFloors.length > 0) {
|
||||
relevantTasks = relevantTasks.filter(task =>
|
||||
selectedFloors.includes(task?.workItem?.workArea?.floor?.floorName)
|
||||
if (overrideFloors?.length > 0) {
|
||||
filteredTasks = filteredTasks.filter((task) =>
|
||||
overrideFloors.includes(task?.workItem?.workArea?.floor?.floorName)
|
||||
);
|
||||
}
|
||||
|
||||
const values = relevantTasks.map(task => {
|
||||
if (key === 'building') return task?.workItem?.workArea?.floor?.building?.name;
|
||||
if (key === 'floor') return task?.workItem?.workArea?.floor?.floorName;
|
||||
if (key === 'activity') return task?.workItem?.activityMaster?.activityName;
|
||||
const values = filteredTasks.map((task) => {
|
||||
if (key === "building") return task?.workItem?.workArea?.floor?.building?.name;
|
||||
if (key === "floor") return task?.workItem?.workArea?.floor?.floorName;
|
||||
if (key === "activity") return task?.workItem?.activityMaster?.activityName;
|
||||
return null;
|
||||
}).filter(Boolean); // Remove null or undefined values
|
||||
return [...new Set(values)].sort(); // Sort for consistent order
|
||||
});
|
||||
|
||||
return [...new Set(values.filter(Boolean))].sort();
|
||||
};
|
||||
|
||||
const uniqueBuildings = getUniqueFilterValues('building');
|
||||
const uniqueFloors = getUniqueFilterValues('floor');
|
||||
const uniqueActivities = getUniqueFilterValues('activity');
|
||||
const uniqueBuildings = getUniqueFilterValues("building");
|
||||
const uniqueFloors = getUniqueFilterValues("floor", selectedBuilding);
|
||||
const uniqueActivities = getUniqueFilterValues("activity", selectedBuilding, selectedFloors);
|
||||
|
||||
// Handle filter selection with dependency logic
|
||||
const handleFilterChange = (filterType, value) => {
|
||||
let newSelectedBuilding = selectedBuilding;
|
||||
let newSelectedFloors = [...selectedFloors];
|
||||
let newSelectedActivities = [...selectedActivities];
|
||||
let updatedBuilding = selectedBuilding;
|
||||
let updatedFloors = [...selectedFloors];
|
||||
let updatedActivities = [...selectedActivities];
|
||||
|
||||
if (filterType === 'building') {
|
||||
if (selectedBuilding !== value) {
|
||||
newSelectedFloors = [];
|
||||
newSelectedActivities = [];
|
||||
if (filterType === "building") {
|
||||
updatedBuilding = value;
|
||||
updatedFloors = [];
|
||||
updatedActivities = [];
|
||||
} else if (filterType === "floor") {
|
||||
if (updatedFloors.includes(value)) {
|
||||
updatedFloors = updatedFloors.filter((floor) => floor !== value);
|
||||
} else {
|
||||
updatedFloors.push(value);
|
||||
}
|
||||
newSelectedBuilding = value;
|
||||
} else if (filterType === 'floor') {
|
||||
newSelectedFloors = selectedFloors.includes(value) ? selectedFloors.filter(item => item !== value) : [...selectedFloors, value];
|
||||
if (!newSelectedFloors.includes(value) && selectedFloors.includes(value)) {
|
||||
newSelectedActivities = [];
|
||||
|
||||
const validActivities = getUniqueFilterValues("activity", updatedBuilding, updatedFloors);
|
||||
updatedActivities = updatedActivities.filter((act) => validActivities.includes(act));
|
||||
} else if (filterType === "activity") {
|
||||
if (updatedActivities.includes(value)) {
|
||||
updatedActivities = updatedActivities.filter((act) => act !== value);
|
||||
} else {
|
||||
updatedActivities.push(value);
|
||||
}
|
||||
} else if (filterType === 'activity') {
|
||||
newSelectedActivities = selectedActivities.includes(value) ? selectedActivities.filter(item => item !== value) : [...selectedActivities, value];
|
||||
}
|
||||
|
||||
setSelectedBuilding(newSelectedBuilding);
|
||||
setSelectedFloors(newSelectedFloors);
|
||||
setSelectedActivities(newSelectedActivities);
|
||||
setSelectedBuilding(updatedBuilding);
|
||||
setSelectedFloors(updatedFloors);
|
||||
setSelectedActivities(updatedActivities);
|
||||
|
||||
// Communicate the updated filter states back to the parent
|
||||
onApplyFilters({
|
||||
selectedBuilding: newSelectedBuilding,
|
||||
selectedFloors: newSelectedFloors,
|
||||
selectedActivities: newSelectedActivities,
|
||||
selectedBuilding: updatedBuilding,
|
||||
selectedFloors: updatedFloors,
|
||||
selectedActivities: updatedActivities,
|
||||
});
|
||||
};
|
||||
|
||||
const clearAllFilters = () => {
|
||||
setSelectedBuilding('');
|
||||
setSelectedBuilding("");
|
||||
setSelectedFloors([]);
|
||||
setSelectedActivities([]);
|
||||
// Communicate cleared filters back to the parent
|
||||
|
||||
onApplyFilters({
|
||||
selectedBuilding: '',
|
||||
selectedBuilding: "",
|
||||
selectedFloors: [],
|
||||
selectedActivities: [],
|
||||
});
|
||||
@ -98,20 +106,19 @@ const FilterIcon = ({ taskListData, onApplyFilters, currentSelectedBuilding, cur
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<i className="fa-solid fa-filter bx-sm "></i>
|
||||
|
||||
<i
|
||||
className="fa-solid fa-filter bx-sm"
|
||||
style={{ color: selectedBuilding || selectedFloors.length > 0 || selectedActivities.length > 0 ? "#7161EF" : "gray" }}
|
||||
></i>
|
||||
|
||||
</a>
|
||||
<ul
|
||||
className="dropdown-menu p-2"
|
||||
aria-labelledby="filterDropdown"
|
||||
style={{
|
||||
minWidth: "360px",
|
||||
fontSize: "13px",
|
||||
}}
|
||||
// Prevent dropdown from closing when clicking inside it
|
||||
style={{ minWidth: "360px", fontSize: "13px" }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Building Filter - Now a Dropdown */}
|
||||
{/* Building */}
|
||||
<li>
|
||||
<div className="fw-bold text-dark mb-1">Building</div>
|
||||
<div className="row">
|
||||
@ -122,33 +129,26 @@ const FilterIcon = ({ taskListData, onApplyFilters, currentSelectedBuilding, cur
|
||||
onChange={(e) => handleFilterChange("building", e.target.value)}
|
||||
>
|
||||
<option value="">Select Building</option>
|
||||
{uniqueBuildings.length > 0 ? (
|
||||
uniqueBuildings.map((building, idx) => (
|
||||
<option key={`building-option-${idx}`} value={building}>
|
||||
{building}
|
||||
</option>
|
||||
))
|
||||
) : (
|
||||
<option value="" disabled>No buildings available</option>
|
||||
)}
|
||||
{uniqueBuildings.map((building, idx) => (
|
||||
<option key={idx} value={building}>
|
||||
{building}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{/* Floor Filter - Visible only if a building is selected */}
|
||||
{/* Floor */}
|
||||
{selectedBuilding && (
|
||||
<>
|
||||
<li><hr className="my-1" /></li>
|
||||
<li>
|
||||
<hr className="my-1" />
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<div className="fw-bold text-dark mb-1">Floor</div>
|
||||
<div className="fw-bold text-dark mb-1">Floors</div>
|
||||
<div className="row">
|
||||
{uniqueFloors.length > 0 ? (
|
||||
uniqueFloors.map((floor, idx) => (
|
||||
<div className="col-6" key={`floor-${idx}`}>
|
||||
<div className="col-6" key={idx}>
|
||||
<div className="form-check mb-1">
|
||||
<input
|
||||
className="form-check-input"
|
||||
@ -157,36 +157,30 @@ const FilterIcon = ({ taskListData, onApplyFilters, currentSelectedBuilding, cur
|
||||
checked={selectedFloors.includes(floor)}
|
||||
onChange={() => handleFilterChange("floor", floor)}
|
||||
/>
|
||||
<label
|
||||
className="form-check-label"
|
||||
htmlFor={`floor-${floor}`}
|
||||
>
|
||||
<label className="form-check-label" htmlFor={`floor-${floor}`}>
|
||||
{floor}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="col-12 text-muted">No floors for selected building.</div>
|
||||
<div className="col-12 text-muted">No floors found.</div>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Activity Filter - Visible only if a floor is selected */}
|
||||
{/* Activity */}
|
||||
{selectedFloors.length > 0 && (
|
||||
<>
|
||||
<li><hr className="my-1" /></li>
|
||||
<li>
|
||||
<hr className="my-1" />
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<div className="fw-bold text-dark mb-1">Activity</div>
|
||||
<div className="fw-bold text-dark mb-1">Activities</div>
|
||||
<div className="row">
|
||||
{uniqueActivities.length > 0 ? (
|
||||
uniqueActivities.map((activity, idx) => (
|
||||
<div className="col-6" key={`activity-${idx}`}>
|
||||
<div className="col-6" key={idx}>
|
||||
<div className="form-check mb-1">
|
||||
<input
|
||||
className="form-check-input"
|
||||
@ -195,43 +189,60 @@ const FilterIcon = ({ taskListData, onApplyFilters, currentSelectedBuilding, cur
|
||||
checked={selectedActivities.includes(activity)}
|
||||
onChange={() => handleFilterChange("activity", activity)}
|
||||
/>
|
||||
<label
|
||||
className="form-check-label"
|
||||
htmlFor={`activity-${activity}`}
|
||||
>
|
||||
<label className="form-check-label" htmlFor={`activity-${activity}`}>
|
||||
{activity}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="col-12 text-muted">No activities for selected floor(s).</div>
|
||||
<div className="col-12 text-muted">No activities found.</div>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Clear Filters */}
|
||||
{(selectedBuilding ||
|
||||
selectedFloors.length > 0 ||
|
||||
selectedActivities.length > 0) && (
|
||||
<>
|
||||
<li>
|
||||
<hr className="my-1" />
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
className="dropdown-item text-danger px-2 py-1"
|
||||
style={{ fontSize: "13px" }}
|
||||
onClick={clearAllFilters}
|
||||
>
|
||||
Clear All Filters
|
||||
</button>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
{/* Action Buttons */}
|
||||
<li><hr className="my-1" /></li>
|
||||
{(selectedBuilding || selectedFloors.length > 0 || selectedActivities.length > 0) && (
|
||||
<li className="d-flex justify-content-end gap-2 px-2">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm"
|
||||
style={{
|
||||
backgroundColor: "#7161EF",
|
||||
color: "white",
|
||||
fontSize: "13px",
|
||||
padding: "4px 16px",
|
||||
borderRadius: "8px",
|
||||
boxShadow: "0 1px 4px rgba(0,0,0,0.1)"
|
||||
}}
|
||||
onClick={clearAllFilters}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm"
|
||||
style={{
|
||||
backgroundColor: "#7161EF",
|
||||
color: "white",
|
||||
fontSize: "13px",
|
||||
padding: "4px 16px",
|
||||
borderRadius: "8px",
|
||||
boxShadow: "0 1px 4px rgba(0,0,0,0.1)"
|
||||
}}
|
||||
onClick={() => {
|
||||
document.getElementById("filterDropdown").click();
|
||||
}}
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
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,32 +8,45 @@ const GlobalModel = ({
|
||||
dialogClass = '', // For additional custom classes on modal dialog
|
||||
role = 'dialog', // Accessibility role for the modal
|
||||
size = '', // Dynamically set the size (sm, lg, xl)
|
||||
dataAttributes = {} // Additional dynamic data-bs-* attributes
|
||||
dataAttributes = {}, // Additional dynamic data-bs-* attributes
|
||||
IsCloseBtn=true
|
||||
}) => {
|
||||
const modalRef = useRef(null); // Reference to the modal element
|
||||
|
||||
useEffect(() => {
|
||||
const modalElement = modalRef.current;
|
||||
const modalInstance = new window.bootstrap.Modal(modalElement);
|
||||
useEffect(() => {
|
||||
const modalElement = modalRef.current;
|
||||
const modalInstance = new window.bootstrap.Modal(modalElement, {
|
||||
backdrop: false,
|
||||
});
|
||||
|
||||
// Show modal if isOpen is true
|
||||
if (isOpen) {
|
||||
modalInstance.show();
|
||||
} else {
|
||||
modalInstance.hide();
|
||||
}
|
||||
if (isOpen) {
|
||||
modalInstance.show();
|
||||
} else {
|
||||
modalInstance.hide();
|
||||
}
|
||||
|
||||
// Handle modal hide event to invoke the closeModal function
|
||||
const handleHideModal = () => {
|
||||
closeModal(); // Close the modal via React state
|
||||
};
|
||||
const handleHideModal = () => {
|
||||
closeModal();
|
||||
|
||||
// ✅ FIX: Remove any lingering body classes/styles
|
||||
document.body.classList.remove('modal-open');
|
||||
document.body.style.overflow = '';
|
||||
document.body.style.paddingRight = '';
|
||||
};
|
||||
|
||||
modalElement.addEventListener('hidden.bs.modal', handleHideModal);
|
||||
|
||||
return () => {
|
||||
modalElement.removeEventListener('hidden.bs.modal', handleHideModal);
|
||||
|
||||
// Also clean up just in case component unmounts
|
||||
document.body.classList.remove('modal-open');
|
||||
document.body.style.overflow = '';
|
||||
document.body.style.paddingRight = '';
|
||||
};
|
||||
}, [isOpen, closeModal]);
|
||||
|
||||
modalElement.addEventListener('hidden.bs.modal', handleHideModal);
|
||||
|
||||
return () => {
|
||||
modalElement.removeEventListener('hidden.bs.modal', handleHideModal);
|
||||
};
|
||||
}, [isOpen, closeModal]);
|
||||
|
||||
// Dynamically set the modal size classes (modal-sm, modal-lg, modal-xl)
|
||||
const modalSizeClass = size ? `modal-${size}` : ''; // Default is empty if no size is specified
|
||||
@ -41,7 +54,20 @@ const GlobalModel = ({
|
||||
// Dynamically generate data-bs attributes
|
||||
const dataAttributesProps = Object.keys(dataAttributes).map(key => ({
|
||||
[key]: dataAttributes[key],
|
||||
}));
|
||||
} ) );
|
||||
|
||||
|
||||
|
||||
// The gray background
|
||||
const backdropStyle = {
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: 'rgba(0,0,0,0.3)',
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -52,18 +78,19 @@ const GlobalModel = ({
|
||||
aria-hidden="true"
|
||||
ref={modalRef} // Assign the ref to the modal element
|
||||
{...dataAttributesProps}
|
||||
style={backdropStyle}
|
||||
>
|
||||
<div className={`modal-dialog ${dialogClass} ${modalSizeClass } mx-sm-auto mx-1`} role={role}>
|
||||
<div className={`modal-dialog ${dialogClass} ${modalSizeClass } mx-sm-auto mx-1`} role={role} >
|
||||
<div className="modal-content">
|
||||
<div className="modal-header p-0">
|
||||
{/* Close button inside the modal header */}
|
||||
<button
|
||||
{IsCloseBtn && <button
|
||||
type="button"
|
||||
className="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
onClick={closeModal} // Trigger the React closeModal function
|
||||
></button>
|
||||
></button>}
|
||||
</div>
|
||||
<div className="modal-body p-sm-4 p-0">
|
||||
{children} {/* Render children here, which can be the ReportTask component */}
|
||||
|
||||
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(() => {
|
||||
if (
|
||||
Array.isArray(watchedTags) &&
|
||||
JSON.stringify(tags) !== JSON.stringify(watchedTags)
|
||||
) {
|
||||
setTags(watchedTags);
|
||||
}
|
||||
}, [JSON.stringify(watchedTags)]);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(name, tags); // sync to form when tags change
|
||||
}, [tags, name, setValue]);
|
||||
if (input.trim() === "") {
|
||||
setSuggestions([]);
|
||||
} else {
|
||||
const filtered = options?.filter(
|
||||
(opt) =>
|
||||
opt?.name?.toLowerCase()?.includes(input.toLowerCase()) &&
|
||||
!tags?.some((tag) => tag.name === opt.name)
|
||||
);
|
||||
setSuggestions(filtered);
|
||||
}
|
||||
}, [input, options, tags]);
|
||||
|
||||
const addTag = (e) => {
|
||||
e.preventDefault();
|
||||
const trimmed = input.trim();
|
||||
if (trimmed !== "" && !tags.includes(trimmed)) {
|
||||
setTags([...tags, trimmed]);
|
||||
const addTag = async (tagObj) => {
|
||||
if (!tags.some((tag) => tag.name === tagObj.name)) {
|
||||
const cleanedTag = {
|
||||
id: tagObj.id ?? null,
|
||||
name: tagObj.name,
|
||||
};
|
||||
const newTags = [...tags, cleanedTag];
|
||||
setTags(newTags);
|
||||
setValue(name, newTags, { shouldValidate: true });
|
||||
await trigger(name);
|
||||
setInput("");
|
||||
setSuggestions([]);
|
||||
}
|
||||
};
|
||||
|
||||
const removeTag = (removeIndex) => {
|
||||
const updated = tags.filter((_, i) => i !== removeIndex);
|
||||
setTags(updated);
|
||||
const removeTag = (indexToRemove) => {
|
||||
const newTags = tags.filter((_, i) => i !== indexToRemove);
|
||||
setTags(newTags);
|
||||
setValue(name, newTags, { shouldValidate: true });
|
||||
trigger(name);
|
||||
};
|
||||
|
||||
const handleInputKeyDown = (e) => {
|
||||
if ((e.key === "Enter" || e.key === " ")&& input.trim() !== "") {
|
||||
e.preventDefault();
|
||||
const existing = options.find(
|
||||
(opt) => opt.name.toLowerCase() === input.trim().toLowerCase()
|
||||
);
|
||||
const newTag = existing
|
||||
? existing
|
||||
: {
|
||||
id: null,
|
||||
name: input.trim(),
|
||||
description: input.trim(),
|
||||
};
|
||||
addTag(newTag);
|
||||
} else if (e.key === "Backspace" && input === "") {
|
||||
setTags((prev) => prev.slice(0, -1));
|
||||
}
|
||||
};
|
||||
const handleInputKey = (e) => {
|
||||
const key = e.key?.toLowerCase();
|
||||
|
||||
if ((key === "enter" || key === " " || e.code === "Space") && input.trim() !== "") {
|
||||
e.preventDefault();
|
||||
const existing = options.find(
|
||||
(opt) => opt.name.toLowerCase() === input.trim().toLowerCase()
|
||||
);
|
||||
const newTag = existing
|
||||
? existing
|
||||
: {
|
||||
id: null,
|
||||
name: input.trim(),
|
||||
description: input.trim(),
|
||||
};
|
||||
addTag(newTag);
|
||||
} else if ((key === "backspace" || e.code === "Backspace") && input === "") {
|
||||
setTags((prev) => prev.slice(0, -1));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const handleSuggestionClick = (suggestion) => {
|
||||
addTag(suggestion);
|
||||
};
|
||||
|
||||
const backgroundColor = color || "#f8f9fa";
|
||||
@ -34,35 +108,77 @@ const TagInput = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<label htmlFor={name} className="form-label">{label}</label>
|
||||
<div className="form-control form-control-sm d-flex justify-content-start flex-wrap gap-1" style={{ minHeight: "12px" }}>
|
||||
{tags.map((tag, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="d-flex align-items-center"
|
||||
<label htmlFor={name} className="form-label">
|
||||
{label}
|
||||
</label>
|
||||
|
||||
<div
|
||||
className="form-control form-control-sm p-1"
|
||||
style={{ minHeight: "38px", position: "relative" }}
|
||||
>
|
||||
<div className="d-flex flex-wrap align-items-center gap-1">
|
||||
{tags.map((tag, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className="d-flex align-items-center"
|
||||
style={{
|
||||
color: iconColor,
|
||||
backgroundColor,
|
||||
padding: "2px 6px",
|
||||
borderRadius: "2px",
|
||||
fontSize: "0.85rem",
|
||||
}}
|
||||
>
|
||||
{tag.name}
|
||||
<i
|
||||
className="bx bx-x bx-xs ms-1"
|
||||
onClick={() => removeTag(index)}
|
||||
style={{ cursor: "pointer" }}
|
||||
/>
|
||||
</span>
|
||||
))}
|
||||
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
onKeyUp={handleInputKey}
|
||||
placeholder={placeholder}
|
||||
style={{
|
||||
color: iconColor,
|
||||
backgroundColor,
|
||||
padding: "2px 3px",
|
||||
borderRadius: "2px"
|
||||
border: "none",
|
||||
outline: "none",
|
||||
flex: 1,
|
||||
minWidth: "120px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{suggestions.length > 0 && (
|
||||
<ul
|
||||
className="list-group position-absolute mt-1 bg-white w-50 shadow-sm "
|
||||
style={{
|
||||
zIndex: 1000,
|
||||
maxHeight: "150px",
|
||||
overflowY: "auto",
|
||||
boxShadow:"0px 4px 10px rgba(0, 0, 0, 0.2)",borderRadius:"3px",border:"1px solid #ddd"
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
<i className="bx bx-x bx-xs ms-1" onClick={() => removeTag(i)}></i>
|
||||
</span>
|
||||
))}
|
||||
<input
|
||||
type="text"
|
||||
className="border-0 flex-grow-1"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => (e.key === "Enter" ? addTag(e) : null)}
|
||||
placeholder={placeholder}
|
||||
style={{ outline: "none", minWidth: "120px" }}
|
||||
/>
|
||||
{suggestions.map((sugg, i) => (
|
||||
<li
|
||||
key={i}
|
||||
className="dropdown-item p-1 hoverBox"
|
||||
onClick={() => handleSuggestionClick(sugg)}
|
||||
style={{cursor: "pointer", fontSize: "0.875rem"}}
|
||||
|
||||
>
|
||||
{sugg.name}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagInput;
|
||||
|
||||
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,13 +95,16 @@ const MasterModal = ({ modaldata, closeModal }) => {
|
||||
>
|
||||
<div className="modal-content">
|
||||
<div className="modal-body p-sm-4 p-0">
|
||||
<button
|
||||
<div className="d-flex justify-content-between">
|
||||
<h6>{ `${modaldata?.modalType} `}</h6>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-close"
|
||||
data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
onClick={closeModal}
|
||||
></button>
|
||||
</div>
|
||||
|
||||
{modaldata.modalType === "Application Role" && (
|
||||
<CreateRole masmodalType={modaldata.masterType} onClose={closeModal} />
|
||||
@ -125,6 +130,18 @@ const MasterModal = ({ modaldata, closeModal }) => {
|
||||
{modaldata.modalType === "Edit-Work Category" && (
|
||||
<EditWorkCategory data={modaldata.item} onClose={closeModal} />
|
||||
)}
|
||||
{modaldata.modalType === "Contact Category" && (
|
||||
<CreateCategory data={modaldata.item} onClose={closeModal} />
|
||||
)}
|
||||
{modaldata.modalType === "Edit-Contact Category" && (
|
||||
<EditContactCategory data={modaldata.item} onClose={closeModal} />
|
||||
)}
|
||||
{modaldata.modalType === "Contact Tag" && (
|
||||
<CreateContactTag data={modaldata.item} onClose={closeModal} />
|
||||
)}
|
||||
{modaldata.modalType === "Edit-Contact Tag" && (
|
||||
<EditContactTag data={modaldata.item} onClose={closeModal} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -47,10 +47,18 @@ const useMaster = (isMa) => {
|
||||
response = await MasterRespository.getActivites();
|
||||
response = response.data
|
||||
break;
|
||||
case "Work Category":
|
||||
case "Work Category":
|
||||
response = await MasterRespository.getWorkCategory();
|
||||
response = response.data
|
||||
break;
|
||||
case "Contact Category":
|
||||
response = await MasterRespository.getContactCategory();
|
||||
response = response.data
|
||||
break;
|
||||
case "Contact Tag":
|
||||
response = await MasterRespository.getContactTag();
|
||||
response = response.data
|
||||
break;
|
||||
case "Status":
|
||||
response = [{description: null,featurePermission: null,id: "02dd4761-363c-49ed-8851-3d2489a3e98d",status:"status 1"},{description: null,featurePermission: null,id: "03dy9761-363c-49ed-8851-3d2489a3e98d",status:"status 2"},{description: null,featurePermission: null,id: "03dy7761-263c-49ed-8851-3d2489a3e98d",status:"Status 3"}];
|
||||
break;
|
||||
@ -149,4 +157,67 @@ export const useActivitiesMaster = () =>
|
||||
}, [] )
|
||||
|
||||
return {categories,categoryLoading,categoryError}
|
||||
}
|
||||
}
|
||||
|
||||
export const useContactCategory = () =>
|
||||
{
|
||||
const [ contactCategory, setContactCategory ] = useState( [] )
|
||||
const [ loading, setLoading ] = useState( false )
|
||||
const [ Error, setError ] = useState()
|
||||
|
||||
const fetchConatctCategory = async() =>
|
||||
{
|
||||
const cache_Category = getCachedData( "Contact Category" );
|
||||
if ( !cache_Category )
|
||||
{
|
||||
try
|
||||
{
|
||||
let resp = await MasterRespository.getContactCategory();
|
||||
setContactCategory( resp.data );
|
||||
cacheData("Contact Category",resp.data)
|
||||
} catch ( error )
|
||||
{
|
||||
setError(error)
|
||||
}
|
||||
} else
|
||||
{
|
||||
setContactCategory(cache_Category)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect( () =>
|
||||
{
|
||||
fetchConatctCategory()
|
||||
}, [] )
|
||||
return { contactCategory,loading,Error}
|
||||
}
|
||||
export const useContactTags = () => {
|
||||
const [contactTags, setContactTags] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchContactTag = async () => {
|
||||
const cache_Tags = getCachedData("Contact Tag");
|
||||
|
||||
if (!cache_Tags) {
|
||||
setLoading(true);
|
||||
try {
|
||||
const resp = await MasterRespository.getContactTag();
|
||||
setContactTags(resp.data);
|
||||
cacheData("Contact Tag", resp.data);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
} else {
|
||||
setContactTags(cache_Tags);
|
||||
}
|
||||
};
|
||||
|
||||
fetchContactTag();
|
||||
}, []);
|
||||
|
||||
return { contactTags, loading, error };
|
||||
};
|
||||
179
src/hooks/useDirectory.js
Normal file
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 };
|
||||
};
|
||||
@ -164,4 +164,7 @@ padding: 1px !important;
|
||||
|
||||
.accordion-button:not(.collapsed) .toggle-icon {
|
||||
content: "\f146"; /* minus-circle */
|
||||
}
|
||||
.hoverBox:hover{
|
||||
background-color: #f1f3f5;
|
||||
}
|
||||
@ -4,25 +4,30 @@ import Header from "../components/Layout/Header";
|
||||
import Sidebar from "../components/Layout/Sidebar";
|
||||
|
||||
import Footer from "../components/Layout/Footer";
|
||||
import FloatingMenu from "../components/common/FloatingMenu";
|
||||
import { FabProvider } from "../Context/FabContext";
|
||||
|
||||
const HomeLayout = () => {
|
||||
useEffect(() => {
|
||||
Main();
|
||||
}, []);
|
||||
return (
|
||||
<div className="layout-wrapper layout-content-navbar" >
|
||||
<div className="layout-container" >
|
||||
<Sidebar />
|
||||
<div className="layout-page ">
|
||||
<Header />
|
||||
<div className="content-wrapper" >
|
||||
<Outlet />
|
||||
<Footer />
|
||||
<FabProvider>
|
||||
<div className="layout-wrapper layout-content-navbar">
|
||||
<div className="layout-container">
|
||||
<Sidebar />
|
||||
<div className="layout-page ">
|
||||
<Header />
|
||||
<div className="content-wrapper">
|
||||
<Outlet />
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
<FloatingMenu />
|
||||
<div className="layout-overlay layout-menu-toggle"></div>
|
||||
</div>
|
||||
<div className="layout-overlay layout-menu-toggle"></div>
|
||||
</div>
|
||||
</div>
|
||||
</FabProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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>}
|
||||
|
||||
|
||||
@ -26,20 +26,17 @@ const DailyTask = () => {
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
// State for filters (moved to FilterIcon, but we need to receive them here)
|
||||
const [filters, setFilters] = useState({
|
||||
selectedBuilding: "",
|
||||
selectedFloors: [],
|
||||
selectedActivities: [],
|
||||
});
|
||||
|
||||
// Sync projectId (either from URL or pick first accessible one)
|
||||
useEffect(() => {
|
||||
if (!project_loading && projects.length > 0 && !initialized) {
|
||||
if (projectIdFromUrl) {
|
||||
dispatch(setProjectId(projectIdFromUrl));
|
||||
} else if (selectedProject === 1 || selectedProject === undefined) {
|
||||
// If no project from URL or default/undefined, pick the first project
|
||||
dispatch(setProjectId(projects[0].id));
|
||||
}
|
||||
setInitialized(true);
|
||||
@ -57,7 +54,7 @@ const DailyTask = () => {
|
||||
|
||||
const {
|
||||
TaskList,
|
||||
loading: task_loading, // This `loading` state indicates if task data is being fetched
|
||||
loading: task_loading,
|
||||
error: task_error,
|
||||
refetch,
|
||||
} = useTaskList(
|
||||
@ -66,13 +63,11 @@ const DailyTask = () => {
|
||||
initialized ? dateRange.endDate : null
|
||||
);
|
||||
|
||||
const [TaskLists, setTaskLists] = useState([]); // This state holds the *filtered* tasks for display
|
||||
const [TaskLists, setTaskLists] = useState([]);
|
||||
const [dates, setDates] = useState([]);
|
||||
const popoverRefs = useRef([]);
|
||||
|
||||
// Effect to apply filters to TaskList (from useTaskList) and update TaskLists (filtered display)
|
||||
useEffect(() => {
|
||||
// Only filter if TaskList is available (not null or undefined)
|
||||
if (TaskList) {
|
||||
let filteredTasks = TaskList;
|
||||
|
||||
@ -101,8 +96,6 @@ const DailyTask = () => {
|
||||
}
|
||||
setTaskLists(filteredTasks);
|
||||
} else {
|
||||
// If TaskList is null (e.g., during initial load or project change before data arrives),
|
||||
// ensure TaskLists is also empty to avoid displaying stale data.
|
||||
setTaskLists([]);
|
||||
}
|
||||
}, [
|
||||
@ -137,52 +130,22 @@ const DailyTask = () => {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Ensure Bootstrap's Popover is initialized correctly
|
||||
popoverRefs.current.forEach((el) => {
|
||||
if (
|
||||
el &&
|
||||
window.bootstrap &&
|
||||
typeof window.bootstrap.Popover === "function"
|
||||
) {
|
||||
// Dispose existing popovers to prevent duplicates if component re-renders
|
||||
const existingPopover = window.bootstrap.Popover.getInstance(el);
|
||||
if (existingPopover) {
|
||||
existingPopover.dispose();
|
||||
}
|
||||
new window.bootstrap.Popover(el, {
|
||||
if (el) {
|
||||
new bootstrap.Popover(el, {
|
||||
trigger: "focus",
|
||||
placement: "left",
|
||||
html: true,
|
||||
content: el.getAttribute("data-bs-content"),
|
||||
content: el.getAttribute("data-bs-content"),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup function for popovers when component unmounts or dependencies change
|
||||
return () => {
|
||||
popoverRefs.current.forEach((el) => {
|
||||
if (
|
||||
el &&
|
||||
window.bootstrap &&
|
||||
typeof window.bootstrap.Popover === "function"
|
||||
) {
|
||||
const existingPopover = window.bootstrap.Popover.getInstance(el);
|
||||
if (existingPopover) {
|
||||
existingPopover.dispose();
|
||||
}
|
||||
}
|
||||
});
|
||||
popoverRefs.current = []; // Clear the refs array
|
||||
};
|
||||
}, [dates, TaskLists]); // Re-initialize popovers when tasks or dates change
|
||||
|
||||
// Handler for project selection
|
||||
},[dates, TaskLists]);
|
||||
|
||||
const handleProjectChange = (e) => {
|
||||
const newProjectId = e.target.value;
|
||||
dispatch(setProjectId(newProjectId));
|
||||
// --- IMPORTANT: Clear old data immediately to show loading state ---
|
||||
setTaskLists([]); // This makes the table empty, allowing the spinner to show
|
||||
// Reset filters when project changes (communicate to FilterIcon to clear)
|
||||
setTaskLists([]);
|
||||
setFilters({
|
||||
selectedBuilding: "",
|
||||
selectedFloors: [],
|
||||
@ -192,7 +155,6 @@ const DailyTask = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Report Task Modal */}
|
||||
<div
|
||||
className={`modal fade ${isModalOpen ? "show d-block" : ""}`}
|
||||
tabIndex="-1"
|
||||
@ -206,10 +168,8 @@ const DailyTask = () => {
|
||||
refetch={refetch}
|
||||
/>
|
||||
{isModalOpen && <div className="modal-backdrop fade show"></div>}{" "}
|
||||
{/* Add backdrop */}
|
||||
</div>
|
||||
|
||||
{/* Report Task Comments Modal */}
|
||||
<div
|
||||
className={`modal fade ${isModalOpenComment ? "show d-block" : ""}`}
|
||||
tabIndex="-1"
|
||||
@ -222,7 +182,6 @@ const DailyTask = () => {
|
||||
closeModal={closeCommentModal}
|
||||
/>
|
||||
{isModalOpenComment && <div className="modal-backdrop fade show"></div>}{" "}
|
||||
{/* Add backdrop */}
|
||||
</div>
|
||||
|
||||
<div className="container-xxl flex-grow-1 container-p-y">
|
||||
@ -242,15 +201,12 @@ const DailyTask = () => {
|
||||
DateDifference="6"
|
||||
dateFormat="DD-MM-YYYY"
|
||||
/>
|
||||
{/* FilterIcon component now manages its own filter states and logic */}
|
||||
<FilterIcon
|
||||
taskListData={TaskList} // Pass the raw TaskList to FilterIcon
|
||||
onApplyFilters={setFilters} // Callback to receive the filtered states from FilterIcon
|
||||
taskListData={TaskList}
|
||||
onApplyFilters={setFilters}
|
||||
currentSelectedBuilding={filters.selectedBuilding}
|
||||
currentSelectedFloors={filters.selectedFloors}
|
||||
currentSelectedActivities={filters.selectedActivities}
|
||||
// You can pass the project_loading state here if you want to disable filter during project load
|
||||
// isProjectLoading={project_loading}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-4 col-12 text-center mb-2 mb-md-0">
|
||||
@ -261,7 +217,7 @@ const DailyTask = () => {
|
||||
value={selectedProject || ""}
|
||||
onChange={handleProjectChange}
|
||||
aria-label="Select Project"
|
||||
disabled={project_loading} // Disable dropdown while projects are loading
|
||||
disabled={project_loading}
|
||||
>
|
||||
{project_loading && (
|
||||
<option value="" disabled>
|
||||
@ -296,7 +252,6 @@ const DailyTask = () => {
|
||||
<tr>
|
||||
<td colSpan={6} className="text-center">
|
||||
{" "}
|
||||
{/* ColSpan set to 6 based on your table headers */}
|
||||
<div className="mt-10 mb-10 pt-5 pb-10">
|
||||
<div
|
||||
className="spinner-border text-primary"
|
||||
@ -309,7 +264,6 @@ const DailyTask = () => {
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{/* --- "No Reports Found" message only if not loading and no tasks --- */}
|
||||
{!task_loading &&
|
||||
!project_loading &&
|
||||
TaskLists.length === 0 && (
|
||||
@ -317,20 +271,17 @@ const DailyTask = () => {
|
||||
<td colSpan={6} className="text-center">
|
||||
<div className="mt-10 mb-10 pt-10 pb-10">
|
||||
{" "}
|
||||
{/* ColSpan set to 6 */}
|
||||
<p>No Reports Found</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{/* --- Render tasks when not loading and tasks exist --- */}
|
||||
{!task_loading &&
|
||||
TaskLists.length > 0 &&
|
||||
dates.map((date, i) => {
|
||||
const tasksForDate = TaskLists.filter((task) =>
|
||||
task.assignmentDate.includes(date)
|
||||
);
|
||||
// Only render the date header if there are tasks for that date after filtering
|
||||
if (tasksForDate.length === 0) return null;
|
||||
|
||||
return (
|
||||
@ -338,14 +289,13 @@ const DailyTask = () => {
|
||||
<tr className="table-row-header">
|
||||
<td colSpan={6} className="text-start">
|
||||
{" "}
|
||||
{/* ColSpan set to 6 */}
|
||||
<strong>
|
||||
{moment(date).format("DD-MM-YYYY")}
|
||||
</strong>
|
||||
</td>
|
||||
</tr>
|
||||
{tasksForDate.map((task, index) => {
|
||||
const refIndex = `${i}-${index}`;
|
||||
const refIndex = index * 10 + i;
|
||||
return (
|
||||
<React.Fragment key={refIndex}>
|
||||
<tr>
|
||||
|
||||
@ -1,137 +1,478 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import Breadcrumb from "../../components/common/Breadcrumb";
|
||||
import IconButton from "../../components/common/IconButton";
|
||||
import GlobalModel from "../../components/common/GlobalModel";
|
||||
import ManageDirectory from "../../components/Directory/ManageDirectory";
|
||||
import ListViewDirectory from "../../components/Directory/ListViewDirectory";
|
||||
import { useBuckets, useDirectory } from "../../hooks/useDirectory";
|
||||
import { DirectoryRepository } from "../../repositories/DirectoryRepository";
|
||||
import { cacheData, getCachedData } from "../../slices/apiDataManager";
|
||||
import showToast from "../../services/toastService";
|
||||
import UpdateContact from "../../components/Directory/UpdateContact";
|
||||
import CardViewDirectory from "../../components/Directory/CardViewDirectory";
|
||||
import { useContactCategory } from "../../hooks/masterHook/useMaster";
|
||||
import usePagination from "../../hooks/usePagination";
|
||||
import { ITEMS_PER_PAGE } from "../../utils/constants";
|
||||
import ProfileContactDirectory from "../../components/Directory/ProfileContactDirectory";
|
||||
import ConfirmModal from "../../components/common/ConfirmModal";
|
||||
import DirectoryListTableHeader from "./DirectoryListTableHeader";
|
||||
import DirectoryPageHeader from "./DirectoryPageHeader";
|
||||
import ManageBucket from "../../components/Directory/ManageBucket";
|
||||
import { useFab } from "../../Context/FabContext";
|
||||
import { DireProvider, useDir } from "../../Context/DireContext";
|
||||
|
||||
const Directory = () => {
|
||||
const Directory = ({ IsPage = true, prefernceContacts }) => {
|
||||
const [projectPrefernce, setPerfence] = useState(null);
|
||||
const [IsActive, setIsActive] = useState(true);
|
||||
const [isOpenModal, setIsOpenModal] = useState(false);
|
||||
const closedModel = () => setIsOpenModal(false);
|
||||
const [isOpenModalNote, setIsOpenModalNote] = useState(false);
|
||||
const [selectedContact, setSelectedContact] = useState(null);
|
||||
const [open_contact, setOpen_contact] = useState(null);
|
||||
const [ContactList, setContactList] = useState([]);
|
||||
const [contactCategories, setContactCategories] = useState([]);
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [listView, setListView] = useState(false);
|
||||
const [selectedBucketIds, setSelectedBucketIds] = useState([]);
|
||||
const [deleteContact, setDeleteContact] = useState(null);
|
||||
const [IsDeleting, setDeleting] = useState(false);
|
||||
const [openBucketModal, setOpenBucketModal] = useState(false);
|
||||
|
||||
const [tempSelectedBucketIds, setTempSelectedBucketIds] = useState([]);
|
||||
const [tempSelectedCategoryIds, setTempSelectedCategoryIds] = useState([]);
|
||||
const { setActions } = useFab();
|
||||
const { dirActions, setDirActions } = useDir();
|
||||
|
||||
const { contacts, loading, refetch } = useDirectory(
|
||||
IsActive,
|
||||
projectPrefernce
|
||||
);
|
||||
const { contactCategory, loading: contactCategoryLoading } =
|
||||
useContactCategory();
|
||||
const { buckets, refetch: refetchBucket } = useBuckets();
|
||||
|
||||
const submitContact = async (data) => {
|
||||
try {
|
||||
let response;
|
||||
let updatedContacts;
|
||||
const contacts_cache = getCachedData("contacts")?.data || [];
|
||||
|
||||
if (selectedContact) {
|
||||
response = await DirectoryRepository.UpdateContact(data.id, data);
|
||||
updatedContacts = contacts_cache.map((contact) =>
|
||||
contact.id === data.id ? response.data : contact
|
||||
);
|
||||
showToast("Contact updated successfully", "success");
|
||||
setIsOpenModal(false);
|
||||
setSelectedContact(null);
|
||||
} else {
|
||||
response = await DirectoryRepository.CreateContact(data);
|
||||
updatedContacts = [...contacts_cache, response.data];
|
||||
showToast("Contact created successfully", "success");
|
||||
setIsOpenModal(false);
|
||||
}
|
||||
|
||||
// cacheData("Contacts", {data:updatedContacts,isActive:IsActive});
|
||||
// setContactList(updatedContacts);
|
||||
refetch(IsActive, prefernceContacts);
|
||||
refetchBucket();
|
||||
} catch (error) {
|
||||
const msg =
|
||||
error.response?.data?.message ||
|
||||
error.message ||
|
||||
"Error occurred during API call!";
|
||||
showToast(msg, "error");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteContact = async (overrideId = null) => {
|
||||
try {
|
||||
if (!IsActive) {
|
||||
setDirActions((prev) => ({ ...prev, action: true }));
|
||||
} else {
|
||||
setDeleting(true);
|
||||
}
|
||||
const id = overrideId || (!IsActive ? dirActions.id : deleteContact);
|
||||
if (!id) {
|
||||
showToast("No contact selected for deletion", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
await DirectoryRepository.DeleteContact(id, !IsActive);
|
||||
|
||||
const updatedContacts = ContactList.filter((c) => c.id !== id);
|
||||
setContactList(updatedContacts);
|
||||
cacheData("Contacts", { data: updatedContacts, isActive: IsActive });
|
||||
|
||||
showToast(
|
||||
`Contact ${IsActive ? "Deleted" : "Restored"} successfully`,
|
||||
"success"
|
||||
);
|
||||
|
||||
setDeleteContact(null);
|
||||
refetchBucket();
|
||||
setDirActions({ action: false, id: null });
|
||||
setDeleting(false);
|
||||
} catch (error) {
|
||||
const msg =
|
||||
error?.response?.data?.message ||
|
||||
error.message ||
|
||||
"Error occurred during API call";
|
||||
showToast(msg, "error");
|
||||
|
||||
setDeleting(false);
|
||||
setDirActions({ action: false, id: null });
|
||||
}
|
||||
};
|
||||
|
||||
const closedModel = () => {
|
||||
setIsOpenModal(false);
|
||||
setSelectedContact(null);
|
||||
setOpen_contact(null);
|
||||
};
|
||||
const [selectedCategoryIds, setSelectedCategoryIds] = useState(
|
||||
contactCategory.map((category) => category.id)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setContactList(contacts);
|
||||
|
||||
setTempSelectedCategoryIds([]);
|
||||
setTempSelectedBucketIds([]);
|
||||
}, [contacts]);
|
||||
|
||||
const usedCategoryIds = [
|
||||
...new Set(contacts.map((c) => c.contactCategory?.id)),
|
||||
];
|
||||
const filteredCategories = contactCategory.filter((category) =>
|
||||
usedCategoryIds.includes(category.id)
|
||||
);
|
||||
const handleTempBucketChange = (id) => {
|
||||
setTempSelectedBucketIds((prev) =>
|
||||
prev.includes(id) ? prev.filter((bid) => bid !== id) : [...prev, id]
|
||||
);
|
||||
};
|
||||
|
||||
const handleTempCategoryChange = (id) => {
|
||||
setTempSelectedCategoryIds((prev) =>
|
||||
prev.includes(id) ? prev.filter((cid) => cid !== id) : [...prev, id]
|
||||
);
|
||||
};
|
||||
|
||||
const usedBucketIds = [
|
||||
...new Set(contacts.flatMap((c) => c.bucketIds || [])),
|
||||
];
|
||||
|
||||
const filteredBuckets = buckets.filter((bucket) =>
|
||||
usedBucketIds.includes(bucket.id)
|
||||
);
|
||||
|
||||
const filteredContacts = useMemo(() => {
|
||||
return ContactList.filter((c) => {
|
||||
const matchesSearch =
|
||||
c.name.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
c.organization.toLowerCase().includes(searchText.toLowerCase());
|
||||
|
||||
const matchesCategory =
|
||||
selectedCategoryIds.length === 0 ||
|
||||
selectedCategoryIds.includes(c.contactCategory?.id);
|
||||
|
||||
const matchesBucket =
|
||||
selectedBucketIds.length === 0 ||
|
||||
(c.bucketIds || []).some((id) => selectedBucketIds.includes(id));
|
||||
|
||||
return matchesSearch && matchesCategory && matchesBucket;
|
||||
}).sort((a, b) => a.name.localeCompare(b.name));
|
||||
}, [
|
||||
ContactList,
|
||||
searchText,
|
||||
selectedCategoryIds,
|
||||
selectedBucketIds,
|
||||
selectedContact,
|
||||
]);
|
||||
|
||||
const applyFilter = () => {
|
||||
setSelectedBucketIds(tempSelectedBucketIds);
|
||||
setSelectedCategoryIds(tempSelectedCategoryIds);
|
||||
};
|
||||
|
||||
const clearFilter = () => {
|
||||
setTempSelectedBucketIds([]);
|
||||
setTempSelectedCategoryIds([]);
|
||||
setSelectedBucketIds([]);
|
||||
setSelectedCategoryIds([]);
|
||||
};
|
||||
|
||||
const { currentPage, totalPages, currentItems, paginate } = usePagination(
|
||||
filteredContacts,
|
||||
ITEMS_PER_PAGE
|
||||
);
|
||||
|
||||
const renderModalContent = () => {
|
||||
if (selectedContact) {
|
||||
return (
|
||||
<UpdateContact
|
||||
existingContact={selectedContact}
|
||||
submitContact={submitContact}
|
||||
onCLosed={closedModel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!open_contact) {
|
||||
return (
|
||||
<ManageDirectory submitContact={submitContact} onCLosed={closedModel} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const actions = [];
|
||||
|
||||
if (IsPage) {
|
||||
actions.push({
|
||||
label: "Manage Bucket",
|
||||
icon: "fa-solid fa-bucket fs-5",
|
||||
color: "primary",
|
||||
onClick: () => setOpenBucketModal(true),
|
||||
});
|
||||
}
|
||||
if (buckets?.length > 0) {
|
||||
actions.push({
|
||||
label: "New Contact",
|
||||
icon: "bx bx-plus-circle",
|
||||
color: "warning",
|
||||
onClick: () => setIsOpenModal(true),
|
||||
});
|
||||
}
|
||||
|
||||
setActions(actions);
|
||||
|
||||
return () => setActions([]);
|
||||
}, [IsPage, buckets]);
|
||||
useEffect(() => {
|
||||
setPerfence(prefernceContacts);
|
||||
}, [prefernceContacts]);
|
||||
|
||||
return (
|
||||
<div className="container-xxl flex-grow-1 container-p-y">
|
||||
<Breadcrumb
|
||||
data={[
|
||||
{ label: "Home", link: "/dashboard" },
|
||||
{ label: "Directory (Comming Soon)", link: null },
|
||||
]}
|
||||
></Breadcrumb>
|
||||
{IsPage && (
|
||||
<Breadcrumb
|
||||
data={[
|
||||
{ label: "Home", link: "/dashboard" },
|
||||
{ label: "Directory", link: null },
|
||||
]}
|
||||
></Breadcrumb>
|
||||
)}
|
||||
|
||||
<GlobalModel isOpen={isOpenModal} closeModal={closedModel}>
|
||||
<ManageDirectory />
|
||||
</GlobalModel>
|
||||
|
||||
<div className="row">
|
||||
<div className="row mx-0 px-0">
|
||||
<div className="col-md-4 col-6 flex-grow-1 mb-2 px-1">
|
||||
<input
|
||||
type="search"
|
||||
className="form-control form-control-sm"
|
||||
placeholder="Search projects..."
|
||||
{isOpenModal && (
|
||||
<GlobalModel
|
||||
isOpen={isOpenModal}
|
||||
closeModal={() => {
|
||||
setSelectedContact(null);
|
||||
setIsOpenModal(false);
|
||||
}}
|
||||
size="xl"
|
||||
>
|
||||
{renderModalContent()}
|
||||
</GlobalModel>
|
||||
)}
|
||||
{isOpenModalNote && (
|
||||
<GlobalModel
|
||||
isOpen={isOpenModalNote}
|
||||
closeModal={() => {
|
||||
setOpen_contact(null);
|
||||
setIsOpenModalNote(false);
|
||||
}}
|
||||
size="xl"
|
||||
>
|
||||
{open_contact && (
|
||||
<ProfileContactDirectory
|
||||
contact={open_contact}
|
||||
setOpen_contact={setOpen_contact}
|
||||
closeModal={() => setIsOpenModalNote(false)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-8 col-6 text-end flex-grow-1 mb-2 px-1">
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm btn-primary `}
|
||||
onClick={() => setIsOpenModal(true)}
|
||||
>
|
||||
<i className="bx bx-plus-circle me-2"></i>
|
||||
New Contact
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</GlobalModel>
|
||||
)}
|
||||
{deleteContact && (
|
||||
<div
|
||||
className={`modal fade ${deleteContact ? "show" : ""}`}
|
||||
tabIndex="-1"
|
||||
role="dialog"
|
||||
style={{
|
||||
display: deleteContact ? "block" : "none",
|
||||
backgroundColor: deleteContact ? "rgba(0,0,0,0.5)" : "transparent",
|
||||
}}
|
||||
aria-hidden="false"
|
||||
>
|
||||
<ConfirmModal
|
||||
type={"delete"}
|
||||
header={"Delete Contact"}
|
||||
message={"Are you sure you want delete?"}
|
||||
onSubmit={handleDeleteContact}
|
||||
onClose={() => setDeleteContact(null)}
|
||||
loading={IsDeleting}
|
||||
/>
|
||||
</div>
|
||||
<div className="table-responsive text-nowrap py-2 ">
|
||||
<table className="table px-2">
|
||||
<thead>
|
||||
)}
|
||||
|
||||
{openBucketModal && (
|
||||
<GlobalModel
|
||||
isOpen={openBucketModal}
|
||||
closeModal={() => setOpenBucketModal(false)}
|
||||
size="lg"
|
||||
>
|
||||
<ManageBucket buckets={buckets} />
|
||||
</GlobalModel>
|
||||
)}
|
||||
|
||||
<div className="card p-2 card-minHeight">
|
||||
<DirectoryPageHeader
|
||||
searchText={searchText}
|
||||
setSearchText={setSearchText}
|
||||
setIsActive={setIsActive}
|
||||
listView={listView}
|
||||
setListView={setListView}
|
||||
filteredBuckets={filteredBuckets}
|
||||
tempSelectedBucketIds={tempSelectedBucketIds}
|
||||
handleTempBucketChange={handleTempBucketChange}
|
||||
filteredCategories={filteredCategories}
|
||||
tempSelectedCategoryIds={tempSelectedCategoryIds}
|
||||
handleTempCategoryChange={handleTempCategoryChange}
|
||||
clearFilter={clearFilter}
|
||||
applyFilter={applyFilter}
|
||||
loading={loading}
|
||||
IsActive={IsActive}
|
||||
setOpenBucketModal={setOpenBucketModal}
|
||||
/>
|
||||
|
||||
{/* Messages when listView is false */}
|
||||
{!listView && (
|
||||
<div className="d-flex flex-column justify-content-center align-items-center text-center ">
|
||||
{loading && <p className="mt-10">Loading...</p>}
|
||||
{!loading && contacts?.length === 0 && (
|
||||
<p className="mt-10">No contact found</p>
|
||||
)}
|
||||
{!loading && contacts?.length > 0 && currentItems.length === 0 && (
|
||||
<p className="mt-10">No matching contact found</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Table view (listView === true) */}
|
||||
{listView ? (
|
||||
<DirectoryListTableHeader>
|
||||
{loading && (
|
||||
<tr>
|
||||
<th className="text-start" colSpan="2">
|
||||
Name
|
||||
</th>
|
||||
<th className="px-2">
|
||||
<div className="d-flex align-items-center gap-1">
|
||||
<IconButton
|
||||
size={12}
|
||||
iconClass="bx bx-envelope"
|
||||
color="primary"
|
||||
onClick={() => alert("User icon clicked")}
|
||||
/>
|
||||
<span>Email</span>
|
||||
</div>
|
||||
</th>
|
||||
<th className="mx-2">
|
||||
<div className="d-flex align-items-center m-0 p-0 gap-1">
|
||||
<IconButton
|
||||
size={12}
|
||||
iconClass="bx bx-phone"
|
||||
color="warning"
|
||||
onClick={() => alert("User icon clicked")}
|
||||
/>
|
||||
<span>Phone</span>
|
||||
</div>
|
||||
</th>
|
||||
<th className="mx-2">
|
||||
<div className="d-flex align-items-center gap-1">
|
||||
<IconButton
|
||||
size={12}
|
||||
iconClass="bx bxs-grid-alt"
|
||||
color="info"
|
||||
/>
|
||||
<span>Organization</span>
|
||||
</div>
|
||||
</th>
|
||||
<th className="mx-2">
|
||||
<div className="dropdown">
|
||||
<a
|
||||
className="dropdown-toggle hide-arrow cursor-pointer align-items-center"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
Type <i className="bx bx-filter bx-sm"></i>
|
||||
</a>
|
||||
{/* <ul className="dropdown-menu p-2 text-capitalize">
|
||||
{[
|
||||
{ id: 1, label: "Active" },
|
||||
{ id: 2, label: "On Hold" },
|
||||
{ id: 3, label: "Inactive" },
|
||||
{ id: 4, label: "Completed" },
|
||||
].map(({ id, label }) => (
|
||||
<li key={id}>
|
||||
<div className="form-check">
|
||||
<input
|
||||
className="form-check-input "
|
||||
type="checkbox"
|
||||
checked={selectedStatuses.includes(id)}
|
||||
onChange={() => handleStatusChange(id)}
|
||||
/>
|
||||
<label className="form-check-label">
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
*/}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
// className={`mx-2 ${
|
||||
// HasManageProject ? "d-sm-table-cell" : "d-none"
|
||||
// }`}
|
||||
>
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="table-border-bottom-0 overflow-auto ">
|
||||
<tr>
|
||||
<td colSpan="12" className="text-center py-4">
|
||||
comming soon....
|
||||
<td colSpan={10}>
|
||||
{" "}
|
||||
<p className="mt-10">Loading...</p>{" "}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && contacts?.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={10}>
|
||||
<p className="mt-10">No contact found</p>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
{!loading && currentItems.length === 0 && contacts?.length > 0 && (
|
||||
<tr>
|
||||
<td colSpan={10}>
|
||||
<p className="mt-10">No matching contact found</p>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
{!loading &&
|
||||
currentItems.map((contact) => (
|
||||
<ListViewDirectory
|
||||
key={contact.id}
|
||||
IsActive={IsActive}
|
||||
contact={contact}
|
||||
setSelectedContact={setSelectedContact}
|
||||
setIsOpenModal={setIsOpenModal}
|
||||
setOpen_contact={setOpen_contact}
|
||||
setIsOpenModalNote={setIsOpenModalNote}
|
||||
IsDeleted={setDeleteContact}
|
||||
restore={handleDeleteContact}
|
||||
/>
|
||||
))}
|
||||
</DirectoryListTableHeader>
|
||||
) : (
|
||||
<div className="row mt-5">
|
||||
{!loading &&
|
||||
currentItems.map((contact) => (
|
||||
<div
|
||||
key={contact.id}
|
||||
className="col-12 col-sm-6 col-md-4 col-lg-4 mb-4"
|
||||
>
|
||||
<CardViewDirectory
|
||||
IsActive={IsActive}
|
||||
contact={contact}
|
||||
setSelectedContact={setSelectedContact}
|
||||
setIsOpenModal={setIsOpenModal}
|
||||
setOpen_contact={setOpen_contact}
|
||||
setIsOpenModalNote={setIsOpenModalNote}
|
||||
IsDeleted={setDeleteContact}
|
||||
restore={handleDeleteContact}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pagination */}
|
||||
{!loading &&
|
||||
contacts?.length > 0 &&
|
||||
currentItems.length > ITEMS_PER_PAGE && (
|
||||
<nav aria-label="Page navigation">
|
||||
<ul className="pagination pagination-sm justify-content-end py-1">
|
||||
<li
|
||||
className={`page-item ${currentPage === 1 ? "disabled" : ""}`}
|
||||
>
|
||||
<button
|
||||
className="page-link btn-xs"
|
||||
onClick={() => paginate(currentPage - 1)}
|
||||
>
|
||||
«
|
||||
</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;
|
||||
@ -212,15 +212,16 @@ const ChangePasswordPage = ({ onClose }) => {
|
||||
<p className="p-0 m-0">Password must be:</p>
|
||||
<p className="p-0 m-0">- at least 8 characters long</p>
|
||||
<p className="p-0 m-0">
|
||||
- must contain at least one uppercase letter
|
||||
- must contain one uppercase, one lowercase letter, at least one
|
||||
number, at least one special character
|
||||
</p>
|
||||
<p className="p-0 m-0">
|
||||
{/* <p className="p-0 m-0">
|
||||
- must contain at least one lowercase letter
|
||||
</p>
|
||||
<p className="p-0 m-0">- must contain at least one number</p>
|
||||
<p className="p-0 m-0">
|
||||
- must contain at least one special character
|
||||
</p>
|
||||
</p> */}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
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 && (
|
||||
@ -213,7 +220,8 @@ const EmployeeList = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>)}
|
||||
</div> )}
|
||||
|
||||
|
||||
{IsDeleteModalOpen && (
|
||||
<div
|
||||
@ -240,6 +248,11 @@ const EmployeeList = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{IsOpenAsssingModal && ( <GlobalModel isOpen={IsOpenAsssingModal} closeModal={()=>setOpenAssignModal(false)}>
|
||||
<AssignToProject employee={selectedEmployee} onClose={() => setOpenAssignModal( false )} />
|
||||
</GlobalModel>)}
|
||||
|
||||
|
||||
<div className="container-xxl flex-grow-1 container-p-y">
|
||||
<Breadcrumb
|
||||
data={[
|
||||
@ -642,6 +655,19 @@ const EmployeeList = () => {
|
||||
<i className="bx bx-cog bx-sm"></i>{" "}
|
||||
Manage Role
|
||||
</button>
|
||||
<button
|
||||
className="dropdown-item py-1"
|
||||
|
||||
onClick={() =>
|
||||
{
|
||||
setSelectEmployee( item ),
|
||||
setOpenAssignModal(true)
|
||||
}
|
||||
}
|
||||
>
|
||||
<i className='bx bx-select-multiple'></i>{" "}
|
||||
Assign Project
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -10,6 +10,8 @@ import {
|
||||
useEmployees,
|
||||
useEmployeesByProject,
|
||||
} from "../../hooks/useEmployees";
|
||||
import { useProfile } from "../../hooks/useProfile";
|
||||
|
||||
import { useSelector } from "react-redux";
|
||||
import EmployeeRepository from "../../repositories/EmployeeRepository";
|
||||
import { ComingSoonPage } from "../Misc/ComingSoonPage";
|
||||
@ -17,7 +19,11 @@ import { useNavigate } from "react-router-dom";
|
||||
import Avatar from "../../components/common/Avatar";
|
||||
import AttendancesEmployeeRecords from "./AttendancesEmployeeRecords";
|
||||
import ManageEmployee from "../../components/Employee/ManageEmployee";
|
||||
import { useChangePassword } from "../../components/Context/ChangePasswordContext";
|
||||
|
||||
const EmployeeProfile = () => {
|
||||
const { profile } = useProfile();
|
||||
|
||||
const projectID = useSelector((store) => store.localVariables.projectId);
|
||||
const { employeeId } = useParams();
|
||||
// const {employee,loading} = useEmployeeProfile(employeeId)
|
||||
@ -27,17 +33,17 @@ const EmployeeProfile = () => {
|
||||
const tab = SearchParams.get("for");
|
||||
const [activePill, setActivePill] = useState(tab);
|
||||
const [currentEmployee, setCurrentEmployee] = useState();
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
const handlePillClick = (pillKey) => {
|
||||
setActivePill(pillKey);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setShowModal(false);
|
||||
fetchEmployeeProfile(employeeId);
|
||||
};
|
||||
const handleShow = () => setShowModal(true);
|
||||
const closeModal = () => {
|
||||
setShowModal(false);
|
||||
fetchEmployeeProfile(employeeId);
|
||||
};
|
||||
const handleShow = () => setShowModal(true);
|
||||
|
||||
const fetchEmployeeProfile = async (employeeID) => {
|
||||
try {
|
||||
@ -95,179 +101,190 @@ const EmployeeProfile = () => {
|
||||
if (loading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
const { openChangePassword } = useChangePassword();
|
||||
return (
|
||||
<> {showModal && (<div
|
||||
className={`modal fade ${showModal ? "show" : ""} `}
|
||||
tabIndex="-1"
|
||||
role="dialog"
|
||||
style={{ display: showModal ? "block" : "none" }}
|
||||
aria-hidden={!showModal}
|
||||
>
|
||||
<div className="modal-dialog modal-xl modal-dialog-centered ">
|
||||
<div
|
||||
className="modal-content overflow-y-auto overflow-x-hidden"
|
||||
style={{ maxHeight: "90vh" }}
|
||||
>
|
||||
<ManageEmployee
|
||||
employeeId={employeeId}
|
||||
onClosed={closeModal}
|
||||
/>
|
||||
<>
|
||||
{" "}
|
||||
{showModal && (
|
||||
<div
|
||||
className={`modal fade ${showModal ? "show" : ""} `}
|
||||
tabIndex="-1"
|
||||
role="dialog"
|
||||
style={{ display: showModal ? "block" : "none" }}
|
||||
aria-hidden={!showModal}
|
||||
>
|
||||
<div className="modal-dialog modal-xl modal-dialog-centered ">
|
||||
<div
|
||||
className="modal-content overflow-y-auto overflow-x-hidden"
|
||||
style={{ maxHeight: "90vh" }}
|
||||
>
|
||||
<ManageEmployee employeeId={employeeId} onClosed={closeModal} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>)}
|
||||
)}
|
||||
<div className="container-xxl flex-grow-1 container-p-y">
|
||||
<Breadcrumb
|
||||
data={[
|
||||
{ label: "Home", link: "/dashboard" },
|
||||
{ label: "Employees", link: "/employees" },
|
||||
{ label: "Profile", link: null },
|
||||
]}
|
||||
></Breadcrumb>
|
||||
|
||||
<div className="container-xxl flex-grow-1 container-p-y">
|
||||
<Breadcrumb
|
||||
data={[
|
||||
{ label: "Home", link: "/dashboard" },
|
||||
{ label: "Employees", link: "/employees" },
|
||||
{ label: "Profile", link: null },
|
||||
]}
|
||||
></Breadcrumb>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-8 col-lg-4 order-1 order-lg-1">
|
||||
<div className="row">
|
||||
<div className="col-12 mb-4">
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<div className="d-flex flex-row flex-lg-column">
|
||||
<div className="d-flex flex-column justify-content-center align-items-center text-center">
|
||||
<Avatar
|
||||
firstName={`${currentEmployee?.firstName}`}
|
||||
lastName={`${currentEmployee?.lastName}`}
|
||||
size={"lg"}
|
||||
/>
|
||||
<div className="py-2">
|
||||
<p className="h6">{`${currentEmployee?.firstName} ${currentEmployee?.lastName}`}</p>
|
||||
<div className="row">
|
||||
<div className="col-12 col-md-8 col-lg-4 order-1 order-lg-1">
|
||||
<div className="row">
|
||||
<div className="col-12 mb-4">
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<div className="d-flex flex-row flex-lg-column">
|
||||
<div className="d-flex flex-column justify-content-center align-items-center text-center">
|
||||
<Avatar
|
||||
firstName={`${currentEmployee?.firstName}`}
|
||||
lastName={`${currentEmployee?.lastName}`}
|
||||
size={"lg"}
|
||||
/>
|
||||
<div className="py-2">
|
||||
<p className="h6">{`${currentEmployee?.firstName} ${currentEmployee?.lastName}`}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-100 d-flex flex-column justify-content-start">
|
||||
<div className="mt-3 w-100">
|
||||
<h6 className="mb-2 text-muted text-start">
|
||||
Employee Info
|
||||
</h6>
|
||||
<table className="table table-borderless mb-3">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="fw-medium text-start">Email:</td>
|
||||
<td className="text-start">
|
||||
{currentEmployee?.email || <em>NA</em>}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="fw-medium text-start">
|
||||
Phone Number:
|
||||
</td>
|
||||
<td className="text-start">
|
||||
{currentEmployee?.phoneNumber || <em>NA</em>}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="fw-medium text-start">
|
||||
Emergency Contact Person:
|
||||
</td>
|
||||
<td className="text-start">
|
||||
{currentEmployee?.emergencyContactPerson || (
|
||||
<em>NA</em>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="fw-medium text-start">
|
||||
Emergency Contact Number:
|
||||
</td>
|
||||
<td className="text-start">
|
||||
{currentEmployee?.emergencyPhoneNumber || (
|
||||
<em>NA</em>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<div className="w-100 d-flex flex-column justify-content-start">
|
||||
<div className="mt-3 w-100">
|
||||
<h6 className="mb-2 text-muted text-start">
|
||||
Employee Info
|
||||
</h6>
|
||||
<table className="table table-borderless mb-3">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td className="fw-medium text-start">Email:</td>
|
||||
<td className="text-start">
|
||||
{currentEmployee?.email || <em>NA</em>}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="fw-medium text-start">
|
||||
Phone Number:
|
||||
</td>
|
||||
<td className="text-start">
|
||||
{currentEmployee?.phoneNumber || <em>NA</em>}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="fw-medium text-start">
|
||||
Emergency Contact Person:
|
||||
</td>
|
||||
<td className="text-start">
|
||||
{currentEmployee?.emergencyContactPerson || (
|
||||
<em>NA</em>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="fw-medium text-start">
|
||||
Emergency Contact Number:
|
||||
</td>
|
||||
<td className="text-start">
|
||||
{currentEmployee?.emergencyPhoneNumber || (
|
||||
<em>NA</em>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td className="fw-medium text-start">Gender:</td>
|
||||
<td className="text-start">
|
||||
{currentEmployee?.gender || <em>NA</em>}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="fw-medium text-start">
|
||||
Birth Date:
|
||||
</td>
|
||||
<td className="text-start">
|
||||
{currentEmployee?.birthDate ? (
|
||||
new Date(
|
||||
currentEmployee.birthDate
|
||||
).toLocaleDateString()
|
||||
) : (
|
||||
<em>NA</em>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="fw-medium text-start">
|
||||
Gender:
|
||||
</td>
|
||||
<td className="text-start">
|
||||
{currentEmployee?.gender || <em>NA</em>}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="fw-medium text-start">
|
||||
Birth Date:
|
||||
</td>
|
||||
<td className="text-start">
|
||||
{currentEmployee?.birthDate ? (
|
||||
new Date(
|
||||
currentEmployee.birthDate
|
||||
).toLocaleDateString()
|
||||
) : (
|
||||
<em>NA</em>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td className="fw-medium text-start">
|
||||
Joining Date:
|
||||
</td>
|
||||
<td className="text-start">
|
||||
{currentEmployee?.joiningDate ? (
|
||||
new Date(
|
||||
currentEmployee.joiningDate
|
||||
).toLocaleDateString()
|
||||
) : (
|
||||
<em>NA</em>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="fw-medium text-start">
|
||||
Job Role:
|
||||
</td>
|
||||
<td className="text-start">
|
||||
{currentEmployee?.jobRole || <em>NA</em>}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="fw-medium text-start">Address:</td>
|
||||
<td className="text-start">
|
||||
{currentEmployee?.currentAddress || <em>NA</em>}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<tr>
|
||||
<td className="fw-medium text-start">
|
||||
Joining Date:
|
||||
</td>
|
||||
<td className="text-start">
|
||||
{currentEmployee?.joiningDate ? (
|
||||
new Date(
|
||||
currentEmployee.joiningDate
|
||||
).toLocaleDateString()
|
||||
) : (
|
||||
<em>NA</em>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="fw-medium text-start">
|
||||
Job Role:
|
||||
</td>
|
||||
<td className="text-start">
|
||||
{currentEmployee?.jobRole || <em>NA</em>}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className="fw-medium text-start">
|
||||
Address:
|
||||
</td>
|
||||
<td className="text-start">
|
||||
{currentEmployee?.currentAddress || (
|
||||
<em>NA</em>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-primary btn-block"
|
||||
onClick={() => handleShow()}
|
||||
>
|
||||
Edit Profile
|
||||
</button>
|
||||
{currentEmployee?.id == profile?.employeeInfo?.id && (
|
||||
<button
|
||||
className="btn btn-outline-primary btn-block mt-2"
|
||||
onClick={() => openChangePassword()}
|
||||
>
|
||||
Change Password
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-primary btn-block"
|
||||
onClick={() =>
|
||||
handleShow()
|
||||
}
|
||||
>
|
||||
Edit Profile
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-12 col-lg-8 order-2 order-lg-2 mb-4">
|
||||
<div className="row">
|
||||
<EmployeeNav
|
||||
onPillClick={handlePillClick}
|
||||
activePill={activePill}
|
||||
/>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="row row-bordered g-0">{renderContent()}</div>
|
||||
<div className="col-12 col-lg-8 order-2 order-lg-2 mb-4">
|
||||
<div className="row">
|
||||
<EmployeeNav
|
||||
onPillClick={handlePillClick}
|
||||
activePill={activePill}
|
||||
/>
|
||||
</div>
|
||||
<div className="card">
|
||||
<div className="row row-bordered g-0">{renderContent()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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",
|
||||
@ -37,7 +37,7 @@ const ProjectList = () => {
|
||||
const handleShow = () => setShowModal(true);
|
||||
const handleClose = () => setShowModal(false);
|
||||
|
||||
useEffect(() => {
|
||||
const sortingProject = (projects) =>{
|
||||
if (!loading && Array.isArray(projects)) {
|
||||
const grouped = {};
|
||||
projects.forEach((project) => {
|
||||
@ -56,6 +56,10 @@ const ProjectList = () => {
|
||||
|
||||
setProjectList(sortedGrouped);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
sortingProject(projects)
|
||||
}, [projects, loginUser?.projects, loading]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -75,6 +79,7 @@ const ProjectList = () => {
|
||||
setProjectList( ( prev ) => [ ...prev, response.data ] );
|
||||
setloading( false )
|
||||
reset()
|
||||
sortingProject(getCachedData("projectslist"))
|
||||
showToast("Project Created successfully.", "success");
|
||||
setShowModal(false);
|
||||
})
|
||||
@ -118,7 +123,7 @@ const ProjectList = () => {
|
||||
indexOfLastItem
|
||||
);
|
||||
const totalPages = Math.ceil(filteredProjects.length / itemsPerPage);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const tooltipTriggerList = Array.from(
|
||||
document.querySelectorAll('[data-bs-toggle="tooltip"]')
|
||||
@ -268,7 +273,7 @@ const ProjectList = () => {
|
||||
<th className="text-start" colSpan={5}>
|
||||
Project Name
|
||||
</th>
|
||||
<th className="mx-2 text-start">Project Manger</th>
|
||||
<th className="mx-2 text-start">Contact Person</th>
|
||||
<th className="mx-2">START DATE</th>
|
||||
<th className="mx-2">DEADLINE</th>
|
||||
<th className="mx-2">Task</th>
|
||||
@ -336,7 +341,7 @@ const ProjectList = () => {
|
||||
</tr>
|
||||
) : (
|
||||
currentItems.map((project) => (
|
||||
<ProjectListView key={project.id} projectData={project} />
|
||||
<ProjectListView key={project.id} projectData={project} recall={sortingProject} />
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
@ -344,7 +349,7 @@ const ProjectList = () => {
|
||||
</div>
|
||||
) : (
|
||||
currentItems.map((project) => (
|
||||
<ProjectCard key={project.id} projectData={project} />
|
||||
<ProjectCard key={project.id} projectData={project} recall={sortingProject} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -15,7 +15,7 @@ import ManageProjectInfo from "../../components/Project/ManageProjectInfo";
|
||||
import showToast from "../../services/toastService";
|
||||
import { getCachedData, cacheData } from "../../slices/apiDataManager";
|
||||
|
||||
const ProjectListView = ({ projectData }) => {
|
||||
const ProjectListView = ({ projectData, recall }) => {
|
||||
const [projectInfo, setProjectInfo] = useState(projectData);
|
||||
const [projectDetails, setProjectDetails] = useState(null);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
@ -76,6 +76,7 @@ const ProjectListView = ({ projectData }) => {
|
||||
);
|
||||
cacheData("projectslist", updatedProjectsList);
|
||||
}
|
||||
recall(getCachedData("projectslist"));
|
||||
showToast("Project updated successfully.", "success");
|
||||
setShowModal(false);
|
||||
})
|
||||
@ -87,19 +88,21 @@ const ProjectListView = ({ projectData }) => {
|
||||
return (
|
||||
<>
|
||||
{showModal && projectDetails && (
|
||||
<div
|
||||
className="modal fade show"
|
||||
tabIndex="-1"
|
||||
role="dialog"
|
||||
style={{ display: "block" }}
|
||||
aria-hidden="false"
|
||||
>
|
||||
<ManageProjectInfo
|
||||
project={projectDetails}
|
||||
handleSubmitForm={handleFormSubmit}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
</div>
|
||||
<tr>
|
||||
<td
|
||||
className="modal fade show"
|
||||
tabIndex="-1"
|
||||
role="dialog"
|
||||
style={{ display: "block" }}
|
||||
aria-hidden="false"
|
||||
>
|
||||
<ManageProjectInfo
|
||||
project={projectDetails}
|
||||
handleSubmitForm={handleFormSubmit}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
<tr className="py-8">
|
||||
@ -108,7 +111,9 @@ const ProjectListView = ({ projectData }) => {
|
||||
className="text-primary cursor-pointer"
|
||||
onClick={() => navigate(`/projects/${projectInfo.id}`)}
|
||||
>
|
||||
{projectInfo.name}
|
||||
{projectInfo.shortName
|
||||
? `${projectInfo.name} (${projectInfo.shortName})`
|
||||
: projectInfo.name}
|
||||
</strong>
|
||||
</td>
|
||||
<td className="text-start small">{projectInfo.contactPerson}</td>
|
||||
|
||||
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}`),
|
||||
};
|
||||
@ -40,10 +40,20 @@ export const MasterRespository = {
|
||||
"Job Role": ( id ) => api.delete( `/api/roles/jobrole/${ id }` ),
|
||||
"Activity": ( id ) => api.delete( `/api/master/activity/delete/${ id }` ),
|
||||
"Application Role":(id)=>api.delete(`/api/roles/${id}`),
|
||||
"Work Category": (id) => api.delete(`api/master/work-category/${id}`),
|
||||
"Work Category": ( id ) => api.delete( `api/master/work-category/${ id }` ),
|
||||
"Contact Category": ( id ) => api.delete( `/api/master/contact-category` ),
|
||||
"Contact Tag" :(id)=>api.delete(`/api/master/contact-tag/${id}`),
|
||||
|
||||
getWorkCategory:() => api.get(`/api/master/work-categories`),
|
||||
createWorkCategory: (data) => api.post(`/api/master/work-category`,data),
|
||||
updateWorkCategory: (id,data) => api.post(`/api/master/work-category/edit/${id}`,data),
|
||||
updateWorkCategory: ( id, data ) => api.post( `/api/master/work-category/edit/${ id }`, data ),
|
||||
|
||||
getContactCategory: () => api.get( `/api/master/contact-categories` ),
|
||||
createContactCategory: (data ) => api.post( `/api/master/contact-category`, data ),
|
||||
updateContactCategory: ( id, data ) => api.post( `/api/master/contact-category/edit/${ id }`, data ),
|
||||
|
||||
getContactTag: () => api.get( `/api/master/contact-tags` ),
|
||||
createContactTag: (data ) => api.post( `/api/master/contact-tag`, data ),
|
||||
updateContactTag: ( id, data ) => api.post( `/api/master/contact-tag/edit/${ id }`, data )
|
||||
|
||||
}
|
||||
@ -20,7 +20,9 @@ const ProjectRepository = {
|
||||
deleteProjectTask:(id)=> api.delete(`/api/project/task/${id}`),
|
||||
|
||||
updateProject: (id, data) => api.put(`/api/project/update/${id}`, data),
|
||||
deleteProject: (id) => api.delete(`/projects/${id}`),
|
||||
deleteProject: ( id ) => api.delete( `/projects/${ id }` ),
|
||||
getProjectsByEmployee: ( id ) => api.get( `/api/project/assigned-projects/${ id }` ),
|
||||
updateProjectsByEmployee:(id,data)=>api.post(`/api/project/assign-projects/${id}`,data)
|
||||
};
|
||||
|
||||
export const TasksRepository = {
|
||||
|
||||
@ -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