Compare commits
1041 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c8f5c7184 | ||
|
|
a06d3c9365 | ||
|
|
8a0916a47f | ||
|
|
3bc6504ae6 | ||
|
|
e2e5f3f885 | ||
|
|
7a88dd1aae | ||
|
|
521aa81591 | ||
|
|
d70fee6125 | ||
|
|
1a8fad26e5 | ||
|
|
d69050f006 | ||
|
|
abe1e80844 | ||
|
|
58e897db31 | ||
|
|
b7ed6734a0 | ||
|
|
7d5a866a5c | ||
|
|
23becf6494 | ||
|
|
64ee2c1526 | ||
|
|
c033c0fbc5 | ||
|
|
f8ddfeb7d0 | ||
|
|
bc42d19dff | ||
|
|
c4f7c57c24 | ||
|
|
c0bf829dc0 | ||
|
|
51cd61c932 | ||
|
|
4ab77de591 | ||
|
|
acc6633271 | ||
|
|
2336617077 | ||
|
|
836e9b846a | ||
|
|
897efde14d | ||
|
|
98f7147378 | ||
|
|
fd01746f7d | ||
|
|
54b8f564e4 | ||
|
|
591284c972 | ||
|
|
9e0dae2adf | ||
|
|
4155203e88 | ||
|
|
24a92e69f2 | ||
|
|
4ec171d93b | ||
|
|
c9cd4c51e8 | ||
|
|
e0f4b6daf2 | ||
|
|
608988c67c | ||
|
|
9f6854c87b | ||
|
|
8cee795d70 | ||
|
|
8da4d0b0f1 | ||
|
|
75ef93f0e2 | ||
|
|
c0dc5f50e3 | ||
|
|
a54668e030 | ||
|
|
53f0ec6c63 | ||
|
|
af03a1b4e3 | ||
|
|
f645498743 | ||
|
|
b3c948f0c7 | ||
|
|
b955eb01b4 | ||
|
|
39640d254a | ||
|
|
2386457cf5 | ||
|
|
206257ed3b | ||
|
|
45944ae8c9 | ||
|
|
53d15b0d45 | ||
|
|
a630fc5556 | ||
|
|
532eb842b3 | ||
|
|
d315617d87 | ||
|
|
2c32a4891b | ||
|
|
d869dbe97b | ||
|
|
1ece04ed3e | ||
|
|
b8a298fc28 | ||
|
|
2b9fe61d79 | ||
|
|
7fdf109e14 | ||
|
|
95751103a2 | ||
|
|
2209f49696 | ||
|
|
c7a2c8209a | ||
|
|
e1b00f5081 | ||
|
|
8ca4c5d7fa | ||
|
|
cfbf59cd48 | ||
|
|
6a09209659 | ||
|
|
962f471f0f | ||
|
|
cec5f6e6e7 | ||
|
|
82acaa35e1 | ||
|
|
cc7cea7139 | ||
|
|
eaea73a955 | ||
|
|
77e966dfe1 | ||
|
|
b052e97b73 | ||
|
|
92a5c27da4 | ||
|
|
09b8a05b5a | ||
|
|
83a1952880 | ||
|
|
9949d12317 | ||
|
|
e5d55f27b5 | ||
|
|
bfde72eed8 | ||
|
|
8fbd08d57f | ||
|
|
20bddb43b6 | ||
|
|
b38e0f611b | ||
|
|
c84fbcaba1 | ||
|
|
8f752d575a | ||
|
|
2fe9ae513d | ||
|
|
0001604552 | ||
|
|
5cb93b1a2c | ||
|
|
a04dcffc4c | ||
|
|
50c99f7a1e | ||
|
|
86f3179bc0 | ||
|
|
6336e7568f | ||
|
|
2b9e0932bd | ||
|
|
f0f199335c | ||
|
|
9c7c9f4b6d | ||
|
|
9001ceaed8 | ||
|
|
ab82e85c57 | ||
|
|
2effe5ef50 | ||
|
|
006a2a5dca | ||
|
|
43b1ad78a3 | ||
|
|
a885bdec74 | ||
|
|
d9c9466953 | ||
|
|
6b3fb00cc0 | ||
|
|
8d2bdb171b | ||
|
|
3fc24677c5 | ||
|
|
5d7eabbfa9 | ||
|
|
a2ada7d88e | ||
|
|
b741e29374 | ||
|
|
3a6af12446 | ||
|
|
b490ab96be | ||
|
|
08d50f90d6 | ||
|
|
ca462f51ec | ||
|
|
44721019fa | ||
|
|
29bbf4badb | ||
|
|
8ed81e9aed | ||
|
|
15ba2a1caf | ||
|
|
aad22f2e2d | ||
|
|
7a11b18037 | ||
|
|
241322fa30 | ||
|
|
f0461270de | ||
|
|
11b906103a | ||
|
|
c85a5eb208 | ||
|
|
801cd724ac | ||
|
|
e7567fdaac | ||
|
|
3f006f431e | ||
|
|
6f2b5e4c55 | ||
|
|
1dbfe24111 | ||
|
|
50d7c5dace | ||
|
|
9ac27b6090 | ||
|
|
da2bec67bf | ||
|
|
51a1b48da9 | ||
|
|
648a9b8f64 | ||
|
|
f3d8ca5711 | ||
|
|
7402679091 | ||
|
|
627174b7d3 | ||
|
|
9fcc01aa9f | ||
|
|
cb46ee5700 | ||
|
|
299d838ab9 | ||
|
|
43bf1fc8cf | ||
|
|
e3340fe408 | ||
|
|
73af18f287 | ||
|
|
90f4977924 | ||
|
|
79a9b534f9 | ||
|
|
c3b184d17b | ||
|
|
db5740d487 | ||
|
|
08c0da1bed | ||
|
|
9a49d1c69e | ||
|
|
4d35976241 | ||
|
|
5edbed3f0b | ||
|
|
3d79be06de | ||
|
|
fd9e7b4d4b | ||
|
|
352551e421 | ||
|
|
2937a07379 | ||
|
|
ad1761096a | ||
|
|
6a7548d11b | ||
|
|
affbb3f168 | ||
|
|
bf5c61bd86 | ||
|
|
0522747b49 | ||
|
|
aec7b40ae2 | ||
|
|
54d319f1e8 | ||
|
|
8d6fba2b61 | ||
|
|
70c31eae9e | ||
|
|
5e871b024d | ||
|
|
f8902efcea | ||
|
|
eb1786d634 | ||
|
|
5e8d0fddbd | ||
|
|
5d690fd71f | ||
|
|
79a2d902cd | ||
|
|
77f340d08c | ||
|
|
0770e7b50d | ||
|
|
3147212b7b | ||
|
|
24cc9762b2 | ||
|
|
3183baf560 | ||
|
|
2d5153da5b | ||
|
|
083534c3f3 | ||
|
|
63397769d2 | ||
|
|
b5b7957b2f | ||
|
|
d50c73c82f | ||
|
|
a4bff1a548 | ||
|
|
5f1daffb3e | ||
|
|
e9dfba7d31 | ||
|
|
2f0838b39c | ||
|
|
d8311c5163 | ||
|
|
62e4843d5b | ||
|
|
6058bb1b8f | ||
|
|
fa6c672583 | ||
|
|
cb4d4e4c2c | ||
|
|
225b57fd58 | ||
|
|
e1ffcba32f | ||
|
|
5c30f33dac | ||
|
|
13908074c6 | ||
|
|
c3c66f9646 | ||
|
|
4433f0f57f | ||
|
|
62dd3d7e8e | ||
|
|
268b1ba9c1 | ||
|
|
239c1502f9 | ||
|
|
457a3b2d7a | ||
|
|
5e2c0f9c4a | ||
|
|
cbc8665636 | ||
|
|
f6506d6073 | ||
|
|
91de311351 | ||
|
|
49044e5669 | ||
|
|
8adaa12618 | ||
|
|
36aad0f140 | ||
|
|
11ab7cd67e | ||
|
|
eacadc01bd | ||
|
|
3ab471e629 | ||
|
|
6504b27eca | ||
|
|
d40579694f | ||
|
|
fa24d87966 | ||
|
|
1b6eab8488 | ||
|
|
e15e92c112 | ||
|
|
fba8cab98a | ||
|
|
141deff41e | ||
|
|
12ed8d3830 | ||
|
|
525f795ce0 | ||
|
|
38f13346e5 | ||
|
|
8229e3593c | ||
|
|
d2e1b32557 | ||
|
|
e202bf9a89 | ||
|
|
1a6e8bc5ba | ||
|
|
cd592b671c | ||
|
|
dd4ba8a467 | ||
|
|
8ad1dd83c6 | ||
|
|
9ccbca2678 | ||
|
|
1cdd905037 | ||
|
|
e734da7adc | ||
|
|
12aec3e3a0 | ||
|
|
5392659db6 | ||
|
|
15151cb4ac | ||
|
|
06afd6da5b | ||
|
|
250faa672f | ||
|
|
ec5258a431 | ||
|
|
fbc7168bde | ||
|
|
f2d9626888 | ||
|
|
e15384d0bf | ||
|
|
261353b511 | ||
|
|
80b66fd7e8 | ||
|
|
45ac56e0bc | ||
|
|
1ff1de8739 | ||
|
|
299a675a9c | ||
|
|
2304e0bf02 | ||
|
|
ea1cc23ee7 | ||
|
|
7cbabf8697 | ||
|
|
289a666b6d | ||
|
|
d2db8c68f3 | ||
|
|
b8836c7ae1 | ||
|
|
eca31c5618 | ||
|
|
7fdbedefce | ||
|
|
ef166a930a | ||
|
|
7140b8d585 | ||
|
|
5eed8d9809 | ||
|
|
57fe5b4c46 | ||
|
|
20252c61c6 | ||
|
|
f266ee1cfe | ||
|
|
9550de5131 | ||
|
|
54dcdb49d3 | ||
|
|
1f76ff882c | ||
|
|
749f73a272 | ||
|
|
9c1774c417 | ||
|
|
e363dca3f0 | ||
|
|
26b3a43ce5 | ||
|
|
78678dd3dc | ||
|
|
9dc4546b2e | ||
|
|
95aa0e45a6 | ||
|
|
ce9a77efcf | ||
|
|
e9e1e820a7 | ||
|
|
b027a4e618 | ||
|
|
0adb6413e4 | ||
|
|
c7fc75aa5c | ||
|
|
98d2372daf | ||
|
|
bf51380167 | ||
|
|
1ec827097f | ||
|
|
89fabf85e1 | ||
|
|
ff7dd7d3ea | ||
|
|
f41416645f | ||
|
|
8cc4f88fa7 | ||
|
|
2439755f9e | ||
|
|
4468a5be2d | ||
|
|
7e6ab3a5ff | ||
|
|
763384f05f | ||
|
|
68b711038c | ||
|
|
34f876f838 | ||
|
|
cba2da8da7 | ||
|
|
f3d8aa3438 | ||
|
|
8f3c71ca07 | ||
|
|
2f3eccf3d8 | ||
|
|
2b3e64d607 | ||
|
|
0c360e48cc | ||
|
|
05b20505bb | ||
|
|
bddeae945c | ||
|
|
5b267f03b9 | ||
|
|
357d916e0a | ||
|
|
6ed12ebe7d | ||
|
|
6703bc025d | ||
|
|
40e75b491b | ||
|
|
387dac6779 | ||
|
|
6f454dd4cb | ||
|
|
1440a60228 | ||
|
|
cb80b79e1d | ||
|
|
f2aa3960aa | ||
|
|
8d4195b596 | ||
|
|
47182c3e99 | ||
|
|
06508f3ad8 | ||
|
|
9e190e7fb7 | ||
|
|
5cbf00b0c8 | ||
|
|
655aeb86fc | ||
|
|
225549275d | ||
|
|
f0717b8b36 | ||
|
|
cc2261d711 | ||
|
|
78771ae750 | ||
|
|
0389908398 | ||
|
|
54bee763df | ||
|
|
1117a94930 | ||
|
|
c6bdb49569 | ||
|
|
5fbfb992c7 | ||
|
|
87b3b65f3e | ||
|
|
82a76a5e3a | ||
|
|
9970190909 | ||
|
|
8eee371a90 | ||
|
|
fd7e9c9d3b | ||
|
|
ba97b1efef | ||
|
|
05a21e8586 | ||
|
|
8d8887c28e | ||
|
|
3b19432974 | ||
|
|
a14b2340b0 | ||
|
|
624f8e77cb | ||
|
|
fb624c817d | ||
|
|
c2b4b66ed1 | ||
|
|
ffec03ab6c | ||
|
|
552163d7b9 | ||
|
|
db1f59578c | ||
|
|
8ec5831ec5 | ||
|
|
0146ac5b7b | ||
|
|
a603e5c0b8 | ||
|
|
9aab47d8f8 | ||
|
|
f2f84e2da8 | ||
|
|
94641ae01d | ||
|
|
338906e288 | ||
|
|
542997b1a7 | ||
|
|
9bb36d2223 | ||
|
|
5fce548666 | ||
|
|
49c3ff9043 | ||
|
|
80322caad0 | ||
|
|
56472d24d9 | ||
|
|
db5dcc271d | ||
|
|
1205e71ea6 | ||
|
|
afc5df7f09 | ||
|
|
73ab02225e | ||
|
|
83a1b7690d | ||
|
|
c9e28b1ed2 | ||
|
|
e118e5bcd5 | ||
|
|
c25c66d00f | ||
|
|
d319ab49d4 | ||
|
|
95ffeeefcd | ||
|
|
a069989ea7 | ||
|
|
8e3aa186cb | ||
|
|
01c55d6277 | ||
|
|
3438907d8d | ||
|
|
ae020b651e | ||
|
|
d22988df15 | ||
|
|
8136a56ad2 | ||
|
|
830f6c0eea | ||
|
|
4c1849289a | ||
|
|
c45a4780e3 | ||
|
|
dba41e7732 | ||
|
|
d4adc4c1aa | ||
|
|
d9e71423f5 | ||
|
|
6cac0f9594 | ||
|
|
f8e65ada76 | ||
|
|
2ab4615642 | ||
|
|
dd5961d419 | ||
|
|
8190958ba3 | ||
|
|
77e009f316 | ||
|
|
a715fccd47 | ||
|
|
2b2738a8d1 | ||
|
|
3d10c9da7f | ||
|
|
e82c77d119 | ||
|
|
855a78be05 | ||
|
|
4f6e1ffc9a | ||
|
|
a29e840797 | ||
|
|
1b30c1ab58 | ||
|
|
80f235f12e | ||
|
|
9b67148522 | ||
|
|
c07efad729 | ||
|
|
6b501e4619 | ||
|
|
42f1d6fa13 | ||
|
|
f65993d097 | ||
|
|
3f247a9227 | ||
|
|
f65ab128f9 | ||
|
|
63b914731b | ||
|
|
b2a7fee021 | ||
|
|
23f8f69bbe | ||
|
|
bcba161ab5 | ||
|
|
fc3ea2bdf8 | ||
|
|
2f04ddc5de | ||
|
|
96e970faf7 | ||
|
|
25d567fd2e | ||
|
|
c133195607 | ||
|
|
84fa129396 | ||
|
|
d75ea2b1a6 | ||
|
|
e6bc52acc6 | ||
|
|
ec0fd840e4 | ||
|
|
971a81fc27 | ||
|
|
19050d31f7 | ||
|
|
e605433379 | ||
|
|
fc3fc7fa9b | ||
|
|
b9ebb70b7a | ||
|
|
3a4f18ef2d | ||
|
|
79ed6f2388 | ||
|
|
2653db5404 | ||
|
|
785449a986 | ||
|
|
0b7d469e0e | ||
|
|
a57156756e | ||
|
|
6435d2f283 | ||
|
|
c4c30d98d4 | ||
|
|
4c8e930eaa | ||
|
|
d4e8803b13 | ||
|
|
1f2786ddec | ||
|
|
fcd5183189 | ||
|
|
e90cda07e4 | ||
|
|
9793daa04c | ||
|
|
1c062f0310 | ||
|
|
117ced8fe7 | ||
|
|
e7909205d1 | ||
|
|
18028a70ab | ||
|
|
eeb8d8d26f | ||
|
|
23659fc412 | ||
|
|
042fafe281 | ||
|
|
ba65057782 | ||
|
|
f5307c9a42 | ||
|
|
60a859cac8 | ||
|
|
0cfe26093c | ||
|
|
d085a9c7c9 | ||
|
|
9156c25f32 | ||
|
|
ec518a0593 | ||
|
|
1cd64ab6f1 | ||
|
|
30d0c28f72 | ||
|
|
26836f662a | ||
|
|
111f280674 | ||
|
|
39bd490d43 | ||
|
|
6e88faa9d8 | ||
|
|
1ca8b2a78d | ||
|
|
cd2a7cad7f | ||
|
|
ed16156957 | ||
|
|
8dc1f7e08f | ||
|
|
2d3c13c587 | ||
|
|
3015d06219 | ||
|
|
5486907639 | ||
|
|
9233cef23a | ||
|
|
a0c814775a | ||
|
|
c16eafe892 | ||
|
|
b479684fe4 | ||
|
|
4201f61548 | ||
|
|
d04fc76840 | ||
|
|
0f84adc752 | ||
|
|
fbefd80959 | ||
|
|
6a691b54c8 | ||
|
|
fc75717d32 | ||
|
|
1459c6e993 | ||
|
|
f50292f9bf | ||
|
|
5b81912bd3 | ||
|
|
bff56ccc96 | ||
|
|
3c98a94c38 | ||
|
|
1d98de6d4d | ||
|
|
34e5a86a79 | ||
|
|
0ce5d9063a | ||
|
|
3b84e1d6ec | ||
|
|
d62f6e2116 | ||
|
|
71a26cc4ac | ||
|
|
1719da3402 | ||
|
|
32441e9406 | ||
|
|
e6dade1206 | ||
|
|
43d34cae07 | ||
|
|
a72a7948fe | ||
|
|
8640b94714 | ||
|
|
a24f6639a1 | ||
|
|
b2a0af32e9 | ||
|
|
cc58d14d32 | ||
|
|
9ce419b949 | ||
|
|
5053816be7 | ||
|
|
30ca34ea93 | ||
|
|
68d1a404b3 | ||
|
|
85e82b85ea | ||
|
|
23467280b4 | ||
|
|
aedad1c48f | ||
|
|
05cc4dd188 | ||
|
|
ea6351ea06 | ||
|
|
87d3ceb408 | ||
|
|
d08dd2b506 | ||
|
|
8a047d14a1 | ||
|
|
e103772aa4 | ||
|
|
c332699dc8 | ||
|
|
5593a9fa80 | ||
|
|
25e6e61d10 | ||
|
|
cdcd6b636a | ||
|
|
7879591bcf | ||
|
|
7fc6556866 | ||
|
|
3f5489ce7e | ||
|
|
5a90854861 | ||
|
|
b4bffcde2b | ||
|
|
8347a8c098 | ||
|
|
2bf074d85a | ||
|
|
ff6e1d535b | ||
|
|
50d47cd679 | ||
|
|
3a4e06eaa2 | ||
|
|
4be71726d4 | ||
|
|
c78db7eb08 | ||
|
|
e4dc711481 | ||
|
|
5114138c67 | ||
|
|
68b8743002 | ||
|
|
8f312bfffb | ||
|
|
7e7e109cfe | ||
|
|
05e5545466 | ||
|
|
ddb0990645 | ||
|
|
04dec6d91c | ||
|
|
a883b817b0 | ||
|
|
5b00ded5f6 | ||
|
|
c5b19d8f22 | ||
|
|
b7423aebf6 | ||
|
|
ee70aeb952 | ||
|
|
74d95e7cbb | ||
|
|
f6f6fab5ba | ||
|
|
699ffc822a | ||
|
|
4e35f5402c | ||
|
|
9b997d0924 | ||
|
|
d705f8211e | ||
|
|
03761bbb2a | ||
|
|
4d0794e90e | ||
|
|
4139626814 | ||
|
|
e615c4a55b | ||
|
|
51eb3423f3 | ||
|
|
d8dcc1a5dd | ||
|
|
f6318666d9 | ||
|
|
544d4b8136 | ||
|
|
edf4846d55 | ||
|
|
0aca25802c | ||
|
|
f3754de843 | ||
|
|
3d920ad151 | ||
|
|
575f056360 | ||
|
|
716d9affb5 | ||
|
|
b01dd52da2 | ||
|
|
0fdafdf1a6 | ||
|
|
c75fddc2c0 | ||
|
|
db0c16f31d | ||
|
|
b286ab2439 | ||
|
|
fa57828ebd | ||
|
|
8052767002 | ||
|
|
932f572fb5 | ||
|
|
328a64eb90 | ||
|
|
c661fce8f1 | ||
|
|
60d1396011 | ||
|
|
3b647dfd37 | ||
|
|
0ccdc1ffb7 | ||
|
|
50fe588949 | ||
|
|
0ced053d21 | ||
|
|
f455892a12 | ||
|
|
b8cf4a4d75 | ||
|
|
ff72657a82 | ||
|
|
06c1f55969 | ||
|
|
92a96fdae6 | ||
|
|
b1a96d55ad | ||
|
|
2890c7d43c | ||
|
|
49657816c6 | ||
|
|
7094b6ffbf | ||
|
|
dfab2e5fed | ||
|
|
ed7c2574eb | ||
|
|
45a9e37342 | ||
|
|
4fc31f60b8 | ||
|
|
9e6a458203 | ||
|
|
55a279a700 | ||
|
|
82e2e332cf | ||
|
|
103d7c2bb2 | ||
|
|
f5f0b75617 | ||
|
|
c163554c3f | ||
|
|
bd75f593c2 | ||
|
|
fbc1866363 | ||
|
|
6480f7f2aa | ||
|
|
480a45a7ef | ||
|
|
4cb3a79429 | ||
|
|
ca521eaeba | ||
|
|
f287ba2dac | ||
|
|
ff46bbbb3f | ||
|
|
50df34a4dc | ||
|
|
cafca35500 | ||
|
|
880d87707e | ||
|
|
9803841617 | ||
|
|
4dffbfe6fa | ||
|
|
6e61159608 | ||
|
|
382492c284 | ||
|
|
3c85de3e34 | ||
|
|
b24a0ea2d7 | ||
|
|
1b5cddd371 | ||
|
|
6a7005299a | ||
|
|
d0df81796a | ||
|
|
1a4c9faab1 | ||
|
|
bfbf34e11d | ||
|
|
646754732d | ||
|
|
439d9e7b74 | ||
|
|
190b1a64e8 | ||
|
|
464f7044f0 | ||
|
|
7cde2f64af | ||
|
|
f674fff930 | ||
|
|
efc1157653 | ||
|
|
0677712d6e | ||
|
|
899699a38a | ||
|
|
2e106a5d07 | ||
|
|
da7b97042e | ||
|
|
f018a2b2a6 | ||
|
|
c3f7d7bad2 | ||
|
|
70d857bfec | ||
|
|
f3265901b6 | ||
|
|
ff2e4b9d3a | ||
|
|
7c8ac50426 | ||
|
|
8ad39fe855 | ||
|
|
13b6218c43 | ||
|
|
bece3278f4 | ||
|
|
4c0a1960ad | ||
|
|
47324422a6 | ||
|
|
3b1da6901d | ||
|
|
8bb6898520 | ||
|
|
fc6ec54233 | ||
|
|
c3c7dfded8 | ||
|
|
64928d0849 | ||
|
|
56a580b1e7 | ||
|
|
f7af3b407b | ||
|
|
9a0674f5d7 | ||
|
|
cc30ea658e | ||
|
|
6613abb024 | ||
|
|
59869def31 | ||
|
|
a5d3f2caf1 | ||
|
|
4ad87a522c | ||
|
|
453812222b | ||
|
|
145cf7cc93 | ||
|
|
c09e22ed96 | ||
|
|
cdb2d4d2d6 | ||
|
|
29f0031c1e | ||
|
|
c9c4c2f2a2 | ||
|
|
e8099e130a | ||
|
|
1cbca1ddf0 | ||
|
|
eeed004fe2 | ||
|
|
5a180b86fb | ||
|
|
e3059b41ae | ||
|
|
1a5c71048c | ||
|
|
fc4e97c9b5 | ||
|
|
3cd3d7414d | ||
|
|
1bb2212e4a | ||
|
|
a088f27f1d | ||
|
|
0e9ad1258d | ||
|
|
2a33f462a3 | ||
|
|
cbc164dbeb | ||
|
|
0bfc7033a9 | ||
|
|
2ec0d90a58 | ||
|
|
6382fdf19c | ||
|
|
9287e6608d | ||
|
|
0fcee5b25e | ||
|
|
d221763064 | ||
|
|
b39a5b755e | ||
|
|
30cb4ef562 | ||
|
|
449330441a | ||
|
|
fcab5e6ef2 | ||
|
|
0212b837ea | ||
|
|
e7438a099e | ||
|
|
b3303e3c38 | ||
|
|
c69c86d193 | ||
|
|
73ec8b8a70 | ||
|
|
af09796df8 | ||
|
|
954504de8d | ||
|
|
0aba040338 | ||
|
|
c3bfe87674 | ||
|
|
9aa1279144 | ||
|
|
4e6c45b195 | ||
|
|
4fdb939bd2 | ||
|
|
062a1dcc72 | ||
|
|
7b420b1855 | ||
|
|
40f61bbc8f | ||
|
|
f5d821c394 | ||
|
|
3958ec9189 | ||
|
|
1e4f52e541 | ||
|
|
5cc5cb444e | ||
|
|
4acf0c59ca | ||
|
|
2858a5e871 | ||
|
|
24496d3ee1 | ||
|
|
0a5df69b12 | ||
|
|
80efea02c6 | ||
|
|
9f5c282b41 | ||
|
|
b2602c3385 | ||
|
|
0e584af424 | ||
|
|
cdc3de2a33 | ||
|
|
3bfa556b02 | ||
|
|
44cb7577e2 | ||
|
|
46d2b08477 | ||
|
|
0193ff9e65 | ||
|
|
fd9a51209f | ||
|
|
d0a7b87e04 | ||
|
|
799b24c90e | ||
|
|
3e1a8c87d1 | ||
|
|
c886d874de | ||
|
|
4dfb020089 | ||
|
|
bc6f05acbc | ||
|
|
2701bbd501 | ||
|
|
1f2040d97c | ||
|
|
43963a3e91 | ||
|
|
4287311adb | ||
|
|
d0e8589a76 | ||
|
|
c4bab72947 | ||
|
|
aa4b4998fa | ||
|
|
ed4566e00f | ||
|
|
5c2cdfe16c | ||
|
|
12c75357b5 | ||
|
|
d40f3ee45a | ||
|
|
96a0def846 | ||
|
|
1fd595d0de | ||
|
|
52cf4f3d1f | ||
|
|
4d9be1d232 | ||
|
|
fb2bc20b4f | ||
|
|
744593e96a | ||
|
|
1e9308be9b | ||
|
|
411605e121 | ||
|
|
1da8d6abb3 | ||
|
|
cdcef798df | ||
|
|
f7207a9f3f | ||
|
|
7a54b55bd4 | ||
|
|
991dfc2ad5 | ||
|
|
718c8291a8 | ||
|
|
f1e84c348b | ||
|
|
2a2d399a98 | ||
|
|
5f513a8bef | ||
|
|
4b96d5a707 | ||
|
|
220f3d4410 | ||
|
|
841f62bd84 | ||
|
|
f3f16b78d5 | ||
|
|
91e2e7931b | ||
|
|
1e855799f8 | ||
|
|
3c6faf8473 | ||
|
|
c994eaaa8e | ||
|
|
517d8f4163 | ||
|
|
9deb2964a5 | ||
|
|
9cf9f8b844 | ||
|
|
ad46ea74c0 | ||
|
|
2a28855e4b | ||
|
|
8d25f60097 | ||
|
|
982a51f16e | ||
|
|
68d02648d7 | ||
|
|
6e8122849a | ||
|
|
b04ae84941 | ||
|
|
932979d5fb | ||
|
|
f7ef32c58d | ||
|
|
f7108b4b8c | ||
|
|
882038a794 | ||
|
|
aec23fe46b | ||
|
|
89d5b1cfe4 | ||
|
|
35ac0b0c6a | ||
|
|
2a2a0f8961 | ||
|
|
d9902b9744 | ||
|
|
f82478a362 | ||
|
|
bb3d3fbe72 | ||
|
|
4fa0593bb5 | ||
|
|
41517ca7d4 | ||
|
|
35c9f649ad | ||
|
|
ad2f2e55a5 | ||
|
|
41c446ddb3 | ||
|
|
7d6aa8489d | ||
|
|
63f1e0f07c | ||
|
|
98f4423624 | ||
|
|
1ac4cbb59f | ||
|
|
24ebfbfbf5 | ||
|
|
7ff1051d3c | ||
|
|
8af3364660 | ||
|
|
02f4677aef | ||
|
|
11785f3b86 | ||
|
|
90532427b6 | ||
|
|
cc9979ff4b | ||
|
|
c89e4f1b41 | ||
|
|
c3e6d3dc48 | ||
|
|
ad1ce7b220 | ||
|
|
fd4dbdfb3a | ||
|
|
153cf6a840 | ||
|
|
a567d0d6dd | ||
|
|
297599a45b | ||
|
|
678ca591c1 | ||
|
|
c19f8167e8 | ||
|
|
cc2d474fda | ||
|
|
9058aca16e | ||
|
|
1c186f7fa5 | ||
|
|
46da3285f8 | ||
|
|
b419929ad7 | ||
|
|
8018daa2dc | ||
|
|
1e7c285fef | ||
|
|
0b072e6089 | ||
|
|
4fd6203987 | ||
|
|
51d264098c | ||
|
|
680a66b156 | ||
|
|
481a14e529 | ||
|
|
f3e43334c4 | ||
|
|
0054b00d01 | ||
|
|
82ecb5533f | ||
|
|
d3289d85f1 | ||
|
|
e628b1364c | ||
|
|
6c421c1447 | ||
|
|
99369e7040 | ||
|
|
01cbdf14a9 | ||
|
|
f691aca241 | ||
|
|
85495a11e3 | ||
|
|
134ce05d27 | ||
|
|
3498fbc8f1 | ||
|
|
f49f72ce7f | ||
|
|
a5e3b6ce33 | ||
|
|
0fd945b859 | ||
|
|
879eba0247 | ||
|
|
bb49dd77a1 | ||
|
|
ae705322f8 | ||
|
|
36d92d4060 | ||
|
|
3ce2b1ab19 | ||
|
|
52e756a78a | ||
|
|
5a36cb7cf1 | ||
|
|
9138f4be16 | ||
|
|
df93357cec | ||
|
|
8ab23c4ca6 | ||
|
|
f179d69420 | ||
|
|
730a7a233d | ||
|
|
84ad10fa9c | ||
|
|
b1cda41f56 | ||
|
|
0bce921f69 | ||
|
|
97282740f5 | ||
|
|
150ae02978 | ||
|
|
70058b6bd4 | ||
|
|
67c63f81d9 | ||
|
|
abeffb2a19 | ||
|
|
fb8452b2bb | ||
|
|
1f281e8439 | ||
|
|
223c705e8f | ||
|
|
00d8b533f4 | ||
|
|
3b25a8fe07 | ||
|
|
a57b9cddb5 | ||
|
|
064ed1bb8b | ||
|
|
efda254981 | ||
|
|
3fa6b8b6ac | ||
|
|
4603643240 | ||
|
|
f82b02958f | ||
|
|
5d1f61753b | ||
|
|
883043cde3 | ||
|
|
85ce7c638d | ||
|
|
547e279693 | ||
|
|
b9bdefe898 | ||
|
|
c963cb6fcc | ||
|
|
fa6d4cce2a | ||
|
|
c7188d5a71 | ||
|
|
c333d72743 | ||
|
|
2a1ec4eff3 | ||
|
|
190b2e5b5c | ||
|
|
3e2a517272 | ||
|
|
a6cd35bd06 | ||
|
|
ba3032e553 | ||
|
|
20fd452335 | ||
|
|
8f83a5f5e6 | ||
|
|
4e417d1b10 | ||
|
|
3ee63a503a | ||
|
|
8dd0e12398 | ||
|
|
253b4d0ddc | ||
|
|
a8b0931659 | ||
|
|
11a182c68a | ||
|
|
2583d21cf1 | ||
|
|
aa6032d74f | ||
|
|
e1bac5fe9f | ||
|
|
0c74848c54 | ||
|
|
1c4f2d2de0 | ||
|
|
d67d2aa064 | ||
|
|
208f3281a9 | ||
|
|
fb7e3bc812 | ||
|
|
884adb963c | ||
|
|
29c4df9f76 | ||
|
|
f1940a320c | ||
|
|
a297bba193 | ||
|
|
375a3fe386 | ||
|
|
d837754ef6 | ||
|
|
49a1f0c42c | ||
|
|
a5033e10cd | ||
|
|
00f0952ab2 | ||
|
|
ec5b636c26 | ||
|
|
7126cc0d56 | ||
|
|
568d8887ce | ||
|
|
4927e84a88 | ||
|
|
d2d8039cb9 | ||
|
|
5da242d785 | ||
|
|
87f25b1ed4 | ||
|
|
4f9afc8578 | ||
|
|
defadf70e3 | ||
|
|
82be3853f0 | ||
|
|
ff184926fc | ||
|
|
0e9768c0f6 | ||
|
|
805c924a15 | ||
|
|
10ff8fc432 | ||
|
|
e2db0bab9f | ||
|
|
92ee5169ab | ||
|
|
5cf2345dca | ||
|
|
0accf6a18f | ||
|
|
52e46aad86 | ||
|
|
43f49e5e51 | ||
|
|
60908b123d | ||
|
|
5169fd178a | ||
|
|
bbc446ef01 | ||
|
|
65bb8c6f26 | ||
|
|
171f40819c | ||
|
|
4baa26cb25 | ||
|
|
01da7fde31 | ||
|
|
3c8234d715 | ||
|
|
866f242465 | ||
|
|
f690596825 | ||
|
|
5f494e4b78 | ||
|
|
7b814329da | ||
|
|
2fc0e1cf50 | ||
|
|
ba3f98aeb4 | ||
|
|
f49818ade3 | ||
|
|
b37c70494b | ||
|
|
ff1db26f41 | ||
|
|
3550c97966 | ||
|
|
956c686360 | ||
|
|
508b0d7711 | ||
|
|
abb1464e30 | ||
|
|
704d5415d6 | ||
|
|
6cc1cfd1b0 | ||
|
|
e6455a7fd3 | ||
|
|
a9aad2a751 | ||
|
|
1bf745345d | ||
|
|
63c71ed923 | ||
|
|
3a844aefa0 | ||
|
|
93c92e8976 | ||
|
|
9523e5534d | ||
|
|
d20048026c | ||
|
|
15c7b6f1c8 | ||
|
|
3149c3cfc6 | ||
|
|
f91e0ec39f | ||
|
|
c31d4096c6 | ||
|
|
118f14ed4c | ||
|
|
0d0791cc41 | ||
|
|
f45aa69917 | ||
|
|
4b44ff29ee | ||
|
|
f15f5d7309 | ||
|
|
ea55b1a797 | ||
|
|
668366e886 | ||
|
|
eb1b2aef55 | ||
|
|
906430add5 | ||
|
|
f8fbbd2323 | ||
|
|
adab6ef0c6 | ||
|
|
ddfd91617f | ||
|
|
8418b4cb91 | ||
|
|
e593943d99 | ||
|
|
b58b5e65dc | ||
|
|
6d393987e8 | ||
|
|
852231b503 | ||
|
|
870878d151 | ||
|
|
cdf7bcf839 | ||
|
|
8a4fee7aea | ||
|
|
fa578efee4 | ||
|
|
4b6b4c0c63 | ||
|
|
8199ab83ef | ||
|
|
8e50d0ba53 | ||
|
|
7d72d66a97 | ||
|
|
3454694887 | ||
|
|
2a51c8a2bf | ||
|
|
524f623fd4 | ||
|
|
7ab7e18d96 | ||
|
|
6f2c8dba5a | ||
|
|
162d8bfffe | ||
|
|
61569d97cb | ||
|
|
975622a31c | ||
|
|
7524d06126 | ||
|
|
7f75eeadb9 | ||
|
|
4db2b73397 | ||
|
|
2f493c63f8 | ||
|
|
2761356f02 | ||
|
|
8207a52b6b | ||
|
|
8bbe7a1f0f | ||
|
|
2b052b3d43 | ||
|
|
94e7bf3f92 | ||
|
|
64e3f5f2ac | ||
|
|
bb7f7deb50 | ||
|
|
55c10890bd | ||
|
|
edef0b194d | ||
|
|
37f0c98e75 | ||
|
|
48f18514af | ||
|
|
76c13700be | ||
|
|
8ae8737fe2 | ||
|
|
36f7b7a1a1 | ||
|
|
b0a5f2d998 | ||
|
|
19c980bc3b | ||
|
|
78d6b9699b | ||
|
|
82d8cb22df | ||
|
|
9c41af82d7 | ||
|
|
2997dd4e4d | ||
|
|
4602648fe5 | ||
|
|
a8f2ca5643 | ||
|
|
cd055f7344 | ||
|
|
c43709395f | ||
|
|
10f60752c8 | ||
|
|
870f8463db | ||
|
|
5567a69142 | ||
|
|
762fec1d9a | ||
|
|
0946b0d3a3 | ||
|
|
976dd2cbfb | ||
|
|
b6956b3649 | ||
|
|
6cf4a50a83 | ||
|
|
b2b4902d23 | ||
|
|
6f69129829 | ||
|
|
d846894d22 | ||
|
|
7d3101c877 | ||
|
|
c3f0e6f10c | ||
|
|
d0da2dd66c | ||
|
|
877b3ab39c | ||
|
|
8f031a78c1 | ||
|
|
69b36a4c34 | ||
|
|
c7b8df5655 | ||
|
|
d85768b2ac | ||
|
|
a569c1f4f9 | ||
|
|
07a8e5b216 | ||
|
|
38bf58c613 | ||
|
|
ba90d72d55 | ||
|
|
9889bee924 | ||
|
|
a19e4e8f16 | ||
|
|
5121852fbc | ||
|
|
1d1ff5d20b | ||
|
|
ec00697d31 | ||
|
|
c25714b68e | ||
|
|
dc0147c5f9 | ||
|
|
296afdbeee | ||
|
|
2f8f058c5c | ||
|
|
133d593689 | ||
|
|
68784018e6 | ||
|
|
19dfec2a34 | ||
|
|
55d729339f | ||
|
|
c3108a17f4 | ||
|
|
d47ae64bd6 | ||
|
|
5206ba7a74 | ||
|
|
095e1e9789 | ||
|
|
a0b9f99dd3 | ||
|
|
a33a92207b | ||
|
|
f647e1ff11 |
@@ -5,6 +5,7 @@ orbs:
|
|||||||
aws-s3: circleci/aws-s3@4.0.0
|
aws-s3: circleci/aws-s3@4.0.0
|
||||||
aws-cli: circleci/aws-cli@4.0
|
aws-cli: circleci/aws-cli@4.0
|
||||||
eb: circleci/aws-elastic-beanstalk@2.0.1
|
eb: circleci/aws-elastic-beanstalk@2.0.1
|
||||||
|
jira: circleci/jira@2.1.0
|
||||||
jobs:
|
jobs:
|
||||||
imex-api-deploy:
|
imex-api-deploy:
|
||||||
docker:
|
docker:
|
||||||
@@ -18,6 +19,12 @@ jobs:
|
|||||||
eb status --verbose
|
eb status --verbose
|
||||||
eb deploy
|
eb deploy
|
||||||
eb status
|
eb status
|
||||||
|
- jira/notify:
|
||||||
|
environment: Production (ImEX) - API
|
||||||
|
environment_type: production
|
||||||
|
job_type: deployment
|
||||||
|
pipeline_id: << pipeline.id >>
|
||||||
|
pipeline_number: << pipeline.number >>
|
||||||
|
|
||||||
imex-hasura-migrate:
|
imex-hasura-migrate:
|
||||||
docker:
|
docker:
|
||||||
@@ -33,11 +40,16 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Execute migration
|
name: Execute migration
|
||||||
command: |
|
command: |
|
||||||
npm install hasura-cli -g
|
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
|
||||||
hasura migrate apply --endpoint https://db.imex.online/ --admin-secret << parameters.secret >>
|
hasura migrate apply --endpoint https://db.imex.online/ --admin-secret << parameters.secret >>
|
||||||
hasura metadata apply --endpoint https://db.imex.online/ --admin-secret << parameters.secret >>
|
hasura metadata apply --endpoint https://db.imex.online/ --admin-secret << parameters.secret >>
|
||||||
hasura metadata reload --endpoint https://db.imex.online/ --admin-secret << parameters.secret >>
|
hasura metadata reload --endpoint https://db.imex.online/ --admin-secret << parameters.secret >>
|
||||||
|
- jira/notify:
|
||||||
|
environment: Production (ImEX) - Hasura
|
||||||
|
environment_type: production
|
||||||
|
pipeline_id: << pipeline.id >>
|
||||||
|
job_type: deployment
|
||||||
|
pipeline_number: << pipeline.number >>
|
||||||
imex-app-build:
|
imex-app-build:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:18.18.2
|
- image: cimg/node:18.18.2
|
||||||
@@ -62,6 +74,7 @@ jobs:
|
|||||||
to: "s3://imex-online-production/"
|
to: "s3://imex-online-production/"
|
||||||
arguments: "--exclude '*.map'"
|
arguments: "--exclude '*.map'"
|
||||||
|
|
||||||
|
|
||||||
imex-app-beta-build:
|
imex-app-beta-build:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:18.18.2
|
- image: cimg/node:18.18.2
|
||||||
@@ -86,6 +99,12 @@ jobs:
|
|||||||
from: dist
|
from: dist
|
||||||
to: "s3://imex-online-beta/"
|
to: "s3://imex-online-beta/"
|
||||||
arguments: "--exclude '*.map'"
|
arguments: "--exclude '*.map'"
|
||||||
|
- jira/notify:
|
||||||
|
environment: Production (ImEX) - Front End
|
||||||
|
environment_type: production
|
||||||
|
pipeline_id: << pipeline.id >>
|
||||||
|
job_type: deployment
|
||||||
|
pipeline_number: << pipeline.number >>
|
||||||
|
|
||||||
rome-api-deploy:
|
rome-api-deploy:
|
||||||
docker:
|
docker:
|
||||||
@@ -99,7 +118,12 @@ jobs:
|
|||||||
eb status --verbose
|
eb status --verbose
|
||||||
eb deploy
|
eb deploy
|
||||||
eb status
|
eb status
|
||||||
|
- jira/notify:
|
||||||
|
environment: Production (Rome) - API
|
||||||
|
environment_type: production
|
||||||
|
pipeline_id: << pipeline.id >>
|
||||||
|
job_type: deployment
|
||||||
|
pipeline_number: << pipeline.number >>
|
||||||
rome-hasura-migrate:
|
rome-hasura-migrate:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:18.18.2
|
- image: cimg/node:18.18.2
|
||||||
@@ -114,11 +138,16 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Execute migration
|
name: Execute migration
|
||||||
command: |
|
command: |
|
||||||
npm install hasura-cli -g
|
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
|
||||||
hasura migrate apply --endpoint https://db.romeonline.io/ --admin-secret << parameters.secret >>
|
hasura migrate apply --endpoint https://db.romeonline.io/ --admin-secret << parameters.secret >>
|
||||||
hasura metadata apply --endpoint https://db.romeonline.io/ --admin-secret << parameters.secret >>
|
hasura metadata apply --endpoint https://db.romeonline.io/ --admin-secret << parameters.secret >>
|
||||||
hasura metadata reload --endpoint https://db.romeonline.io/ --admin-secret << parameters.secret >>
|
hasura metadata reload --endpoint https://db.romeonline.io/ --admin-secret << parameters.secret >>
|
||||||
|
- jira/notify:
|
||||||
|
environment: Production (Rome) - Hasura
|
||||||
|
environment_type: production
|
||||||
|
pipeline_id: << pipeline.id >>
|
||||||
|
job_type: deployment
|
||||||
|
pipeline_number: << pipeline.number >>
|
||||||
rome-app-build:
|
rome-app-build:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:18.18.2
|
- image: cimg/node:18.18.2
|
||||||
@@ -143,31 +172,12 @@ jobs:
|
|||||||
from: dist
|
from: dist
|
||||||
to: "s3://rome-online-production/"
|
to: "s3://rome-online-production/"
|
||||||
arguments: "--exclude '*.map'"
|
arguments: "--exclude '*.map'"
|
||||||
|
- jira/notify:
|
||||||
promanager-app-build:
|
environment: Production (Rome) - Front End
|
||||||
docker:
|
environment_type: production
|
||||||
- image: cimg/node:18.18.2
|
pipeline_id: << pipeline.id >>
|
||||||
|
job_type: deployment
|
||||||
working_directory: ~/repo/client
|
pipeline_number: << pipeline.number >>
|
||||||
|
|
||||||
steps:
|
|
||||||
- checkout:
|
|
||||||
path: ~/repo
|
|
||||||
- run:
|
|
||||||
name: Install Dependencies
|
|
||||||
command: npm i
|
|
||||||
|
|
||||||
- run: npm run build:production:promanager
|
|
||||||
|
|
||||||
- aws-cli/setup:
|
|
||||||
aws_access_key_id: AWS_ACCESS_KEY_ID
|
|
||||||
aws_secret_access_key: AWS_SECRET_ACCESS_KEY
|
|
||||||
region: AWS_REGION
|
|
||||||
|
|
||||||
- aws-s3/sync:
|
|
||||||
from: dist
|
|
||||||
to: "s3://promanager-production/"
|
|
||||||
arguments: "--exclude '*.map'"
|
|
||||||
|
|
||||||
test-rome-hasura-migrate:
|
test-rome-hasura-migrate:
|
||||||
docker:
|
docker:
|
||||||
@@ -183,10 +193,18 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Execute migration
|
name: Execute migration
|
||||||
command: |
|
command: |
|
||||||
npm install hasura-cli -g
|
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
|
||||||
hasura migrate apply --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >>
|
hasura migrate apply --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >>
|
||||||
|
sleep 5
|
||||||
hasura metadata apply --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >>
|
hasura metadata apply --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >>
|
||||||
|
sleep 10
|
||||||
hasura metadata reload --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >>
|
hasura metadata reload --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >>
|
||||||
|
- jira/notify:
|
||||||
|
environment: Test (Rome) - Hasura
|
||||||
|
environment_type: testing
|
||||||
|
pipeline_id: << pipeline.id >>
|
||||||
|
job_type: deployment
|
||||||
|
pipeline_number: << pipeline.number >>
|
||||||
|
|
||||||
test-rome-app-build:
|
test-rome-app-build:
|
||||||
docker:
|
docker:
|
||||||
@@ -212,31 +230,12 @@ jobs:
|
|||||||
from: dist
|
from: dist
|
||||||
to: "s3://rome-online-test/"
|
to: "s3://rome-online-test/"
|
||||||
arguments: "--exclude '*.map'"
|
arguments: "--exclude '*.map'"
|
||||||
|
- jira/notify:
|
||||||
test-promanager-app-build:
|
environment: Test (Rome) - Front End
|
||||||
docker:
|
environment_type: testing
|
||||||
- image: cimg/node:18.18.2
|
pipeline_id: << pipeline.id >>
|
||||||
|
job_type: deployment
|
||||||
working_directory: ~/repo/client
|
pipeline_number: << pipeline.number >>
|
||||||
|
|
||||||
steps:
|
|
||||||
- checkout:
|
|
||||||
path: ~/repo
|
|
||||||
- run:
|
|
||||||
name: Install Dependencies
|
|
||||||
command: npm i
|
|
||||||
|
|
||||||
- run: npm run build:test:promanager
|
|
||||||
|
|
||||||
- aws-cli/setup:
|
|
||||||
aws_access_key_id: AWS_ACCESS_KEY_ID
|
|
||||||
aws_secret_access_key: AWS_SECRET_ACCESS_KEY
|
|
||||||
region: AWS_REGION
|
|
||||||
|
|
||||||
- aws-s3/sync:
|
|
||||||
from: dist
|
|
||||||
to: "s3://promanager-testing/"
|
|
||||||
arguments: "--exclude '*.map'"
|
|
||||||
|
|
||||||
test-hasura-migrate:
|
test-hasura-migrate:
|
||||||
docker:
|
docker:
|
||||||
@@ -252,10 +251,18 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Execute migration
|
name: Execute migration
|
||||||
command: |
|
command: |
|
||||||
npm install hasura-cli -g
|
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
|
||||||
hasura migrate apply --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >>
|
hasura migrate apply --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >>
|
||||||
|
sleep 15
|
||||||
hasura metadata apply --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >>
|
hasura metadata apply --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >>
|
||||||
|
sleep 30
|
||||||
hasura metadata reload --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >>
|
hasura metadata reload --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >>
|
||||||
|
- jira/notify:
|
||||||
|
environment: Test (ImEX) - Hasura
|
||||||
|
environment_type: testing
|
||||||
|
pipeline_id: << pipeline.id >>
|
||||||
|
job_type: deployment
|
||||||
|
pipeline_number: << pipeline.number >>
|
||||||
|
|
||||||
imex-test-app-build:
|
imex-test-app-build:
|
||||||
docker:
|
docker:
|
||||||
@@ -302,7 +309,12 @@ jobs:
|
|||||||
from: dist
|
from: dist
|
||||||
to: "s3://imex-online-test-beta/"
|
to: "s3://imex-online-test-beta/"
|
||||||
arguments: "--exclude '*.map'"
|
arguments: "--exclude '*.map'"
|
||||||
|
- jira/notify:
|
||||||
|
environment: Test (ImEX) - Front End
|
||||||
|
environment_type: testing
|
||||||
|
pipeline_id: << pipeline.id >>
|
||||||
|
job_type: deployment
|
||||||
|
pipeline_number: << pipeline.number >>
|
||||||
|
|
||||||
admin-app-build:
|
admin-app-build:
|
||||||
docker:
|
docker:
|
||||||
@@ -353,7 +365,7 @@ workflows:
|
|||||||
secret: ${HASURA_PROD_SECRET}
|
secret: ${HASURA_PROD_SECRET}
|
||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
only: master
|
only: master-AIO
|
||||||
- rome-api-deploy:
|
- rome-api-deploy:
|
||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
@@ -363,7 +375,7 @@ workflows:
|
|||||||
branches:
|
branches:
|
||||||
only: master-AIO
|
only: master-AIO
|
||||||
- rome-hasura-migrate:
|
- rome-hasura-migrate:
|
||||||
secret: ${HASURA_PROD_SECRET}
|
secret: ${HASURA_ROME_PROD_SECRET}
|
||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
only: master-AIO
|
only: master-AIO
|
||||||
@@ -384,14 +396,6 @@ workflows:
|
|||||||
filters:
|
filters:
|
||||||
branches:
|
branches:
|
||||||
only: test-AIO
|
only: test-AIO
|
||||||
- test-promanager-app-build:
|
|
||||||
filters:
|
|
||||||
branches:
|
|
||||||
only: test-AIO
|
|
||||||
- promanager-app-build:
|
|
||||||
filters:
|
|
||||||
branches:
|
|
||||||
only: master-AIO
|
|
||||||
- test-rome-hasura-migrate:
|
- test-rome-hasura-migrate:
|
||||||
secret: ${HASURA_ROME_TEST_SECRET}
|
secret: ${HASURA_ROME_TEST_SECRET}
|
||||||
filters:
|
filters:
|
||||||
|
|||||||
24
.dockerignore
Normal file
24
.dockerignore
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Directories to exclude
|
||||||
|
.circleci
|
||||||
|
.idea
|
||||||
|
.platform
|
||||||
|
.vscode
|
||||||
|
_reference
|
||||||
|
client
|
||||||
|
redis/dockerdata
|
||||||
|
hasura
|
||||||
|
node_modules
|
||||||
|
# Files to exclude
|
||||||
|
.ebignore
|
||||||
|
.editorconfig
|
||||||
|
.eslintrc.json
|
||||||
|
.gitignore
|
||||||
|
.prettierrc.js
|
||||||
|
Dockerfile
|
||||||
|
README.MD
|
||||||
|
bodyshop_translations.babel
|
||||||
|
docker-compose.yml
|
||||||
|
ecosystem.config.js
|
||||||
|
|
||||||
|
# Optional: Exclude logs and temporary files
|
||||||
|
*.log
|
||||||
81
.gitattributes
vendored
Normal file
81
.gitattributes
vendored
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
# Ensure all text files use LF for line endings
|
||||||
|
* text eol=lf
|
||||||
|
|
||||||
|
# Binary files should not be modified by Git
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.ico binary
|
||||||
|
*.webp binary
|
||||||
|
*.svg binary
|
||||||
|
|
||||||
|
# Fonts
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
|
*.ttf binary
|
||||||
|
*.otf binary
|
||||||
|
*.eot binary
|
||||||
|
|
||||||
|
# Videos
|
||||||
|
*.mp4 binary
|
||||||
|
*.mov binary
|
||||||
|
*.avi binary
|
||||||
|
*.mkv binary
|
||||||
|
*.webm binary
|
||||||
|
|
||||||
|
# Audio
|
||||||
|
*.mp3 binary
|
||||||
|
*.wav binary
|
||||||
|
*.ogg binary
|
||||||
|
*.flac binary
|
||||||
|
|
||||||
|
# Archives and compressed files
|
||||||
|
*.zip binary
|
||||||
|
*.gz binary
|
||||||
|
*.tar binary
|
||||||
|
*.7z binary
|
||||||
|
*.rar binary
|
||||||
|
|
||||||
|
# PDF and documents
|
||||||
|
*.pdf binary
|
||||||
|
*.doc binary
|
||||||
|
*.docx binary
|
||||||
|
*.xls binary
|
||||||
|
*.xlsx binary
|
||||||
|
*.ppt binary
|
||||||
|
*.pptx binary
|
||||||
|
|
||||||
|
# Exclude JSON and other data files from text processing, if necessary
|
||||||
|
*.json text
|
||||||
|
*.xml text
|
||||||
|
*.csv text
|
||||||
|
|
||||||
|
# Scripts and code files should maintain LF endings
|
||||||
|
*.js text eol=lf
|
||||||
|
*.jsx text eol=lf
|
||||||
|
*.ts text eol=lf
|
||||||
|
*.tsx text eol=lf
|
||||||
|
*.css text eol=lf
|
||||||
|
*.scss text eol=lf
|
||||||
|
*.html text eol=lf
|
||||||
|
*.yml text eol=lf
|
||||||
|
*.yaml text eol=lf
|
||||||
|
*.md text eol=lf
|
||||||
|
*.sh text eol=lf
|
||||||
|
*.py text eol=lf
|
||||||
|
*.rb text eol=lf
|
||||||
|
*.java text eol=lf
|
||||||
|
*.php text eol=lf
|
||||||
|
*.sql text eol=lf
|
||||||
|
|
||||||
|
# Git configuration files
|
||||||
|
.gitattributes text eol=lf
|
||||||
|
.gitignore text eol=lf
|
||||||
|
*.gitattributes text eol=lf
|
||||||
|
|
||||||
|
# Exclude some other potential binary files
|
||||||
|
*.db binary
|
||||||
|
*.sqlite binary
|
||||||
|
*.exe binary
|
||||||
|
*.dll binary
|
||||||
0
.localstack/.gitkeep
Normal file
0
.localstack/.gitkeep
Normal file
24
.platform/hooks/predeploy/00-install-fonts.sh
Normal file
24
.platform/hooks/predeploy/00-install-fonts.sh
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Install required packages
|
||||||
|
dnf install -y fontconfig freetype
|
||||||
|
|
||||||
|
# Move to the /tmp directory for temporary download and extraction
|
||||||
|
cd /tmp
|
||||||
|
|
||||||
|
# Download the Montserrat font zip file
|
||||||
|
wget https://images.imex.online/fonts/montserrat.zip -O montserrat.zip
|
||||||
|
|
||||||
|
# Unzip the downloaded font file
|
||||||
|
unzip montserrat.zip -d montserrat
|
||||||
|
|
||||||
|
# Move the font files to the system fonts directory
|
||||||
|
mv montserrat/montserrat/*.ttf /usr/share/fonts
|
||||||
|
|
||||||
|
# Rebuild the font cache
|
||||||
|
fc-cache -fv
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
rm -rf /tmp/montserrat /tmp/montserrat.zip
|
||||||
|
|
||||||
|
echo "Montserrat fonts installed and cached successfully."
|
||||||
5
.platform/hooks/predeploy/01-install-dd.sh
Normal file
5
.platform/hooks/predeploy/01-install-dd.sh
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
DD_API_KEY=58d91898a70c6fd659f6eea768a57976 DD_SITE="us3.datadoghq.com" bash -c "$(curl -L https://install.datadoghq.com/scripts/install_script_agent7.sh)"
|
||||||
|
|
||||||
|
echo "Datadog agent installed."
|
||||||
@@ -1 +1,2 @@
|
|||||||
client_max_body_size 50M;
|
client_max_body_size 50M;
|
||||||
|
client_body_buffer_size 5M;
|
||||||
|
|||||||
15
.vscode/launch.json
vendored
15
.vscode/launch.json
vendored
@@ -14,6 +14,21 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"url": "http://localhost:3000",
|
"url": "http://localhost:3000",
|
||||||
"webRoot": "${workspaceRoot}/client/src"
|
"webRoot": "${workspaceRoot}/client/src"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Attach to Node.js in Docker",
|
||||||
|
"type": "node",
|
||||||
|
"request": "attach",
|
||||||
|
"address": "localhost",
|
||||||
|
"port": 9229,
|
||||||
|
"localRoot": "${workspaceFolder}",
|
||||||
|
"remoteRoot": "/app",
|
||||||
|
"protocol": "inspector",
|
||||||
|
"restart": true,
|
||||||
|
"sourceMaps": true,
|
||||||
|
"skipFiles": [
|
||||||
|
"<node_internals>/**"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
33
.vscode/settings.json
vendored
33
.vscode/settings.json
vendored
@@ -8,5 +8,36 @@
|
|||||||
"pattern": "**/IMEX.xml",
|
"pattern": "**/IMEX.xml",
|
||||||
"systemId": "logs/IMEX.xsd"
|
"systemId": "logs/IMEX.xsd"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"cSpell.words": [
|
||||||
|
"antd",
|
||||||
|
"appointmentconfirmation",
|
||||||
|
"appt",
|
||||||
|
"autohouse",
|
||||||
|
"autohouseid",
|
||||||
|
"billlines",
|
||||||
|
"bodyshop",
|
||||||
|
"bodyshopid",
|
||||||
|
"bodyshops",
|
||||||
|
"CIECA",
|
||||||
|
"claimscorp",
|
||||||
|
"claimscorpid",
|
||||||
|
"Dinero",
|
||||||
|
"driveable",
|
||||||
|
"IMEX",
|
||||||
|
"imexshopid",
|
||||||
|
"jobid",
|
||||||
|
"joblines",
|
||||||
|
"Kaizen",
|
||||||
|
"labhrs",
|
||||||
|
"larhrs",
|
||||||
|
"mixdata",
|
||||||
|
"ownr",
|
||||||
|
"promanager",
|
||||||
|
"shopname",
|
||||||
|
"smartscheduling",
|
||||||
|
"timetickets",
|
||||||
|
"touchtime"
|
||||||
|
],
|
||||||
|
"eslint.workingDirectories": ["./", "./client"]
|
||||||
}
|
}
|
||||||
|
|||||||
59
Dockerfile
Normal file
59
Dockerfile
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# Use Amazon Linux 2023 as the base image
|
||||||
|
FROM amazonlinux:2023
|
||||||
|
|
||||||
|
# Install Git and Node.js (Amazon Linux 2023 uses the DNF package manager)
|
||||||
|
RUN dnf install -y git \
|
||||||
|
&& curl -sL https://rpm.nodesource.com/setup_20.x | bash - \
|
||||||
|
&& dnf install -y nodejs \
|
||||||
|
&& dnf clean all
|
||||||
|
|
||||||
|
# Install dependencies required by node-canvas
|
||||||
|
RUN dnf install -y \
|
||||||
|
gcc \
|
||||||
|
gcc-c++ \
|
||||||
|
cairo-devel \
|
||||||
|
pango-devel \
|
||||||
|
libjpeg-turbo-devel \
|
||||||
|
giflib-devel \
|
||||||
|
libpng-devel \
|
||||||
|
make \
|
||||||
|
python3 \
|
||||||
|
fontconfig \
|
||||||
|
freetype \
|
||||||
|
python3-pip \
|
||||||
|
wget \
|
||||||
|
unzip \
|
||||||
|
&& dnf clean all
|
||||||
|
|
||||||
|
# Install Montserrat fonts
|
||||||
|
RUN cd /tmp \
|
||||||
|
&& wget https://images.imex.online/fonts/montserrat.zip -O montserrat.zip \
|
||||||
|
&& unzip montserrat.zip -d montserrat \
|
||||||
|
&& mv montserrat/montserrat/*.ttf /usr/share/fonts \
|
||||||
|
&& fc-cache -fv \
|
||||||
|
&& rm -rf /tmp/montserrat /tmp/montserrat.zip \
|
||||||
|
&& echo "Montserrat fonts installed and cached successfully."
|
||||||
|
|
||||||
|
# Set the working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# This is because our test route uses a git commit hash
|
||||||
|
RUN git config --global --add safe.directory /app
|
||||||
|
|
||||||
|
# Copy package.json and package-lock.json
|
||||||
|
COPY package.json ./
|
||||||
|
|
||||||
|
# Install Nodemon
|
||||||
|
RUN npm install -g nodemon
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm i --no-package-lock
|
||||||
|
|
||||||
|
# Copy the rest of your application code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Expose the port your app runs on (adjust if necessary)
|
||||||
|
EXPOSE 4000 9229
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
CMD ["nodemon", "--legacy-watch", "--inspect=0.0.0.0:9229", "server.js"]
|
||||||
@@ -2,7 +2,7 @@ NGROK TEsting:
|
|||||||
./ngrok.exe http http://localhost:4000 -host-header="localhost:4000"
|
./ngrok.exe http http://localhost:4000 -host-header="localhost:4000"
|
||||||
|
|
||||||
Finding deadfiles - run from client directory
|
Finding deadfiles - run from client directory
|
||||||
npx deadfile ./src/index.js --exclude build templates
|
npx deadfile ./src/index.jsx --exclude build templates
|
||||||
|
|
||||||
#Crushing all hasura migrations by creating a new initialization from the server.
|
#Crushing all hasura migrations by creating a new initialization from the server.
|
||||||
hasura migrate create "Init" --from-server --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#'
|
hasura migrate create "Init" --from-server --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#'
|
||||||
@@ -11,4 +11,4 @@ Production-ImEXOnline!@#'
|
|||||||
hasura migrate status --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#'
|
hasura migrate status --endpoint https://db.imex.online/ --admin-secret 'Production-ImEXOnline!@#'
|
||||||
|
|
||||||
Generate the license file:
|
Generate the license file:
|
||||||
$ generate-license-file --input package.json --output third-party-licenses.txt --overwrite
|
$ generate-license-file --input package.json --output third-party-licenses.txt --overwrite
|
||||||
|
|||||||
64
_reference/Documents/dockerreadme.md
Normal file
64
_reference/Documents/dockerreadme.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Setting up External Networking and Static IP for WSL2 using Hyper-V
|
||||||
|
|
||||||
|
This guide will walk you through the steps to configure your WSL2 (Windows Subsystem for Linux) instance to use an external Hyper-V virtual switch, enabling it to connect directly to your local network. Additionally, you'll learn how to assign a static IP address to your WSL2 instance.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. **Windows 11**
|
||||||
|
2. **Docker Desktop For Windows (Latest Version)
|
||||||
|
|
||||||
|
# Docker Setup
|
||||||
|
Inside the root of the project exists the `docker-compose.yaml` file, you can simply run
|
||||||
|
`docker-compose up` to launch the backend.
|
||||||
|
|
||||||
|
Things to note:
|
||||||
|
- When installing NPM packages, you will need to rebuild the `node-app` container
|
||||||
|
- Making changes to the server files will restart the `node-app`
|
||||||
|
|
||||||
|
# Local Stack
|
||||||
|
- LocalStack Front end (Optional) - https://apps.microsoft.com/detail/9ntrnft9zws2?hl=en-us&gl=US
|
||||||
|
- http://localhost:4566/_aws/ses will allow you to see emails sent
|
||||||
|
|
||||||
|
# Docker Commands
|
||||||
|
|
||||||
|
## General `docker-compose` Commands:
|
||||||
|
1. Bring up the services, force a rebuild of all services, and do not use the cache: `docker-compose up --build --no-cache`
|
||||||
|
2. Start Containers in Detached Mode: This will run the containers in the background (detached mode): `docker-compose up -d`
|
||||||
|
3. Stop and Remove Containers: Stops and removes the containers gracefully: `docker-compose down`
|
||||||
|
4. Stop containers without removing them: `docker-compose stop`
|
||||||
|
5. Remove Containers, Volumes, and Networks: `docker-compose down --volumes`
|
||||||
|
6. Force rebuild of containers: `docker-compose build --no-cache`
|
||||||
|
7. View running Containers: `docker-compose ps`
|
||||||
|
8. View a specific containers logs: `docker-compose logs <container-name>`
|
||||||
|
9. Scale services (multiple instances of a service): `docker-compose up --scale <container-name>=<instances number> -d`
|
||||||
|
10. Watch a specific containers logs in realtime with timestamps: `docker-compose logs -f --timestamps <container-name>`
|
||||||
|
|
||||||
|
## Volume Management Commands
|
||||||
|
1. List Docker volumes: `docker volume ls`
|
||||||
|
2. Remove Unused volumes `docker volume prune`
|
||||||
|
3. Remove specific volumes `docker volume rm <volume-name>`
|
||||||
|
4. Inspect a volume: `docker volume inspect <volume-name>`
|
||||||
|
|
||||||
|
## Container Image Management Commands:
|
||||||
|
1. List running containers: `docker ps`
|
||||||
|
2. List all containers: `docker os -a`
|
||||||
|
3. Remove Stopped containers: `docker container prune`
|
||||||
|
4. Remove a specific container: `docker container rm <container-name>`
|
||||||
|
5. Remove a specific image: `docker rmi <image-name>:<version>`
|
||||||
|
6. Remove all unused images: `docker image prune -a`
|
||||||
|
|
||||||
|
## Network Management Commands:
|
||||||
|
1. List networks: `docker network ls`
|
||||||
|
2. Inspect a specific network: `docker network inspect <network-name>`
|
||||||
|
3. Remove a specific network: `docker network rm <network-name>`
|
||||||
|
4. Remove unused networks: `docker network prune`
|
||||||
|
|
||||||
|
## Debugging and maintenance:
|
||||||
|
1. Enter a Running container: `docker exec -it <container name> /bin/bash` (could also be `/bin/sh` or for example `redis-cli` on a redis node)
|
||||||
|
2. View container resource usage: `docker stats`
|
||||||
|
3. Check Disk space used by Docker: `docker system df`
|
||||||
|
4. Remove all unused Data (Nuclear option): `docker system prune`
|
||||||
|
|
||||||
|
## Specific examples
|
||||||
|
1. To simulate a Clean state, one should run `docker system prune` followed by `docker volume prune -a`
|
||||||
|
2. You can run `docker-compose up` without the `-d` option, and you will get what is identical to the experience you were used to, this includes being able to control-c and bring the entire stack down
|
||||||
41
_reference/Documents/productionBoardNotes.md
Normal file
41
_reference/Documents/productionBoardNotes.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Production Board Notes:
|
||||||
|
|
||||||
|
## General Notes
|
||||||
|
|
||||||
|
- You can single click the lane footer to collapse/un-collapse the lane
|
||||||
|
- You can double click the lane header to collapse/un-collapse the lane
|
||||||
|
- If you need to scroll horizontally, you can hold shift and use the mouse scroll wheel, or press the mouse scroll wheel while scrolling
|
||||||
|
|
||||||
|
## Board Settings
|
||||||
|
|
||||||
|
#### Layout
|
||||||
|
|
||||||
|
- Board Orientation (Vertical or Horizontal)
|
||||||
|
- This determines the orientation of the card layout on the board.
|
||||||
|
- Horizontal is the default setting, and how the prior board was set up.
|
||||||
|
- Vertical is the new setting and allows lanes to be displayed vertically, with a grid of cards
|
||||||
|
- Card Size (Small, Medium, Large)
|
||||||
|
- This determines the size of the cards on the board.
|
||||||
|
- Small is the default setting, and how the prior board was set up.
|
||||||
|
- Medium and Large are new settings and allow for larger cards to be displayed on the board.
|
||||||
|
- Compact Cards (Tall or Wide)
|
||||||
|
- Formally called 'Compact'
|
||||||
|
- When on, data is displayed on the card vertically
|
||||||
|
- when turned off, some fields may share horizontal space, tightening the card layout
|
||||||
|
- Colored Cards (On or Off)
|
||||||
|
- When on, cards are colored based on the Status color
|
||||||
|
- Kiosk Mode (On or Off)
|
||||||
|
- This should be turned on if the shop is using it on a tablet (Ipad)
|
||||||
|
|
||||||
|
#### Information
|
||||||
|
|
||||||
|
These allow users to turn fields on or off, turning them all off will show the card in the most minimal form
|
||||||
|
|
||||||
|
|
||||||
|
### Statistics
|
||||||
|
|
||||||
|
- The statistics section allows users to see accumulations of both jobs on the board, and jobs in production.
|
||||||
|
- you can click a statistic to turn it on and off, and drag and drop the statistics to rearrange them
|
||||||
|
|
||||||
|
### Filters
|
||||||
|
- Allows you to set, and persist filters for estimators and insurance companies
|
||||||
@@ -4,7 +4,7 @@ Clone Repository for:
|
|||||||
{
|
{
|
||||||
"name": "node-webhook-scripts",
|
"name": "node-webhook-scripts",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "index.js",
|
"main": "index.jsx",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.16.4"
|
"express": "^4.16.4"
|
||||||
},
|
},
|
||||||
@@ -10,5 +10,8 @@
|
|||||||
"courtesycars": "date",
|
"courtesycars": "date",
|
||||||
"media": "date",
|
"media": "date",
|
||||||
"visualboard": "date",
|
"visualboard": "date",
|
||||||
"scoreboard": "date"
|
"scoreboard": "date",
|
||||||
|
"checklist": "date",
|
||||||
|
"smartscheduling" :"date",
|
||||||
|
"roguard": "date"
|
||||||
}
|
}
|
||||||
1
_reference/localEmailViewer/.gitignore
vendored
Normal file
1
_reference/localEmailViewer/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node_modules
|
||||||
7
_reference/localEmailViewer/README.md
Normal file
7
_reference/localEmailViewer/README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
This will connect to your dockers local stack session and render the email in HTML.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
node index.js
|
||||||
|
```
|
||||||
|
|
||||||
|
http://localhost:3334
|
||||||
116
_reference/localEmailViewer/index.js
Normal file
116
_reference/localEmailViewer/index.js
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
// index.js
|
||||||
|
|
||||||
|
import express from 'express';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
import {simpleParser} from 'mailparser';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = 3334;
|
||||||
|
|
||||||
|
app.get('/', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost:4566/_aws/ses');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Network response was not ok');
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
const messagesHtml = await parseMessages(data.messages);
|
||||||
|
res.send(renderHtml(messagesHtml));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching messages:', error);
|
||||||
|
res.status(500).send('Error fetching messages');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function parseMessages(messages) {
|
||||||
|
const parsedMessages = await Promise.all(
|
||||||
|
messages.map(async (message, index) => {
|
||||||
|
try {
|
||||||
|
const parsed = await simpleParser(message.RawData);
|
||||||
|
return `
|
||||||
|
<div class="shadow-md rounded-lg p-4 mb-6" style="background-color: lightgray">
|
||||||
|
<div class="shadow-md rounded-lg p-4 mb-6" style="background-color: white">
|
||||||
|
<div class="mb-2">
|
||||||
|
<span class="font-bold text-lg">Message ${index + 1}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<span class="font-semibold">From:</span> ${message.Source}
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<span class="font-semibold">Region:</span> ${message.Region}
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<span class="font-semibold">Timestamp:</span> ${message.Timestamp}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="prose">
|
||||||
|
${parsed.html || parsed.textAsHtml || 'No HTML content available'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing email:', error);
|
||||||
|
return `
|
||||||
|
<div class="bg-white shadow-md rounded-lg p-4 mb-6">
|
||||||
|
<div class="mb-2">
|
||||||
|
<span class="font-bold text-lg">Message ${index + 1}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<span class="font-semibold">From:</span> ${message.Source}
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<span class="font-semibold">Region:</span> ${message.Region}
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<span class="font-semibold">Timestamp:</span> ${message.Timestamp}
|
||||||
|
</div>
|
||||||
|
<div class="text-red-500">
|
||||||
|
Error parsing email content
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return parsedMessages.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHtml(messagesHtml) {
|
||||||
|
return `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Email Messages Viewer</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 50px auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.prose {
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container bg-white shadow-lg rounded-lg p-6">
|
||||||
|
<h1 class="text-2xl font-bold text-center mb-6">Email Messages Viewer</h1>
|
||||||
|
<div id="messages-container">
|
||||||
|
${messagesHtml}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Server is running on http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
1214
_reference/localEmailViewer/package-lock.json
generated
Normal file
1214
_reference/localEmailViewer/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
_reference/localEmailViewer/package.json
Normal file
18
_reference/localEmailViewer/package.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "localemailviewer",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.21.1",
|
||||||
|
"mailparser": "^3.7.1",
|
||||||
|
"node-fetch": "^3.3.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
59
_reference/prHelper/index.html
Normal file
59
_reference/prHelper/index.html
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>IMEX IO Extractor</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
.output-box {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
.copy-button {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>IMEX IO Extractor</h1>
|
||||||
|
<textarea id="inputText" placeholder="Paste your text here..."></textarea>
|
||||||
|
<br>
|
||||||
|
<button onclick="extractIO()">Extract</button>
|
||||||
|
|
||||||
|
<div class="output-box" id="outputBox" contenteditable="true"></div>
|
||||||
|
<button class="copy-button" onclick="copyToClipboard()">Copy to Clipboard</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function extractIO() {
|
||||||
|
const inputText = document.getElementById('inputText').value;
|
||||||
|
const ioNumbers = [...new Set(inputText.match(/IO-\d{4}/g))] // Extract unique IO-#### matches
|
||||||
|
.map(io => ({ io, num: parseInt(io.split('-')[1]) })) // Extract number part for sorting
|
||||||
|
.sort((a, b) => a.num - b.num) // Sort by the number
|
||||||
|
.map(item => item.io); // Extract sorted IO-####
|
||||||
|
|
||||||
|
document.getElementById('outputBox').innerText = ioNumbers.join(', '); // Display horizontally
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyToClipboard() {
|
||||||
|
const outputBox = document.getElementById('outputBox');
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(outputBox);
|
||||||
|
const selection = window.getSelection();
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
document.execCommand('copy');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -11,7 +11,7 @@ module.exports = {
|
|||||||
|
|
||||||
{
|
{
|
||||||
name: "Bitbucket Webhook",
|
name: "Bitbucket Webhook",
|
||||||
script: "./webhook/index.js",
|
script: "./webhook/index.jsx",
|
||||||
env: {
|
env: {
|
||||||
NODE_ENV: "production"
|
NODE_ENV: "production"
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
20
certs/cert.pem
Normal file
20
certs/cert.pem
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDWzCCAkOgAwIBAgIUD/QBSAXy/AlJ/cS4DaPWJLpChxgwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwPTELMAkGA1UEBhMCQ0ExCzAJBgNVBAgMAk9OMSEwHwYDVQQKDBhJbnRlcm5l
|
||||||
|
dCBXaWRnaXRzIFB0eSBMdGQwHhcNMjQwOTA5MTU0MjA1WhcNMjUwOTA5MTU0MjA1
|
||||||
|
WjA9MQswCQYDVQQGEwJDQTELMAkGA1UECAwCT04xITAfBgNVBAoMGEludGVybmV0
|
||||||
|
IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
|
||||||
|
AKSd0l7NJCNBwvtPU+dVPQkteg0AfC3sGqRnZMQteCRVa2oIgC4NoF3A9BK/yHbF
|
||||||
|
ZF25OnXTck5vzc8yb3v73ndfTD9ASKNoiaZE84/GFBsxqlKR8cs0qVwzuAsdijMv
|
||||||
|
vlMPNlMRyE1Rb7nR6HXGkPXNyxgMko03NXPkvIje9zRudm0Lf8L4q/hPyPkS7Mrm
|
||||||
|
/uQfAAJe+xFcupkEX2XY7r0x1C+z6E8lA1UcuhK3SHdW7CWYqp1vU5/dnnUiXwCa
|
||||||
|
GiC6Y1bCJB0pDAVISzy3JUDdINZdiqGR+y8ho3pstChf2mp/76s3N9eG9KA/qaFK
|
||||||
|
BrGk2PvCoZ8/Aj1aMsRYFHECAwEAAaNTMFEwHQYDVR0OBBYEFDLJ2fbWP4VUJgOp
|
||||||
|
PSs+NGHcVgRmMB8GA1UdIwQYMBaAFDLJ2fbWP4VUJgOpPSs+NGHcVgRmMA8GA1Ud
|
||||||
|
EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBABfv5ut/y03atq0NMB0jeDY4
|
||||||
|
AvW4ukk0k1svyqxFZCw9o7m2lHb/IjmVrZG1Sj4JWrrSv0s02ccb26/t6vazNa5L
|
||||||
|
Powe3eyfHgfjTZJmgs8hyeMwKS0wWk/SPuu9JDhIJakiquqD+UVBGkHpP+XYvhDv
|
||||||
|
vhS2XRlW+aEjpUmr1oCyyrc6WbzrYRNadqEsn/AxwcMyUbht3Ugjkg+OpidcTIQp
|
||||||
|
5lv63waKo6I1vQofzBQ3L7JYsKo8kC0vAP7wkLxvzBii335uZJzzpFYFVOyVNezi
|
||||||
|
dJdazPbRYbXz4LjltdEn/SNfRuKX8ZRiN2OSo7OfSrZaMTS87SfCSFJGgQM8Yrk=
|
||||||
|
-----END CERTIFICATE-----
|
||||||
27
certs/id_rsa.key
Normal file
27
certs/id_rsa.key
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
|
||||||
|
NhAAAAAwEAAQAAAQEAvNl5fuVmLNv72BZNxnTqX5CHf5Xi8UxjYaYxHITSCx7blnhpVYLd
|
||||||
|
qXvcOWXzbsfjch/den73QiW4n2FYz75oGMhUGlOYzdWKA9I9Sj09Qy1R06RhwDiZGd5qaM
|
||||||
|
swEeXpkNmi2u4Qd2kJeDfUQUigjC09V81O/vrniGtQAJScfiG/itdm+Ufn09Z4MYk0HWjq
|
||||||
|
iDokNEskoEPsibYIrb+Q6vdtuPkZO+wU/smXhPtgw5ST6oQdmm/gVNsRg5XNzxrire+z1G
|
||||||
|
WatnnVL3hPnnfpnf8W589dyms7GGJwhPerSGTN1bn0T4+9C69Cd7LBJtxiuFdRmdlGLLLP
|
||||||
|
RR48Rur71wAAA9AEfVsdBH1bHQAAAAdzc2gtcnNhAAABAQC82Xl+5WYs2/vYFk3GdOpfkI
|
||||||
|
d/leLxTGNhpjEchNILHtuWeGlVgt2pe9w5ZfNux+NyH916fvdCJbifYVjPvmgYyFQaU5jN
|
||||||
|
1YoD0j1KPT1DLVHTpGHAOJkZ3mpoyzAR5emQ2aLa7hB3aQl4N9RBSKCMLT1XzU7++ueIa1
|
||||||
|
AAlJx+Ib+K12b5R+fT1ngxiTQdaOqIOiQ0SySgQ+yJtgitv5Dq9224+Rk77BT+yZeE+2DD
|
||||||
|
lJPqhB2ab+BU2xGDlc3PGuKt77PUZZq2edUveE+ed+md/xbnz13KazsYYnCE96tIZM3Vuf
|
||||||
|
RPj70Lr0J3ssEm3GK4V1GZ2UYsss9FHjxG6vvXAAAAAwEAAQAAAQAQTosSLQbMmtY9S3e9
|
||||||
|
yjyusdExcCTfhyQRu4MEHmfws+JsNMuLqbgwOVTD1AzYJQR7x0qdmDcLjCxL/uDnV16vvS
|
||||||
|
Sd/Vf1dhnryIyoS29tzI0DRG94ZKq7tBvmHp1w/jRT4KcSVnovhW9e5Rs74+SRFhr06PKI
|
||||||
|
S+wQOIv48Nwue9+QUMsMCpWgKXHx7SHNTHvnAfqdhi9O29SWlMA+v+mELZ5Cl+HU0UTt2I
|
||||||
|
A1BxOe1N8FjN7KE2viJexsl3is1PuqMkpLl/wyHBJTVzUadl6DRALJQIm7/YO5goE72YOV
|
||||||
|
Lpo27do3zjhC87dlKdATvZUzfKV0LuUVdxq/PNDZMUbBAAAAgQDShAqDZiDrdTUaGXfUVm
|
||||||
|
QzcnVNbh2/KgZh4uux9QNHST562W6cnN7qxoRwVrM4BCOk1Kl73QQZW4nDvXX3PVC5j038
|
||||||
|
8AXkcBHS9j9f4h72ue7D2jqlbHFa7aGU9zYgk9mbBF+GX3tDntkAIQjLtwOLfj1iiJ/clX
|
||||||
|
mHFUAY1V4L8AAAAIEA3E4t/v0yU5D9AOI0r17UNYqfeyDoKAEDR4QbbFjO1l0kLnEJy7Zx
|
||||||
|
Mhj18GilYg2y0P0v8dSM/oWXS8Hua2t5i9Exlv6gHhGlQ80mwYcVGIxewZ/pPeCPw0U+kt
|
||||||
|
EKUjt09m9Oe7+6xHQsTBj9hY8/vqPmQwRalZFcLdhHiDiVKTcAAACBANtykaPXdVzEFx7D
|
||||||
|
UOlsjVL7zM0EVOFXf9JJQ6BhazhmsEI2PYt3IpgGMo8cXkoUofAOIYjf421AabN1BqSO5J
|
||||||
|
XTMxM0ZV3JmLLi804Mu9h1iFrVTBdLYOMJdc2VCo1EwHWpo9SXOyjxce/znvcIOU04aZhu
|
||||||
|
TaPg816X+E+gw5JhAAAAFGRhdmVARGF2ZVJpY2hlci1JTUVYAQIDBAUG
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
||||||
1
certs/id_rsa.pub
Normal file
1
certs/id_rsa.pub
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC82Xl+5WYs2/vYFk3GdOpfkId/leLxTGNhpjEchNILHtuWeGlVgt2pe9w5ZfNux+NyH916fvdCJbifYVjPvmgYyFQaU5jN1YoD0j1KPT1DLVHTpGHAOJkZ3mpoyzAR5emQ2aLa7hB3aQl4N9RBSKCMLT1XzU7++ueIa1AAlJx+Ib+K12b5R+fT1ngxiTQdaOqIOiQ0SySgQ+yJtgitv5Dq9224+Rk77BT+yZeE+2DDlJPqhB2ab+BU2xGDlc3PGuKt77PUZZq2edUveE+ed+md/xbnz13KazsYYnCE96tIZM3VufRPj70Lr0J3ssEm3GK4V1GZ2UYsss9FHjxG6vvX dave@DaveRicher-IMEX
|
||||||
12
certs/io-ftp-test.key
Normal file
12
certs/io-ftp-test.key
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAArAAAABNlY2RzYS
|
||||||
|
1zaGEyLW5pc3RwNTIxAAAACG5pc3RwNTIxAAAAhQQBYJnAujo17diR0fM2Ze1d1Ft6XHm5
|
||||||
|
U31pXdFEN+rGC4SoYTdZE8q3relxMS5GwwBOvgvVUuayfid2XS8ls/CMDiMBJAYqEK4CRY
|
||||||
|
PbbPB7lLnMWsF7muFhvs+SIpPQC+vtDwM2TKlxF0Y8p+iVRpvCADoggsSze7skmJWKmMTt
|
||||||
|
8jEdEOcAAAEQIyXsOSMl7DkAAAATZWNkc2Etc2hhMi1uaXN0cDUyMQAAAAhuaXN0cDUyMQ
|
||||||
|
AAAIUEAWCZwLo6Ne3YkdHzNmXtXdRbelx5uVN9aV3RRDfqxguEqGE3WRPKt63pcTEuRsMA
|
||||||
|
Tr4L1VLmsn4ndl0vJbPwjA4jASQGKhCuAkWD22zwe5S5zFrBe5rhYb7PkiKT0Avr7Q8DNk
|
||||||
|
ypcRdGPKfolUabwgA6IILEs3u7JJiVipjE7fIxHRDnAAAAQUO5dO9G7i0bxGTP0zV3eIwv
|
||||||
|
5g0NhrQJfW/bMHS6XWwaxdpr+QZ+DbBJVzZPwYC0wLMW4bJAf+kjqUnj4wGocoTeAAAAD2
|
||||||
|
lvLWZ0cC10ZXN0LWtleQECAwQ=
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
||||||
1
certs/io-ftp-test.key.pub
Normal file
1
certs/io-ftp-test.key.pub
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAFgmcC6OjXt2JHR8zZl7V3UW3pceblTfWld0UQ36sYLhKhhN1kTyret6XExLkbDAE6+C9VS5rJ+J3ZdLyWz8IwOIwEkBioQrgJFg9ts8HuUucxawXua4WG+z5Iik9AL6+0PAzZMqXEXRjyn6JVGm8IAOiCCxLN7uySYlYqYxO3yMR0Q5w== io-ftp-test-key
|
||||||
12
certs/io-ftp-test.ppk
Normal file
12
certs/io-ftp-test.ppk
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
PuTTY-User-Key-File-3: ecdsa-sha2-nistp521
|
||||||
|
Encryption: none
|
||||||
|
Comment: io-ftp-test-key
|
||||||
|
Public-Lines: 4
|
||||||
|
AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAFgmcC6OjXt
|
||||||
|
2JHR8zZl7V3UW3pceblTfWld0UQ36sYLhKhhN1kTyret6XExLkbDAE6+C9VS5rJ+
|
||||||
|
J3ZdLyWz8IwOIwEkBioQrgJFg9ts8HuUucxawXua4WG+z5Iik9AL6+0PAzZMqXEX
|
||||||
|
Rjyn6JVGm8IAOiCCxLN7uySYlYqYxO3yMR0Q5w==
|
||||||
|
Private-Lines: 2
|
||||||
|
AAAAQUO5dO9G7i0bxGTP0zV3eIwv5g0NhrQJfW/bMHS6XWwaxdpr+QZ+DbBJVzZP
|
||||||
|
wYC0wLMW4bJAf+kjqUnj4wGocoTe
|
||||||
|
Private-MAC: d67001d47e13c43dc8bdb9c68a25356a96c1c4a6714f3c5a1836fca646b78b54
|
||||||
28
certs/key.pem
Normal file
28
certs/key.pem
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCkndJezSQjQcL7
|
||||||
|
T1PnVT0JLXoNAHwt7BqkZ2TELXgkVWtqCIAuDaBdwPQSv8h2xWRduTp103JOb83P
|
||||||
|
Mm97+953X0w/QEijaImmRPOPxhQbMapSkfHLNKlcM7gLHYozL75TDzZTEchNUW+5
|
||||||
|
0eh1xpD1zcsYDJKNNzVz5LyI3vc0bnZtC3/C+Kv4T8j5EuzK5v7kHwACXvsRXLqZ
|
||||||
|
BF9l2O69MdQvs+hPJQNVHLoSt0h3VuwlmKqdb1Of3Z51Il8AmhogumNWwiQdKQwF
|
||||||
|
SEs8tyVA3SDWXYqhkfsvIaN6bLQoX9pqf++rNzfXhvSgP6mhSgaxpNj7wqGfPwI9
|
||||||
|
WjLEWBRxAgMBAAECggEAUNpHYlLFxh9dokujPUMreF+Cy/IKDBAkQc2au5RNpyLh
|
||||||
|
YDIOqw/8TTAhcTgLQPLQygvZP9f8E7RsVLFD+pSJ/v2qmIJ9au1Edor1Sg+S/oxV
|
||||||
|
SLrwFMunx2aLpcH7iAqSI3+cQg7A3+D4zD7iOz6tIl3Su9wo+v073tFhHKTOrEwv
|
||||||
|
Qgr9Jf3viIiKV1ym+uQEVQndocfsj46FnFpXTQ2qs7kAF6FgAOLDGfQQwzkiqEBD
|
||||||
|
NsqsDmbYIx6foZL+DEz1ZVO2M5B+xxpbNK82KwuQilVpimW8ui4LZHCe+RIFzt9+
|
||||||
|
BK6KGlLpSEwTFliivI3nahy18JzskZsfyah0CPZlQQKBgQDVv+A0qIPGvOP3Sx+9
|
||||||
|
HyeQCV23SkvvSvw8p8pMB0gvwv63YdJ7N/rJzBGS6YUHFWWZZgEeTgkJ6VJvoe0r
|
||||||
|
8JL1el9uSUa7f0eayjmFBOGuzpktNVdIn2Tg7A9MWA4JqPNNC69RMOh86ewGD4J3
|
||||||
|
a8Hz2a1bHxAmy/AZt2ukypY6eQKBgQDFJ7kqeOPkRBz9WbALRgVIXo8YWf5di0sQ
|
||||||
|
r0HC03GAISHQ725A2IFBPHJWeqj0jaMiIZD0y+Obgp7KAskrJaLfsd7Ug775kFfw
|
||||||
|
oUI9UAl6kRuPKvm3BaVAm46SQm+56VsgxTi73YN0NUp75THHZgAJjepF9zSpVJxq
|
||||||
|
VY9DjEGruQKBgQCQCpGIatcCol/tUg69X7VFd0pULhkl1J5OMbQ9r9qRdRI5eg5h
|
||||||
|
QsQaIQ7mtb8TmvOwf/DY/zVQHI+U8sXlCmW+TwzoQTENQSR7xzMj1LpRFqBaustr
|
||||||
|
AR72A537kItFLzll/i3SxOam5uxK2UDOQSuerF4KPdCglGXkrpo3nt3F4QKBgQCa
|
||||||
|
RArPAOjQo7tLQfJN3+wiRFsTYtd1uphx5bA/EdOtvj8HjVFnzADXWsTchf3N3UXY
|
||||||
|
XwtdgGwIMpys1KEz8a8P+c2x26SDAj7NOmDqOMYx8Xju/WGHpBM6Cn30U6e4gK+d
|
||||||
|
ZLSPyzQgqdIuP5hDvbwpvbGiLVw3Ys1BJtGCuSxpgQJ/eHnRiuSi5Zq5jGg+GpA+
|
||||||
|
FEEc9NCy772rR+4uzEOqyIwqewffqzSuVWuIsY/8MP3fh+NDxl/mU6cB5QVeD54Z
|
||||||
|
JZUKwmpM26muiM6WvVnM4ExPdSGA2+l4pZjby/KKd6F/w0tgZ1jb9Pb2/0vN3qVA
|
||||||
|
Y4U4XNTMt2fxUACqiL4SHA==
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
|
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.imex.online/v1/graphql
|
||||||
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
|
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.imex.online/v1/graphql
|
||||||
VITE_APP_GA_CODE=231099835
|
VITE_APP_GA_CODE=231099835
|
||||||
VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
|
VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
|
||||||
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
|
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
|
||||||
@@ -8,7 +8,7 @@ VITE_APP_CLOUDINARY_API_KEY=957865933348715
|
|||||||
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
||||||
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
|
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
|
||||||
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
||||||
VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000
|
VITE_APP_AXIOS_BASE_API_URL=/api/
|
||||||
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
|
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
|
||||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||||
VITE_APP_INSTANCE=IMEX
|
VITE_APP_INSTANCE=IMEX
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
|
|
||||||
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
|
|
||||||
VITE_APP_GA_CODE=231099835
|
|
||||||
VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
|
|
||||||
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
|
|
||||||
VITE_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/io-test
|
|
||||||
VITE_APP_CLOUDINARY_API_KEY=957865933348715
|
|
||||||
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
|
||||||
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
|
|
||||||
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
|
||||||
VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000
|
|
||||||
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
|
|
||||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
|
||||||
VITE_APP_INSTANCE=PROMANAGER
|
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
|
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.imex.online/v1/graphql
|
||||||
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
|
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.imex.online/v1/graphql
|
||||||
VITE_APP_GA_CODE=231099835
|
VITE_APP_GA_CODE=231099835
|
||||||
VITE_APP_FIREBASE_CONFIG={ "apiKey": "AIzaSyAuLQR9SV5LsVxjU8wh9hvFLdhcAHU6cxE", "authDomain": "rome-prod-1.firebaseapp.com", "projectId": "rome-prod-1", "storageBucket": "rome-prod-1.appspot.com", "messagingSenderId": "147786367145", "appId": "1:147786367145:web:9d4cba68071c3f29a8a9b8", "measurementId": "G-G8Z9DRHTZS"}
|
# VITE_APP_FIREBASE_CONFIG={ "apiKey": "AIzaSyAuLQR9SV5LsVxjU8wh9hvFLdhcAHU6cxE", "authDomain": "rome-prod-1.firebaseapp.com", "projectId": "rome-prod-1", "storageBucket": "rome-prod-1.appspot.com", "messagingSenderId": "147786367145", "appId": "1:147786367145:web:9d4cba68071c3f29a8a9b8", "measurementId": "G-G8Z9DRHTZS"}
|
||||||
|
VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
|
||||||
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
|
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
|
||||||
VITE_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/io-test
|
VITE_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/io-test
|
||||||
VITE_APP_CLOUDINARY_API_KEY=957865933348715
|
VITE_APP_CLOUDINARY_API_KEY=957865933348715
|
||||||
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
||||||
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BP1B7ZTYpn-KMt6nOxlld6aS8Skt3Q7ZLEqP0hAvGHxG4UojPYiXZ6kPlzZkUC5jH-EcWXomTLtmadAIxurfcHo'
|
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BP1B7ZTYpn-KMt6nOxlld6aS8Skt3Q7ZLEqP0hAvGHxG4UojPYiXZ6kPlzZkUC5jH-EcWXomTLtmadAIxurfcHo'
|
||||||
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
||||||
VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000
|
VITE_APP_AXIOS_BASE_API_URL=/api/
|
||||||
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
|
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
|
||||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
||||||
VITE_APP_COUNTRY=USA
|
VITE_APP_COUNTRY=USA
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
GENERATE_SOURCEMAP=true
|
|
||||||
VITE_APP_GRAPHQL_ENDPOINT=https://db.romeonline.io/v1/graphql
|
|
||||||
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.romeonline.io/v1/graphql
|
|
||||||
VITE_APP_GA_CODE=231103507
|
|
||||||
VITE_APP_FIREBASE_CONFIG={ "apiKey": "AIzaSyAuLQR9SV5LsVxjU8wh9hvFLdhcAHU6cxE", "authDomain": "rome-prod-1.firebaseapp.com", "projectId": "rome-prod-1", "storageBucket": "rome-prod-1.appspot.com", "messagingSenderId": "147786367145", "appId": "1:147786367145:web:9d4cba68071c3f29a8a9b8", "measurementId": "G-G8Z9DRHTZS"}
|
|
||||||
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/bodyshop
|
|
||||||
VITE_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/bodyshop
|
|
||||||
VITE_APP_CLOUDINARY_API_KEY=473322739956866
|
|
||||||
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
|
||||||
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BMgZT1NZztW2DsJl8Mg2L04hgY9FzAg6b8fbzgNAfww2VDzH3VE63Ot9EaP_U7KWS2JT-7HPHaw0T_Tw_5vkZc8'
|
|
||||||
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
|
||||||
VITE_APP_AXIOS_BASE_API_URL=https://api.romeonline.io/
|
|
||||||
VITE_APP_REPORTS_SERVER_URL=https://reports.romeonline.io
|
|
||||||
VITE_APP_SPLIT_API=et9pjkik6bn67he5evpmpr1agoo7gactphgk
|
|
||||||
VITE_APP_INSTANCE=PROMANAGER
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
VITE_APP_GRAPHQL_ENDPOINT=https://db.test.romeonline.io/v1/graphql
|
|
||||||
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.test.romeonline.io/v1/graphql
|
|
||||||
VITE_APP_GA_CODE=231099835
|
|
||||||
VITE_APP_FIREBASE_CONFIG={ "apiKey": "AIzaSyAuLQR9SV5LsVxjU8wh9hvFLdhcAHU6cxE", "authDomain": "rome-prod-1.firebaseapp.com", "projectId": "rome-prod-1", "storageBucket": "rome-prod-1.appspot.com", "messagingSenderId": "147786367145", "appId": "1:147786367145:web:9d4cba68071c3f29a8a9b8", "measurementId": "G-G8Z9DRHTZS"}
|
|
||||||
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/bodyshop
|
|
||||||
VITE_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/bodyshop
|
|
||||||
VITE_APP_CLOUDINARY_API_KEY=473322739956866
|
|
||||||
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
|
|
||||||
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BN2GcDPjipR5MTEosO5dT4CfQ3cmrdBIsI4juoOQrRijn_5aRiHlwj1mlq0W145mOusx6xynEKl_tvYJhpCc9lo'
|
|
||||||
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
|
|
||||||
VITE_APP_AXIOS_BASE_API_URL=https://api.test.romeonline.io/
|
|
||||||
VITE_APP_REPORTS_SERVER_URL=https://reports.test.romeonline.io
|
|
||||||
VITE_APP_IS_TEST=true
|
|
||||||
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
|
|
||||||
VITE_APP_INSTANCE=PROMANAGER
|
|
||||||
1
client/.gitignore
vendored
1
client/.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
# Sentry Config File
|
# Sentry Config File
|
||||||
.sentryclirc
|
.sentryclirc
|
||||||
|
/dev-dist
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
// craco.config.js
|
|
||||||
const TerserPlugin = require("terser-webpack-plugin");
|
|
||||||
const CracoLessPlugin = require("craco-less");
|
|
||||||
const { convertLegacyToken } = require("@ant-design/compatible/lib");
|
|
||||||
const { theme } = require("antd/lib");
|
|
||||||
|
|
||||||
const { defaultAlgorithm, defaultSeed } = theme;
|
|
||||||
|
|
||||||
const mapToken = defaultAlgorithm(defaultSeed);
|
|
||||||
const v4Token = convertLegacyToken(mapToken);
|
|
||||||
|
|
||||||
// TODO, At the moment we are using less in the Dashboard. Once we remove this we can remove the less processor entirely.
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
plugins: [
|
|
||||||
{
|
|
||||||
plugin: CracoLessPlugin,
|
|
||||||
options: {
|
|
||||||
lessLoaderOptions: {
|
|
||||||
lessOptions: {
|
|
||||||
modifyVars: { ...v4Token },
|
|
||||||
javascriptEnabled: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
webpack: {
|
|
||||||
configure: (webpackConfig) => {
|
|
||||||
return {
|
|
||||||
...webpackConfig,
|
|
||||||
// Required for Dev Server
|
|
||||||
devServer: {
|
|
||||||
...webpackConfig.devServer,
|
|
||||||
allowedHosts: "all"
|
|
||||||
},
|
|
||||||
optimization: {
|
|
||||||
...webpackConfig.optimization,
|
|
||||||
// Workaround for CircleCI bug caused by the number of CPUs shown
|
|
||||||
// https://github.com/facebook/create-react-app/issues/8320
|
|
||||||
minimizer: webpackConfig.optimization.minimizer.map((item) => {
|
|
||||||
if (item instanceof TerserPlugin) {
|
|
||||||
item.options.parallel = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
return item;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
devtool: "source-map"
|
|
||||||
};
|
|
||||||
@@ -12,6 +12,6 @@ module.exports = defineConfig({
|
|||||||
setupNodeEvents(on, config) {
|
setupNodeEvents(on, config) {
|
||||||
return require("./cypress/plugins/index.js")(on, config);
|
return require("./cypress/plugins/index.js")(on, config);
|
||||||
},
|
},
|
||||||
baseUrl: "http://localhost:3000"
|
baseUrl: "https://localhost:3000"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
// ***********************************************************
|
// ***********************************************************
|
||||||
// This example plugins/index.js can be used to load plugins
|
// This example plugins/index.jsx can be used to load plugins
|
||||||
//
|
//
|
||||||
// You can change the location of this file or turn off loading
|
// You can change the location of this file or turn off loading
|
||||||
// the plugins file with the 'pluginsFile' configuration option.
|
// the plugins file with the 'pluginsFile' configuration option.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// ***********************************************************
|
// ***********************************************************
|
||||||
// This example support/index.js is processed and
|
// This example support/index.jsx is processed and
|
||||||
// loaded automatically before your test files.
|
// loaded automatically before your test files.
|
||||||
//
|
//
|
||||||
// This is a great place to put global configuration and
|
// This is a great place to put global configuration and
|
||||||
|
|||||||
21
client/eslint.config.js
Normal file
21
client/eslint.config.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import globals from "globals";
|
||||||
|
import pluginJs from "@eslint/js";
|
||||||
|
import pluginReact from "eslint-plugin-react";
|
||||||
|
|
||||||
|
/** @type {import('eslint').Linter.Config[]} */
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
files: ["**/*.{js,mjs,cjs,jsx}"]
|
||||||
|
},
|
||||||
|
{ languageOptions: { globals: globals.browser } },
|
||||||
|
pluginJs.configs.recommended,
|
||||||
|
{
|
||||||
|
...pluginReact.configs.flat.recommended,
|
||||||
|
rules: {
|
||||||
|
...pluginReact.configs.flat.recommended.rules,
|
||||||
|
"react/prop-types": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pluginReact.configs.flat["jsx-runtime"]
|
||||||
|
];
|
||||||
@@ -1,154 +1,98 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
|
<meta http-equiv="Pragma" content="no-cache" />
|
||||||
|
<meta http-equiv="Expires" content="0" />
|
||||||
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
|
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
|
||||||
<link rel="icon" href="/favicon.png" />
|
<link rel="icon" href="/favicon.png" />
|
||||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
|
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
|
||||||
<link rel="icon" href="/ro-favicon.png" />
|
<link rel="icon" href="/ro-favicon.png" />
|
||||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'PROMANAGER') { %>
|
|
||||||
<link rel="icon" href="/pm/pm-favicon.ico" />
|
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#1690ff" />
|
<meta name="theme-color" content="#1690ff" />
|
||||||
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
|
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
|
||||||
<!-- TODO:AIo Update the individual logos for each.-->
|
<link rel="apple-touch-icon" href="/logo192.png" />
|
||||||
<link rel="apple-touch-icon" href="public/logo192.png" />
|
<link rel="mask-icon" href="/mask-icon.svg" color="#FFFFFF" />
|
||||||
<link rel="mask-icon" href="/mask-icon.svg" color="#FFFFFF">
|
|
||||||
<!--
|
<!--
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
manifest.json provides metadata used when your web app is installed on a
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||||
-->
|
-->
|
||||||
<!--
|
<!--
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
Only files inside the `public` folder can be referenced from the HTML.
|
Only files inside the `public` folder can be referenced from the HTML.
|
||||||
|
|
||||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||||
work correctly both with client-side routing and a non-root public URL.
|
work correctly both with client-side routing and a non-root public URL.
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
Learn how to configure a non-root public URL by running `npm run build`.
|
||||||
-->
|
-->
|
||||||
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
|
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
|
||||||
<meta name="description" content="ImEX Online" />
|
<meta name="description" content="ImEX Online" />
|
||||||
<title>ImEX Online</title>
|
<title>ImEX Online</title>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
window.$crisp = [];
|
window.$crisp = [];
|
||||||
window.CRISP_WEBSITE_ID = '36724f62-2eb0-4b29-9cdd-9905fb99913e';
|
window.CRISP_WEBSITE_ID = "36724f62-2eb0-4b29-9cdd-9905fb99913e";
|
||||||
(function () {
|
(function () {
|
||||||
d = document;
|
d = document;
|
||||||
s = d.createElement('script');
|
s = d.createElement("script");
|
||||||
s.src = 'https://client.crisp.chat/l.js';
|
s.src = "https://client.crisp.chat/l.js";
|
||||||
s.async = 1;
|
s.async = 1;
|
||||||
d.getElementsByTagName('head')[0].appendChild(s);
|
d.getElementsByTagName("head")[0].appendChild(s);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
|
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
|
||||||
<meta name="description" content="Rome Online" />
|
<meta name="description" content="Rome Online" />
|
||||||
<title>Rome Online</title>
|
<title>Rome Online</title>
|
||||||
|
<script type="text/javascript" id="zsiqchat">
|
||||||
<!--Use the below code snippet to provide real time updates to the live chat plugin without the need of copying and paste each time to your website when changes are made via PBX-->
|
var $zoho = $zoho || {};
|
||||||
|
$zoho.salesiq = $zoho.salesiq || {
|
||||||
<call-us-selector phonesystem-url=https://rometech.east.3cx.us:5001 party="LiveChat528346"></call-us-selector>
|
widgetcode: "siq01bb8ac617280bdacddfeb528f07734dadc64ef3f05efef9f769c1ec171af666",
|
||||||
|
values: {},
|
||||||
<!--Incase you don't want real time updates to the live chat plugin when options are changed, use the below code snippet. Please note that each time you change the settings you will need to copy and paste the snippet code to your website-->
|
ready: function () {}
|
||||||
|
};
|
||||||
<!--<call-us
|
var d = document;
|
||||||
|
s = d.createElement("script");
|
||||||
phonesystem-url=https://rometech.east.3cx.us:5001
|
s.type = "text/javascript";
|
||||||
|
s.id = "zsiqscript";
|
||||||
style="position:fixed;font-size:16px;line-height:17px;z-index: 99999;right: 20px; bottom: 20px;"
|
s.defer = true;
|
||||||
|
s.src = "https://salesiq.zohopublic.com/widget";
|
||||||
id="wp-live-chat-by-3CX"
|
t = d.getElementsByTagName("script")[0];
|
||||||
|
t.parentNode.insertBefore(s, t);
|
||||||
minimized="true"
|
</script>
|
||||||
|
|
||||||
animation-style="noanimation"
|
|
||||||
|
|
||||||
party="LiveChat528346"
|
|
||||||
|
|
||||||
minimized-style="bubbleright"
|
|
||||||
|
|
||||||
allow-call="true"
|
|
||||||
|
|
||||||
allow-video="false"
|
|
||||||
|
|
||||||
allow-soundnotifications="true"
|
|
||||||
|
|
||||||
enable-mute="true"
|
|
||||||
|
|
||||||
enable-onmobile="true"
|
|
||||||
|
|
||||||
offline-enabled="true"
|
|
||||||
|
|
||||||
enable="true"
|
|
||||||
|
|
||||||
ignore-queueownership="false"
|
|
||||||
|
|
||||||
authentication="both"
|
|
||||||
|
|
||||||
show-operator-actual-name="true"
|
|
||||||
|
|
||||||
aknowledge-received="true"
|
|
||||||
|
|
||||||
gdpr-enabled="false"
|
|
||||||
|
|
||||||
message-userinfo-format="name"
|
|
||||||
|
|
||||||
message-dateformat="both"
|
|
||||||
|
|
||||||
lang="browser"
|
|
||||||
|
|
||||||
button-icon-type="default"
|
|
||||||
|
|
||||||
greeting-visibility="none"
|
|
||||||
|
|
||||||
greeting-offline-visibility="none"
|
|
||||||
|
|
||||||
chat-delay="2000"
|
|
||||||
|
|
||||||
enable-direct-call="true"
|
|
||||||
|
|
||||||
enable-ga="false"
|
|
||||||
|
|
||||||
></call-us>-->
|
|
||||||
|
|
||||||
<script defer src=https://downloads-global.3cx.com/downloads/livechatandtalk/v1/callus.js id="tcx-callus-js" charset="utf-8"></script>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'PROMANAGER') { %>
|
|
||||||
<title>ProManager</title>
|
|
||||||
<meta name="description" content="ProManager" />
|
|
||||||
|
|
||||||
<% } %>
|
<% } %>
|
||||||
<script>
|
<script>
|
||||||
!(function () {
|
!(function () {
|
||||||
'use strict';
|
"use strict";
|
||||||
var e = [
|
var e = [
|
||||||
'debug',
|
"debug",
|
||||||
'destroy',
|
"destroy",
|
||||||
'do',
|
"do",
|
||||||
'help',
|
"help",
|
||||||
'identify',
|
"identify",
|
||||||
'is',
|
"is",
|
||||||
'off',
|
"off",
|
||||||
'on',
|
"on",
|
||||||
'ready',
|
"ready",
|
||||||
'render',
|
"render",
|
||||||
'reset',
|
"reset",
|
||||||
'safe',
|
"safe",
|
||||||
'set',
|
"set"
|
||||||
];
|
];
|
||||||
if (window.noticeable) console.warn('Noticeable SDK code snippet loaded more than once');
|
if (window.noticeable) console.warn("Noticeable SDK code snippet loaded more than once");
|
||||||
else {
|
else {
|
||||||
var n = (window.noticeable = window.noticeable || []);
|
var n = (window.noticeable = window.noticeable || []);
|
||||||
|
|
||||||
function t(e) {
|
function t(e) {
|
||||||
return function () {
|
return function () {
|
||||||
var t = Array.prototype.slice.call(arguments);
|
var t = Array.prototype.slice.call(arguments);
|
||||||
return t.unshift(e), n.push(t), n;
|
return t.unshift(e), n.push(t), n;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
!(function () {
|
!(function () {
|
||||||
for (var o = 0; o < e.length; o++) {
|
for (var o = 0; o < e.length; o++) {
|
||||||
var r = e[o];
|
var r = e[o];
|
||||||
@@ -156,8 +100,8 @@
|
|||||||
}
|
}
|
||||||
})(),
|
})(),
|
||||||
(function () {
|
(function () {
|
||||||
var e = document.createElement('script');
|
var e = document.createElement("script");
|
||||||
(e.async = !0), (e.src = 'https://sdk.noticeable.io/l.js');
|
(e.async = !0), (e.src = "https://sdk.noticeable.io/l.js");
|
||||||
var n = document.head;
|
var n = document.head;
|
||||||
n.insertBefore(e, n.firstChild);
|
n.insertBefore(e, n.firstChild);
|
||||||
})();
|
})();
|
||||||
|
|||||||
22829
client/package-lock.json
generated
22829
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,112 +2,106 @@
|
|||||||
"name": "bodyshop",
|
"name": "bodyshop",
|
||||||
"version": "0.2.1",
|
"version": "0.2.1",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "18.18.2"
|
"node": ">=18.18.2"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"proxy": "http://localhost:4000",
|
"proxy": "http://localhost:4000",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/compatible": "^5.1.2",
|
"@ant-design/pro-layout": "^7.19.12",
|
||||||
"@ant-design/pro-layout": "^7.17.16",
|
"@apollo/client": "^3.11.8",
|
||||||
"@apollo/client": "^3.8.10",
|
"@emotion/is-prop-valid": "^1.3.1",
|
||||||
"@asseinfo/react-kanban": "^2.2.0",
|
"@fingerprintjs/fingerprintjs": "^4.5.0",
|
||||||
"@fingerprintjs/fingerprintjs": "^4.2.2",
|
|
||||||
"@jsreport/browser-client": "^3.1.0",
|
"@jsreport/browser-client": "^3.1.0",
|
||||||
"@reduxjs/toolkit": "^2.2.1",
|
"@reduxjs/toolkit": "^2.2.7",
|
||||||
"@sentry/cli": "^2.28.6",
|
"@sentry/cli": "^2.36.2",
|
||||||
"@sentry/react": "^7.104.0",
|
"@sentry/react": "^7.114.0",
|
||||||
"@splitsoftware/splitio-react": "^1.11.0",
|
"@splitsoftware/splitio-react": "^1.13.0",
|
||||||
"@tanem/react-nprogress": "^5.0.51",
|
"@tanem/react-nprogress": "^5.0.51",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"antd": "^5.15.3",
|
"antd": "^5.20.1",
|
||||||
"apollo-link-logger": "^2.0.1",
|
"apollo-link-logger": "^2.0.1",
|
||||||
"apollo-link-sentry": "^3.3.0",
|
"apollo-link-sentry": "^3.3.0",
|
||||||
"axios": "^1.6.7",
|
"autosize": "^6.0.1",
|
||||||
"dayjs": "^1.11.10",
|
"axios": "^1.7.7",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
|
"css-box-model": "^1.2.1",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"dayjs-business-days2": "^1.2.2",
|
"dayjs-business-days2": "^1.2.2",
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"env-cmd": "^10.1.0",
|
"env-cmd": "^10.1.0",
|
||||||
"exifr": "^7.1.3",
|
"exifr": "^7.1.3",
|
||||||
"firebase": "^10.8.1",
|
"firebase": "^10.13.2",
|
||||||
"graphql": "^16.6.0",
|
"graphql": "^16.9.0",
|
||||||
"i18next": "^23.10.0",
|
"i18next": "^23.15.1",
|
||||||
"i18next-browser-languagedetector": "^7.0.2",
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
"libphonenumber-js": "^1.10.57",
|
"immutability-helper": "^3.1.1",
|
||||||
"logrocket": "^8.0.1",
|
"libphonenumber-js": "^1.11.9",
|
||||||
"markerjs2": "^2.32.0",
|
"logrocket": "^8.1.2",
|
||||||
"normalize-url": "^8.0.0",
|
"markerjs2": "^2.32.2",
|
||||||
|
"memoize-one": "^6.0.0",
|
||||||
|
"normalize-url": "^8.0.1",
|
||||||
|
"object-hash": "^3.0.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"query-string": "^9.0.0",
|
"query-string": "^9.1.0",
|
||||||
"react": "^18.2.0",
|
"raf-schd": "^4.0.3",
|
||||||
"react-big-calendar": "^1.11.0",
|
"react": "^18.3.1",
|
||||||
|
"react-big-calendar": "^1.14.1",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
"react-cookie": "^7.1.0",
|
"react-cookie": "^7.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.3.1",
|
||||||
"react-drag-listview": "^2.0.0",
|
"react-drag-listview": "^2.0.0",
|
||||||
"react-grid-gallery": "^1.0.0",
|
"react-grid-gallery": "^1.0.1",
|
||||||
"react-grid-layout": "1.3.4",
|
"react-grid-layout": "1.3.4",
|
||||||
"react-i18next": "^14.0.5",
|
"react-i18next": "^14.1.3",
|
||||||
"react-icons": "^5.0.1",
|
"react-icons": "^5.3.0",
|
||||||
"react-image-lightbox": "^5.1.4",
|
"react-image-lightbox": "^5.1.4",
|
||||||
"react-joyride": "^2.7.4",
|
|
||||||
"react-markdown": "^9.0.1",
|
"react-markdown": "^9.0.1",
|
||||||
"react-number-format": "^5.3.3",
|
"react-number-format": "^5.4.2",
|
||||||
"react-product-fruits": "^2.2.6",
|
"react-popopo": "^2.1.9",
|
||||||
"react-redux": "^9.1.0",
|
"react-product-fruits": "^2.2.61",
|
||||||
|
"react-redux": "^9.1.2",
|
||||||
"react-resizable": "^3.0.5",
|
"react-resizable": "^3.0.5",
|
||||||
"react-router-dom": "^6.22.2",
|
"react-router-dom": "^6.26.2",
|
||||||
"react-scripts": "^5.0.1",
|
|
||||||
"react-sticky": "^6.0.3",
|
"react-sticky": "^6.0.3",
|
||||||
"react-virtualized": "^9.22.5",
|
"react-virtualized": "^9.22.5",
|
||||||
"recharts": "^2.12.2",
|
"react-virtuoso": "^4.10.4",
|
||||||
|
"recharts": "^2.12.7",
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
|
"redux-actions": "^3.0.3",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"redux-saga": "^1.3.0",
|
"redux-saga": "^1.3.0",
|
||||||
"redux-state-sync": "^3.1.4",
|
"redux-state-sync": "^3.1.4",
|
||||||
"reselect": "^5.1.0",
|
"reselect": "^5.1.1",
|
||||||
"sass": "^1.71.1",
|
"sass": "^1.79.3",
|
||||||
"socket.io-client": "^4.7.4",
|
"socket.io-client": "^4.8.0",
|
||||||
"styled-components": "^6.1.8",
|
"styled-components": "^6.1.13",
|
||||||
"subscriptions-transport-ws": "^0.11.0",
|
"subscriptions-transport-ws": "^0.11.0",
|
||||||
"terser-webpack-plugin": "^5.3.10",
|
"use-memo-one": "^1.1.3",
|
||||||
"userpilot": "^1.3.1",
|
"userpilot": "^1.3.6",
|
||||||
"vite-plugin-ejs": "^1.7.0",
|
"vite-plugin-ejs": "^1.7.0",
|
||||||
"web-vitals": "^3.5.2",
|
"web-vitals": "^3.5.2"
|
||||||
"workbox-core": "^7.0.0",
|
|
||||||
"workbox-expiration": "^7.0.0",
|
|
||||||
"workbox-navigation-preload": "^7.0.0",
|
|
||||||
"workbox-precaching": "^7.0.0",
|
|
||||||
"workbox-routing": "^7.0.0",
|
|
||||||
"workbox-strategies": "^7.0.0"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"postinstall": "echo 'when updating react-big-calendar, remember to check to localizer in the calendar wrapper'",
|
||||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"build": "vite build",
|
"build": "dotenvx run --env-file=.env.development.imex -- vite build",
|
||||||
"start:imex": "dotenvx run --env-file=.env.development.imex -- vite",
|
"start:imex": "dotenvx run --env-file=.env.development.imex -- vite",
|
||||||
"start:rome": "dotenvx run --env-file=.env.development.rome -- vite",
|
"start:rome": "dotenvx run --env-file=.env.development.rome -- vite",
|
||||||
"start:promanager": "dotenvx run --env-file=.env.development.promanager -- vite",
|
"preview:imex": "dotenvx run --env-file=.env.development.imex -- vite preview",
|
||||||
|
"preview:rome": "dotenvx run --env-file=.env.development.rome -- vite preview",
|
||||||
"build:test:imex": "env-cmd -f .env.test.imex npm run build",
|
"build:test:imex": "env-cmd -f .env.test.imex npm run build",
|
||||||
"build:test:rome": "env-cmd -f .env.test.rome npm run build",
|
"build:test:rome": "env-cmd -f .env.test.rome npm run build",
|
||||||
"build:test:promanager": "env-cmd -f .env.test.promanager npm run build",
|
|
||||||
"build:production:imex": "env-cmd -f .env.production.imex npm run build",
|
"build:production:imex": "env-cmd -f .env.production.imex npm run build",
|
||||||
"build:production:rome": "env-cmd -f .env.production.rome npm run build",
|
"build:production:rome": "env-cmd -f .env.production.rome npm run build",
|
||||||
"build:production:promanager": "env-cmd -f .env.production.promanager npm run build",
|
|
||||||
"test": "cypress open",
|
"test": "cypress open",
|
||||||
"eject": "react-scripts eject",
|
"eject": "react-scripts eject",
|
||||||
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .",
|
"madge": "madge --image ./madge-graph.svg --extensions js,jsx,ts,tsx --circular .",
|
||||||
"eulaize": "node src/utils/eulaize.js",
|
"eulaize": "node src/utils/eulaize.js",
|
||||||
"sentry:sourcemaps:imex": "sentry-cli sourcemaps inject --org imex --project imexonline ./build && sentry-cli sourcemaps upload --org imex --project imexonline ./build"
|
"sentry:sourcemaps:imex": "sentry-cli sourcemaps inject --org imex --project imexonline ./build && sentry-cli sourcemaps upload --org imex --project imexonline ./build"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
|
||||||
"extends": [
|
|
||||||
"react-app",
|
|
||||||
"react-app/jest",
|
|
||||||
"plugin:cypress/recommended"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
">0.2%",
|
">0.2%",
|
||||||
@@ -127,33 +121,36 @@
|
|||||||
"@rollup/rollup-linux-x64-gnu": "4.6.1"
|
"@rollup/rollup-linux-x64-gnu": "4.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@ant-design/icons": "^5.5.1",
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-react": "^7.23.3",
|
"@babel/preset-react": "^7.24.7",
|
||||||
"@dotenvx/dotenvx": "^0.15.4",
|
"@dotenvx/dotenvx": "^1.14.1",
|
||||||
"@emotion/babel-plugin": "^11.11.0",
|
"@emotion/babel-plugin": "^11.12.0",
|
||||||
"@emotion/react": "^11.11.3",
|
"@emotion/react": "^11.13.3",
|
||||||
"@sentry/webpack-plugin": "^2.14.2",
|
"@eslint/js": "^9.15.0",
|
||||||
"@swc/core": "^1.3.107",
|
"@sentry/webpack-plugin": "^2.22.4",
|
||||||
"@swc/plugin-styled-components": "^1.5.108",
|
"@testing-library/cypress": "^10.0.2",
|
||||||
"@testing-library/cypress": "^10.0.1",
|
"browserslist": "^4.23.3",
|
||||||
"browserslist": "^4.22.3",
|
|
||||||
"browserslist-to-esbuild": "^2.1.1",
|
"browserslist-to-esbuild": "^2.1.1",
|
||||||
|
"chalk": "^5.3.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cypress": "^13.6.6",
|
"cypress": "^13.14.2",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.1",
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"eslint-plugin-cypress": "^2.15.1",
|
"eslint-plugin-cypress": "^2.15.1",
|
||||||
"memfs": "^4.6.0",
|
"eslint-plugin-react": "^7.37.2",
|
||||||
|
"globals": "^15.12.0",
|
||||||
|
"memfs": "^4.12.0",
|
||||||
"os-browserify": "^0.3.0",
|
"os-browserify": "^0.3.0",
|
||||||
"react-error-overlay": "6.0.11",
|
"react-error-overlay": "6.0.11",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"source-map-explorer": "^2.5.3",
|
"source-map-explorer": "^2.5.3",
|
||||||
"vite": "^5.0.11",
|
"vite": "^5.4.7",
|
||||||
"vite-plugin-babel": "^1.2.0",
|
"vite-plugin-babel": "^1.2.0",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-plugin-legacy": "^2.1.0",
|
"vite-plugin-node-polyfills": "^0.22.0",
|
||||||
"vite-plugin-node-polyfills": "^0.19.0",
|
"vite-plugin-pwa": "^0.20.5",
|
||||||
"vite-plugin-pwa": "^0.19.0",
|
"vite-plugin-style-import": "^2.0.0",
|
||||||
"vite-plugin-style-import": "^2.0.0"
|
"workbox-window": "^7.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16422,7 +16422,7 @@ For when you don't want to write the same thing over and over to cache a method
|
|||||||
$ npm install --save-dev stubs
|
$ npm install --save-dev stubs
|
||||||
```
|
```
|
||||||
```js
|
```js
|
||||||
var mylib = require('./lib/index.js')
|
var mylib = require('./lib/index.jsx')
|
||||||
var stubs = require('stubs')
|
var stubs = require('stubs')
|
||||||
|
|
||||||
// make it a noop
|
// make it a noop
|
||||||
|
|||||||
@@ -16567,7 +16567,7 @@ even more slower.
|
|||||||
## Benchmarks
|
## Benchmarks
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ node benchmarks/index.js
|
$ node benchmarks/index.jsx
|
||||||
Benchmarking: sign
|
Benchmarking: sign
|
||||||
elliptic#sign x 262 ops/sec ±0.51% (177 runs sampled)
|
elliptic#sign x 262 ops/sec ±0.51% (177 runs sampled)
|
||||||
eccjs#sign x 55.91 ops/sec ±0.90% (144 runs sampled)
|
eccjs#sign x 55.91 ops/sec ±0.90% (144 runs sampled)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// Scripts for firebase and firebase messaging
|
// Scripts for firebase and firebase messaging
|
||||||
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-app.js");
|
importScripts("https://www.gstatic.com/firebasejs/10.14.1/firebase-app-compat.js");
|
||||||
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-messaging.js");
|
importScripts("https://www.gstatic.com/firebasejs/10.14.1/firebase-messaging-compat.js");
|
||||||
|
|
||||||
// Initialize the Firebase app in the service worker by passing the generated config
|
// Initialize the Firebase app in the service worker by passing the generated config
|
||||||
let firebaseConfig;
|
let firebaseConfig;
|
||||||
@@ -14,7 +14,7 @@ switch (this.location.hostname) {
|
|||||||
storageBucket: "imex-dev.appspot.com",
|
storageBucket: "imex-dev.appspot.com",
|
||||||
messagingSenderId: "759548147434",
|
messagingSenderId: "759548147434",
|
||||||
appId: "1:759548147434:web:e8239868a48ceb36700993",
|
appId: "1:759548147434:web:e8239868a48ceb36700993",
|
||||||
measurementId: "G-K5XRBVVB4S",
|
measurementId: "G-K5XRBVVB4S"
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case "test.imex.online":
|
case "test.imex.online":
|
||||||
@@ -24,7 +24,7 @@ switch (this.location.hostname) {
|
|||||||
projectId: "imex-test",
|
projectId: "imex-test",
|
||||||
storageBucket: "imex-test.appspot.com",
|
storageBucket: "imex-test.appspot.com",
|
||||||
messagingSenderId: "991923618608",
|
messagingSenderId: "991923618608",
|
||||||
appId: "1:991923618608:web:633437569cdad78299bef5",
|
appId: "1:991923618608:web:633437569cdad78299bef5"
|
||||||
// measurementId: "${config.measurementId}",
|
// measurementId: "${config.measurementId}",
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
@@ -38,7 +38,7 @@ switch (this.location.hostname) {
|
|||||||
storageBucket: "imex-prod.appspot.com",
|
storageBucket: "imex-prod.appspot.com",
|
||||||
messagingSenderId: "253497221485",
|
messagingSenderId: "253497221485",
|
||||||
appId: "1:253497221485:web:3c81c483b94db84b227a64",
|
appId: "1:253497221485:web:3c81c483b94db84b227a64",
|
||||||
measurementId: "G-NTWBKG2L0M",
|
measurementId: "G-NTWBKG2L0M"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,8 +49,6 @@ const messaging = firebase.messaging();
|
|||||||
|
|
||||||
messaging.onBackgroundMessage(function (payload) {
|
messaging.onBackgroundMessage(function (payload) {
|
||||||
// Customize notification here
|
// Customize notification here
|
||||||
const channel = new BroadcastChannel("imex-sw-messages");
|
console.log("[firebase-messaging-sw.js] Received background message ", payload);
|
||||||
channel.postMessage(payload);
|
self.registration.showNotification(notificationTitle, notificationOptions);
|
||||||
|
|
||||||
//self.registration.showNotification(notificationTitle, notificationOptions);
|
|
||||||
});
|
});
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 6.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB |
@@ -2,8 +2,6 @@ import { ApolloProvider } from "@apollo/client";
|
|||||||
import { SplitFactoryProvider, SplitSdk } from "@splitsoftware/splitio-react";
|
import { SplitFactoryProvider, SplitSdk } from "@splitsoftware/splitio-react";
|
||||||
import { ConfigProvider } from "antd";
|
import { ConfigProvider } from "antd";
|
||||||
import enLocale from "antd/es/locale/en_US";
|
import enLocale from "antd/es/locale/en_US";
|
||||||
import dayjs from "../utils/day";
|
|
||||||
import "dayjs/locale/en";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
||||||
@@ -19,8 +17,6 @@ if (import.meta.env.DEV) {
|
|||||||
Userpilot.initialize("NX-69145f08");
|
Userpilot.initialize("NX-69145f08");
|
||||||
}
|
}
|
||||||
|
|
||||||
dayjs.locale("en");
|
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
core: {
|
core: {
|
||||||
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
|
authorizationKey: import.meta.env.VITE_APP_SPLIT_API,
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ import { checkUserSession } from "../redux/user/user.actions";
|
|||||||
import { selectBodyshop, selectCurrentEula, selectCurrentUser } from "../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentEula, selectCurrentUser } from "../redux/user/user.selectors";
|
||||||
import PrivateRoute from "../components/PrivateRoute";
|
import PrivateRoute from "../components/PrivateRoute";
|
||||||
import "./App.styles.scss";
|
import "./App.styles.scss";
|
||||||
import handleBeta from "../utils/betaHandler";
|
|
||||||
import Eula from "../components/eula/eula.component";
|
import Eula from "../components/eula/eula.component";
|
||||||
import InstanceRenderMgr from "../utils/instanceRenderMgr";
|
import InstanceRenderMgr from "../utils/instanceRenderMgr";
|
||||||
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
|
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
|
||||||
|
import { SocketProvider } from "../contexts/SocketIO/socketContext.jsx";
|
||||||
|
|
||||||
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
|
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
|
||||||
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
|
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
|
||||||
@@ -96,8 +96,7 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
|||||||
LogRocket.init(
|
LogRocket.init(
|
||||||
InstanceRenderMgr({
|
InstanceRenderMgr({
|
||||||
imex: "gvfvfw/bodyshopapp",
|
imex: "gvfvfw/bodyshopapp",
|
||||||
rome: "rome-online/rome-online",
|
rome: "rome-online/rome-online"
|
||||||
promanager: "" // TODO: AIO Add in log rocket for promanager instances.
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -108,8 +107,6 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
|||||||
return <LoadingSpinner message={t("general.labels.loggingin")} />;
|
return <LoadingSpinner message={t("general.labels.loggingin")} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBeta();
|
|
||||||
|
|
||||||
if (!online) {
|
if (!online) {
|
||||||
return (
|
return (
|
||||||
<Result
|
<Result
|
||||||
@@ -136,8 +133,7 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
|||||||
<LoadingSpinner
|
<LoadingSpinner
|
||||||
message={InstanceRenderMgr({
|
message={InstanceRenderMgr({
|
||||||
imex: t("titles.imexonline"),
|
imex: t("titles.imexonline"),
|
||||||
rome: t("titles.romeonline"),
|
rome: t("titles.romeonline")
|
||||||
promanager: t("titles.promanager")
|
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -146,8 +142,7 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
|||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
workspaceCode={InstanceRenderMgr({
|
workspaceCode={InstanceRenderMgr({
|
||||||
imex: null,
|
imex: null,
|
||||||
rome: "9BkbEseqNqxw8jUH",
|
rome: "9BkbEseqNqxw8jUH"
|
||||||
promanager: "aoJoEifvezYI0Z0P"
|
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -204,7 +199,9 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
|||||||
path="/manage/*"
|
path="/manage/*"
|
||||||
element={
|
element={
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
<SocketProvider bodyshop={bodyshop}>
|
||||||
|
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||||
|
</SocketProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -214,7 +211,9 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
|||||||
path="/tech/*"
|
path="/tech/*"
|
||||||
element={
|
element={
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<PrivateRoute isAuthorized={currentUser.authorized} />
|
<SocketProvider bodyshop={bodyshop}>
|
||||||
|
<PrivateRoute isAuthorized={currentUser.authorized} />
|
||||||
|
</SocketProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -161,3 +161,15 @@
|
|||||||
.rowWithColor > td {
|
.rowWithColor > td {
|
||||||
background-color: var(--bgColor) !important;
|
background-color: var(--bgColor) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.muted-button {
|
||||||
|
color: lightgray;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px; /* Adjust as needed */
|
||||||
|
}
|
||||||
|
|
||||||
|
.muted-button:hover {
|
||||||
|
color: darkgrey;
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,6 @@ ProductFruitsWrapper.propTypes = {
|
|||||||
currentUser: PropTypes.shape({
|
currentUser: PropTypes.shape({
|
||||||
authorized: PropTypes.bool,
|
authorized: PropTypes.bool,
|
||||||
email: PropTypes.string
|
email: PropTypes.string
|
||||||
}).isRequired,
|
}),
|
||||||
workspaceCode: PropTypes.string.isRequired
|
workspaceCode: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,13 +26,11 @@ const defaultTheme = {
|
|||||||
token: {
|
token: {
|
||||||
colorPrimary: InstanceRenderMgr({
|
colorPrimary: InstanceRenderMgr({
|
||||||
imex: "#1890ff",
|
imex: "#1890ff",
|
||||||
rome: "#326ade",
|
rome: "#326ade"
|
||||||
promanager: "#1d69a6"
|
|
||||||
}),
|
}),
|
||||||
colorInfo: InstanceRenderMgr({
|
colorInfo: InstanceRenderMgr({
|
||||||
imex: "#1890ff",
|
imex: "#1890ff",
|
||||||
rome: "#326ade",
|
rome: "#326ade"
|
||||||
promanager: "#1d69a6"
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
|||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import { pageLimit } from "../../utils/config";
|
import { exportPageLimit } from "../../utils/config";
|
||||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||||
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
|
import PayableExportAll from "../payable-export-all-button/payable-export-all-button.component";
|
||||||
@@ -175,7 +175,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, bills, ref
|
|||||||
<Table
|
<Table
|
||||||
loading={loading}
|
loading={loading}
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
pagination={{ position: "top", pageSize: pageLimit }}
|
pagination={{ position: "top", pageSize: exportPageLimit }}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
|||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
import { pageLimit } from "../../utils/config";
|
import { exportPageLimit } from "../../utils/config";
|
||||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||||
@@ -177,7 +177,7 @@ export function AccountingPayablesTableComponent({ bodyshop, loading, payments,
|
|||||||
<Table
|
<Table
|
||||||
loading={loading}
|
loading={loading}
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
pagination={{ position: "top", pageSize: pageLimit }}
|
pagination={{ position: "top", pageSize: exportPageLimit }}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
|
|||||||
@@ -1,18 +1,19 @@
|
|||||||
import { Button, Card, Input, Space, Table } from "antd";
|
import { Button, Card, Input, Space, Table } from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import { exportPageLimit } from "../../utils/config";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
||||||
|
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
||||||
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
|
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
|
||||||
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
|
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
|
||||||
|
import JobMarkSelectedExported from "../jobs-mark-selected-exported/jobs-mark-selected-exported";
|
||||||
import { connect } from "react-redux";
|
|
||||||
import { createStructuredSelector } from "reselect";
|
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
|
||||||
import ExportLogsCountDisplay from "../export-logs-count-display/export-logs-count-display.component";
|
|
||||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||||
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||||
|
|
||||||
@@ -170,13 +171,22 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
|
|||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
{!bodyshop.cdk_dealerid && !bodyshop.pbs_serialnumber && (
|
{!bodyshop.cdk_dealerid && !bodyshop.pbs_serialnumber && (
|
||||||
<JobsExportAllButton
|
<>
|
||||||
jobIds={selectedJobs}
|
<JobMarkSelectedExported
|
||||||
disabled={transInProgress || selectedJobs.length === 0}
|
jobIds={selectedJobs}
|
||||||
loadingCallback={setTransInProgress}
|
disabled={transInProgress || selectedJobs.length === 0}
|
||||||
completedCallback={setSelectedJobs}
|
loadingCallback={setTransInProgress}
|
||||||
refetch={refetch}
|
completedCallback={setSelectedJobs}
|
||||||
/>
|
refetch={refetch}
|
||||||
|
/>
|
||||||
|
<JobsExportAllButton
|
||||||
|
jobIds={selectedJobs}
|
||||||
|
disabled={transInProgress || selectedJobs.length === 0}
|
||||||
|
loadingCallback={setTransInProgress}
|
||||||
|
completedCallback={setSelectedJobs}
|
||||||
|
refetch={refetch}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
|
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
|
||||||
<Input.Search
|
<Input.Search
|
||||||
@@ -191,7 +201,7 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
|
|||||||
<Table
|
<Table
|
||||||
loading={loading}
|
loading={loading}
|
||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
pagination={{ position: "top" }}
|
pagination={{ position: "top", pageSize: exportPageLimit }}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { PageHeader } from "@ant-design/pro-layout";
|
||||||
import { useMutation, useQuery } from "@apollo/client";
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
import { Button, Divider, Form, Popconfirm, Space } from "antd";
|
import { Button, Divider, Form, Popconfirm, Space } from "antd";
|
||||||
import dayjs from "../../utils/day";
|
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -13,6 +13,7 @@ import { insertAuditTrail } from "../../redux/application/application.actions";
|
|||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
|
import dayjs from "../../utils/day";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import BillFormContainer from "../bill-form/bill-form.container";
|
import BillFormContainer from "../bill-form/bill-form.container";
|
||||||
import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
|
import BillMarkExportedButton from "../bill-mark-exported-button/bill-mark-exported-button.component";
|
||||||
@@ -22,7 +23,6 @@ import JobDocumentsGallery from "../jobs-documents-gallery/jobs-documents-galler
|
|||||||
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
|
import JobsDocumentsLocalGallery from "../jobs-documents-local-gallery/jobs-documents-local-gallery.container";
|
||||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||||
import BillDetailEditReturn from "./bill-detail-edit-return.component";
|
import BillDetailEditReturn from "./bill-detail-edit-return.component";
|
||||||
import { PageHeader } from "@ant-design/pro-layout";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -98,7 +98,7 @@ export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail
|
|||||||
});
|
});
|
||||||
|
|
||||||
billlines.forEach((billline) => {
|
billlines.forEach((billline) => {
|
||||||
const { deductedfromlbr, inventories, jobline, ...il } = billline;
|
const { deductedfromlbr, inventories, jobline, original_actual_price, create_ppc, ...il } = billline;
|
||||||
delete il.__typename;
|
delete il.__typename;
|
||||||
|
|
||||||
if (il.id) {
|
if (il.id) {
|
||||||
@@ -153,6 +153,7 @@ export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail
|
|||||||
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
|
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
|
||||||
|
|
||||||
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
|
const exported = data && data.bills_by_pk && data.bills_by_pk.exported;
|
||||||
|
const isinhouse = data && data.bills_by_pk && data.bills_by_pk.isinhouse;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -188,7 +189,7 @@ export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Form form={form} onFinish={handleFinish} initialValues={transformData(data)} layout="vertical">
|
<Form form={form} onFinish={handleFinish} initialValues={transformData(data)} layout="vertical">
|
||||||
<BillFormContainer form={form} billEdit disabled={exported} />
|
<BillFormContainer form={form} billEdit disabled={exported} disableInHouse={isinhouse} />
|
||||||
<Divider orientation="left">{t("general.labels.media")}</Divider>
|
<Divider orientation="left">{t("general.labels.media")}</Divider>
|
||||||
{bodyshop.uselocalmediaserver ? (
|
{bodyshop.uselocalmediaserver ? (
|
||||||
<JobsDocumentsLocalGallery
|
<JobsDocumentsLocalGallery
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import dayjs from "../../utils/day";
|
|||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
|
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
|
||||||
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
|
||||||
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
|
||||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
import JobSearchSelect from "../job-search-select/job-search-select.component";
|
import JobSearchSelect from "../job-search-select/job-search-select.component";
|
||||||
@@ -22,6 +21,7 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
|||||||
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
||||||
import BillFormLines from "./bill-form.lines.component";
|
import BillFormLines from "./bill-form.lines.component";
|
||||||
import { CalculateBillTotal } from "./bill-form.totals.utility";
|
import { CalculateBillTotal } from "./bill-form.totals.utility";
|
||||||
|
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
@@ -41,7 +41,8 @@ export function BillFormComponent({
|
|||||||
job,
|
job,
|
||||||
loadOutstandingReturns,
|
loadOutstandingReturns,
|
||||||
loadInventory,
|
loadInventory,
|
||||||
preferredMake
|
preferredMake,
|
||||||
|
disableInHouse
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
@@ -177,7 +178,7 @@ export function BillFormComponent({
|
|||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<VendorSearchSelect
|
<VendorSearchSelect
|
||||||
disabled={disabled}
|
disabled={disabled || disableInHouse}
|
||||||
options={vendorAutoCompleteOptions}
|
options={vendorAutoCompleteOptions}
|
||||||
preferredMake={preferredMake}
|
preferredMake={preferredMake}
|
||||||
onSelect={handleVendorSelect}
|
onSelect={handleVendorSelect}
|
||||||
@@ -243,7 +244,7 @@ export function BillFormComponent({
|
|||||||
})
|
})
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Input disabled={disabled || disableInvNumber} />
|
<Input disabled={disabled || disableInvNumber || disableInHouse} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bills.fields.date")}
|
label={t("bills.fields.date")}
|
||||||
@@ -275,7 +276,7 @@ export function BillFormComponent({
|
|||||||
})
|
})
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<FormDatePicker disabled={disabled} />
|
<DateTimePicker isDateOnly disabled={disabled} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("bills.fields.is_credit_memo")}
|
label={t("bills.fields.is_credit_memo")}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableInvNumber }) {
|
export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableInvNumber, disableInHouse }) {
|
||||||
const {
|
const {
|
||||||
treatments: { Simple_Inventory }
|
treatments: { Simple_Inventory }
|
||||||
} = useSplitTreatments({
|
} = useSplitTreatments({
|
||||||
@@ -47,6 +47,7 @@ export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableI
|
|||||||
job={lineData ? lineData.jobs_by_pk : null}
|
job={lineData ? lineData.jobs_by_pk : null}
|
||||||
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
||||||
disableInvNumber={disableInvNumber}
|
disableInvNumber={disableInvNumber}
|
||||||
|
disableInHouse={disableInHouse}
|
||||||
loadOutstandingReturns={loadOutstandingReturns}
|
loadOutstandingReturns={loadOutstandingReturns}
|
||||||
loadInventory={loadInventory}
|
loadInventory={loadInventory}
|
||||||
preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null}
|
preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null}
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ import { connect } from "react-redux";
|
|||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import CiecaSelect from "../../utils/Ciecaselect";
|
import CiecaSelect from "../../utils/Ciecaselect";
|
||||||
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
import BillLineSearchSelect from "../bill-line-search-select/bill-line-search-select.component";
|
import BillLineSearchSelect from "../bill-line-search-select/bill-line-search-select.component";
|
||||||
import BilllineAddInventory from "../billline-add-inventory/billline-add-inventory.component";
|
import BilllineAddInventory from "../billline-add-inventory/billline-add-inventory.component";
|
||||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
@@ -72,7 +72,14 @@ export function BillEnterModalLinesComponent({
|
|||||||
<BillLineSearchSelect
|
<BillLineSearchSelect
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
options={lineData}
|
options={lineData}
|
||||||
style={{ width: "100%", minWidth: "10rem" }}
|
style={{
|
||||||
|
width: "20rem",
|
||||||
|
maxWidth: "20rem",
|
||||||
|
minWidth: "10rem",
|
||||||
|
whiteSpace: "normal",
|
||||||
|
height: "auto",
|
||||||
|
minHeight: "32px" // default height of Ant Design inputs
|
||||||
|
}}
|
||||||
allowRemoved={form.getFieldValue("is_credit_memo") || false}
|
allowRemoved={form.getFieldValue("is_credit_memo") || false}
|
||||||
onSelect={(value, opt) => {
|
onSelect={(value, opt) => {
|
||||||
setFieldsValue({
|
setFieldsValue({
|
||||||
@@ -105,7 +112,7 @@ export function BillEnterModalLinesComponent({
|
|||||||
title: t("billlines.fields.line_desc"),
|
title: t("billlines.fields.line_desc"),
|
||||||
dataIndex: "line_desc",
|
dataIndex: "line_desc",
|
||||||
editable: true,
|
editable: true,
|
||||||
|
width: "20rem",
|
||||||
formItemProps: (field) => {
|
formItemProps: (field) => {
|
||||||
return {
|
return {
|
||||||
key: `${field.index}line_desc`,
|
key: `${field.index}line_desc`,
|
||||||
@@ -119,7 +126,7 @@ export function BillEnterModalLinesComponent({
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
formInput: (record, index) => <Input disabled={disabled} />
|
formInput: (record, index) => <Input.TextArea disabled={disabled} autoSize />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("billlines.fields.quantity"),
|
title: t("billlines.fields.quantity"),
|
||||||
@@ -221,7 +228,6 @@ export function BillEnterModalLinesComponent({
|
|||||||
}}
|
}}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)
|
)
|
||||||
//Do not need to set for promanager as it will default to Rome.
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -455,7 +461,6 @@ export function BillEnterModalLinesComponent({
|
|||||||
|
|
||||||
...InstanceRenderManager({
|
...InstanceRenderManager({
|
||||||
rome: [],
|
rome: [],
|
||||||
promanager: [],
|
|
||||||
imex: [
|
imex: [
|
||||||
{
|
{
|
||||||
title: t("billlines.fields.federal_tax_applicable"),
|
title: t("billlines.fields.federal_tax_applicable"),
|
||||||
@@ -469,7 +474,6 @@ export function BillEnterModalLinesComponent({
|
|||||||
initialValue: InstanceRenderManager({
|
initialValue: InstanceRenderManager({
|
||||||
imex: true,
|
imex: true,
|
||||||
rome: false,
|
rome: false,
|
||||||
promanager: false
|
|
||||||
}),
|
}),
|
||||||
name: [field.name, "applicable_taxes", "federal"]
|
name: [field.name, "applicable_taxes", "federal"]
|
||||||
};
|
};
|
||||||
@@ -496,7 +500,6 @@ export function BillEnterModalLinesComponent({
|
|||||||
|
|
||||||
...InstanceRenderManager({
|
...InstanceRenderManager({
|
||||||
rome: [],
|
rome: [],
|
||||||
promanager: [],
|
|
||||||
imex: [
|
imex: [
|
||||||
{
|
{
|
||||||
title: t("billlines.fields.local_tax_applicable"),
|
title: t("billlines.fields.local_tax_applicable"),
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import React, { forwardRef } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import InstanceRenderMgr from "../../utils/instanceRenderMgr";
|
import InstanceRenderMgr from "../../utils/instanceRenderMgr";
|
||||||
|
|
||||||
//To be used as a form element only.
|
|
||||||
const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps }, ref) => {
|
const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps }, ref) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -12,7 +11,7 @@ const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps },
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
showSearch
|
showSearch
|
||||||
popupMatchSelectWidth={false}
|
popupMatchSelectWidth={true}
|
||||||
optionLabelProp={"name"}
|
optionLabelProp={"name"}
|
||||||
// optionFilterProp="line_desc"
|
// optionFilterProp="line_desc"
|
||||||
filterOption={(inputValue, option) => {
|
filterOption={(inputValue, option) => {
|
||||||
@@ -44,7 +43,7 @@ const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps },
|
|||||||
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||||
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim(),
|
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim(),
|
||||||
label: (
|
label: (
|
||||||
<>
|
<div style={{ whiteSpace: 'normal', wordBreak: 'break-word' }}>
|
||||||
<span>
|
<span>
|
||||||
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
||||||
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||||
@@ -58,7 +57,7 @@ const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps },
|
|||||||
<span style={{ float: "right", paddingleft: "1rem" }}>
|
<span style={{ float: "right", paddingleft: "1rem" }}>
|
||||||
{item.act_price ? `$${item.act_price && item.act_price.toFixed(2)}` : ``}
|
{item.act_price ? `$${item.act_price && item.act_price.toFixed(2)}` : ``}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</div>
|
||||||
)
|
)
|
||||||
}))
|
}))
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { EditFilled, SyncOutlined } from "@ant-design/icons";
|
|||||||
import { Button, Card, Checkbox, Input, Space, Table } from "antd";
|
import { Button, Card, Checkbox, Input, Space, Table } from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { FaTasks } from "react-icons/fa";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
@@ -13,8 +14,10 @@ import { alphaSort, dateSort } from "../../utils/sorters";
|
|||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
|
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
|
||||||
import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component";
|
import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component";
|
||||||
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
|
import LockerWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
||||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||||
import { FaTasks } from "react-icons/fa";
|
import UpsellComponent, { upsellEnum } from "../upsell/upsell.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
@@ -170,6 +173,8 @@ export function BillsListTableComponent({
|
|||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
const hasBillsAccess = HasFeatureAccess({ bodyshop, featureName: "bills" });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
title={t("bills.labels.bills")}
|
title={t("bills.labels.bills")}
|
||||||
@@ -181,6 +186,7 @@ export function BillsListTableComponent({
|
|||||||
{job && job.converted ? (
|
{job && job.converted ? (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
|
disabled={!hasBillsAccess}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setBillEnterContext({
|
setBillEnterContext({
|
||||||
actions: { refetch: billsQuery.refetch },
|
actions: { refetch: billsQuery.refetch },
|
||||||
@@ -190,9 +196,10 @@ export function BillsListTableComponent({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("jobs.actions.postbills")}
|
<LockerWrapperComponent featureName="bills">{t("jobs.actions.postbills")}</LockerWrapperComponent>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
disabled={!hasBillsAccess}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setReconciliationContext({
|
setReconciliationContext({
|
||||||
actions: { refetch: billsQuery.refetch },
|
actions: { refetch: billsQuery.refetch },
|
||||||
@@ -203,7 +210,7 @@ export function BillsListTableComponent({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("jobs.actions.reconcile")}
|
<LockerWrapperComponent featureName="bills"> {t("jobs.actions.reconcile")}</LockerWrapperComponent>
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -211,6 +218,7 @@ export function BillsListTableComponent({
|
|||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder={t("general.labels.search")}
|
placeholder={t("general.labels.search")}
|
||||||
value={searchText}
|
value={searchText}
|
||||||
|
disabled={!hasBillsAccess}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSearchText(e.target.value);
|
setSearchText(e.target.value);
|
||||||
@@ -226,8 +234,13 @@ export function BillsListTableComponent({
|
|||||||
}}
|
}}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
dataSource={filteredBills}
|
dataSource={hasBillsAccess ? filteredBills : []}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
|
locale={{
|
||||||
|
...(!hasBillsAccess && {
|
||||||
|
emptyText: <UpsellComponent upsell={upsellEnum().bills.general} />
|
||||||
|
})
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { DeleteFilled } from "@ant-design/icons";
|
import { CopyFilled, DeleteFilled } from "@ant-design/icons";
|
||||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||||
import { Button, Card, Col, Form, Input, Row, Space, Spin, Statistic, notification } from "antd";
|
import { Button, Card, Col, Form, Input, message, notification, Row, Space, Spin, Statistic } from "antd";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -14,51 +14,73 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
|||||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
||||||
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
|
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
|
||||||
|
import { getCurrentUser } from "../../firebase/firebase.utils";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
cardPaymentModal: selectCardPayment,
|
cardPaymentModal: selectCardPayment,
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop,
|
||||||
|
currentUser: getCurrentUser
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })),
|
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||||
|
dispatch(
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid,
|
||||||
|
operation,
|
||||||
|
type
|
||||||
|
})
|
||||||
|
),
|
||||||
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment"))
|
toggleModalVisible: () => dispatch(toggleModalVisible("cardPayment"))
|
||||||
});
|
});
|
||||||
|
|
||||||
const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisible, insertAuditTrail }) => {
|
const CardPaymentModalComponent = ({
|
||||||
|
bodyshop,
|
||||||
|
currentUser,
|
||||||
|
cardPaymentModal,
|
||||||
|
toggleModalVisible,
|
||||||
|
insertAuditTrail
|
||||||
|
}) => {
|
||||||
const { context, actions } = cardPaymentModal;
|
const { context, actions } = cardPaymentModal;
|
||||||
|
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
const [paymentLink, setPaymentLink] = useState();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
// const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
|
|
||||||
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
|
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [, { data, refetch, queryLoading }] = useLazyQuery(QUERY_RO_AND_OWNER_BY_JOB_PKS, {
|
const [, { data, refetch, queryLoading }] = useLazyQuery(QUERY_RO_AND_OWNER_BY_JOB_PKS, {
|
||||||
variables: { jobids: [context.jobid] },
|
variables: { jobids: [context.jobid] },
|
||||||
skip: true
|
skip: !context?.jobid
|
||||||
});
|
});
|
||||||
|
|
||||||
//Initialize the intellipay window.
|
const collectIPayFields = () => {
|
||||||
|
const iPayFields = document.querySelectorAll(".ipayfield");
|
||||||
|
const iPayData = {};
|
||||||
|
iPayFields.forEach((field) => {
|
||||||
|
iPayData[field.dataset.ipayname] = field.value;
|
||||||
|
});
|
||||||
|
return iPayData;
|
||||||
|
};
|
||||||
|
|
||||||
const SetIntellipayCallbackFunctions = () => {
|
const SetIntellipayCallbackFunctions = () => {
|
||||||
console.log("*** Set IntelliPay callback functions.");
|
console.log("*** Set IntelliPay callback functions.");
|
||||||
|
|
||||||
window.intellipay.runOnClose(() => {
|
window.intellipay.runOnClose(() => {
|
||||||
//window.intellipay.initialize();
|
//window.intellipay.initialize();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.intellipay.runOnApproval(async function (response) {
|
window.intellipay.runOnApproval(() => {
|
||||||
//2024-04-25: Nothing is going to happen here anymore. We'll completely rely on the callback.
|
//2024-04-25: Nothing is going to happen here anymore. We'll completely rely on the callback.
|
||||||
//Add a slight delay to allow the refetch to properly get the data.
|
//Add a slight delay to allow the refetch to properly get the data.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (actions && actions.refetch && typeof actions.refetch === "function")
|
if (actions?.refetch) actions.refetch();
|
||||||
actions.refetch();
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
}, 750);
|
}, 750);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.intellipay.runOnNonApproval(async function (response) {
|
window.intellipay.runOnNonApproval(async (response) => {
|
||||||
// Mutate unsuccessful payment
|
// Mutate unsuccessful payment
|
||||||
|
|
||||||
const { payments } = form.getFieldsValue();
|
const { payments } = form.getFieldsValue();
|
||||||
@@ -86,22 +108,26 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleIntelliPayCharge = async () => {
|
const handleIntelliPayCharge = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
//Validate
|
//Validate
|
||||||
try {
|
try {
|
||||||
await form.validateFields();
|
await form.validateFields();
|
||||||
} catch (error) {
|
} catch {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const iPayData = collectIPayFields();
|
||||||
|
const { payments } = form.getFieldsValue();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post("/intellipay/lightbox_credentials", {
|
const response = await axios.post("/intellipay/lightbox_credentials", {
|
||||||
bodyshop,
|
bodyshop,
|
||||||
refresh: !!window.intellipay,
|
refresh: !!window.intellipay,
|
||||||
paymentSplitMeta: form.getFieldsValue(),
|
paymentSplitMeta: form.getFieldsValue(),
|
||||||
|
iPayData: iPayData,
|
||||||
|
comment: btoa(JSON.stringify({ payments, userEmail: currentUser.email }))
|
||||||
});
|
});
|
||||||
|
|
||||||
if (window.intellipay) {
|
if (window.intellipay) {
|
||||||
@@ -110,8 +136,8 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
|||||||
SetIntellipayCallbackFunctions();
|
SetIntellipayCallbackFunctions();
|
||||||
window.intellipay.autoOpen();
|
window.intellipay.autoOpen();
|
||||||
} else {
|
} else {
|
||||||
var rg = document.createRange();
|
const rg = document.createRange();
|
||||||
let node = rg.createContextualFragment(response.data);
|
const node = rg.createContextualFragment(response.data);
|
||||||
document.documentElement.appendChild(node);
|
document.documentElement.appendChild(node);
|
||||||
SetIntellipayCallbackFunctions();
|
SetIntellipayCallbackFunctions();
|
||||||
window.intellipay.isAutoOpen = true;
|
window.intellipay.isAutoOpen = true;
|
||||||
@@ -126,6 +152,44 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleIntelliPayChargeShortLink = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
//Validate
|
||||||
|
try {
|
||||||
|
await form.validateFields();
|
||||||
|
} catch {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iPayData = collectIPayFields();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { payments } = form.getFieldsValue();
|
||||||
|
const response = await axios.post("/intellipay/generate_payment_url", {
|
||||||
|
bodyshop,
|
||||||
|
amount: payments.reduce((acc, val) => acc + (val?.amount || 0), 0),
|
||||||
|
account: payments && data?.jobs?.length > 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null,
|
||||||
|
comment: btoa(JSON.stringify({ payments, userEmail: currentUser.email })),
|
||||||
|
paymentSplitMeta: form.getFieldsValue(),
|
||||||
|
iPayData: iPayData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response?.data?.shorUrl) {
|
||||||
|
setPaymentLink(response.data.shorUrl);
|
||||||
|
await navigator.clipboard.writeText(response.data.shorUrl);
|
||||||
|
message.success(t("general.actions.copied"));
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
} catch (error) {
|
||||||
|
notification.open({
|
||||||
|
type: "error",
|
||||||
|
message: t("job_payments.notifications.error.openingip")
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title="Card Payment">
|
<Card title="Card Payment">
|
||||||
<Spin spinning={loading}>
|
<Spin spinning={loading}>
|
||||||
@@ -137,81 +201,56 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Form.List name={["payments"]}>
|
<Form.List name={["payments"]}>
|
||||||
{(fields, { add, remove, move }) => {
|
{(fields, { add, remove }) => (
|
||||||
return (
|
<div>
|
||||||
<div>
|
{fields.map((field, index) => (
|
||||||
{fields.map((field, index) => (
|
<Form.Item key={field.key}>
|
||||||
<Form.Item key={field.key}>
|
<Row gutter={[16, 16]}>
|
||||||
<Row gutter={[16, 16]}>
|
<Col span={16}>
|
||||||
<Col span={16}>
|
<Form.Item
|
||||||
<Form.Item
|
key={`${index}jobid`}
|
||||||
key={`${index}jobid`}
|
label={t("jobs.fields.ro_number")}
|
||||||
label={t("jobs.fields.ro_number")}
|
name={[field.name, "jobid"]}
|
||||||
name={[field.name, "jobid"]}
|
rules={[{ required: true }]}
|
||||||
rules={[
|
>
|
||||||
{
|
<JobSearchSelectComponent notExported={false} clm_no />
|
||||||
required: true
|
</Form.Item>
|
||||||
//message: t("general.validation.required"),
|
</Col>
|
||||||
}
|
<Col span={6}>
|
||||||
]}
|
<Form.Item
|
||||||
>
|
key={`${index}amount`}
|
||||||
<JobSearchSelectComponent notExported={false} clm_no />
|
label={t("payments.fields.amount")}
|
||||||
</Form.Item>
|
name={[field.name, "amount"]}
|
||||||
</Col>
|
rules={[{ required: true }]}
|
||||||
<Col span={6}>
|
>
|
||||||
<Form.Item
|
<CurrencyFormItemComponent />
|
||||||
key={`${index}amount`}
|
</Form.Item>
|
||||||
label={t("payments.fields.amount")}
|
</Col>
|
||||||
name={[field.name, "amount"]}
|
<Col span={2}>
|
||||||
rules={[
|
<DeleteFilled style={{ margin: "1rem" }} onClick={() => remove(field.name)} />
|
||||||
{
|
</Col>
|
||||||
required: true
|
</Row>
|
||||||
//message: t("general.validation.required"),
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<CurrencyFormItemComponent />
|
|
||||||
</Form.Item>
|
|
||||||
</Col>
|
|
||||||
<Col span={2}>
|
|
||||||
<DeleteFilled
|
|
||||||
style={{ margin: "1rem" }}
|
|
||||||
onClick={() => {
|
|
||||||
remove(field.name);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Form.Item>
|
|
||||||
))}
|
|
||||||
<Form.Item>
|
|
||||||
<Button
|
|
||||||
type="dashed"
|
|
||||||
onClick={() => {
|
|
||||||
add();
|
|
||||||
}}
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
>
|
|
||||||
{t("general.actions.add")}
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
))}
|
||||||
);
|
<Form.Item>
|
||||||
}}
|
<Button type="dashed" onClick={() => add()} style={{ width: "100%" }}>
|
||||||
|
{t("general.actions.add")}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Form.List>
|
</Form.List>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
shouldUpdate={(prevValues, curValues) =>
|
shouldUpdate={(prevValues, curValues) =>
|
||||||
prevValues.payments?.map((p) => p?.jobid).join() !== curValues.payments?.map((p) => p?.jobid).join()
|
prevValues.payments?.map((p) => p?.jobid + p?.amount).join() !==
|
||||||
|
curValues.payments?.map((p) => p?.jobid + p?.amount).join()
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{() => {
|
{() => {
|
||||||
//If all of the job ids have been fileld in, then query and update the IP field.
|
//If all of the job ids have been fileld in, then query and update the IP field.
|
||||||
const { payments } = form.getFieldsValue();
|
const { payments } = form.getFieldsValue();
|
||||||
if (
|
if (payments?.length > 0 && payments?.filter((p) => p?.jobid).length === payments?.length) {
|
||||||
payments?.length > 0 &&
|
|
||||||
payments?.filter((p) => p?.jobid).length === payments?.length
|
|
||||||
) {
|
|
||||||
refetch({ jobids: payments.map((p) => p.jobid) });
|
refetch({ jobids: payments.map((p) => p.jobid) });
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@@ -243,10 +282,7 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
|||||||
>
|
>
|
||||||
{() => {
|
{() => {
|
||||||
const { payments } = form.getFieldsValue();
|
const { payments } = form.getFieldsValue();
|
||||||
const totalAmountToCharge = payments?.reduce((acc, val) => {
|
const totalAmountToCharge = payments?.reduce((acc, val) => acc + (val?.amount || 0), 0);
|
||||||
return acc + (val?.amount || 0);
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space style={{ float: "right" }}>
|
<Space style={{ float: "right" }}>
|
||||||
<Statistic title="Amount To Charge" value={totalAmountToCharge} precision={2} />
|
<Statistic title="Amount To Charge" value={totalAmountToCharge} precision={2} />
|
||||||
@@ -273,11 +309,36 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
|||||||
>
|
>
|
||||||
{t("job_payments.buttons.proceedtopayment")}
|
{t("job_payments.buttons.proceedtopayment")}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Space direction="vertical" align="center">
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
// data-ipayname="submit"
|
||||||
|
className="ipayfield"
|
||||||
|
loading={queryLoading || loading}
|
||||||
|
disabled={!(totalAmountToCharge > 0)}
|
||||||
|
onClick={handleIntelliPayChargeShortLink}
|
||||||
|
>
|
||||||
|
{t("job_payments.buttons.create_short_link")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
{paymentLink && (
|
||||||
|
<Space
|
||||||
|
style={{ cursor: "pointer", float: "right" }}
|
||||||
|
align="end"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(paymentLink);
|
||||||
|
message.success(t("general.actions.copied"));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div>{paymentLink}</div>
|
||||||
|
<CopyFilled />
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
</Spin>
|
</Spin>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,89 +1,50 @@
|
|||||||
import { useApolloClient } from "@apollo/client";
|
import { useApolloClient } from "@apollo/client";
|
||||||
import { getToken, onMessage } from "@firebase/messaging";
|
import { getToken } from "@firebase/messaging";
|
||||||
import { Button, notification, Space } from "antd";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import React, { useEffect } from "react";
|
import React, { useContext, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import SocketContext from "../../contexts/SocketIO/socketContext";
|
||||||
import { messaging, requestForToken } from "../../firebase/firebase.utils";
|
import { messaging, requestForToken } from "../../firebase/firebase.utils";
|
||||||
import FcmHandler from "../../utils/fcm-handler";
|
|
||||||
import ChatPopupComponent from "../chat-popup/chat-popup.component";
|
import ChatPopupComponent from "../chat-popup/chat-popup.component";
|
||||||
import "./chat-affix.styles.scss";
|
import "./chat-affix.styles.scss";
|
||||||
|
import { registerMessagingHandlers, unregisterMessagingHandlers } from "./registerMessagingSocketHandlers";
|
||||||
|
|
||||||
export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
|
const { socket } = useContext(SocketContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!bodyshop || !bodyshop.messagingservicesid) return;
|
if (!bodyshop || !bodyshop.messagingservicesid) return;
|
||||||
|
|
||||||
async function SubscribeToTopic() {
|
async function SubscribeToTopicForFCMNotification() {
|
||||||
try {
|
try {
|
||||||
const r = await axios.post("/notifications/subscribe", {
|
await requestForToken();
|
||||||
|
await axios.post("/notifications/subscribe", {
|
||||||
fcm_tokens: await getToken(messaging, {
|
fcm_tokens: await getToken(messaging, {
|
||||||
vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY
|
vapidKey: import.meta.env.VITE_APP_FIREBASE_PUBLIC_VAPID_KEY
|
||||||
}),
|
}),
|
||||||
type: "messaging",
|
type: "messaging",
|
||||||
imexshopid: bodyshop.imexshopid
|
imexshopid: bodyshop.imexshopid
|
||||||
});
|
});
|
||||||
console.log("FCM Topic Subscription", r.data);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error attempting to subscribe to messaging topic: ", error);
|
console.log("Error attempting to subscribe to messaging topic: ", error);
|
||||||
notification.open({
|
|
||||||
key: "fcm",
|
|
||||||
type: "warning",
|
|
||||||
message: t("general.errors.fcm"),
|
|
||||||
btn: (
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
onClick={async () => {
|
|
||||||
await requestForToken();
|
|
||||||
SubscribeToTopic();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("general.actions.tryagain")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
const win = window.open(
|
|
||||||
"https://help.imex.online/en/article/enabling-notifications-o978xi/",
|
|
||||||
"_blank"
|
|
||||||
);
|
|
||||||
win.focus();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t("general.labels.help")}
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SubscribeToTopic();
|
SubscribeToTopicForFCMNotification();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [bodyshop]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
//Register WS handlers
|
||||||
function handleMessage(payload) {
|
if (socket && socket.connected) {
|
||||||
FcmHandler({
|
registerMessagingHandlers({ socket, client });
|
||||||
client,
|
|
||||||
payload: (payload && payload.data && payload.data.data) || payload.data
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let stopMessageListener, channel;
|
|
||||||
try {
|
|
||||||
stopMessageListener = onMessage(messaging, handleMessage);
|
|
||||||
channel = new BroadcastChannel("imex-sw-messages");
|
|
||||||
channel.addEventListener("message", handleMessage);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Unable to set event listeners.");
|
|
||||||
}
|
|
||||||
return () => {
|
return () => {
|
||||||
stopMessageListener && stopMessageListener();
|
if (socket && socket.connected) {
|
||||||
channel && channel.removeEventListener("message", handleMessage);
|
unregisterMessagingHandlers({ socket });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [client]);
|
}, [bodyshop, socket, t, client]);
|
||||||
|
|
||||||
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,434 @@
|
|||||||
|
import { CONVERSATION_LIST_QUERY, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries";
|
||||||
|
import { gql } from "@apollo/client";
|
||||||
|
|
||||||
|
const logLocal = (message, ...args) => {
|
||||||
|
if (import.meta.env.VITE_APP_IS_TEST || !import.meta.env.PROD) {
|
||||||
|
console.log(`==================== ${message} ====================`);
|
||||||
|
console.dir({ ...args });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Utility function to enrich conversation data
|
||||||
|
const enrichConversation = (conversation, isOutbound) => ({
|
||||||
|
...conversation,
|
||||||
|
updated_at: conversation.updated_at || new Date().toISOString(),
|
||||||
|
unreadcnt: conversation.unreadcnt || 0,
|
||||||
|
archived: conversation.archived || false,
|
||||||
|
label: conversation.label || null,
|
||||||
|
job_conversations: conversation.job_conversations || [],
|
||||||
|
messages_aggregate: conversation.messages_aggregate || {
|
||||||
|
__typename: "messages_aggregate",
|
||||||
|
aggregate: {
|
||||||
|
__typename: "messages_aggregate_fields",
|
||||||
|
count: isOutbound ? 0 : 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
__typename: "conversations"
|
||||||
|
});
|
||||||
|
|
||||||
|
export const registerMessagingHandlers = ({ socket, client }) => {
|
||||||
|
if (!(socket && client)) return;
|
||||||
|
|
||||||
|
const handleNewMessageSummary = async (message) => {
|
||||||
|
const { conversationId, newConversation, existingConversation, isoutbound } = message;
|
||||||
|
|
||||||
|
logLocal("handleNewMessageSummary - Start", { message, isNew: !existingConversation });
|
||||||
|
|
||||||
|
const queryVariables = { offset: 0 };
|
||||||
|
|
||||||
|
if (!existingConversation && conversationId) {
|
||||||
|
// Attempt to read from the cache to determine if this is actually a new conversation
|
||||||
|
try {
|
||||||
|
const cachedConversation = client.cache.readFragment({
|
||||||
|
id: client.cache.identify({ __typename: "conversations", id: conversationId }),
|
||||||
|
fragment: gql`
|
||||||
|
fragment ExistingConversationCheck on conversations {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cachedConversation) {
|
||||||
|
logLocal("handleNewMessageSummary - Existing Conversation inferred from cache", {
|
||||||
|
conversationId
|
||||||
|
});
|
||||||
|
return handleNewMessageSummary({
|
||||||
|
...message,
|
||||||
|
existingConversation: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logLocal("handleNewMessageSummary - Cache miss", { conversationId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle new conversation
|
||||||
|
if (!existingConversation && newConversation?.phone_num) {
|
||||||
|
logLocal("handleNewMessageSummary - New Conversation", newConversation);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const queryResults = client.cache.readQuery({
|
||||||
|
query: CONVERSATION_LIST_QUERY,
|
||||||
|
variables: queryVariables
|
||||||
|
});
|
||||||
|
|
||||||
|
const existingConversations = queryResults?.conversations || [];
|
||||||
|
const enrichedConversation = enrichConversation(newConversation, isoutbound);
|
||||||
|
|
||||||
|
if (!existingConversations.some((conv) => conv.id === enrichedConversation.id)) {
|
||||||
|
client.cache.modify({
|
||||||
|
id: "ROOT_QUERY",
|
||||||
|
fields: {
|
||||||
|
conversations(existingConversations = []) {
|
||||||
|
return [enrichedConversation, ...existingConversations];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating cache for new conversation:", error);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle existing conversation
|
||||||
|
if (existingConversation) {
|
||||||
|
try {
|
||||||
|
client.cache.modify({
|
||||||
|
id: client.cache.identify({ __typename: "conversations", id: conversationId }),
|
||||||
|
fields: {
|
||||||
|
updated_at: () => new Date().toISOString(),
|
||||||
|
archived: () => false,
|
||||||
|
messages_aggregate(cached = { aggregate: { count: 0 } }) {
|
||||||
|
const currentCount = cached.aggregate?.count || 0;
|
||||||
|
if (!isoutbound) {
|
||||||
|
return {
|
||||||
|
__typename: "messages_aggregate",
|
||||||
|
aggregate: {
|
||||||
|
__typename: "messages_aggregate_fields",
|
||||||
|
count: currentCount + 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating cache for existing conversation:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logLocal("New Conversation Summary finished without work", { message });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNewMessageDetailed = (message) => {
|
||||||
|
const { conversationId, newMessage } = message;
|
||||||
|
|
||||||
|
logLocal("handleNewMessageDetailed - Start", message);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if the conversation exists in the cache
|
||||||
|
const queryResults = client.cache.readQuery({
|
||||||
|
query: GET_CONVERSATION_DETAILS,
|
||||||
|
variables: { conversationId }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!queryResults?.conversations_by_pk) {
|
||||||
|
console.warn("Conversation not found in cache:", { conversationId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append the new message to the conversation's message list using cache.modify
|
||||||
|
client.cache.modify({
|
||||||
|
id: client.cache.identify({ __typename: "conversations", id: conversationId }),
|
||||||
|
fields: {
|
||||||
|
messages(existingMessages = []) {
|
||||||
|
return [...existingMessages, newMessage];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logLocal("handleNewMessageDetailed - Message appended successfully", {
|
||||||
|
conversationId,
|
||||||
|
newMessage
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating conversation messages in cache:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMessageChanged = (message) => {
|
||||||
|
if (!message) {
|
||||||
|
logLocal("handleMessageChanged - No message provided", message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logLocal("handleMessageChanged - Start", message);
|
||||||
|
|
||||||
|
try {
|
||||||
|
client.cache.modify({
|
||||||
|
id: client.cache.identify({ __typename: "conversations", id: message.conversationid }),
|
||||||
|
fields: {
|
||||||
|
messages(existingMessages = [], { readField }) {
|
||||||
|
return existingMessages.map((messageRef) => {
|
||||||
|
// Check if this is the message to update
|
||||||
|
if (readField("id", messageRef) === message.id) {
|
||||||
|
const currentStatus = readField("status", messageRef);
|
||||||
|
|
||||||
|
// Handle known types of message changes
|
||||||
|
switch (message.type) {
|
||||||
|
case "status-changed":
|
||||||
|
// Prevent overwriting if the current status is already "delivered"
|
||||||
|
if (currentStatus === "delivered") {
|
||||||
|
logLocal("handleMessageChanged - Status already delivered, skipping update", {
|
||||||
|
messageId: message.id
|
||||||
|
});
|
||||||
|
return messageRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the status field
|
||||||
|
return {
|
||||||
|
...messageRef,
|
||||||
|
status: message.status
|
||||||
|
};
|
||||||
|
|
||||||
|
case "text-updated":
|
||||||
|
// Handle changes to the message text
|
||||||
|
return {
|
||||||
|
...messageRef,
|
||||||
|
text: message.text
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add cases for other known message types as needed
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Log a warning for unhandled message types
|
||||||
|
logLocal("handleMessageChanged - Unhandled message type", { type: message.type });
|
||||||
|
return messageRef;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return messageRef; // Keep other messages unchanged
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logLocal("handleMessageChanged - Message updated successfully", {
|
||||||
|
messageId: message.id,
|
||||||
|
type: message.type
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("handleMessageChanged - Error modifying cache:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConversationChanged = async (data) => {
|
||||||
|
if (!data) {
|
||||||
|
logLocal("handleConversationChanged - No data provided", data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { conversationId, type, job_conversations, messageIds, ...fields } = data;
|
||||||
|
logLocal("handleConversationChanged - Start", data);
|
||||||
|
|
||||||
|
const updatedAt = new Date().toISOString();
|
||||||
|
|
||||||
|
const updateConversationList = (newConversation) => {
|
||||||
|
try {
|
||||||
|
const existingList = client.cache.readQuery({
|
||||||
|
query: CONVERSATION_LIST_QUERY,
|
||||||
|
variables: { offset: 0 }
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedList = existingList?.conversations
|
||||||
|
? [
|
||||||
|
newConversation,
|
||||||
|
...existingList.conversations.filter((conv) => conv.id !== newConversation.id) // Prevent duplicates
|
||||||
|
]
|
||||||
|
: [newConversation];
|
||||||
|
|
||||||
|
client.cache.writeQuery({
|
||||||
|
query: CONVERSATION_LIST_QUERY,
|
||||||
|
variables: { offset: 0 },
|
||||||
|
data: {
|
||||||
|
conversations: updatedList
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
logLocal("handleConversationChanged - Conversation list updated successfully", newConversation);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating conversation list in the cache:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle specific types
|
||||||
|
try {
|
||||||
|
switch (type) {
|
||||||
|
case "conversation-marked-read":
|
||||||
|
if (conversationId && messageIds?.length > 0) {
|
||||||
|
client.cache.modify({
|
||||||
|
id: client.cache.identify({ __typename: "conversations", id: conversationId }),
|
||||||
|
fields: {
|
||||||
|
messages(existingMessages = [], { readField }) {
|
||||||
|
return existingMessages.map((message) => {
|
||||||
|
if (messageIds.includes(readField("id", message))) {
|
||||||
|
return { ...message, read: true };
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
messages_aggregate: () => ({
|
||||||
|
__typename: "messages_aggregate",
|
||||||
|
aggregate: { __typename: "messages_aggregate_fields", count: 0 }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "conversation-created":
|
||||||
|
updateConversationList({ ...fields, job_conversations, updated_at: updatedAt });
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "conversation-unarchived":
|
||||||
|
case "conversation-archived":
|
||||||
|
// Would like to someday figure out how to get this working without refetch queries,
|
||||||
|
// But I have but a solid 4 hours into it, and there are just too many weird occurrences
|
||||||
|
try {
|
||||||
|
const listQueryVariables = { offset: 0 };
|
||||||
|
const detailsQueryVariables = { conversationId };
|
||||||
|
|
||||||
|
// Check if conversation details exist in the cache
|
||||||
|
const detailsExist = !!client.cache.readQuery({
|
||||||
|
query: GET_CONVERSATION_DETAILS,
|
||||||
|
variables: detailsQueryVariables
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refetch conversation list
|
||||||
|
await client.refetchQueries({
|
||||||
|
include: [CONVERSATION_LIST_QUERY, ...(detailsExist ? [GET_CONVERSATION_DETAILS] : [])],
|
||||||
|
variables: [
|
||||||
|
{ query: CONVERSATION_LIST_QUERY, variables: listQueryVariables },
|
||||||
|
...(detailsExist
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
query: GET_CONVERSATION_DETAILS,
|
||||||
|
variables: detailsQueryVariables
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: [])
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
logLocal("handleConversationChanged - Refetched queries after state change", {
|
||||||
|
conversationId,
|
||||||
|
type
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error refetching queries after conversation state change:", error);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "tag-added":
|
||||||
|
// Ensure `job_conversations` is properly formatted
|
||||||
|
const formattedJobConversations = job_conversations.map((jc) => ({
|
||||||
|
__typename: "job_conversations",
|
||||||
|
jobid: jc.jobid || jc.job?.id,
|
||||||
|
conversationid: conversationId,
|
||||||
|
job: jc.job || {
|
||||||
|
__typename: "jobs",
|
||||||
|
id: data.selectedJob.id,
|
||||||
|
ro_number: data.selectedJob.ro_number,
|
||||||
|
ownr_co_nm: data.selectedJob.ownr_co_nm,
|
||||||
|
ownr_fn: data.selectedJob.ownr_fn,
|
||||||
|
ownr_ln: data.selectedJob.ownr_ln
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
client.cache.modify({
|
||||||
|
id: client.cache.identify({ __typename: "conversations", id: conversationId }),
|
||||||
|
fields: {
|
||||||
|
job_conversations: (existing = []) => {
|
||||||
|
// Ensure no duplicates based on both `conversationid` and `jobid`
|
||||||
|
const existingLinks = new Set(
|
||||||
|
existing.map((jc) => {
|
||||||
|
const jobId = client.cache.readFragment({
|
||||||
|
id: client.cache.identify(jc),
|
||||||
|
fragment: gql`
|
||||||
|
fragment JobConversationLinkAdded on job_conversations {
|
||||||
|
jobid
|
||||||
|
conversationid
|
||||||
|
}
|
||||||
|
`
|
||||||
|
})?.jobid;
|
||||||
|
return `${jobId}:${conversationId}`; // Unique identifier for a job-conversation link
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const newItems = formattedJobConversations.filter((jc) => {
|
||||||
|
const uniqueLink = `${jc.jobid}:${jc.conversationid}`;
|
||||||
|
return !existingLinks.has(uniqueLink);
|
||||||
|
});
|
||||||
|
|
||||||
|
return [...existing, ...newItems];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "tag-removed":
|
||||||
|
try {
|
||||||
|
const conversationCacheId = client.cache.identify({ __typename: "conversations", id: conversationId });
|
||||||
|
|
||||||
|
// Evict the specific cache entry for job_conversations
|
||||||
|
client.cache.evict({
|
||||||
|
id: conversationCacheId,
|
||||||
|
fieldName: "job_conversations"
|
||||||
|
});
|
||||||
|
|
||||||
|
// Garbage collect evicted entries
|
||||||
|
client.cache.gc();
|
||||||
|
|
||||||
|
logLocal("handleConversationChanged - tag removed - Refetched conversation list after state change", {
|
||||||
|
conversationId,
|
||||||
|
type
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error refetching queries after conversation state change: (Tag Removed)", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logLocal("handleConversationChanged - Unhandled type", { type });
|
||||||
|
client.cache.modify({
|
||||||
|
id: client.cache.identify({ __typename: "conversations", id: conversationId }),
|
||||||
|
fields: {
|
||||||
|
...Object.fromEntries(
|
||||||
|
Object.entries(fields).map(([key, value]) => [key, (cached) => (value !== undefined ? value : cached)])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error handling conversation changes:", { type, error });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.on("new-message-summary", handleNewMessageSummary);
|
||||||
|
socket.on("new-message-detailed", handleNewMessageDetailed);
|
||||||
|
socket.on("message-changed", handleMessageChanged);
|
||||||
|
socket.on("conversation-changed", handleConversationChanged);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const unregisterMessagingHandlers = ({ socket }) => {
|
||||||
|
if (!socket) return;
|
||||||
|
socket.off("new-message-summary");
|
||||||
|
socket.off("new-message-detailed");
|
||||||
|
socket.off("message-changed");
|
||||||
|
socket.off("conversation-changed");
|
||||||
|
};
|
||||||
@@ -1,27 +1,49 @@
|
|||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Button } from "antd";
|
import { Button } from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useContext, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { TOGGLE_CONVERSATION_ARCHIVE } from "../../graphql/conversations.queries";
|
import { TOGGLE_CONVERSATION_ARCHIVE } from "../../graphql/conversations.queries";
|
||||||
|
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
export default function ChatArchiveButton({ conversation }) {
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
|
export function ChatArchiveButton({ conversation, bodyshop }) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [updateConversation] = useMutation(TOGGLE_CONVERSATION_ARCHIVE);
|
const [updateConversation] = useMutation(TOGGLE_CONVERSATION_ARCHIVE);
|
||||||
|
const { socket } = useContext(SocketContext);
|
||||||
|
|
||||||
const handleToggleArchive = async () => {
|
const handleToggleArchive = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
await updateConversation({
|
const updatedConversation = await updateConversation({
|
||||||
variables: { id: conversation.id, archived: !conversation.archived },
|
variables: { id: conversation.id, archived: !conversation.archived }
|
||||||
refetchQueries: ["CONVERSATION_LIST_QUERY"]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (socket) {
|
||||||
|
socket.emit("conversation-modified", {
|
||||||
|
type: "conversation-archived",
|
||||||
|
conversationId: conversation.id,
|
||||||
|
bodyshopId: bodyshop.id,
|
||||||
|
archived: updatedConversation.data.update_conversations_by_pk.archived
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button onClick={handleToggleArchive} loading={loading} type="primary">
|
<Button onClick={handleToggleArchive} loading={loading} className="archive-button" type="primary">
|
||||||
{conversation.archived ? t("messaging.labels.unarchive") : t("messaging.labels.archive")}
|
{conversation.archived ? t("messaging.labels.unarchive") : t("messaging.labels.archive")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ChatArchiveButton);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Badge, Card, List, Space, Tag } from "antd";
|
import { Badge, Card, List, Space, Tag } from "antd";
|
||||||
import React from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { AutoSizer, CellMeasurer, CellMeasurerCache, List as VirtualizedList } from "react-virtualized";
|
import { Virtuoso } from "react-virtuoso";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
|
import { setSelectedConversation } from "../../redux/messaging/messaging.actions";
|
||||||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||||
@@ -19,19 +19,26 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
setSelectedConversation: (conversationId) => dispatch(setSelectedConversation(conversationId))
|
setSelectedConversation: (conversationId) => dispatch(setSelectedConversation(conversationId))
|
||||||
});
|
});
|
||||||
|
|
||||||
function ChatConversationListComponent({
|
function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation }) {
|
||||||
conversationList,
|
// That comma is there for a reason, do not remove it
|
||||||
selectedConversation,
|
const [, forceUpdate] = useState(false);
|
||||||
setSelectedConversation,
|
|
||||||
loadMoreConversations
|
|
||||||
}) {
|
|
||||||
const cache = new CellMeasurerCache({
|
|
||||||
fixedWidth: true,
|
|
||||||
defaultHeight: 60
|
|
||||||
});
|
|
||||||
|
|
||||||
const rowRenderer = ({ index, key, style, parent }) => {
|
// Re-render every minute
|
||||||
const item = conversationList[index];
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
forceUpdate((prev) => !prev); // Toggle state to trigger re-render
|
||||||
|
}, 60000); // 1 minute in milliseconds
|
||||||
|
|
||||||
|
return () => clearInterval(interval); // Cleanup on unmount
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Memoize the sorted conversation list
|
||||||
|
const sortedConversationList = React.useMemo(() => {
|
||||||
|
return _.orderBy(conversationList, ["updated_at"], ["desc"]);
|
||||||
|
}, [conversationList]);
|
||||||
|
|
||||||
|
const renderConversation = (index) => {
|
||||||
|
const item = sortedConversationList[index];
|
||||||
const cardContentRight = <TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
|
const cardContentRight = <TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
|
||||||
const cardContentLeft =
|
const cardContentLeft =
|
||||||
item.job_conversations.length > 0
|
item.job_conversations.length > 0
|
||||||
@@ -52,7 +59,8 @@ function ChatConversationListComponent({
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
const cardExtra = <Badge count={item.messages_aggregate.aggregate.count || 0} />;
|
|
||||||
|
const cardExtra = <Badge count={item.messages_aggregate.aggregate.count} />;
|
||||||
|
|
||||||
const getCardStyle = () =>
|
const getCardStyle = () =>
|
||||||
item.id === selectedConversation
|
item.id === selectedConversation
|
||||||
@@ -60,40 +68,26 @@ function ChatConversationListComponent({
|
|||||||
: { backgroundColor: index % 2 === 0 ? "#f0f2f5" : "#ffffff" };
|
: { backgroundColor: index % 2 === 0 ? "#f0f2f5" : "#ffffff" };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CellMeasurer key={key} cache={cache} parent={parent} columnIndex={0} rowIndex={index}>
|
<List.Item
|
||||||
<List.Item
|
key={item.id}
|
||||||
onClick={() => setSelectedConversation(item.id)}
|
onClick={() => setSelectedConversation(item.id)}
|
||||||
style={style}
|
className={`chat-list-item ${item.id === selectedConversation ? "chat-list-selected-conversation" : ""}`}
|
||||||
className={`chat-list-item
|
>
|
||||||
${item.id === selectedConversation ? "chat-list-selected-conversation" : null}`}
|
<Card style={getCardStyle()} bordered={false} size="small" extra={cardExtra} title={cardTitle}>
|
||||||
>
|
<div style={{ display: "inline-block", width: "70%", textAlign: "left" }}>{cardContentLeft}</div>
|
||||||
<Card style={getCardStyle()} bordered={false} size="small" extra={cardExtra} title={cardTitle}>
|
<div style={{ display: "inline-block", width: "30%", textAlign: "right" }}>{cardContentRight}</div>
|
||||||
<div style={{ display: "inline-block", width: "70%", textAlign: "left" }}>{cardContentLeft}</div>
|
</Card>
|
||||||
<div style={{ display: "inline-block", width: "30%", textAlign: "right" }}>{cardContentRight}</div>
|
</List.Item>
|
||||||
</Card>
|
|
||||||
</List.Item>
|
|
||||||
</CellMeasurer>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-list-container">
|
<div className="chat-list-container">
|
||||||
<AutoSizer>
|
<Virtuoso
|
||||||
{({ height, width }) => (
|
data={sortedConversationList}
|
||||||
<VirtualizedList
|
itemContent={(index) => renderConversation(index)}
|
||||||
height={height}
|
style={{ height: "100%", width: "100%" }}
|
||||||
width={width}
|
/>
|
||||||
rowCount={conversationList.length}
|
|
||||||
rowHeight={cache.rowHeight}
|
|
||||||
rowRenderer={rowRenderer}
|
|
||||||
onScroll={({ scrollTop, scrollHeight, clientHeight }) => {
|
|
||||||
if (scrollTop + clientHeight === scrollHeight) {
|
|
||||||
loadMoreConversations();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AutoSizer>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.chat-list-container {
|
.chat-list-container {
|
||||||
overflow: hidden;
|
height: 100%; /* Ensure it takes up the full available height */
|
||||||
height: 100%;
|
|
||||||
border: 1px solid gainsboro;
|
border: 1px solid gainsboro;
|
||||||
|
overflow: auto; /* Allow scrolling for the Virtuoso component */
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-list-item {
|
.chat-list-item {
|
||||||
@@ -14,3 +14,24 @@
|
|||||||
color: #ff7a00;
|
color: #ff7a00;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Virtuoso item container adjustments */
|
||||||
|
.chat-list-container > div {
|
||||||
|
height: 100%; /* Ensure Virtuoso takes full height */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add spacing and better alignment for items */
|
||||||
|
.chat-list-item {
|
||||||
|
padding: 0.5rem 0; /* Add spacing between list items */
|
||||||
|
|
||||||
|
.ant-card {
|
||||||
|
border-radius: 8px; /* Slight rounding for card edges */
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); /* Subtle shadow for better definition */
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .ant-card {
|
||||||
|
border-color: #ff7a00; /* Highlight border on hover */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,18 +1,29 @@
|
|||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Tag } from "antd";
|
import { Tag } from "antd";
|
||||||
import React from "react";
|
import React, { useContext } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||||
|
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
export default function ChatConversationTitleTags({ jobConversations }) {
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
|
export function ChatConversationTitleTags({ jobConversations, bodyshop }) {
|
||||||
const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG);
|
const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG);
|
||||||
|
const { socket } = useContext(SocketContext);
|
||||||
|
|
||||||
const handleRemoveTag = (jobId) => {
|
const handleRemoveTag = async (jobId) => {
|
||||||
const convId = jobConversations[0].conversationid;
|
const convId = jobConversations[0].conversationid;
|
||||||
if (!!convId) {
|
if (!!convId) {
|
||||||
removeJobConversation({
|
await removeJobConversation({
|
||||||
variables: {
|
variables: {
|
||||||
conversationId: convId,
|
conversationId: convId,
|
||||||
jobId: jobId
|
jobId: jobId
|
||||||
@@ -28,6 +39,17 @@ export default function ChatConversationTitleTags({ jobConversations }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (socket) {
|
||||||
|
// Emit the `conversation-modified` event
|
||||||
|
socket.emit("conversation-modified", {
|
||||||
|
bodyshopId: bodyshop.id,
|
||||||
|
conversationId: convId,
|
||||||
|
type: "tag-removed",
|
||||||
|
jobId: jobId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
logImEXEvent("messaging_remove_job_tag", {
|
logImEXEvent("messaging_remove_job_tag", {
|
||||||
conversationId: convId,
|
conversationId: convId,
|
||||||
jobId: jobId
|
jobId: jobId
|
||||||
@@ -54,3 +76,5 @@ export default function ChatConversationTitleTags({ jobConversations }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ChatConversationTitleTags);
|
||||||
|
|||||||
@@ -6,10 +6,16 @@ import ChatConversationTitleTags from "../chat-conversation-title-tags/chat-conv
|
|||||||
import ChatLabelComponent from "../chat-label/chat-label.component";
|
import ChatLabelComponent from "../chat-label/chat-label.component";
|
||||||
import ChatPrintButton from "../chat-print-button/chat-print-button.component";
|
import ChatPrintButton from "../chat-print-button/chat-print-button.component";
|
||||||
import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container";
|
import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
export default function ChatConversationTitle({ conversation }) {
|
const mapStateToProps = createStructuredSelector({});
|
||||||
|
|
||||||
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
|
export function ChatConversationTitle({ conversation }) {
|
||||||
return (
|
return (
|
||||||
<Space wrap>
|
<Space className="chat-title" wrap>
|
||||||
<PhoneNumberFormatter>{conversation && conversation.phone_num}</PhoneNumberFormatter>
|
<PhoneNumberFormatter>{conversation && conversation.phone_num}</PhoneNumberFormatter>
|
||||||
<ChatLabelComponent conversation={conversation} />
|
<ChatLabelComponent conversation={conversation} />
|
||||||
<ChatPrintButton conversation={conversation} />
|
<ChatPrintButton conversation={conversation} />
|
||||||
@@ -19,3 +25,5 @@ export default function ChatConversationTitle({ conversation }) {
|
|||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ChatConversationTitle);
|
||||||
|
|||||||
@@ -5,10 +5,26 @@ import ChatMessageListComponent from "../chat-messages-list/chat-message-list.co
|
|||||||
import ChatSendMessage from "../chat-send-message/chat-send-message.component";
|
import ChatSendMessage from "../chat-send-message/chat-send-message.component";
|
||||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component.jsx";
|
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component.jsx";
|
||||||
import "./chat-conversation.styles.scss";
|
import "./chat-conversation.styles.scss";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
export default function ChatConversationComponent({ subState, conversation, messages, handleMarkConversationAsRead }) {
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
|
export function ChatConversationComponent({
|
||||||
|
subState,
|
||||||
|
conversation,
|
||||||
|
messages,
|
||||||
|
handleMarkConversationAsRead,
|
||||||
|
bodyshop
|
||||||
|
}) {
|
||||||
const [loading, error] = subState;
|
const [loading, error] = subState;
|
||||||
|
|
||||||
|
if (conversation?.archived) return null;
|
||||||
if (loading) return <LoadingSkeleton />;
|
if (loading) return <LoadingSkeleton />;
|
||||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||||
|
|
||||||
@@ -18,9 +34,11 @@ export default function ChatConversationComponent({ subState, conversation, mess
|
|||||||
onMouseDown={handleMarkConversationAsRead}
|
onMouseDown={handleMarkConversationAsRead}
|
||||||
onKeyDown={handleMarkConversationAsRead}
|
onKeyDown={handleMarkConversationAsRead}
|
||||||
>
|
>
|
||||||
<ChatConversationTitle conversation={conversation} />
|
<ChatConversationTitle conversation={conversation} bodyshop={bodyshop} />
|
||||||
<ChatMessageListComponent messages={messages} />
|
<ChatMessageListComponent messages={messages} />
|
||||||
<ChatSendMessage conversation={conversation} />
|
<ChatSendMessage conversation={conversation} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ChatConversationComponent);
|
||||||
|
|||||||
@@ -1,22 +1,25 @@
|
|||||||
import { useMutation, useQuery, useSubscription } from "@apollo/client";
|
import { gql, useApolloClient, useQuery, useSubscription } from "@apollo/client";
|
||||||
import React, { useState } from "react";
|
import axios from "axios";
|
||||||
|
import React, { useCallback, useContext, useEffect, useState } from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { CONVERSATION_SUBSCRIPTION_BY_PK, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries";
|
import SocketContext from "../../contexts/SocketIO/socketContext";
|
||||||
import { MARK_MESSAGES_AS_READ_BY_CONVERSATION } from "../../graphql/messages.queries";
|
import { GET_CONVERSATION_DETAILS, CONVERSATION_SUBSCRIPTION_BY_PK } from "../../graphql/conversations.queries";
|
||||||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||||||
import ChatConversationComponent from "./chat-conversation.component";
|
|
||||||
import axios from "axios";
|
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import ChatConversationComponent from "./chat-conversation.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
selectedConversation: selectSelectedConversation,
|
selectedConversation: selectSelectedConversation,
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, null)(ChatConversationContainer);
|
function ChatConversationContainer({ bodyshop, selectedConversation }) {
|
||||||
|
const client = useApolloClient();
|
||||||
|
const { socket } = useContext(SocketContext);
|
||||||
|
const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false);
|
||||||
|
|
||||||
export function ChatConversationContainer({ bodyshop, selectedConversation }) {
|
// Fetch conversation details
|
||||||
const {
|
const {
|
||||||
loading: convoLoading,
|
loading: convoLoading,
|
||||||
error: convoError,
|
error: convoError,
|
||||||
@@ -27,55 +30,145 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) {
|
|||||||
nextFetchPolicy: "network-only"
|
nextFetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
|
|
||||||
const { loading, error, data } = useSubscription(CONVERSATION_SUBSCRIPTION_BY_PK, {
|
// Subscription for conversation updates
|
||||||
variables: { conversationId: selectedConversation }
|
useSubscription(CONVERSATION_SUBSCRIPTION_BY_PK, {
|
||||||
});
|
skip: socket?.connected,
|
||||||
|
|
||||||
const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false);
|
|
||||||
|
|
||||||
const [markConversationRead] = useMutation(MARK_MESSAGES_AS_READ_BY_CONVERSATION, {
|
|
||||||
variables: { conversationId: selectedConversation },
|
variables: { conversationId: selectedConversation },
|
||||||
refetchQueries: ["UNREAD_CONVERSATION_COUNT"],
|
onData: ({ data: subscriptionResult, client }) => {
|
||||||
update(cache) {
|
// Extract the messages array from the result
|
||||||
cache.modify({
|
const messages = subscriptionResult?.data?.messages;
|
||||||
id: cache.identify({
|
if (!messages || messages.length === 0) {
|
||||||
__typename: "conversations",
|
console.warn("No messages found in subscription result.");
|
||||||
id: selectedConversation
|
return;
|
||||||
}),
|
}
|
||||||
fields: {
|
|
||||||
messages_aggregate(cached) {
|
messages.forEach((message) => {
|
||||||
return { aggregate: { count: 0 } };
|
const messageRef = client.cache.identify(message);
|
||||||
|
// Write the new message to the cache
|
||||||
|
client.cache.writeFragment({
|
||||||
|
id: messageRef,
|
||||||
|
fragment: gql`
|
||||||
|
fragment NewMessage on messages {
|
||||||
|
id
|
||||||
|
status
|
||||||
|
text
|
||||||
|
isoutbound
|
||||||
|
image
|
||||||
|
image_path
|
||||||
|
userid
|
||||||
|
created_at
|
||||||
|
read
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
data: message
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the conversation cache to include the new message
|
||||||
|
client.cache.modify({
|
||||||
|
id: client.cache.identify({ __typename: "conversations", id: selectedConversation }),
|
||||||
|
fields: {
|
||||||
|
messages(existingMessages = []) {
|
||||||
|
const alreadyExists = existingMessages.some((msg) => msg.__ref === messageRef);
|
||||||
|
if (alreadyExists) return existingMessages;
|
||||||
|
return [...existingMessages, { __ref: messageRef }];
|
||||||
|
},
|
||||||
|
updated_at() {
|
||||||
|
return message.created_at;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const unreadCount =
|
const updateCacheWithReadMessages = useCallback(
|
||||||
data &&
|
(conversationId, messageIds) => {
|
||||||
data.messages &&
|
if (!conversationId || !messageIds?.length) return;
|
||||||
data.messages.reduce((acc, val) => {
|
|
||||||
return !val.read && !val.isoutbound ? acc + 1 : acc;
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
const handleMarkConversationAsRead = async () => {
|
messageIds.forEach((messageId) => {
|
||||||
if (unreadCount > 0 && !!selectedConversation && !markingAsReadInProgress) {
|
client.cache.modify({
|
||||||
setMarkingAsReadInProgress(true);
|
id: client.cache.identify({ __typename: "messages", id: messageId }),
|
||||||
await markConversationRead({});
|
fields: {
|
||||||
await axios.post("/sms/markConversationRead", {
|
read: () => true
|
||||||
conversationid: selectedConversation,
|
}
|
||||||
imexshopid: bodyshop.imexshopid
|
});
|
||||||
});
|
});
|
||||||
setMarkingAsReadInProgress(false);
|
},
|
||||||
|
[client.cache]
|
||||||
|
);
|
||||||
|
|
||||||
|
// WebSocket event handlers
|
||||||
|
useEffect(() => {
|
||||||
|
if (!socket?.connected) return;
|
||||||
|
|
||||||
|
const handleConversationChange = (data) => {
|
||||||
|
if (data.type === "conversation-marked-read") {
|
||||||
|
const { conversationId, messageIds } = data;
|
||||||
|
updateCacheWithReadMessages(conversationId, messageIds);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.on("conversation-changed", handleConversationChange);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.off("conversation-changed", handleConversationChange);
|
||||||
|
};
|
||||||
|
}, [socket, updateCacheWithReadMessages]);
|
||||||
|
|
||||||
|
// Join and leave conversation via WebSocket
|
||||||
|
useEffect(() => {
|
||||||
|
if (!socket?.connected || !selectedConversation || !bodyshop?.id) return;
|
||||||
|
|
||||||
|
socket.emit("join-bodyshop-conversation", {
|
||||||
|
bodyshopId: bodyshop.id,
|
||||||
|
conversationId: selectedConversation
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.emit("leave-bodyshop-conversation", {
|
||||||
|
bodyshopId: bodyshop.id,
|
||||||
|
conversationId: selectedConversation
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, [socket, bodyshop, selectedConversation]);
|
||||||
|
|
||||||
|
// Mark conversation as read
|
||||||
|
const handleMarkConversationAsRead = async () => {
|
||||||
|
if (!convoData || markingAsReadInProgress) return;
|
||||||
|
|
||||||
|
const conversation = convoData.conversations_by_pk;
|
||||||
|
if (!conversation) return;
|
||||||
|
|
||||||
|
const unreadMessageIds = conversation.messages
|
||||||
|
?.filter((message) => !message.read && !message.isoutbound)
|
||||||
|
.map((message) => message.id);
|
||||||
|
|
||||||
|
if (unreadMessageIds?.length > 0) {
|
||||||
|
setMarkingAsReadInProgress(true);
|
||||||
|
try {
|
||||||
|
await axios.post("/sms/markConversationRead", {
|
||||||
|
conversation,
|
||||||
|
imexshopid: bodyshop?.imexshopid,
|
||||||
|
bodyshopid: bodyshop?.id
|
||||||
|
});
|
||||||
|
|
||||||
|
updateCacheWithReadMessages(selectedConversation, unreadMessageIds);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error marking conversation as read:", error.message);
|
||||||
|
} finally {
|
||||||
|
setMarkingAsReadInProgress(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChatConversationComponent
|
<ChatConversationComponent
|
||||||
subState={[loading || convoLoading, error || convoError]}
|
subState={[convoLoading, convoError]}
|
||||||
conversation={convoData ? convoData.conversations_by_pk : {}}
|
conversation={convoData?.conversations_by_pk || {}}
|
||||||
messages={data ? data.messages : []}
|
messages={convoData?.conversations_by_pk?.messages || []}
|
||||||
handleMarkConversationAsRead={handleMarkConversationAsRead}
|
handleMarkConversationAsRead={handleMarkConversationAsRead}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(ChatConversationContainer);
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
import { PlusOutlined } from "@ant-design/icons";
|
import { PlusOutlined } from "@ant-design/icons";
|
||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Input, notification, Spin, Tag, Tooltip } from "antd";
|
import { Input, notification, Spin, Tag, Tooltip } from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useContext, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { UPDATE_CONVERSATION_LABEL } from "../../graphql/conversations.queries";
|
import { UPDATE_CONVERSATION_LABEL } from "../../graphql/conversations.queries";
|
||||||
|
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
export default function ChatLabel({ conversation }) {
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({});
|
||||||
|
|
||||||
|
export function ChatLabel({ conversation, bodyshop }) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [value, setValue] = useState(conversation.label);
|
const [value, setValue] = useState(conversation.label);
|
||||||
|
const { socket } = useContext(SocketContext);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [updateLabel] = useMutation(UPDATE_CONVERSATION_LABEL);
|
const [updateLabel] = useMutation(UPDATE_CONVERSATION_LABEL);
|
||||||
@@ -26,6 +37,14 @@ export default function ChatLabel({ conversation }) {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
if (socket) {
|
||||||
|
socket.emit("conversation-modified", {
|
||||||
|
type: "label-updated",
|
||||||
|
conversationId: conversation.id,
|
||||||
|
bodyshopId: bodyshop.id,
|
||||||
|
label: value
|
||||||
|
});
|
||||||
|
}
|
||||||
setEditing(false);
|
setEditing(false);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -57,3 +76,5 @@ export default function ChatLabel({ conversation }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ChatLabel);
|
||||||
|
|||||||
@@ -1,106 +1,87 @@
|
|||||||
import Icon from "@ant-design/icons";
|
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Tooltip } from "antd";
|
import { Virtuoso } from "react-virtuoso";
|
||||||
import i18n from "i18next";
|
import { renderMessage } from "./renderMessage";
|
||||||
import dayjs from "../../utils/day";
|
|
||||||
import React, { useEffect, useRef } from "react";
|
|
||||||
import { MdDone, MdDoneAll } from "react-icons/md";
|
|
||||||
import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from "react-virtualized";
|
|
||||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
|
||||||
import "./chat-message-list.styles.scss";
|
import "./chat-message-list.styles.scss";
|
||||||
|
|
||||||
export default function ChatMessageListComponent({ messages }) {
|
export default function ChatMessageListComponent({ messages }) {
|
||||||
const virtualizedListRef = useRef(null);
|
const virtuosoRef = useRef(null);
|
||||||
|
const [atBottom, setAtBottom] = useState(true);
|
||||||
|
const loadedImagesRef = useRef(0);
|
||||||
|
|
||||||
const _cache = new CellMeasurerCache({
|
const handleScrollStateChange = (isAtBottom) => {
|
||||||
fixedWidth: true,
|
setAtBottom(isAtBottom);
|
||||||
// minHeight: 50,
|
|
||||||
defaultHeight: 100
|
|
||||||
});
|
|
||||||
|
|
||||||
const scrollToBottom = (renderedrows) => {
|
|
||||||
//console.log("Scrolling to", messages.length);
|
|
||||||
// !!virtualizedListRef.current &&
|
|
||||||
// virtualizedListRef.current.scrollToRow(messages.length);
|
|
||||||
// Outstanding isue on virtualization: https://github.com/bvaughn/react-virtualized/issues/1179
|
|
||||||
//Scrolling does not work on this version of React.
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(scrollToBottom, [messages]);
|
const resetImageLoadState = () => {
|
||||||
|
loadedImagesRef.current = 0;
|
||||||
const _rowRenderer = ({ index, key, parent, style }) => {
|
|
||||||
return (
|
|
||||||
<CellMeasurer cache={_cache} key={key} rowIndex={index} parent={parent}>
|
|
||||||
{({ measure, registerChild }) => (
|
|
||||||
<div
|
|
||||||
ref={registerChild}
|
|
||||||
onLoad={measure}
|
|
||||||
style={style}
|
|
||||||
className={`${messages[index].isoutbound ? "mine messages" : "yours messages"}`}
|
|
||||||
>
|
|
||||||
<div className="message msgmargin">
|
|
||||||
{MessageRender(messages[index])}
|
|
||||||
{StatusRender(messages[index].status)}
|
|
||||||
</div>
|
|
||||||
{messages[index].isoutbound && (
|
|
||||||
<div style={{ fontSize: 10 }}>
|
|
||||||
{i18n.t("messaging.labels.sentby", {
|
|
||||||
by: messages[index].userid,
|
|
||||||
time: dayjs(messages[index].created_at).format("MM/DD/YYYY @ hh:mm a")
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CellMeasurer>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const preloadImages = useCallback((imagePaths, onComplete) => {
|
||||||
|
resetImageLoadState();
|
||||||
|
|
||||||
|
if (imagePaths.length === 0) {
|
||||||
|
onComplete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
imagePaths.forEach((url) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.src = url;
|
||||||
|
img.onload = img.onerror = () => {
|
||||||
|
loadedImagesRef.current += 1;
|
||||||
|
if (loadedImagesRef.current === imagePaths.length) {
|
||||||
|
onComplete();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Ensure all images are loaded on initial render
|
||||||
|
useEffect(() => {
|
||||||
|
const imagePaths = messages
|
||||||
|
.filter((message) => message.image && message.image_path?.length > 0)
|
||||||
|
.flatMap((message) => message.image_path);
|
||||||
|
|
||||||
|
preloadImages(imagePaths, () => {
|
||||||
|
if (virtuosoRef.current) {
|
||||||
|
virtuosoRef.current.scrollToIndex({
|
||||||
|
index: messages.length - 1,
|
||||||
|
align: "end",
|
||||||
|
behavior: "auto"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [messages, preloadImages]);
|
||||||
|
|
||||||
|
// Handle scrolling when new messages are added
|
||||||
|
useEffect(() => {
|
||||||
|
if (!atBottom) return;
|
||||||
|
|
||||||
|
const latestMessage = messages[messages.length - 1];
|
||||||
|
const imagePaths = latestMessage?.image_path || [];
|
||||||
|
|
||||||
|
preloadImages(imagePaths, () => {
|
||||||
|
if (virtuosoRef.current) {
|
||||||
|
virtuosoRef.current.scrollToIndex({
|
||||||
|
index: messages.length - 1,
|
||||||
|
align: "end",
|
||||||
|
behavior: "smooth"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [messages, atBottom, preloadImages]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat">
|
<div className="chat">
|
||||||
<AutoSizer>
|
<Virtuoso
|
||||||
{({ height, width }) => (
|
ref={virtuosoRef}
|
||||||
<List
|
data={messages}
|
||||||
ref={virtualizedListRef}
|
overscan={!!messages.reduce((acc, message) => acc + (message.image_path?.length || 0), 0) ? messages.length : 0}
|
||||||
width={width}
|
itemContent={(index) => renderMessage(messages, index)}
|
||||||
height={height}
|
followOutput={(isAtBottom) => handleScrollStateChange(isAtBottom)}
|
||||||
rowHeight={_cache.rowHeight}
|
initialTopMostItemIndex={messages.length - 1}
|
||||||
rowRenderer={_rowRenderer}
|
style={{ height: "100%", width: "100%" }}
|
||||||
rowCount={messages.length}
|
/>
|
||||||
overscanRowCount={10}
|
|
||||||
estimatedRowSize={150}
|
|
||||||
scrollToIndex={messages.length}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AutoSizer>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageRender = (message) => {
|
|
||||||
return (
|
|
||||||
<Tooltip title={DateTimeFormatter({ children: message.created_at })}>
|
|
||||||
<div>
|
|
||||||
{message.image_path &&
|
|
||||||
message.image_path.map((i, idx) => (
|
|
||||||
<div key={idx} style={{ display: "flex", justifyContent: "center" }}>
|
|
||||||
<a href={i} target="__blank">
|
|
||||||
<img alt="Received" className="message-img" src={i} />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div>{message.text}</div>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const StatusRender = (status) => {
|
|
||||||
switch (status) {
|
|
||||||
case "sent":
|
|
||||||
return <Icon component={MdDone} className="message-icon" />;
|
|
||||||
case "delivered":
|
|
||||||
return <Icon component={MdDoneAll} className="message-icon" />;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,119 +1,131 @@
|
|||||||
.message-icon {
|
|
||||||
//position: absolute;
|
|
||||||
// bottom: 0rem;
|
|
||||||
color: whitesmoke;
|
|
||||||
border: #000000;
|
|
||||||
position: absolute;
|
|
||||||
margin: 0 0.1rem;
|
|
||||||
bottom: 0.1rem;
|
|
||||||
right: 0.3rem;
|
|
||||||
|
|
||||||
z-index: 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat {
|
.chat {
|
||||||
flex: 1;
|
|
||||||
//width: 300px;
|
|
||||||
//border: solid 1px #eee;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin: 0.8rem 0rem;
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.archive-button {
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.chat-title {
|
||||||
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messages {
|
.messages {
|
||||||
//margin-top: 30px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
padding: 0.5rem; // Prevent edge clipping
|
||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
|
position: relative;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
padding: 0.25rem 0.8rem;
|
padding: 0.25rem 0.8rem;
|
||||||
//margin-top: 5px;
|
word-wrap: break-word;
|
||||||
// margin-bottom: 5px;
|
|
||||||
//display: inline-block;
|
|
||||||
|
|
||||||
.message-img {
|
&-img {
|
||||||
max-width: 10rem;
|
max-width: 10rem;
|
||||||
max-height: 10rem;
|
max-height: 10rem;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
margin: 0.2rem;
|
margin: 0.2rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-images {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.chat-send-message-button{
|
||||||
.yours {
|
margin: 0.3rem;
|
||||||
align-items: flex-start;
|
padding-left: 0.5rem;
|
||||||
|
|
||||||
|
}
|
||||||
|
.message-icon {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0.1rem;
|
||||||
|
right: 0.3rem;
|
||||||
|
margin: 0 0.1rem;
|
||||||
|
color: whitesmoke;
|
||||||
|
z-index: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.msgmargin {
|
.msgmargin {
|
||||||
margin-top: 0.1rem;
|
margin: 0.1rem 0;
|
||||||
margin-bottom: 0.1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.yours .message {
|
.yours,
|
||||||
margin-right: 20%;
|
.mine {
|
||||||
background-color: #eee;
|
display: flex;
|
||||||
position: relative;
|
flex-direction: column;
|
||||||
|
|
||||||
|
.message {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:last-child:before,
|
||||||
|
&:last-child:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child:after {
|
||||||
|
width: 10px;
|
||||||
|
background: white;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.yours .message.last:before {
|
/* "Yours" (incoming) message styles */
|
||||||
content: "";
|
.yours {
|
||||||
position: absolute;
|
align-items: flex-start;
|
||||||
z-index: 0;
|
|
||||||
bottom: 0;
|
.message {
|
||||||
left: -7px;
|
margin-right: 20%;
|
||||||
height: 20px;
|
background-color: #eee;
|
||||||
width: 20px;
|
|
||||||
background: #eee;
|
&:last-child:before {
|
||||||
border-bottom-right-radius: 15px;
|
left: -7px;
|
||||||
}
|
background: #eee;
|
||||||
|
border-bottom-right-radius: 15px;
|
||||||
.yours .message.last:after {
|
}
|
||||||
content: "";
|
|
||||||
position: absolute;
|
&:last-child:after {
|
||||||
z-index: 1;
|
left: -10px;
|
||||||
bottom: 0;
|
border-bottom-right-radius: 10px;
|
||||||
left: -10px;
|
}
|
||||||
width: 10px;
|
}
|
||||||
height: 20px;
|
|
||||||
background: white;
|
|
||||||
border-bottom-right-radius: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* "Mine" (outgoing) message styles */
|
||||||
.mine {
|
.mine {
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
|
|
||||||
|
.message {
|
||||||
|
color: white;
|
||||||
|
margin-left: 25%;
|
||||||
|
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
|
||||||
|
padding-bottom: 0.6rem;
|
||||||
|
|
||||||
|
&:last-child:before {
|
||||||
|
right: -8px;
|
||||||
|
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
|
||||||
|
border-bottom-left-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child:after {
|
||||||
|
right: -10px;
|
||||||
|
border-bottom-left-radius: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mine .message {
|
.virtuoso-container {
|
||||||
color: white;
|
flex: 1;
|
||||||
margin-left: 25%;
|
overflow: auto;
|
||||||
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
|
|
||||||
background-attachment: fixed;
|
|
||||||
position: relative;
|
|
||||||
padding-bottom: 0.6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mine .message.last:before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
z-index: 0;
|
|
||||||
bottom: 0;
|
|
||||||
right: -8px;
|
|
||||||
height: 20px;
|
|
||||||
width: 20px;
|
|
||||||
background: linear-gradient(to bottom, #00d0ea 0%, #0085d1 100%);
|
|
||||||
background-attachment: fixed;
|
|
||||||
border-bottom-left-radius: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mine .message.last:after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
bottom: 0;
|
|
||||||
right: -10px;
|
|
||||||
width: 10px;
|
|
||||||
height: 20px;
|
|
||||||
background: white;
|
|
||||||
border-bottom-left-radius: 10px;
|
|
||||||
}
|
}
|
||||||
|
|||||||
52
client/src/components/chat-messages-list/renderMessage.jsx
Normal file
52
client/src/components/chat-messages-list/renderMessage.jsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import Icon from "@ant-design/icons";
|
||||||
|
import { Tooltip } from "antd";
|
||||||
|
import i18n from "i18next";
|
||||||
|
import dayjs from "../../utils/day";
|
||||||
|
import { MdDone, MdDoneAll } from "react-icons/md";
|
||||||
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
|
|
||||||
|
export const renderMessage = (messages, index) => {
|
||||||
|
const message = messages[index];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={index} className={`${message.isoutbound ? "mine messages" : "yours messages"}`}>
|
||||||
|
<div className="message msgmargin">
|
||||||
|
<Tooltip title={DateTimeFormatter({ children: message.created_at })}>
|
||||||
|
<div>
|
||||||
|
{/* Render images if available */}
|
||||||
|
{message.image && message.image_path?.length > 0 && (
|
||||||
|
<div className="message-images">
|
||||||
|
{message.image_path.map((url, idx) => (
|
||||||
|
<div key={idx} style={{ display: "flex", justifyContent: "center" }}>
|
||||||
|
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||||
|
<img alt="Received" className="message-img" src={url} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Render text if available */}
|
||||||
|
{message.text && <div>{message.text}</div>}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{/* Message status icons */}
|
||||||
|
{message.status && (message.status === "sent" || message.status === "delivered") && (
|
||||||
|
<div className="message-status">
|
||||||
|
<Icon component={message.status === "sent" ? MdDone : MdDoneAll} className="message-icon" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Outbound message metadata */}
|
||||||
|
{message.isoutbound && (
|
||||||
|
<div style={{ fontSize: 10 }}>
|
||||||
|
{i18n.t("messaging.labels.sentby", {
|
||||||
|
by: message.userid,
|
||||||
|
time: dayjs(message.created_at).format("MM/DD/YYYY @ hh:mm a")
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { PlusCircleFilled } from "@ant-design/icons";
|
import { PlusCircleFilled } from "@ant-design/icons";
|
||||||
import { Button, Form, Popover } from "antd";
|
import { Button, Form, Popover } from "antd";
|
||||||
import React from "react";
|
import React, { useContext } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
||||||
import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
|
import PhoneFormItem, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
|
||||||
|
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
@@ -17,8 +18,10 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
export function ChatNewConversation({ openChatByPhone }) {
|
export function ChatNewConversation({ openChatByPhone }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
const { socket } = useContext(SocketContext);
|
||||||
|
|
||||||
const handleFinish = (values) => {
|
const handleFinish = (values) => {
|
||||||
openChatByPhone({ phone_num: values.phoneNumber });
|
openChatByPhone({ phone_num: values.phoneNumber, socket });
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { notification } from "antd";
|
import { notification } from "antd";
|
||||||
import parsePhoneNumber from "libphonenumber-js";
|
import parsePhoneNumber from "libphonenumber-js";
|
||||||
import React from "react";
|
import React, { useContext } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
import { openChatByPhone } from "../../redux/messaging/messaging.actions";
|
||||||
@@ -9,6 +9,7 @@ import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
|||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { searchingForConversation } from "../../redux/messaging/messaging.selectors";
|
import { searchingForConversation } from "../../redux/messaging/messaging.selectors";
|
||||||
|
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -21,6 +22,8 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
|
|
||||||
export function ChatOpenButton({ bodyshop, searchingForConversation, phone, jobid, openChatByPhone }) {
|
export function ChatOpenButton({ bodyshop, searchingForConversation, phone, jobid, openChatByPhone }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { socket } = useContext(SocketContext);
|
||||||
|
|
||||||
if (!phone) return <></>;
|
if (!phone) return <></>;
|
||||||
|
|
||||||
if (!bodyshop.messagingservicesid) return <PhoneNumberFormatter>{phone}</PhoneNumberFormatter>;
|
if (!bodyshop.messagingservicesid) return <PhoneNumberFormatter>{phone}</PhoneNumberFormatter>;
|
||||||
@@ -33,7 +36,7 @@ export function ChatOpenButton({ bodyshop, searchingForConversation, phone, jobi
|
|||||||
const p = parsePhoneNumber(phone, "CA");
|
const p = parsePhoneNumber(phone, "CA");
|
||||||
if (searchingForConversation) return; //This is to prevent finding the same thing twice.
|
if (searchingForConversation) return; //This is to prevent finding the same thing twice.
|
||||||
if (p && p.isValid()) {
|
if (p && p.isValid()) {
|
||||||
openChatByPhone({ phone_num: p.formatInternational(), jobid: jobid });
|
openChatByPhone({ phone_num: p.formatInternational(), jobid: jobid, socket });
|
||||||
} else {
|
} else {
|
||||||
notification["error"]({ message: t("messaging.error.invalidphone") });
|
notification["error"]({ message: t("messaging.error.invalidphone") });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { InfoCircleOutlined, MessageOutlined, ShrinkOutlined, SyncOutlined } from "@ant-design/icons";
|
import { InfoCircleOutlined, MessageOutlined, ShrinkOutlined, SyncOutlined } from "@ant-design/icons";
|
||||||
import { useLazyQuery, useQuery } from "@apollo/client";
|
import { useApolloClient, useLazyQuery, useQuery } from "@apollo/client";
|
||||||
import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd";
|
import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd";
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -13,61 +13,102 @@ import ChatConversationContainer from "../chat-conversation/chat-conversation.co
|
|||||||
import ChatNewConversation from "../chat-new-conversation/chat-new-conversation.component";
|
import ChatNewConversation from "../chat-new-conversation/chat-new-conversation.component";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
import "./chat-popup.styles.scss";
|
import "./chat-popup.styles.scss";
|
||||||
|
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
selectedConversation: selectSelectedConversation,
|
selectedConversation: selectSelectedConversation,
|
||||||
chatVisible: selectChatVisible
|
chatVisible: selectChatVisible
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
toggleChatVisible: () => dispatch(toggleChatVisible())
|
toggleChatVisible: () => dispatch(toggleChatVisible())
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ChatPopupComponent({ chatVisible, selectedConversation, toggleChatVisible }) {
|
export function ChatPopupComponent({ chatVisible, selectedConversation, toggleChatVisible }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [pollInterval, setpollInterval] = useState(0);
|
const [pollInterval, setPollInterval] = useState(0);
|
||||||
|
const { socket } = useContext(SocketContext);
|
||||||
|
const client = useApolloClient(); // Apollo Client instance for cache operations
|
||||||
|
|
||||||
const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, {
|
// Lazy query for conversations
|
||||||
fetchPolicy: "network-only",
|
const [getConversations, { loading, data, refetch }] = useLazyQuery(CONVERSATION_LIST_QUERY, {
|
||||||
nextFetchPolicy: "network-only",
|
|
||||||
...(pollInterval > 0 ? { pollInterval } : {})
|
|
||||||
});
|
|
||||||
|
|
||||||
const [getConversations, { loading, data, refetch, fetchMore }] = useLazyQuery(CONVERSATION_LIST_QUERY, {
|
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
skip: !chatVisible,
|
skip: !chatVisible,
|
||||||
...(pollInterval > 0 ? { pollInterval } : {})
|
...(pollInterval > 0 ? { pollInterval } : {})
|
||||||
});
|
});
|
||||||
|
|
||||||
const fcmToken = sessionStorage.getItem("fcmtoken");
|
// Query for unread count when chat is not visible
|
||||||
|
const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, {
|
||||||
|
fetchPolicy: "network-only",
|
||||||
|
nextFetchPolicy: "network-only",
|
||||||
|
skip: chatVisible, // Skip when chat is visible
|
||||||
|
...(pollInterval > 0 ? { pollInterval } : {})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Socket connection status
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (fcmToken) {
|
const handleSocketStatus = () => {
|
||||||
setpollInterval(0);
|
if (socket?.connected) {
|
||||||
} else {
|
setPollInterval(15 * 60 * 1000); // 15 minutes
|
||||||
setpollInterval(60000);
|
} else {
|
||||||
}
|
setPollInterval(60 * 1000); // 60 seconds
|
||||||
}, [fcmToken]);
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSocketStatus();
|
||||||
|
|
||||||
|
if (socket) {
|
||||||
|
socket.on("connect", handleSocketStatus);
|
||||||
|
socket.on("disconnect", handleSocketStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (socket) {
|
||||||
|
socket.off("connect", handleSocketStatus);
|
||||||
|
socket.off("disconnect", handleSocketStatus);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [socket]);
|
||||||
|
|
||||||
|
// Fetch conversations when chat becomes visible
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chatVisible)
|
if (chatVisible)
|
||||||
getConversations({
|
getConversations({
|
||||||
variables: {
|
variables: {
|
||||||
offset: 0
|
offset: 0
|
||||||
}
|
}
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error(`Error fetching conversations: ${(err, err.message || "")}`);
|
||||||
});
|
});
|
||||||
}, [chatVisible, getConversations]);
|
}, [chatVisible, getConversations]);
|
||||||
|
|
||||||
const loadMoreConversations = useCallback(() => {
|
// Get unread count from the cache
|
||||||
if (data)
|
const unreadCount = (() => {
|
||||||
fetchMore({
|
if (chatVisible) {
|
||||||
variables: {
|
try {
|
||||||
offset: data.conversations.length
|
const cachedData = client.readQuery({
|
||||||
}
|
query: CONVERSATION_LIST_QUERY,
|
||||||
});
|
variables: { offset: 0 }
|
||||||
}, [data, fetchMore]);
|
});
|
||||||
|
|
||||||
const unreadCount = unreadData?.messages_aggregate.aggregate.count || 0;
|
if (!cachedData?.conversations) return 0;
|
||||||
|
|
||||||
|
// Aggregate unread message count
|
||||||
|
return cachedData.conversations.reduce((total, conversation) => {
|
||||||
|
const unread = conversation.messages_aggregate?.aggregate?.count || 0;
|
||||||
|
return total + unread;
|
||||||
|
}, 0);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Unread count not found in cache:", error);
|
||||||
|
return 0; // Fallback if not in cache
|
||||||
|
}
|
||||||
|
} else if (unreadData?.messages_aggregate?.aggregate?.count) {
|
||||||
|
// Use the unread count from the query result
|
||||||
|
return unreadData.messages_aggregate.aggregate.count;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
})();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge count={unreadCount}>
|
<Badge count={unreadCount}>
|
||||||
@@ -81,7 +122,7 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh
|
|||||||
<InfoCircleOutlined />
|
<InfoCircleOutlined />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<SyncOutlined style={{ cursor: "pointer" }} onClick={() => refetch()} />
|
<SyncOutlined style={{ cursor: "pointer" }} onClick={() => refetch()} />
|
||||||
{pollInterval > 0 && <Tag color="yellow">{t("messaging.labels.nopush")}</Tag>}
|
{!socket?.connected && <Tag color="yellow">{t("messaging.labels.nopush")}</Tag>}
|
||||||
</Space>
|
</Space>
|
||||||
<ShrinkOutlined
|
<ShrinkOutlined
|
||||||
onClick={() => toggleChatVisible()}
|
onClick={() => toggleChatVisible()}
|
||||||
@@ -93,10 +134,7 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
) : (
|
) : (
|
||||||
<ChatConversationListComponent
|
<ChatConversationListComponent conversationList={data ? data.conversations : []} />
|
||||||
conversationList={data ? data.conversations : []}
|
|
||||||
loadMoreConversations={loadMoreConversations}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={16}>{selectedConversation ? <ChatConversationContainer /> : null}</Col>
|
<Col span={16}>{selectedConversation ? <ChatConversationContainer /> : null}</Col>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSending, message, setMessage }) {
|
function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSending, message, setMessage }) {
|
||||||
const inputArea = useRef(null);
|
const inputArea = useRef(null);
|
||||||
const [selectedMedia, setSelectedMedia] = useState([]);
|
const [selectedMedia, setSelectedMedia] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
inputArea.current.focus();
|
inputArea.current.focus();
|
||||||
}, [isSending, setMessage]);
|
}, [isSending, setMessage]);
|
||||||
@@ -37,14 +38,15 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
|
|||||||
logImEXEvent("messaging_send_message");
|
logImEXEvent("messaging_send_message");
|
||||||
|
|
||||||
if (selectedImages.length < 11) {
|
if (selectedImages.length < 11) {
|
||||||
sendMessage({
|
const newMessage = {
|
||||||
to: conversation.phone_num,
|
to: conversation.phone_num,
|
||||||
body: message || "",
|
body: message || "",
|
||||||
messagingServiceSid: bodyshop.messagingservicesid,
|
messagingServiceSid: bodyshop.messagingservicesid,
|
||||||
conversationid: conversation.id,
|
conversationid: conversation.id,
|
||||||
selectedMedia: selectedImages,
|
selectedMedia: selectedImages,
|
||||||
imexshopid: bodyshop.imexshopid
|
imexshopid: bodyshop.imexshopid
|
||||||
});
|
};
|
||||||
|
sendMessage(newMessage);
|
||||||
setSelectedMedia(
|
setSelectedMedia(
|
||||||
selectedMedia.map((i) => {
|
selectedMedia.map((i) => {
|
||||||
return { ...i, isSelected: false };
|
return { ...i, isSelected: false };
|
||||||
@@ -79,7 +81,7 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<SendOutlined
|
<SendOutlined
|
||||||
className="imex-flex-row__margin"
|
className="chat-send-message-button"
|
||||||
// disabled={message === "" || !message}
|
// disabled={message === "" || !message}
|
||||||
onClick={handleEnter}
|
onClick={handleEnter}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,22 +2,33 @@ import { PlusOutlined } from "@ant-design/icons";
|
|||||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||||
import { Tag } from "antd";
|
import { Tag } from "antd";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React, { useState } from "react";
|
import React, { useContext, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
||||||
import { SEARCH_FOR_JOBS } from "../../graphql/jobs.queries";
|
import { SEARCH_FOR_JOBS } from "../../graphql/jobs.queries";
|
||||||
import ChatTagRo from "./chat-tag-ro.component";
|
import ChatTagRo from "./chat-tag-ro.component";
|
||||||
|
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
export default function ChatTagRoContainer({ conversation }) {
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
|
export function ChatTagRoContainer({ conversation, bodyshop }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const { socket } = useContext(SocketContext);
|
||||||
|
|
||||||
const [loadRo, { loading, data }] = useLazyQuery(SEARCH_FOR_JOBS);
|
const [loadRo, { loading, data }] = useLazyQuery(SEARCH_FOR_JOBS);
|
||||||
|
|
||||||
const executeSearch = (v) => {
|
const executeSearch = (v) => {
|
||||||
logImEXEvent("messaging_search_job_tag", { searchTerm: v });
|
logImEXEvent("messaging_search_job_tag", { searchTerm: v });
|
||||||
loadRo(v);
|
loadRo(v).catch((e) => console.error("Error in ChatTagRoContainer executeSearch:", e));
|
||||||
};
|
};
|
||||||
|
|
||||||
const debouncedExecuteSearch = _.debounce(executeSearch, 500);
|
const debouncedExecuteSearch = _.debounce(executeSearch, 500);
|
||||||
@@ -30,9 +41,40 @@ export default function ChatTagRoContainer({ conversation }) {
|
|||||||
variables: { conversationId: conversation.id }
|
variables: { conversationId: conversation.id }
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleInsertTag = (value, option) => {
|
const handleInsertTag = async (value, option) => {
|
||||||
logImEXEvent("messaging_add_job_tag");
|
logImEXEvent("messaging_add_job_tag");
|
||||||
insertTag({ variables: { jobId: option.key } });
|
|
||||||
|
await insertTag({
|
||||||
|
variables: { jobId: option.key }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (socket) {
|
||||||
|
// Find the job details from the search data
|
||||||
|
const selectedJob = data?.search_jobs.find((job) => job.id === option.key);
|
||||||
|
if (!selectedJob) return;
|
||||||
|
socket.emit("conversation-modified", {
|
||||||
|
conversationId: conversation.id,
|
||||||
|
bodyshopId: bodyshop.id,
|
||||||
|
type: "tag-added",
|
||||||
|
selectedJob,
|
||||||
|
job_conversations: [
|
||||||
|
{
|
||||||
|
__typename: "job_conversations",
|
||||||
|
jobid: selectedJob.id,
|
||||||
|
conversationid: conversation.id,
|
||||||
|
job: {
|
||||||
|
__typename: "jobs",
|
||||||
|
id: selectedJob.id,
|
||||||
|
ro_number: selectedJob.ro_number,
|
||||||
|
ownr_co_nm: selectedJob.ownr_co_nm,
|
||||||
|
ownr_fn: selectedJob.ownr_fn,
|
||||||
|
ownr_ln: selectedJob.ownr_ln
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -50,9 +92,10 @@ export default function ChatTagRoContainer({ conversation }) {
|
|||||||
handleSearch={handleSearch}
|
handleSearch={handleSearch}
|
||||||
handleInsertTag={handleInsertTag}
|
handleInsertTag={handleInsertTag}
|
||||||
setOpen={setOpen}
|
setOpen={setOpen}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Tag onClick={() => setOpen(true)}>
|
<Tag style={{ cursor: "pointer" }} onClick={() => setOpen(true)}>
|
||||||
<PlusOutlined />
|
<PlusOutlined />
|
||||||
{t("messaging.actions.link")}
|
{t("messaging.actions.link")}
|
||||||
</Tag>
|
</Tag>
|
||||||
@@ -60,3 +103,5 @@ export default function ChatTagRoContainer({ conversation }) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ChatTagRoContainer);
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ export default function ConflictComponent() {
|
|||||||
{t("general.labels.instanceconflictext", {
|
{t("general.labels.instanceconflictext", {
|
||||||
app: InstanceRenderManager({
|
app: InstanceRenderManager({
|
||||||
imex: "$t(titles.imexonline)",
|
imex: "$t(titles.imexonline)",
|
||||||
rome: "$t(titles.romeonline)",
|
rome: "$t(titles.romeonline)"
|
||||||
promanager: "$t(titles.promanager)"
|
|
||||||
})
|
})
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user