Compare commits
554 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1cda41f56 | ||
|
|
97282740f5 | ||
|
|
150ae02978 | ||
|
|
abeffb2a19 | ||
|
|
70058b6bd4 | ||
|
|
67c63f81d9 | ||
|
|
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 | ||
|
|
4a16df36dd | ||
|
|
93c92e8976 | ||
|
|
9523e5534d | ||
|
|
d20048026c | ||
|
|
7ba3ed2b89 | ||
|
|
2cc0b7d741 | ||
|
|
97693fbcff | ||
|
|
5e94b1a71e | ||
|
|
970275e62a | ||
|
|
15c7b6f1c8 | ||
|
|
aa076da255 | ||
|
|
3dd154de79 | ||
|
|
72a2366abe | ||
|
|
36f517e8e1 | ||
|
|
981fb57d36 | ||
|
|
3149c3cfc6 | ||
|
|
a059c2b5a8 | ||
|
|
f91e0ec39f | ||
|
|
c31d4096c6 | ||
|
|
118f14ed4c | ||
|
|
207bb39672 | ||
|
|
0d0791cc41 | ||
|
|
16e34e4ed9 | ||
|
|
cd04f2b2b2 | ||
|
|
26e164b4d1 | ||
|
|
f45aa69917 | ||
|
|
4b44ff29ee | ||
|
|
f15f5d7309 | ||
|
|
ae6b2fe4f5 | ||
|
|
4ca686126a | ||
|
|
ea55b1a797 | ||
|
|
668366e886 | ||
|
|
eb1b2aef55 | ||
|
|
906430add5 | ||
|
|
f8fbbd2323 | ||
|
|
adab6ef0c6 | ||
|
|
ddfd91617f | ||
|
|
8418b4cb91 | ||
|
|
e593943d99 | ||
|
|
b58b5e65dc | ||
|
|
6d393987e8 | ||
|
|
c254a8abfe | ||
|
|
852231b503 | ||
|
|
f864e40a90 | ||
|
|
802f70dde8 | ||
|
|
6cb4fd6b93 | ||
|
|
70ca6edcb2 | ||
|
|
9dcc861740 | ||
|
|
fecb7fb24b | ||
|
|
c5b94db8af | ||
|
|
6690c9c692 | ||
|
|
506edcb3c6 | ||
|
|
c45e53e38b | ||
|
|
d8d8a4701e | ||
|
|
970c4ee9e1 | ||
|
|
c2386f43ef | ||
|
|
870878d151 | ||
|
|
cdf7bcf839 | ||
|
|
0fd3e75862 | ||
|
|
fcd3234fd9 | ||
|
|
2afa810e6c | ||
|
|
ff33b924b2 | ||
|
|
8a4fee7aea | ||
|
|
fa578efee4 | ||
|
|
4b6b4c0c63 | ||
|
|
8199ab83ef | ||
|
|
8e50d0ba53 | ||
|
|
7d72d66a97 | ||
|
|
3454694887 | ||
|
|
2a51c8a2bf | ||
|
|
524f623fd4 | ||
|
|
7ab7e18d96 | ||
|
|
6f2c8dba5a | ||
|
|
162d8bfffe | ||
|
|
61569d97cb | ||
|
|
975622a31c | ||
|
|
7524d06126 | ||
|
|
7f75eeadb9 | ||
|
|
4db2b73397 | ||
|
|
84bb25985b | ||
|
|
2f493c63f8 | ||
|
|
2761356f02 | ||
|
|
e38a58550f | ||
|
|
25ef4c6228 | ||
|
|
8207a52b6b | ||
|
|
8bbe7a1f0f | ||
|
|
2b052b3d43 | ||
|
|
94e7bf3f92 | ||
|
|
277fbeebc8 | ||
|
|
64e3f5f2ac | ||
|
|
399df78957 | ||
|
|
294325343b | ||
|
|
ed17eec948 | ||
|
|
f87c95079c | ||
|
|
327149ffc9 | ||
|
|
f81b21b933 | ||
|
|
a559b56983 | ||
|
|
6a9030b653 | ||
|
|
bb7f7deb50 | ||
|
|
55c10890bd | ||
|
|
fc7da187f4 | ||
|
|
edef0b194d | ||
|
|
f593f83ec1 | ||
|
|
5752f123ac | ||
|
|
37f0c98e75 | ||
|
|
6396d68584 | ||
|
|
bfd29f25dd | ||
|
|
e75e35e4ee | ||
|
|
48f18514af | ||
|
|
d7ddbf7e8d | ||
|
|
1b0198af63 | ||
|
|
ace16ba873 | ||
|
|
76c13700be | ||
|
|
f57a4bd948 | ||
|
|
8ae8737fe2 | ||
|
|
fef4393f9c | ||
|
|
47adb6d40a | ||
|
|
4940b10910 | ||
|
|
9c8e241ef7 | ||
|
|
36f7b7a1a1 | ||
|
|
c3d6b98c89 | ||
|
|
4b49654083 | ||
|
|
78223078f4 | ||
|
|
ca4b78d44c | ||
|
|
b0a5f2d998 | ||
|
|
19c980bc3b | ||
|
|
78d6b9699b | ||
|
|
82d8cb22df | ||
|
|
d7bfc789e2 | ||
|
|
b54d0ed62a | ||
|
|
8386443cb0 | ||
|
|
9c41af82d7 | ||
|
|
2997dd4e4d | ||
|
|
4602648fe5 | ||
|
|
a8f2ca5643 | ||
|
|
7959dc67ce | ||
|
|
0d2cdec75c | ||
|
|
cd055f7344 | ||
|
|
a4116b6c28 | ||
|
|
269ef25ece | ||
|
|
c43709395f | ||
|
|
575fbd5357 | ||
|
|
10f60752c8 | ||
|
|
870f8463db | ||
|
|
2ad887fb82 | ||
|
|
40a1a86f72 | ||
|
|
5567a69142 | ||
|
|
95d43d936c | ||
|
|
9f56568680 | ||
|
|
762fec1d9a | ||
|
|
3428940c72 | ||
|
|
ea604a5e64 | ||
|
|
0946b0d3a3 | ||
|
|
5b76473cbc | ||
|
|
db5359e086 | ||
|
|
976dd2cbfb | ||
|
|
35046f11c2 | ||
|
|
f4c4005a2a | ||
|
|
b6956b3649 | ||
|
|
69d8d27ad3 | ||
|
|
6cf4a50a83 | ||
|
|
755acd24f0 | ||
|
|
b2b4902d23 | ||
|
|
079d6cfee6 | ||
|
|
6f69129829 | ||
|
|
3e3b3c269a | ||
|
|
39f1af7d4b | ||
|
|
1ea4d616d7 | ||
|
|
fdf0ecf6f6 | ||
|
|
d846894d22 | ||
|
|
7d3101c877 | ||
|
|
e5f7285253 | ||
|
|
e46c304f7c | ||
|
|
c3f0e6f10c | ||
|
|
1aa570db90 | ||
|
|
1c2be3c890 | ||
|
|
05e4eacf34 | ||
|
|
d0da2dd66c | ||
|
|
e1417b03f9 | ||
|
|
7b99de8046 | ||
|
|
877b3ab39c | ||
|
|
6dc03d7f67 | ||
|
|
2a5e6a51aa | ||
|
|
885477fa68 | ||
|
|
8f031a78c1 | ||
|
|
c7db792793 | ||
|
|
31be51ef79 | ||
|
|
e4066c1570 | ||
|
|
69b36a4c34 | ||
|
|
c7b8df5655 | ||
|
|
d85768b2ac | ||
|
|
ec480f2cb1 | ||
|
|
a569c1f4f9 | ||
|
|
f5b30c9376 | ||
|
|
412508fbcf | ||
|
|
dbfd9bce54 | ||
|
|
07a8e5b216 | ||
|
|
38bf58c613 | ||
|
|
ba90d72d55 | ||
|
|
9889bee924 | ||
|
|
a19e4e8f16 | ||
|
|
5121852fbc | ||
|
|
52e0558c79 | ||
|
|
1d1ff5d20b | ||
|
|
ec00697d31 | ||
|
|
c25714b68e | ||
|
|
562d0b8641 | ||
|
|
dc0147c5f9 | ||
|
|
296afdbeee | ||
|
|
d48d6cdd91 | ||
|
|
9363790541 | ||
|
|
2bceba948d | ||
|
|
2f8f058c5c | ||
|
|
168d4246af | ||
|
|
133d593689 | ||
|
|
cdf6050ec2 | ||
|
|
68784018e6 | ||
|
|
19dfec2a34 | ||
|
|
55d729339f | ||
|
|
c3108a17f4 | ||
|
|
d47ae64bd6 | ||
|
|
5206ba7a74 | ||
|
|
f451155689 | ||
|
|
9f06e19346 | ||
|
|
f4be3e9668 | ||
|
|
095e1e9789 | ||
|
|
a0b9f99dd3 | ||
|
|
a33a92207b | ||
|
|
ee613db0cb | ||
|
|
55b892a74e | ||
|
|
16754d9657 | ||
|
|
33db67122c | ||
|
|
b9b88b0e23 | ||
|
|
992ad71910 | ||
|
|
51d1f926c2 | ||
|
|
c70447f337 | ||
|
|
dc367e1a30 | ||
|
|
f647e1ff11 | ||
|
|
ee7997ffbc | ||
|
|
2dbb5adbbb | ||
|
|
cc9f342575 | ||
|
|
252262f4a7 | ||
|
|
f77a16648f | ||
|
|
d6e3c54b68 | ||
|
|
7294e96a77 | ||
|
|
a4f4f45251 | ||
|
|
acc91abc0c | ||
|
|
18d5176cb9 | ||
|
|
d6d6ced7a4 | ||
|
|
52809cc849 | ||
|
|
c525b7ea3f | ||
|
|
e799417aaf | ||
|
|
ef077c2d48 | ||
|
|
e78b114544 | ||
|
|
df170ddd27 | ||
|
|
17928741e3 | ||
|
|
a6b825ffdf | ||
|
|
0d3bc0e95f | ||
|
|
0f62a2d73d | ||
|
|
bcdd32f92f | ||
|
|
6865fcbffc | ||
|
|
8e623c71a9 | ||
|
|
1e06502464 | ||
|
|
f2914868e2 | ||
|
|
561f0313f9 | ||
|
|
1e27c4e1ae | ||
|
|
46676ba8eb | ||
|
|
02a49efbea | ||
|
|
5f2a5e1025 | ||
|
|
42552e4f4f | ||
|
|
f2af78f056 | ||
|
|
269d55bde3 | ||
|
|
71f3dbbeb4 | ||
|
|
73dfb74ed7 | ||
|
|
7dd6baef33 | ||
|
|
519b532091 | ||
|
|
4fb9c37c0d | ||
|
|
c9b63be29f | ||
|
|
84bc735dce | ||
|
|
a44b9417a1 | ||
|
|
6b157ed43c | ||
|
|
9050276ea7 | ||
|
|
683293d042 | ||
|
|
70d009ab49 | ||
|
|
dbc2d10d6d | ||
|
|
059b854db9 | ||
|
|
13569a1785 | ||
|
|
6fd70b165b | ||
|
|
11d94cf286 | ||
|
|
c98a48ea14 | ||
|
|
b8fa80419b | ||
|
|
2a8846297f | ||
|
|
fb322f760f | ||
|
|
0b9f718106 | ||
|
|
57b3b3a9cd | ||
|
|
6513432993 | ||
|
|
80a564d4b6 | ||
|
|
132ecffc40 | ||
|
|
cdf02a8eac | ||
|
|
7d22dcab7c | ||
|
|
45c943a78f | ||
|
|
cf82a8013f | ||
|
|
25d145d864 | ||
|
|
7822e3f90e | ||
|
|
a4a612fbe4 | ||
|
|
07da472e82 | ||
|
|
e9096632a4 | ||
|
|
6475a8bdce | ||
|
|
535f92b7d2 | ||
|
|
00b91616f5 | ||
|
|
b8c34762ed | ||
|
|
9f92347936 | ||
|
|
b657a893ad | ||
|
|
387670212a | ||
|
|
b8e42544ae | ||
|
|
bcea47c2c6 | ||
|
|
b38aaba56c | ||
|
|
8099607d90 | ||
|
|
2d9412e4e8 | ||
|
|
069d508528 | ||
|
|
d68ce67e4f | ||
|
|
a9d38e743f | ||
|
|
878e81dc8f | ||
|
|
5ccf74f99c | ||
|
|
6c823f4914 | ||
|
|
de35155ffe | ||
|
|
470eb19a2f | ||
|
|
947bc33946 | ||
|
|
8bcaabfb57 | ||
|
|
a1f7e7b755 | ||
|
|
c8f8a86a98 | ||
|
|
bb205af019 | ||
|
|
443c6046f9 | ||
|
|
1620b94a7b | ||
|
|
81f94eac6c | ||
|
|
ffada75d9e | ||
|
|
34d773bcd8 | ||
|
|
ec7509670d | ||
|
|
eceac11af2 | ||
|
|
dd64598850 | ||
|
|
650ace6be6 | ||
|
|
1c71a5c5e0 | ||
|
|
9c699a634b | ||
|
|
a47d17bbf5 | ||
|
|
164f67d6ce | ||
|
|
465b9e7177 | ||
|
|
f5a914c318 | ||
|
|
de486d2e73 | ||
|
|
d7f946ec2a | ||
|
|
4d1480bb61 | ||
|
|
25a49473f9 | ||
|
|
8e86e7fba5 | ||
|
|
f9b380a0d4 | ||
|
|
f664a56b16 | ||
|
|
0acfd3c4b1 | ||
|
|
bfc4cb1ad9 | ||
|
|
66dd1a9a2b | ||
|
|
1f42be2e54 | ||
|
|
30d344af6b | ||
|
|
3ca989fd8c | ||
|
|
73c38b3ae4 | ||
|
|
08749b0c92 | ||
|
|
ea73af371e | ||
|
|
ce2086a480 | ||
|
|
63f7106d2b | ||
|
|
8cce6ea6e3 | ||
|
|
77c486b4c9 | ||
|
|
7c8f276bb0 | ||
|
|
e991586254 | ||
|
|
d61ab796a7 | ||
|
|
e634369975 | ||
|
|
453236cf3a | ||
|
|
3530476b07 | ||
|
|
e75d8d1874 | ||
|
|
719fa6a67d | ||
|
|
148cd43c5d | ||
|
|
3d753a2d19 | ||
|
|
693d02de87 | ||
|
|
80b4ef3ae8 | ||
|
|
6b9269eb2d | ||
|
|
15c9529885 | ||
|
|
512cd70d13 | ||
|
|
e5599ff4c4 | ||
|
|
32041ee3fc | ||
|
|
07bf84ed69 | ||
|
|
0c0449aa17 | ||
|
|
26cb527d37 | ||
|
|
33c282051b | ||
|
|
df0f8ef9dc | ||
|
|
e23f13a1b3 | ||
|
|
f5b8bf1d74 | ||
|
|
61766017ea | ||
|
|
706984a53b | ||
|
|
e137feca20 | ||
|
|
9e66d7c929 | ||
|
|
9605bf5c21 | ||
|
|
c2cc7b1e9e | ||
|
|
fe55eccbf9 | ||
|
|
47e17dc78a | ||
|
|
88a71dd647 | ||
|
|
ce0b3a8635 | ||
|
|
bd3d86a6dd | ||
|
|
c3b395c99e | ||
|
|
eb4e5d9576 | ||
|
|
19a03ec080 | ||
|
|
33d5d9b462 | ||
|
|
b5a371d0cf | ||
|
|
b61fd17879 | ||
|
|
6722f8b1e5 | ||
|
|
69ff75157d | ||
|
|
7fad968ad2 | ||
|
|
7c619f5439 | ||
|
|
3b35f38ad5 | ||
|
|
096017c3d6 | ||
|
|
4ff2ab1bc8 | ||
|
|
ef698529d7 | ||
|
|
167d5bd89a | ||
|
|
c328a55453 | ||
|
|
83a976e98f | ||
|
|
d004133ad6 | ||
|
|
1f5c1b9658 | ||
|
|
56dfd174dd | ||
|
|
532cd4937b | ||
|
|
91bc73baf2 | ||
|
|
ab031c01de | ||
|
|
e51f72ff98 | ||
|
|
3eb010285d | ||
|
|
2b172f9999 | ||
|
|
0803f5af35 | ||
|
|
69ac2f0a6c | ||
|
|
0c842e0e15 | ||
|
|
d94678d4f4 | ||
|
|
90814f41a2 | ||
|
|
282dbd0913 | ||
|
|
1343b68cc6 | ||
|
|
ae07f71e76 | ||
|
|
54dc9c8587 | ||
|
|
9f9fa3b952 | ||
|
|
cc7c98336f | ||
|
|
dc22b96bed | ||
|
|
ae9e9f4b72 | ||
|
|
301c680bff | ||
|
|
595159f24d | ||
|
|
3bf1ec25c1 | ||
|
|
40c801592d | ||
|
|
9012e4deec | ||
|
|
ab2323e5c1 | ||
|
|
f31ae9ac6d | ||
|
|
27c24619c3 | ||
|
|
38aef71269 |
@@ -9,10 +9,10 @@ const config = {
|
||||
arrowParens: "always",
|
||||
jsxSingleQuote: false,
|
||||
bracketSameLine: false,
|
||||
endOfLine: "lf",
|
||||
importOrder: ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"],
|
||||
importOrderSeparation: true,
|
||||
importOrderSortSpecifiers: true
|
||||
endOfLine: "lf"
|
||||
// importOrder: ["^@core/(.*)$", "^@server/(.*)$", "^@ui/(.*)$", "^[./]"],
|
||||
// importOrderSeparation: true,
|
||||
// importOrderSortSpecifiers: true
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
@@ -2,7 +2,7 @@ NGROK TEsting:
|
||||
./ngrok.exe http http://localhost:4000 -host-header="localhost:4000"
|
||||
|
||||
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.
|
||||
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!@#'
|
||||
|
||||
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
|
||||
|
||||
41
_reference/productionBoardNotes.md
Normal file
41
_reference/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",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"main": "index.jsx",
|
||||
"dependencies": {
|
||||
"express": "^4.16.4"
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ module.exports = {
|
||||
|
||||
{
|
||||
name: "Bitbucket Webhook",
|
||||
script: "./webhook/index.js",
|
||||
script: "./webhook/index.jsx",
|
||||
env: {
|
||||
NODE_ENV: "production"
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
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": "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=https://res.cloudinary.com/io-test
|
||||
VITE_APP_CLOUDINARY_API_KEY=957865933348715
|
||||
|
||||
8
client/.eslintrc
Normal file
8
client/.eslintrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": [
|
||||
"react-app"
|
||||
],
|
||||
"rules": {
|
||||
"no-useless-rename": "off"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
/// <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
|
||||
// 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.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
|
||||
@@ -1,174 +1,174 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
|
||||
<link rel="icon" href="/ro-favicon.png" />
|
||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'PROMANAGER') { %>
|
||||
<link rel="icon" href="/pm/pm-favicon.ico" />
|
||||
<% } %>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
|
||||
<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="theme-color" content="#1690ff" />
|
||||
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
|
||||
<!-- TODO:AIo Update the individual logos for each.-->
|
||||
<link rel="apple-touch-icon" href="public/logo192.png" />
|
||||
<link rel="mask-icon" href="/mask-icon.svg" color="#FFFFFF">
|
||||
<!--
|
||||
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/
|
||||
-->
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
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.
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#1690ff" />
|
||||
<!-- <link rel="apple-touch-icon" href="logo192.png" /> -->
|
||||
<!-- TODO:AIo Update the individual logos for each.-->
|
||||
<link rel="apple-touch-icon" href="public/logo192.png" />
|
||||
<link rel="mask-icon" href="/mask-icon.svg" color="#FFFFFF">
|
||||
<!--
|
||||
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/
|
||||
-->
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
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.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
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`.
|
||||
-->
|
||||
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
|
||||
<meta name="description" content="ImEX Online" />
|
||||
<title>ImEX Online</title>
|
||||
<script type="text/javascript">
|
||||
window.$crisp = [];
|
||||
window.CRISP_WEBSITE_ID = '36724f62-2eb0-4b29-9cdd-9905fb99913e';
|
||||
(function () {
|
||||
d = document;
|
||||
s = d.createElement('script');
|
||||
s.src = 'https://client.crisp.chat/l.js';
|
||||
s.async = 1;
|
||||
d.getElementsByTagName('head')[0].appendChild(s);
|
||||
})();
|
||||
</script>
|
||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
|
||||
<meta name="description" content="Rome Online" />
|
||||
<title>Rome Online</title>
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
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`.
|
||||
-->
|
||||
<% if (env.VITE_APP_INSTANCE === 'IMEX') { %>
|
||||
<meta name="description" content="ImEX Online" />
|
||||
<title>ImEX Online</title>
|
||||
<script type="text/javascript">
|
||||
window.$crisp = [];
|
||||
window.CRISP_WEBSITE_ID = '36724f62-2eb0-4b29-9cdd-9905fb99913e';
|
||||
(function () {
|
||||
d = document;
|
||||
s = d.createElement('script');
|
||||
s.src = 'https://client.crisp.chat/l.js';
|
||||
s.async = 1;
|
||||
d.getElementsByTagName('head')[0].appendChild(s);
|
||||
})();
|
||||
</script>
|
||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'ROME') { %>
|
||||
<meta name="description" content="Rome Online" />
|
||||
<title>Rome Online</title>
|
||||
|
||||
<!--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-->
|
||||
<!--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-->
|
||||
|
||||
<call-us-selector phonesystem-url=https://rometech.east.3cx.us:5001 party="LiveChat528346"></call-us-selector>
|
||||
<call-us-selector phonesystem-url=https://rometech.east.3cx.us:5001 party="LiveChat528346"></call-us-selector>
|
||||
|
||||
<!--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-->
|
||||
|
||||
<!--<call-us
|
||||
|
||||
phonesystem-url=https://rometech.east.3cx.us:5001
|
||||
|
||||
style="position:fixed;font-size:16px;line-height:17px;z-index: 99999;right: 20px; bottom: 20px;"
|
||||
|
||||
id="wp-live-chat-by-3CX"
|
||||
|
||||
minimized="true"
|
||||
|
||||
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>
|
||||
<!--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-->
|
||||
|
||||
<!--<call-us
|
||||
|
||||
phonesystem-url=https://rometech.east.3cx.us:5001
|
||||
|
||||
style="position:fixed;font-size:16px;line-height:17px;z-index: 99999;right: 20px; bottom: 20px;"
|
||||
|
||||
id="wp-live-chat-by-3CX"
|
||||
|
||||
minimized="true"
|
||||
|
||||
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" />
|
||||
<% } %> <% if (env.VITE_APP_INSTANCE === 'PROMANAGER') { %>
|
||||
<title>ProManager</title>
|
||||
<meta name="description" content="ProManager" />
|
||||
|
||||
<% } %>
|
||||
<script>
|
||||
!(function () {
|
||||
'use strict';
|
||||
var e = [
|
||||
'debug',
|
||||
'destroy',
|
||||
'do',
|
||||
'help',
|
||||
'identify',
|
||||
'is',
|
||||
'off',
|
||||
'on',
|
||||
'ready',
|
||||
'render',
|
||||
'reset',
|
||||
'safe',
|
||||
'set',
|
||||
];
|
||||
if (window.noticeable) console.warn('Noticeable SDK code snippet loaded more than once');
|
||||
else {
|
||||
var n = (window.noticeable = window.noticeable || []);
|
||||
function t(e) {
|
||||
return function () {
|
||||
var t = Array.prototype.slice.call(arguments);
|
||||
return t.unshift(e), n.push(t), n;
|
||||
};
|
||||
}
|
||||
!(function () {
|
||||
for (var o = 0; o < e.length; o++) {
|
||||
var r = e[o];
|
||||
n[r] = t(r);
|
||||
}
|
||||
})(),
|
||||
(function () {
|
||||
var e = document.createElement('script');
|
||||
(e.async = !0), (e.src = 'https://sdk.noticeable.io/l.js');
|
||||
var n = document.head;
|
||||
n.insertBefore(e, n.firstChild);
|
||||
})();
|
||||
<% } %>
|
||||
<script>
|
||||
!(function () {
|
||||
'use strict';
|
||||
var e = [
|
||||
'debug',
|
||||
'destroy',
|
||||
'do',
|
||||
'help',
|
||||
'identify',
|
||||
'is',
|
||||
'off',
|
||||
'on',
|
||||
'ready',
|
||||
'render',
|
||||
'reset',
|
||||
'safe',
|
||||
'set',
|
||||
];
|
||||
if (window.noticeable) console.warn('Noticeable SDK code snippet loaded more than once');
|
||||
else {
|
||||
var n = (window.noticeable = window.noticeable || []);
|
||||
function t(e) {
|
||||
return function () {
|
||||
var t = Array.prototype.slice.call(arguments);
|
||||
return t.unshift(e), n.push(t), n;
|
||||
};
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
!(function () {
|
||||
for (var o = 0; o < e.length; o++) {
|
||||
var r = e[o];
|
||||
n[r] = t(r);
|
||||
}
|
||||
})(),
|
||||
(function () {
|
||||
var e = document.createElement('script');
|
||||
(e.async = !0), (e.src = 'https://sdk.noticeable.io/l.js');
|
||||
var n = document.head;
|
||||
n.insertBefore(e, n.firstChild);
|
||||
})();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
|
||||
<script type="module" src="src/index.jsx"></script>
|
||||
</body>
|
||||
<script type="module" src="src/index.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
24091
client/package-lock.json
generated
24091
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,90 +2,91 @@
|
||||
"name": "bodyshop",
|
||||
"version": "0.2.1",
|
||||
"engines": {
|
||||
"node": "18.18.2"
|
||||
"node": ">=18.18.2"
|
||||
},
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"proxy": "http://localhost:4000",
|
||||
"dependencies": {
|
||||
"@ant-design/compatible": "^5.1.2",
|
||||
"@ant-design/pro-layout": "^7.17.16",
|
||||
"@apollo/client": "^3.8.10",
|
||||
"@asseinfo/react-kanban": "^2.2.0",
|
||||
"@fingerprintjs/fingerprintjs": "^4.2.2",
|
||||
"@ant-design/pro-layout": "^7.19.12",
|
||||
"@apollo/client": "^3.11.4",
|
||||
"@emotion/is-prop-valid": "^1.3.0",
|
||||
"@fingerprintjs/fingerprintjs": "^4.4.3",
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@reduxjs/toolkit": "^2.2.1",
|
||||
"@sentry/cli": "^2.28.6",
|
||||
"@sentry/react": "^7.104.0",
|
||||
"@splitsoftware/splitio-react": "^1.11.0",
|
||||
"@reduxjs/toolkit": "^2.2.7",
|
||||
"@sentry/cli": "^2.33.1",
|
||||
"@sentry/react": "^7.114.0",
|
||||
"@splitsoftware/splitio-react": "^1.12.1",
|
||||
"@tanem/react-nprogress": "^5.0.51",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"antd": "^5.15.3",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"antd": "^5.20.1",
|
||||
"apollo-link-logger": "^2.0.1",
|
||||
"apollo-link-sentry": "^3.3.0",
|
||||
"axios": "^1.6.7",
|
||||
"dayjs": "^1.11.10",
|
||||
"autosize": "^6.0.1",
|
||||
"axios": "^1.7.4",
|
||||
"classnames": "^2.5.1",
|
||||
"css-box-model": "^1.2.1",
|
||||
"dayjs": "^1.11.12",
|
||||
"dayjs-business-days2": "^1.2.2",
|
||||
"dinero.js": "^1.9.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"env-cmd": "^10.1.0",
|
||||
"exifr": "^7.1.3",
|
||||
"firebase": "^10.8.1",
|
||||
"graphql": "^16.6.0",
|
||||
"i18next": "^23.10.0",
|
||||
"i18next-browser-languagedetector": "^7.0.2",
|
||||
"libphonenumber-js": "^1.10.57",
|
||||
"logrocket": "^8.0.1",
|
||||
"markerjs2": "^2.32.0",
|
||||
"normalize-url": "^8.0.0",
|
||||
"firebase": "^10.12.5",
|
||||
"graphql": "^16.9.0",
|
||||
"i18next": "^23.12.3",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"libphonenumber-js": "^1.11.5",
|
||||
"logrocket": "^8.1.2",
|
||||
"markerjs2": "^2.32.1",
|
||||
"memoize-one": "^6.0.0",
|
||||
"normalize-url": "^8.0.1",
|
||||
"object-hash": "^3.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^9.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-big-calendar": "^1.11.0",
|
||||
"query-string": "^9.1.0",
|
||||
"raf-schd": "^4.0.3",
|
||||
"react": "^18.3.1",
|
||||
"react-big-calendar": "^1.13.2",
|
||||
"react-color": "^2.19.3",
|
||||
"react-cookie": "^7.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-cookie": "^7.2.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"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-i18next": "^14.0.5",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-i18next": "^14.1.3",
|
||||
"react-icons": "^5.3.0",
|
||||
"react-image-lightbox": "^5.1.4",
|
||||
"react-joyride": "^2.7.4",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-number-format": "^5.3.3",
|
||||
"react-number-format": "^5.4.0",
|
||||
"react-popopo": "^2.1.9",
|
||||
"react-product-fruits": "^2.2.6",
|
||||
"react-redux": "^9.1.0",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-resizable": "^3.0.5",
|
||||
"react-router-dom": "^6.22.2",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-router-dom": "^6.26.0",
|
||||
"react-sticky": "^6.0.3",
|
||||
"react-virtualized": "^9.22.5",
|
||||
"recharts": "^2.12.2",
|
||||
"react-virtuoso": "^4.10.1",
|
||||
"recharts": "^2.12.7",
|
||||
"redux": "^5.0.1",
|
||||
"redux-actions": "^3.0.3",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-saga": "^1.3.0",
|
||||
"redux-state-sync": "^3.1.4",
|
||||
"reselect": "^5.1.0",
|
||||
"sass": "^1.71.1",
|
||||
"socket.io-client": "^4.7.4",
|
||||
"styled-components": "^6.1.8",
|
||||
"reselect": "^5.1.1",
|
||||
"sass": "^1.77.8",
|
||||
"socket.io-client": "^4.7.5",
|
||||
"styled-components": "^6.1.12",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
"terser-webpack-plugin": "^5.3.10",
|
||||
"userpilot": "^1.3.1",
|
||||
"use-memo-one": "^1.1.3",
|
||||
"userpilot": "^1.3.5",
|
||||
"vite-plugin-ejs": "^1.7.0",
|
||||
"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"
|
||||
"web-vitals": "^3.5.2"
|
||||
},
|
||||
"scripts": {
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||
"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:rome": "dotenvx run --env-file=.env.development.rome -- vite",
|
||||
"start:promanager": "dotenvx run --env-file=.env.development.promanager -- vite",
|
||||
@@ -128,29 +129,30 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/preset-react": "^7.23.3",
|
||||
"@dotenvx/dotenvx": "^0.15.4",
|
||||
"@emotion/babel-plugin": "^11.11.0",
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@sentry/webpack-plugin": "^2.14.2",
|
||||
"@swc/core": "^1.3.107",
|
||||
"@swc/plugin-styled-components": "^1.5.108",
|
||||
"@testing-library/cypress": "^10.0.1",
|
||||
"browserslist": "^4.22.3",
|
||||
"@babel/preset-react": "^7.24.7",
|
||||
"@dotenvx/dotenvx": "^1.7.0",
|
||||
"@emotion/babel-plugin": "^11.12.0",
|
||||
"@emotion/react": "^11.13.0",
|
||||
"@sentry/webpack-plugin": "^2.22.2",
|
||||
"@testing-library/cypress": "^10.0.2",
|
||||
"browserslist": "^4.23.3",
|
||||
"browserslist-to-esbuild": "^2.1.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"cypress": "^13.6.6",
|
||||
"cypress": "^13.13.3",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-cypress": "^2.15.1",
|
||||
"memfs": "^4.6.0",
|
||||
"memfs": "^4.11.1",
|
||||
"os-browserify": "^0.3.0",
|
||||
"react-error-overlay": "6.0.11",
|
||||
"redux-logger": "^3.0.6",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"vite": "^5.0.11",
|
||||
"vite": "^5.4.0",
|
||||
"vite-plugin-babel": "^1.2.0",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-legacy": "^2.1.0",
|
||||
"vite-plugin-node-polyfills": "^0.19.0",
|
||||
"vite-plugin-pwa": "^0.19.0",
|
||||
"vite-plugin-node-polyfills": "^0.22.0",
|
||||
"vite-plugin-pwa": "^0.20.1",
|
||||
"vite-plugin-style-import": "^2.0.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
|
||||
```
|
||||
```js
|
||||
var mylib = require('./lib/index.js')
|
||||
var mylib = require('./lib/index.jsx')
|
||||
var stubs = require('stubs')
|
||||
|
||||
// make it a noop
|
||||
|
||||
@@ -16567,7 +16567,7 @@ even more slower.
|
||||
## Benchmarks
|
||||
|
||||
```bash
|
||||
$ node benchmarks/index.js
|
||||
$ node benchmarks/index.jsx
|
||||
Benchmarking: sign
|
||||
elliptic#sign x 262 ops/sec ±0.51% (177 runs sampled)
|
||||
eccjs#sign x 55.91 ops/sec ±0.90% (144 runs sampled)
|
||||
|
||||
56
client/public/firebase-messaging-sw.js
Normal file
56
client/public/firebase-messaging-sw.js
Normal file
@@ -0,0 +1,56 @@
|
||||
// Scripts for firebase and firebase messaging
|
||||
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-app.js");
|
||||
importScripts("https://www.gstatic.com/firebasejs/8.2.0/firebase-messaging.js");
|
||||
|
||||
// Initialize the Firebase app in the service worker by passing the generated config
|
||||
let firebaseConfig;
|
||||
switch (this.location.hostname) {
|
||||
case "localhost":
|
||||
firebaseConfig = {
|
||||
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",
|
||||
};
|
||||
break;
|
||||
case "test.imex.online":
|
||||
firebaseConfig = {
|
||||
apiKey: "AIzaSyBw7_GTy7GtQyfkIRPVrWHEGKfcqeyXw0c",
|
||||
authDomain: "imex-test.firebaseapp.com",
|
||||
projectId: "imex-test",
|
||||
storageBucket: "imex-test.appspot.com",
|
||||
messagingSenderId: "991923618608",
|
||||
appId: "1:991923618608:web:633437569cdad78299bef5",
|
||||
// measurementId: "${config.measurementId}",
|
||||
};
|
||||
break;
|
||||
case "imex.online":
|
||||
default:
|
||||
firebaseConfig = {
|
||||
apiKey: "AIzaSyDSezy-jGJreo7ulgpLdlpOwAOrgcaEkhU",
|
||||
authDomain: "imex-prod.firebaseapp.com",
|
||||
databaseURL: "https://imex-prod.firebaseio.com",
|
||||
projectId: "imex-prod",
|
||||
storageBucket: "imex-prod.appspot.com",
|
||||
messagingSenderId: "253497221485",
|
||||
appId: "1:253497221485:web:3c81c483b94db84b227a64",
|
||||
measurementId: "G-NTWBKG2L0M",
|
||||
};
|
||||
}
|
||||
|
||||
firebase.initializeApp(firebaseConfig);
|
||||
|
||||
// Retrieve firebase messaging
|
||||
const messaging = firebase.messaging();
|
||||
|
||||
messaging.onBackgroundMessage(function (payload) {
|
||||
// Customize notification here
|
||||
const channel = new BroadcastChannel("imex-sw-messages");
|
||||
channel.postMessage(payload);
|
||||
|
||||
//self.registration.showNotification(notificationTitle, notificationOptions);
|
||||
});
|
||||
@@ -7,9 +7,7 @@ import { connect } from "react-redux";
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import DocumentEditorContainer from "../components/document-editor/document-editor.container";
|
||||
import ErrorBoundary from "../components/error-boundary/error-boundary.component";
|
||||
|
||||
//Component Imports
|
||||
import ErrorBoundary from "../components/error-boundary/error-boundary.component"; // Component Imports
|
||||
import LoadingSpinner from "../components/loading-spinner/loading-spinner.component";
|
||||
import DisclaimerPage from "../pages/disclaimer/disclaimer.page";
|
||||
import LandingPage from "../pages/landing/landing.page";
|
||||
@@ -23,20 +21,21 @@ import "./App.styles.scss";
|
||||
import handleBeta from "../utils/betaHandler";
|
||||
import Eula from "../components/eula/eula.component";
|
||||
import InstanceRenderMgr from "../utils/instanceRenderMgr";
|
||||
import { ProductFruits } from "react-product-fruits";
|
||||
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
|
||||
|
||||
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
|
||||
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
|
||||
const SignInPage = lazy(() => import("../pages/sign-in/sign-in.page"));
|
||||
|
||||
const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
|
||||
const MobilePaymentContainer = lazy(() => import("../pages/mobile-payment/mobile-payment.container"));
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
online: selectOnline,
|
||||
bodyshop: selectBodyshop,
|
||||
currentEula: selectCurrentEula
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
checkUserSession: () => dispatch(checkUserSession()),
|
||||
setOnline: (isOnline) => dispatch(setOnline(isOnline))
|
||||
@@ -60,11 +59,11 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
||||
|
||||
// Associate event listeners, memoize to prevent multiple listeners being added
|
||||
useEffect(() => {
|
||||
const offlineListener = (e) => {
|
||||
const offlineListener = () => {
|
||||
setOnline(false);
|
||||
};
|
||||
|
||||
const onlineListener = (e) => {
|
||||
const onlineListener = () => {
|
||||
setOnline(true);
|
||||
};
|
||||
|
||||
@@ -98,7 +97,7 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
||||
InstanceRenderMgr({
|
||||
imex: "gvfvfw/bodyshopapp",
|
||||
rome: "rome-online/rome-online",
|
||||
promanager: "" //TODO:AIO Add in log rocket for promanager instances.
|
||||
promanager: "" // TODO: AIO Add in log rocket for promanager instances.
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -111,24 +110,20 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
||||
|
||||
handleBeta();
|
||||
|
||||
if (!online)
|
||||
if (!online) {
|
||||
return (
|
||||
<Result
|
||||
status="warning"
|
||||
title={t("general.labels.nointernet")}
|
||||
subTitle={t("general.labels.nointernet_sub")}
|
||||
extra={
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
<Button type="primary" onClick={() => window.location.reload()}>
|
||||
{t("general.actions.refresh")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (currentEula && !currentUser.eulaIsAccepted) {
|
||||
return <Eula />;
|
||||
@@ -147,18 +142,13 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ProductFruits
|
||||
<ProductFruitsWrapper
|
||||
currentUser={currentUser}
|
||||
workspaceCode={InstanceRenderMgr({
|
||||
imex: null,
|
||||
rome: null,
|
||||
rome: "9BkbEseqNqxw8jUH",
|
||||
promanager: "aoJoEifvezYI0Z0P"
|
||||
})}
|
||||
debug
|
||||
language="en"
|
||||
user={{
|
||||
email: currentUser.email,
|
||||
username: currentUser.email
|
||||
}}
|
||||
/>
|
||||
|
||||
<Routes>
|
||||
|
||||
@@ -161,3 +161,15 @@
|
||||
.rowWithColor > td {
|
||||
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;
|
||||
}
|
||||
|
||||
32
client/src/App/ProductFruitsWrapper.jsx
Normal file
32
client/src/App/ProductFruitsWrapper.jsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import { ProductFruits } from "react-product-fruits";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const ProductFruitsWrapper = React.memo(({ currentUser, workspaceCode }) => {
|
||||
return (
|
||||
workspaceCode &&
|
||||
currentUser?.authorized === true &&
|
||||
currentUser?.email && (
|
||||
<ProductFruits
|
||||
lifeCycle="unmount"
|
||||
workspaceCode={workspaceCode}
|
||||
debug
|
||||
language="en"
|
||||
user={{
|
||||
email: currentUser.email,
|
||||
username: currentUser.email
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
export default ProductFruitsWrapper;
|
||||
|
||||
ProductFruitsWrapper.propTypes = {
|
||||
currentUser: PropTypes.shape({
|
||||
authorized: PropTypes.bool,
|
||||
email: PropTypes.string
|
||||
}),
|
||||
workspaceCode: PropTypes.string
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import { Button, Divider, Form, Popconfirm, Space } from "antd";
|
||||
import dayjs from "../../utils/day";
|
||||
import queryString from "query-string";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -13,6 +13,7 @@ import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import dayjs from "../../utils/day";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import BillFormContainer from "../bill-form/bill-form.container";
|
||||
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 LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import BillDetailEditReturn from "./bill-detail-edit-return.component";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -153,6 +153,7 @@ export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail
|
||||
if (!search.billid) return <></>; //<div>{t("bills.labels.noneselected")}</div>;
|
||||
|
||||
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 (
|
||||
<>
|
||||
@@ -188,7 +189,7 @@ export function BillDetailEditcontainer({ setPartsOrderContext, insertAuditTrail
|
||||
}
|
||||
/>
|
||||
<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>
|
||||
{bodyshop.uselocalmediaserver ? (
|
||||
<JobsDocumentsLocalGallery
|
||||
|
||||
@@ -34,6 +34,7 @@ export function BillDetailEditReturn({ setPartsOrderContext, insertAuditTrail, b
|
||||
actions: {},
|
||||
context: {
|
||||
jobId: data.bills_by_pk.jobid,
|
||||
job: data.bills_by_pk.job,
|
||||
vendorId: data.bills_by_pk.vendorid,
|
||||
returnFromBill: data.bills_by_pk.id,
|
||||
invoiceNumber: data.bills_by_pk.invoice_number,
|
||||
|
||||
@@ -41,7 +41,8 @@ export function BillFormComponent({
|
||||
job,
|
||||
loadOutstandingReturns,
|
||||
loadInventory,
|
||||
preferredMake
|
||||
preferredMake,
|
||||
disableInHouse
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const client = useApolloClient();
|
||||
@@ -177,7 +178,7 @@ export function BillFormComponent({
|
||||
]}
|
||||
>
|
||||
<VendorSearchSelect
|
||||
disabled={disabled}
|
||||
disabled={disabled || disableInHouse}
|
||||
options={vendorAutoCompleteOptions}
|
||||
preferredMake={preferredMake}
|
||||
onSelect={handleVendorSelect}
|
||||
@@ -243,7 +244,7 @@ export function BillFormComponent({
|
||||
})
|
||||
]}
|
||||
>
|
||||
<Input disabled={disabled || disableInvNumber} />
|
||||
<Input disabled={disabled || disableInvNumber || disableInHouse} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("bills.fields.date")}
|
||||
|
||||
@@ -16,7 +16,7 @@ const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableInvNumber }) {
|
||||
export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableInvNumber, disableInHouse }) {
|
||||
const {
|
||||
treatments: { Simple_Inventory }
|
||||
} = useSplitTreatments({
|
||||
@@ -47,6 +47,7 @@ export function BillFormContainer({ bodyshop, form, billEdit, disabled, disableI
|
||||
job={lineData ? lineData.jobs_by_pk : null}
|
||||
responsibilityCenters={bodyshop.md_responsibility_centers || null}
|
||||
disableInvNumber={disableInvNumber}
|
||||
disableInHouse={disableInHouse}
|
||||
loadOutstandingReturns={loadOutstandingReturns}
|
||||
loadInventory={loadInventory}
|
||||
preferredMake={lineData ? lineData.jobs_by_pk.v_make_desc : null}
|
||||
|
||||
@@ -3,8 +3,6 @@ import React, { forwardRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import InstanceRenderMgr from "../../utils/instanceRenderMgr";
|
||||
|
||||
//To be used as a form element only.
|
||||
const { Option } = Select;
|
||||
const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -25,31 +23,27 @@ const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps },
|
||||
);
|
||||
}}
|
||||
notFoundContent={"Removed."}
|
||||
{...restProps}
|
||||
>
|
||||
<Select.Option key={null} value={"noline"} cost={0} line_desc={""}>
|
||||
{t("billlines.labels.other")}
|
||||
</Select.Option>
|
||||
{options
|
||||
? options.map((item) => (
|
||||
<Option
|
||||
disabled={allowRemoved ? false : item.removed}
|
||||
key={item.id}
|
||||
value={item.id}
|
||||
cost={item.act_price ? item.act_price : 0}
|
||||
part_type={item.part_type}
|
||||
line_desc={item.line_desc}
|
||||
part_qty={item.part_qty}
|
||||
oem_partno={item.oem_partno}
|
||||
alt_partno={item.alt_partno}
|
||||
act_price={item.act_price}
|
||||
style={{
|
||||
...(item.removed ? { textDecoration: "line-through" } : {})
|
||||
}}
|
||||
name={`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
||||
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim()}
|
||||
>
|
||||
options={[
|
||||
{ value: "noline", label: t("billlines.labels.other"), name: t("billlines.labels.other") },
|
||||
...options.map((item) => ({
|
||||
disabled: allowRemoved ? false : item.removed,
|
||||
key: item.id,
|
||||
value: item.id,
|
||||
cost: item.act_price ? item.act_price : 0,
|
||||
part_type: item.part_type,
|
||||
line_desc: item.line_desc,
|
||||
part_qty: item.part_qty,
|
||||
oem_partno: item.oem_partno,
|
||||
alt_partno: item.alt_partno,
|
||||
act_price: item.act_price,
|
||||
style: {
|
||||
...(item.removed ? { textDecoration: "line-through" } : {})
|
||||
},
|
||||
name: `${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
||||
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||
}${item.alt_partno ? ` (${item.alt_partno})` : ""}`.trim(),
|
||||
label: (
|
||||
<>
|
||||
<span>
|
||||
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
||||
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||
@@ -60,14 +54,15 @@ const BillLineSearchSelect = ({ options, disabled, allowRemoved, ...restProps },
|
||||
<span style={{ float: "right", paddingleft: "1rem" }}>{`${item.mod_lb_hrs} units`}</span>
|
||||
)
|
||||
})}
|
||||
|
||||
<span style={{ float: "right", paddingleft: "1rem" }}>
|
||||
{item.act_price ? `$${item.act_price && item.act_price.toFixed(2)}` : ``}
|
||||
</span>
|
||||
</Option>
|
||||
))
|
||||
: null}
|
||||
</Select>
|
||||
</>
|
||||
)
|
||||
}))
|
||||
]}
|
||||
{...restProps}
|
||||
></Select>
|
||||
);
|
||||
};
|
||||
export default forwardRef(BillLineSearchSelect);
|
||||
|
||||
@@ -14,6 +14,7 @@ import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import BillDeleteButton from "../bill-delete-button/bill-delete-button.component";
|
||||
import BillDetailEditReturnComponent from "../bill-detail-edit/bill-detail-edit-return.component";
|
||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||
import { FaTasks } from "react-icons/fa";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
@@ -21,9 +22,21 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
||||
setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" })),
|
||||
setReconciliationContext: (context) => dispatch(setModalContext({ context: context, modal: "reconciliation" }))
|
||||
setBillEnterContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "billEnter"
|
||||
})
|
||||
),
|
||||
setReconciliationContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "reconciliation"
|
||||
})
|
||||
),
|
||||
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
|
||||
});
|
||||
|
||||
export function BillsListTableComponent({
|
||||
@@ -32,9 +45,9 @@ export function BillsListTableComponent({
|
||||
job,
|
||||
billsQuery,
|
||||
handleOnRowClick,
|
||||
setPartsOrderContext,
|
||||
setBillEnterContext,
|
||||
setReconciliationContext
|
||||
setReconciliationContext,
|
||||
setTaskUpsertContext
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -48,6 +61,7 @@ export function BillsListTableComponent({
|
||||
const Templates = TemplateList("bill");
|
||||
const bills = billsQuery.data ? billsQuery.data.bills : [];
|
||||
const { refetch } = billsQuery;
|
||||
|
||||
const recordActions = (record, showView = false) => (
|
||||
<Space wrap>
|
||||
{showView && (
|
||||
@@ -55,9 +69,22 @@ export function BillsListTableComponent({
|
||||
<EditFilled />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
title={t("tasks.buttons.create")}
|
||||
onClick={() => {
|
||||
setTaskUpsertContext({
|
||||
context: {
|
||||
jobid: job.id,
|
||||
billid: record.id
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FaTasks />
|
||||
</Button>
|
||||
<BillDeleteButton bill={record} jobid={job.id} />
|
||||
<BillDetailEditReturnComponent
|
||||
data={{ bills_by_pk: { ...record, jobid: job.id } }}
|
||||
data={{ bills_by_pk: { ...record, jobid: job.id, job: job } }}
|
||||
disabled={record.is_credit_memo || record.vendorid === bodyshop.inhousevendorid || jobRO}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { DeleteFilled } from "@ant-design/icons";
|
||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||
import { Button, Card, Col, Form, Input, notification, Row, Space, Spin, Statistic } from "antd";
|
||||
import { Button, Card, Col, Form, Input, Row, Space, Spin, Statistic, notification } from "antd";
|
||||
import axios from "axios";
|
||||
import dayjs from "../../utils/day";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { INSERT_PAYMENT_RESPONSE, QUERY_RO_AND_OWNER_BY_JOB_PKS } from "../../graphql/payment_response.queries";
|
||||
import { INSERT_NEW_PAYMENT } from "../../graphql/payments.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
import { selectCardPayment } from "../../redux/modals/modals.selectors";
|
||||
@@ -28,12 +26,12 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
});
|
||||
|
||||
const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisible, insertAuditTrail }) => {
|
||||
const { context } = cardPaymentModal;
|
||||
const { context, actions } = cardPaymentModal;
|
||||
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
|
||||
// const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
|
||||
const [insertPaymentResponse] = useMutation(INSERT_PAYMENT_RESPONSE);
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -42,7 +40,6 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
skip: true
|
||||
});
|
||||
|
||||
console.log("🚀 ~ file: card-payment-modal.component..jsx:61 ~ data:", data);
|
||||
//Initialize the intellipay window.
|
||||
const SetIntellipayCallbackFunctions = () => {
|
||||
console.log("*** Set IntelliPay callback functions.");
|
||||
@@ -51,16 +48,20 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
});
|
||||
|
||||
window.intellipay.runOnApproval(async function (response) {
|
||||
console.warn("*** Running On Approval Script ***");
|
||||
form.setFieldValue("paymentResponse", response);
|
||||
form.submit();
|
||||
//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.
|
||||
setTimeout(() => {
|
||||
if (actions && actions.refetch && typeof actions.refetch === "function")
|
||||
actions.refetch();
|
||||
setLoading(false);
|
||||
toggleModalVisible();
|
||||
}, 750);
|
||||
});
|
||||
|
||||
window.intellipay.runOnNonApproval(async function (response) {
|
||||
// Mutate unsuccessful payment
|
||||
|
||||
const { payments } = form.getFieldsValue();
|
||||
|
||||
await insertPaymentResponse({
|
||||
variables: {
|
||||
paymentResponse: payments.map((payment) => ({
|
||||
@@ -85,50 +86,9 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
});
|
||||
};
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
try {
|
||||
await insertPayment({
|
||||
variables: {
|
||||
paymentInput: values.payments.map((payment) => ({
|
||||
amount: payment.amount,
|
||||
transactionid: (values.paymentResponse.paymentid || "").toString(),
|
||||
payer: t("payments.labels.customer"),
|
||||
type: values.paymentResponse.cardbrand,
|
||||
jobid: payment.jobid,
|
||||
date: dayjs(Date.now()),
|
||||
payment_responses: {
|
||||
data: [
|
||||
{
|
||||
amount: payment.amount,
|
||||
bodyshopid: bodyshop.id,
|
||||
|
||||
jobid: payment.jobid,
|
||||
declinereason: values.paymentResponse.declinereason,
|
||||
ext_paymentid: values.paymentResponse.paymentid.toString(),
|
||||
successful: true,
|
||||
response: values.paymentResponse
|
||||
}
|
||||
]
|
||||
}
|
||||
}))
|
||||
},
|
||||
refetchQueries: ["GET_JOB_BY_PK"]
|
||||
});
|
||||
toggleModalVisible();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
notification.open({
|
||||
type: "error",
|
||||
message: t("payments.errors.inserting", { error: error.message })
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleIntelliPayCharge = async () => {
|
||||
setLoading(true);
|
||||
|
||||
//Validate
|
||||
try {
|
||||
await form.validateFields();
|
||||
@@ -140,7 +100,8 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
try {
|
||||
const response = await axios.post("/intellipay/lightbox_credentials", {
|
||||
bodyshop,
|
||||
refresh: !!window.intellipay
|
||||
refresh: !!window.intellipay,
|
||||
paymentSplitMeta: form.getFieldsValue(),
|
||||
});
|
||||
|
||||
if (window.intellipay) {
|
||||
@@ -169,7 +130,6 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
<Card title="Card Payment">
|
||||
<Spin spinning={loading}>
|
||||
<Form
|
||||
onFinish={handleFinish}
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
@@ -246,18 +206,14 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
}
|
||||
>
|
||||
{() => {
|
||||
console.log("Updating the owner info section.");
|
||||
//If all of the job ids have been fileld in, then query and update the IP field.
|
||||
const { payments } = form.getFieldsValue();
|
||||
if (payments?.length > 0 && payments?.filter((p) => p?.jobid).length === payments?.length) {
|
||||
console.log("**Calling refetch.");
|
||||
if (
|
||||
payments?.length > 0 &&
|
||||
payments?.filter((p) => p?.jobid).length === payments?.length
|
||||
) {
|
||||
refetch({ jobids: payments.map((p) => p.jobid) });
|
||||
}
|
||||
console.log(
|
||||
"Acc info",
|
||||
data,
|
||||
payments && data && data.jobs.length > 0 ? data.jobs.map((j) => j.ro_number).join(", ") : null
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
@@ -300,6 +256,13 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
type="hidden"
|
||||
value={totalAmountToCharge?.toFixed(2)}
|
||||
/>
|
||||
<Input
|
||||
className="ipayfield"
|
||||
data-ipayname="comment"
|
||||
type="hidden"
|
||||
value={btoa(JSON.stringify(payments))}
|
||||
hidden
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
// data-ipayname="submit"
|
||||
@@ -314,11 +277,6 @@ const CardPaymentModalComponent = ({ bodyshop, cardPaymentModal, toggleModalVisi
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
|
||||
{/* Lightbox payment response when it is completed */}
|
||||
<Form.Item name="paymentResponse" hidden>
|
||||
<Input type="hidden" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Spin>
|
||||
</Card>
|
||||
|
||||
@@ -28,6 +28,7 @@ const CourtesyCarStatusComponent = ({ value, onChange }, ref) => {
|
||||
<Option value="courtesycars.status.out">{t("courtesycars.status.out")}</Option>
|
||||
<Option value="courtesycars.status.sold">{t("courtesycars.status.sold")}</Option>
|
||||
<Option value="courtesycars.status.leasereturn">{t("courtesycars.status.leasereturn")}</Option>
|
||||
<Option value="courtesycars.status.unavailable">{t("courtesycars.status.unavailable")}</Option>
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -61,7 +61,11 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
||||
{
|
||||
text: t("courtesycars.status.leasereturn"),
|
||||
value: "courtesycars.status.leasereturn"
|
||||
}
|
||||
},
|
||||
{
|
||||
text: t("courtesycars.status.unavailable"),
|
||||
value: "courtesycars.status.unavailable",
|
||||
},
|
||||
],
|
||||
onFilter: (value, record) => record.status === value,
|
||||
sortOrder: state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||
|
||||
@@ -9,7 +9,6 @@ import axios from "axios";
|
||||
const fortyFiveDaysAgo = () => dayjs().subtract(45, "day").toLocaleString();
|
||||
|
||||
export default function JobLifecycleDashboardComponent({ data, bodyshop, ...cardProps }) {
|
||||
console.log("🚀 ~ JobLifecycleDashboardComponent ~ bodyshop:", bodyshop);
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [lifecycleData, setLifecycleData] = useState(null);
|
||||
@@ -143,7 +142,7 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card
|
||||
>
|
||||
<div>
|
||||
{lifecycleData.summations.map((key) => (
|
||||
<Tag color={key.color} style={{ width: "13vh", padding: "4px", margin: "4px" }}>
|
||||
<Tag key={key.status} color={key.color} style={{ width: "13vh", padding: "4px", margin: "4px" }}>
|
||||
<div
|
||||
aria-label={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||
title={`${key.status} | ${key.roundedPercentage} | ${key.humanReadable}`}
|
||||
@@ -165,6 +164,7 @@ export default function JobLifecycleDashboardComponent({ data, bodyshop, ...card
|
||||
size="small"
|
||||
pagination={false}
|
||||
columns={columns}
|
||||
rowKey={(record) => record.status}
|
||||
dataSource={lifecycleData.summations.sort((a, b) => b.value - a.value).slice(0, 3)}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -36,9 +36,6 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
};
|
||||
});
|
||||
|
||||
console.log("Scheduled Out Today");
|
||||
console.dir(scheduledOutToday);
|
||||
|
||||
const tvFontSize = 18;
|
||||
const tvFontWeight = "bold";
|
||||
|
||||
@@ -89,8 +86,6 @@ export default function DashboardScheduledOutToday({ data, ...cardProps }) {
|
||||
sorter: (a, b) => alphaSort(OwnerNameDisplayFunction(a), OwnerNameDisplayFunction(b)),
|
||||
sortOrder: state.sortedInfo.columnKey === "owner" && state.sortedInfo.order,
|
||||
render: (text, record) => {
|
||||
console.log("Render record out today");
|
||||
console.dir(record);
|
||||
return record.ownerid ? (
|
||||
<Link to={"/manage/owners/" + record.ownerid} onClick={(e) => e.stopPropagation()}>
|
||||
<span style={{ fontSize: tvFontSize, fontWeight: tvFontWeight }}>
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
Typography
|
||||
} from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import dayjs from "../../utils/day";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -22,6 +21,7 @@ import { createStructuredSelector } from "reselect";
|
||||
import { determineDmsType } from "../../pages/dms/dms.container";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import i18n from "../../translations/i18n";
|
||||
import dayjs from "../../utils/day";
|
||||
import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component";
|
||||
import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component";
|
||||
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||
@@ -89,7 +89,7 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
|
||||
job.area_of_damage && job.area_of_damage.impact1
|
||||
? " " +
|
||||
t("jobs.labels.dms.damageto", {
|
||||
area_of_damage: (job.area_of_damage && job.area_of_damage.impact1) || "UNKNOWN"
|
||||
area_of_damage: (job.area_of_damage && job.area_of_damage.impact1.padStart(2, "0")) || "UNKNOWN"
|
||||
})
|
||||
: ""
|
||||
}`.slice(0, 239),
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Select, Space, Tag } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const { Option } = Select;
|
||||
//To be used as a form element only.
|
||||
|
||||
const EmployeeSearchSelectEmail = ({ options, ...props }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Select
|
||||
showSearch
|
||||
// value={option}
|
||||
style={{
|
||||
width: 400
|
||||
}}
|
||||
optionFilterProp="search"
|
||||
{...props}
|
||||
>
|
||||
{options
|
||||
? options.map((o) => (
|
||||
<Option key={o.id} value={o.user_email} search={`${o.employee_number} ${o.first_name} ${o.last_name}`}>
|
||||
<Space>
|
||||
{`${o.employee_number} ${o.first_name} ${o.last_name}`}
|
||||
|
||||
<Tag color="green">
|
||||
{o.flat_rate ? t("timetickets.labels.flat_rate") : t("timetickets.labels.straight_time")}
|
||||
</Tag>
|
||||
</Space>
|
||||
</Option>
|
||||
))
|
||||
: null}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
export default EmployeeSearchSelectEmail;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { DatePicker } from "antd";
|
||||
import dayjs from "../../utils/day.js";
|
||||
import React, { useRef } from "react";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(FormDateTimePickerEnhanced);
|
||||
|
||||
const dateFormat = "MM/DD/YYYY h:mm a";
|
||||
|
||||
export function FormDateTimePickerEnhanced({
|
||||
bodyshop,
|
||||
value,
|
||||
onBlur,
|
||||
onlyFuture,
|
||||
onlyToday,
|
||||
isDateOnly = true,
|
||||
...restProps
|
||||
}) {
|
||||
const ref = useRef();
|
||||
return (
|
||||
<div>
|
||||
<DatePicker
|
||||
ref={ref}
|
||||
value={value ? dayjs(value) : null}
|
||||
format={dateFormat}
|
||||
onBlur={onBlur}
|
||||
showToday={false}
|
||||
disabledDate={(d) => {
|
||||
if (onlyToday) {
|
||||
return !dayjs().isSame(d, "day");
|
||||
} else if (onlyFuture) {
|
||||
return dayjs().subtract(1, "day").isAfter(d);
|
||||
}
|
||||
}}
|
||||
{...restProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { forwardRef } from "react";
|
||||
//import DatePicker from "react-datepicker";
|
||||
//import "react-datepicker/src/stylesheets/datepicker.scss";
|
||||
import { TimePicker } from "antd";
|
||||
import { Space, TimePicker } from "antd";
|
||||
import dayjs from "../../utils/day";
|
||||
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||
//To be used as a form element only.
|
||||
@@ -14,7 +14,7 @@ const DateTimePicker = ({ value, onChange, onBlur, id, onlyFuture, ...restProps
|
||||
// };
|
||||
|
||||
return (
|
||||
<div id={id}>
|
||||
<Space direction="vertical" style={{ width: "100%" }} id={id}>
|
||||
<FormDatePicker
|
||||
{...restProps}
|
||||
{...(onlyFuture && {
|
||||
@@ -39,7 +39,7 @@ const DateTimePicker = ({ value, onChange, onBlur, id, onlyFuture, ...restProps
|
||||
format="hh:mm a"
|
||||
{...restProps}
|
||||
/>
|
||||
</div>
|
||||
</Space>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,14 +3,13 @@ import axios from "axios";
|
||||
import _ from "lodash";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { Link } from "react-router-dom";
|
||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
import OwnerNameDisplay, { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||
|
||||
export default function GlobalSearchOs() {
|
||||
const { t } = useTranslation();
|
||||
const history = useNavigate();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState(false);
|
||||
|
||||
@@ -178,15 +177,7 @@ export default function GlobalSearchOs() {
|
||||
};
|
||||
|
||||
return (
|
||||
<AutoComplete
|
||||
options={data}
|
||||
onSearch={handleSearch}
|
||||
defaultActiveFirstOption
|
||||
onSelect={(val, opt) => {
|
||||
history(opt.label.props.to);
|
||||
}}
|
||||
onClear={() => setData([])}
|
||||
>
|
||||
<AutoComplete options={data} onSearch={handleSearch} defaultActiveFirstOption onClear={() => setData([])}>
|
||||
<Input.Search
|
||||
size="large"
|
||||
placeholder={t("general.labels.globalsearch")}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { AutoComplete, Divider, Input, Space } from "antd";
|
||||
import _ from "lodash";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { Link } from "react-router-dom";
|
||||
import { GLOBAL_SEARCH_QUERY } from "../../graphql/search.queries";
|
||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
@@ -12,7 +12,6 @@ import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.compon
|
||||
|
||||
export default function GlobalSearch() {
|
||||
const { t } = useTranslation();
|
||||
const history = useNavigate();
|
||||
const [callSearch, { loading, error, data }] = useLazyQuery(GLOBAL_SEARCH_QUERY);
|
||||
|
||||
const executeSearch = (v) => {
|
||||
@@ -157,14 +156,7 @@ export default function GlobalSearch() {
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
return (
|
||||
<AutoComplete
|
||||
options={options}
|
||||
onSearch={handleSearch}
|
||||
defaultActiveFirstOption
|
||||
onSelect={(val, opt) => {
|
||||
history(opt.label.props.to);
|
||||
}}
|
||||
>
|
||||
<AutoComplete options={options} onSearch={handleSearch} defaultActiveFirstOption>
|
||||
<Input.Search
|
||||
size="large"
|
||||
placeholder={t("general.labels.globalsearch")}
|
||||
|
||||
@@ -17,6 +17,7 @@ import Icon, {
|
||||
LineChartOutlined,
|
||||
PaperClipOutlined,
|
||||
PhoneOutlined,
|
||||
PlusCircleOutlined,
|
||||
QuestionCircleFilled,
|
||||
ScheduleOutlined,
|
||||
SettingOutlined,
|
||||
@@ -30,7 +31,8 @@ import { Layout, Menu, Switch, Tooltip } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BsKanban } from "react-icons/bs";
|
||||
import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar } from "react-icons/fa";
|
||||
import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar, FaTasks } from "react-icons/fa";
|
||||
import { FiLogOut } from "react-icons/fi";
|
||||
import { GiPayMoney, GiPlayerTime, GiSettingsKnobs } from "react-icons/gi";
|
||||
import { IoBusinessOutline } from "react-icons/io5";
|
||||
import { RiSurveyLine } from "react-icons/ri";
|
||||
@@ -41,7 +43,6 @@ import { selectRecentItems, selectSelectedHeader } from "../../redux/application
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { signOutStart } from "../../redux/user/user.actions";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import { FiLogOut } from "react-icons/fi";
|
||||
import { checkBeta, handleBeta, setBeta } from "../../utils/betaHandler";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
@@ -54,12 +55,43 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" })),
|
||||
setTimeTicketContext: (context) => dispatch(setModalContext({ context: context, modal: "timeTicket" })),
|
||||
setBillEnterContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "billEnter"
|
||||
})
|
||||
),
|
||||
setTimeTicketContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "timeTicket"
|
||||
})
|
||||
),
|
||||
setPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "payment" })),
|
||||
setReportCenterContext: (context) => dispatch(setModalContext({ context: context, modal: "reportCenter" })),
|
||||
setReportCenterContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "reportCenter"
|
||||
})
|
||||
),
|
||||
signOutStart: () => dispatch(signOutStart()),
|
||||
setCardPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "cardPayment" }))
|
||||
setCardPaymentContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "cardPayment"
|
||||
})
|
||||
),
|
||||
setTaskUpsertContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "taskUpsert"
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
function Header({
|
||||
@@ -73,7 +105,8 @@ function Header({
|
||||
setPaymentContext,
|
||||
setReportCenterContext,
|
||||
recentItems,
|
||||
setCardPaymentContext
|
||||
setCardPaymentContext,
|
||||
setTaskUpsertContext
|
||||
}) {
|
||||
const {
|
||||
treatments: { ImEXPay, DmsAp, Simple_Inventory }
|
||||
@@ -108,11 +141,13 @@ function Header({
|
||||
accountingChildren.push(
|
||||
{
|
||||
key: "bills",
|
||||
id: "header-accounting-bills",
|
||||
icon: <Icon component={FaFileInvoiceDollar} />,
|
||||
label: <Link to="/manage/bills">{t("menus.header.bills")}</Link>
|
||||
},
|
||||
{
|
||||
key: "enterbills",
|
||||
id: "header-accounting-enterbills",
|
||||
icon: <Icon component={GiPayMoney} />,
|
||||
label: t("menus.header.enterbills"),
|
||||
onClick: () => {
|
||||
@@ -132,6 +167,7 @@ function Header({
|
||||
},
|
||||
{
|
||||
key: "inventory",
|
||||
id: "header-accounting-inventory",
|
||||
icon: <Icon component={FaFileInvoiceDollar} />,
|
||||
label: <Link to="/manage/inventory">{t("menus.header.inventory")}</Link>
|
||||
}
|
||||
@@ -150,11 +186,13 @@ function Header({
|
||||
},
|
||||
{
|
||||
key: "allpayments",
|
||||
id: "header-accounting-allpayments",
|
||||
icon: <BankFilled />,
|
||||
label: <Link to="/manage/payments">{t("menus.header.allpayments")}</Link>
|
||||
},
|
||||
{
|
||||
key: "enterpayments",
|
||||
id: "header-accounting-enterpayments",
|
||||
icon: <Icon component={FaCreditCard} />,
|
||||
label: t("menus.header.enterpayment"),
|
||||
onClick: () => {
|
||||
@@ -170,6 +208,7 @@ function Header({
|
||||
if (ImEXPay.treatment === "on") {
|
||||
accountingChildren.push({
|
||||
key: "entercardpayments",
|
||||
id: "header-accounting-entercardpayments",
|
||||
icon: <Icon component={FaCreditCard} />,
|
||||
label: t("menus.header.entercardpayment"),
|
||||
onClick: () => {
|
||||
@@ -194,6 +233,7 @@ function Header({
|
||||
},
|
||||
{
|
||||
key: "timetickets",
|
||||
id: "header-accounting-timetickets",
|
||||
icon: <FieldTimeOutlined />,
|
||||
label: <Link to="/manage/timetickets">{t("menus.header.timetickets")}</Link>
|
||||
}
|
||||
@@ -202,6 +242,7 @@ function Header({
|
||||
if (bodyshop?.md_tasks_presets?.use_approvals) {
|
||||
accountingChildren.push({
|
||||
key: "ttapprovals",
|
||||
id: "header-accounting-ttapprovals",
|
||||
icon: <FieldTimeOutlined />,
|
||||
label: <Link to="/manage/ttapprovals">{t("menus.header.ttapprovals")}</Link>
|
||||
});
|
||||
@@ -211,6 +252,7 @@ function Header({
|
||||
key: "entertimetickets",
|
||||
icon: <Icon component={GiPlayerTime} />,
|
||||
label: t("menus.header.entertimeticket"),
|
||||
id: "header-accounting-entertimetickets",
|
||||
onClick: () => {
|
||||
setTimeTicketContext({
|
||||
actions: {},
|
||||
@@ -231,6 +273,7 @@ function Header({
|
||||
const accountingExportChildren = [
|
||||
{
|
||||
key: "receivables",
|
||||
id: "header-accounting-receivables",
|
||||
label: <Link to="/manage/accounting/receivables">{t("menus.header.accounting-receivables")}</Link>
|
||||
}
|
||||
];
|
||||
@@ -238,6 +281,7 @@ function Header({
|
||||
if (!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber)) || DmsAp.treatment === "on") {
|
||||
accountingExportChildren.push({
|
||||
key: "payables",
|
||||
id: "header-accounting-payables",
|
||||
label: <Link to="/manage/accounting/payables">{t("menus.header.accounting-payables")}</Link>
|
||||
});
|
||||
}
|
||||
@@ -245,6 +289,7 @@ function Header({
|
||||
if (!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber))) {
|
||||
accountingExportChildren.push({
|
||||
key: "payments",
|
||||
id: "header-accounting-payments",
|
||||
label: <Link to="/manage/accounting/payments">{t("menus.header.accounting-payments")}</Link>
|
||||
});
|
||||
}
|
||||
@@ -255,6 +300,7 @@ function Header({
|
||||
},
|
||||
{
|
||||
key: "exportlogs",
|
||||
id: "header-accounting-exportlogs",
|
||||
label: <Link to="/manage/accounting/exportlogs">{t("menus.header.export-logs")}</Link>
|
||||
}
|
||||
);
|
||||
@@ -268,6 +314,7 @@ function Header({
|
||||
) {
|
||||
accountingChildren.push({
|
||||
key: "accountingexport",
|
||||
id: "header-accounting-export",
|
||||
icon: <ExportOutlined />,
|
||||
label: t("menus.header.export"),
|
||||
children: accountingExportChildren
|
||||
@@ -278,10 +325,12 @@ function Header({
|
||||
{
|
||||
key: "home",
|
||||
icon: <HomeFilled />,
|
||||
id: "header-home",
|
||||
label: <Link to="/manage/">{t("menus.header.home")}</Link>
|
||||
},
|
||||
{
|
||||
key: "schedule",
|
||||
id: "header-schedule",
|
||||
icon: <Icon component={FaCalendarAlt} />,
|
||||
label: <Link to="/manage/schedule">{t("menus.header.schedule")}</Link>
|
||||
},
|
||||
@@ -293,16 +342,19 @@ function Header({
|
||||
children: [
|
||||
{
|
||||
key: "activejobs",
|
||||
id: "header-active-jobs",
|
||||
icon: <FileFilled />,
|
||||
label: <Link to="/manage/jobs">{t("menus.header.activejobs")}</Link>
|
||||
},
|
||||
{
|
||||
key: "readyjobs",
|
||||
id: "header-ready-jobs",
|
||||
icon: <CheckCircleOutlined />,
|
||||
label: <Link to="/manage/jobs/ready">{t("menus.header.readyjobs")}</Link>
|
||||
},
|
||||
{
|
||||
key: "parts-queue",
|
||||
id: "header-parts-queue",
|
||||
icon: <ToolFilled />,
|
||||
label: <Link to="/manage/partsqueue">{t("menus.header.parts-queue")}</Link>
|
||||
},
|
||||
@@ -314,6 +366,7 @@ function Header({
|
||||
},
|
||||
{
|
||||
key: "newjob",
|
||||
id: "header-new-job",
|
||||
icon: <FileAddOutlined />,
|
||||
label: <Link to="/manage/jobs/new">{t("menus.header.newjob")}</Link>
|
||||
},
|
||||
@@ -322,6 +375,7 @@ function Header({
|
||||
},
|
||||
{
|
||||
key: "alljobs",
|
||||
id: "header-all-jobs",
|
||||
icon: <UnorderedListOutlined />,
|
||||
label: <Link to="/manage/jobs/all">{t("menus.header.alljobs")}</Link>
|
||||
},
|
||||
@@ -330,6 +384,7 @@ function Header({
|
||||
},
|
||||
{
|
||||
key: "productionlist",
|
||||
id: "header-production-list",
|
||||
icon: <ScheduleOutlined />,
|
||||
label: <Link to="/manage/production/list">{t("menus.header.productionlist")}</Link>
|
||||
},
|
||||
@@ -341,6 +396,7 @@ function Header({
|
||||
? [
|
||||
{
|
||||
key: "productionboard",
|
||||
id: "header-production-board",
|
||||
icon: <Icon component={BsKanban} />,
|
||||
label: <Link to="/manage/production/board">{t("menus.header.productionboard")}</Link>
|
||||
}
|
||||
@@ -358,6 +414,7 @@ function Header({
|
||||
},
|
||||
{
|
||||
key: "scoreboard",
|
||||
id: "header-scoreboard",
|
||||
icon: <LineChartOutlined />,
|
||||
label: <Link to="/manage/scoreboard">{t("menus.header.scoreboard")}</Link>
|
||||
}
|
||||
@@ -368,15 +425,18 @@ function Header({
|
||||
{
|
||||
key: "customers",
|
||||
icon: <UserOutlined />,
|
||||
id: "header-customers",
|
||||
label: t("menus.header.customers"),
|
||||
children: [
|
||||
{
|
||||
key: "owners",
|
||||
id: "header-owners",
|
||||
icon: <TeamOutlined />,
|
||||
label: <Link to="/manage/owners">{t("menus.header.owners")}</Link>
|
||||
},
|
||||
{
|
||||
key: "vehicles",
|
||||
id: "header-vehicles",
|
||||
icon: <CarFilled />,
|
||||
label: <Link to="/manage/vehicles">{t("menus.header.vehicles")}</Link>
|
||||
}
|
||||
@@ -390,21 +450,25 @@ function Header({
|
||||
? [
|
||||
{
|
||||
key: "ccs",
|
||||
id: "header-css",
|
||||
icon: <CarFilled />,
|
||||
label: t("menus.header.courtesycars"),
|
||||
children: [
|
||||
{
|
||||
key: "courtesycarsall",
|
||||
id: "header-courtesycars-all",
|
||||
icon: <CarFilled />,
|
||||
label: <Link to="/manage/courtesycars">{t("menus.header.courtesycars-all")}</Link>
|
||||
},
|
||||
{
|
||||
key: "contracts",
|
||||
id: "header-contracts",
|
||||
icon: <FileFilled />,
|
||||
label: <Link to="/manage/courtesycars/contracts">{t("menus.header.courtesycars-contracts")}</Link>
|
||||
},
|
||||
{
|
||||
key: "newcontract",
|
||||
id: "header-newcontract",
|
||||
icon: <FileAddFilled />,
|
||||
label: <Link to="/manage/courtesycars/contracts/new">{t("menus.header.courtesycars-newcontract")}</Link>
|
||||
}
|
||||
@@ -417,6 +481,7 @@ function Header({
|
||||
? [
|
||||
{
|
||||
key: "accounting",
|
||||
id: "header-accounting",
|
||||
icon: <DollarCircleFilled />,
|
||||
label: t("menus.header.accounting"),
|
||||
children: accountingChildren
|
||||
@@ -425,6 +490,7 @@ function Header({
|
||||
: []),
|
||||
{
|
||||
key: "phonebook",
|
||||
id: "header-phonebook",
|
||||
icon: <PhoneOutlined />,
|
||||
label: <Link to="/manage/phonebook">{t("menus.header.phonebook")}</Link>
|
||||
},
|
||||
@@ -436,28 +502,62 @@ function Header({
|
||||
? [
|
||||
{
|
||||
key: "temporarydocs",
|
||||
id: "header-temporarydocs",
|
||||
icon: <PaperClipOutlined />,
|
||||
label: <Link to="/manage/temporarydocs">{t("menus.header.temporarydocs")}</Link>
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: "tasks",
|
||||
id: "tasks",
|
||||
icon: <FaTasks />,
|
||||
label: t("menus.header.tasks"),
|
||||
children: [
|
||||
{
|
||||
key: "createTask",
|
||||
icon: <PlusCircleOutlined />,
|
||||
label: t("menus.header.create_task"),
|
||||
onClick: () => {
|
||||
setTaskUpsertContext({
|
||||
actions: {},
|
||||
context: {}
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
key: "mytasks",
|
||||
icon: <FaTasks />,
|
||||
label: <Link to="/manage/tasks/mytasks">{t("menus.header.my_tasks")}</Link>
|
||||
},
|
||||
{
|
||||
key: "all_tasks",
|
||||
icon: <FaTasks />,
|
||||
label: <Link to="/manage/tasks/alltasks">{t("menus.header.all_tasks")}</Link>
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "shopsubmenu",
|
||||
id: "header-shopsubmenu",
|
||||
icon: <SettingOutlined />,
|
||||
label: t("menus.header.shop"),
|
||||
children: [
|
||||
{
|
||||
key: "shop",
|
||||
id: "header-shop",
|
||||
icon: <Icon component={GiSettingsKnobs} />,
|
||||
label: <Link to="/manage/shop?tab=info">{t("menus.header.shop_config")}</Link>
|
||||
},
|
||||
{
|
||||
key: "dashboard",
|
||||
id: "header-dashboard",
|
||||
icon: <DashboardFilled />,
|
||||
label: <Link to="/manage/dashboard">{t("menus.header.dashboard")}</Link>
|
||||
},
|
||||
{
|
||||
key: "reportcenter",
|
||||
id: "header-reportcenter",
|
||||
icon: <BarChartOutlined />,
|
||||
label: t("menus.header.reportcenter"),
|
||||
onClick: () => {
|
||||
@@ -469,6 +569,7 @@ function Header({
|
||||
},
|
||||
{
|
||||
key: "shop-vendors",
|
||||
id: "header-shop-vendors",
|
||||
icon: <Icon component={IoBusinessOutline} />,
|
||||
label: <Link to="/manage/shop/vendors">{t("menus.header.shop_vendors")}</Link>
|
||||
},
|
||||
@@ -480,6 +581,7 @@ function Header({
|
||||
? [
|
||||
{
|
||||
key: "shop-csi",
|
||||
id: "header-shop-csi",
|
||||
icon: <Icon component={RiSurveyLine} />,
|
||||
label: <Link to="/manage/shop/csi">{t("menus.header.shop_csi")}</Link>
|
||||
}
|
||||
@@ -493,6 +595,7 @@ function Header({
|
||||
children: [
|
||||
{
|
||||
key: "signout",
|
||||
id: "header-signout",
|
||||
icon: <Icon component={FiLogOut} />,
|
||||
danger: true,
|
||||
label: t("user.actions.signout"),
|
||||
@@ -500,6 +603,7 @@ function Header({
|
||||
},
|
||||
{
|
||||
key: "help",
|
||||
id: "header-help",
|
||||
icon: <Icon component={QuestionCircleFilled} />,
|
||||
label: t("menus.header.help"),
|
||||
onClick: () => {
|
||||
@@ -514,14 +618,22 @@ function Header({
|
||||
);
|
||||
}
|
||||
},
|
||||
// {
|
||||
// key: 'rescue',
|
||||
// icon: <Icon component={CarFilled}/>,
|
||||
// label: t("menus.header.rescueme"),
|
||||
// onClick: () => {
|
||||
// window.open("https://imexrescue.com/", "_blank");
|
||||
// }
|
||||
// },
|
||||
...(InstanceRenderManager({
|
||||
imex: true,
|
||||
rome: false,
|
||||
promanager: false
|
||||
})
|
||||
? [
|
||||
{
|
||||
key: "rescue",
|
||||
icon: <Icon component={CarFilled} />,
|
||||
label: t("menus.header.rescueme"),
|
||||
onClick: () => {
|
||||
window.open("https://imexrescue.com/", "_blank");
|
||||
}
|
||||
}
|
||||
]
|
||||
: []),
|
||||
|
||||
...(InstanceRenderManager({
|
||||
imex: true,
|
||||
@@ -531,6 +643,7 @@ function Header({
|
||||
? [
|
||||
{
|
||||
key: "shiftclock",
|
||||
id: "header-shiftclock",
|
||||
icon: <Icon component={GiPlayerTime} />,
|
||||
label: <Link to="/manage/shiftclock">{t("menus.header.shiftclock")}</Link>
|
||||
}
|
||||
@@ -538,6 +651,7 @@ function Header({
|
||||
: []),
|
||||
{
|
||||
key: "profile",
|
||||
id: "header-profile",
|
||||
icon: <UserOutlined />,
|
||||
label: <Link to="/manage/profile">{t("menus.currentuser.profile")}</Link>
|
||||
}
|
||||
@@ -573,6 +687,7 @@ function Header({
|
||||
{
|
||||
key: "recent",
|
||||
icon: <ClockCircleFilled />,
|
||||
id: "header-recent",
|
||||
children: recentItems.map((i, idx) => ({
|
||||
key: idx,
|
||||
label: <Link to={i.url}>{i.label}</Link>
|
||||
@@ -586,6 +701,7 @@ function Header({
|
||||
imex: () => {
|
||||
menuItems.push({
|
||||
key: "beta-switch",
|
||||
id: "header-beta-switch",
|
||||
style: { marginLeft: "auto" },
|
||||
label: (
|
||||
<Tooltip
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Alert, Card, Col, Row, Space, Statistic, Tooltip, Typography } from 'antd';
|
||||
import Dinero from 'dinero.js';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import InstanceRenderManager from '../../utils/instanceRenderMgr';
|
||||
import AlertComponent from '../alert/alert.component';
|
||||
import LoadingSkeleton from '../loading-skeleton/loading-skeleton.component';
|
||||
import './job-bills-total.styles.scss';
|
||||
import { Alert, Card, Col, Row, Space, Statistic, Tooltip, Typography } from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import "./job-bills-total.styles.scss";
|
||||
|
||||
export default function JobBillsTotalComponent({
|
||||
loading,
|
||||
@@ -13,16 +13,16 @@ export default function JobBillsTotalComponent({
|
||||
partsOrders,
|
||||
jobTotals,
|
||||
showWarning,
|
||||
warningCallback,
|
||||
warningCallback
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (loading) return <LoadingSkeleton />;
|
||||
if (!!!jobTotals) {
|
||||
if (showWarning && warningCallback && typeof warningCallback === 'function') {
|
||||
warningCallback({ key: 'bills', warning: t('jobs.errors.nofinancial') });
|
||||
if (showWarning && warningCallback && typeof warningCallback === "function") {
|
||||
warningCallback({ key: "bills", warning: t("jobs.errors.nofinancial") });
|
||||
}
|
||||
return <AlertComponent type="error" message={t('jobs.errors.nofinancial')} />;
|
||||
return <AlertComponent type="error" message={t("jobs.errors.nofinancial")} />;
|
||||
}
|
||||
|
||||
const totals = jobTotals;
|
||||
@@ -109,64 +109,60 @@ export default function JobBillsTotalComponent({
|
||||
const discrepWithCms = discrepWithLbrAdj.add(totalReturns);
|
||||
const calculatedCreditsNotReceived = totalReturns.subtract(billCms); //billCms is tracked as a negative number.
|
||||
|
||||
if (showWarning && warningCallback && typeof warningCallback === 'function') {
|
||||
if (
|
||||
discrepWithCms.getAmount() !== 0 ||
|
||||
discrepWithLbrAdj.getAmount() !== 0 ||
|
||||
discrepancy.getAmount() !== 0
|
||||
) {
|
||||
if (showWarning && warningCallback && typeof warningCallback === "function") {
|
||||
if (discrepWithCms.getAmount() !== 0) {
|
||||
warningCallback({
|
||||
key: 'bills',
|
||||
warning: t('jobs.labels.outstanding_reconciliation_discrep'),
|
||||
key: "bills",
|
||||
warning: t("jobs.labels.outstanding_reconciliation_discrep")
|
||||
});
|
||||
}
|
||||
if (calculatedCreditsNotReceived.getAmount() > 0) {
|
||||
warningCallback({ key: 'cm', warning: t('jobs.labels.outstanding_credit_memos') });
|
||||
warningCallback({ key: "cm", warning: t("jobs.labels.outstanding_credit_memos") });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col md={24} lg={18}>
|
||||
<Card title={t('jobs.labels.jobtotals')} style={{ height: '100%' }}>
|
||||
<Card title={t("jobs.labels.jobtotals")} style={{ height: "100%" }}>
|
||||
<Space wrap size="large">
|
||||
<Tooltip
|
||||
title={
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t('jobs.labels.plitooltips.partstotal'),
|
||||
__html: t("jobs.labels.plitooltips.partstotal")
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Statistic title={t('jobs.labels.rosaletotal')} value={totalPartsSublet.toFormat()} />
|
||||
<Statistic title={t("jobs.labels.rosaletotal")} value={totalPartsSublet.toFormat()} />
|
||||
</Tooltip>
|
||||
<Typography.Title>-</Typography.Title>
|
||||
<Tooltip
|
||||
title={
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t('jobs.labels.plitooltips.billtotal'),
|
||||
__html: t("jobs.labels.plitooltips.billtotal")
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Statistic title={t('bills.labels.retailtotal')} value={billTotals.toFormat()} />
|
||||
<Statistic title={t("bills.labels.retailtotal")} value={billTotals.toFormat()} />
|
||||
</Tooltip>
|
||||
<Typography.Title>=</Typography.Title>
|
||||
<Tooltip
|
||||
title={
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t('jobs.labels.plitooltips.discrep1'),
|
||||
__html: t("jobs.labels.plitooltips.discrep1")
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Statistic
|
||||
title={t('bills.labels.discrepancy')}
|
||||
title={t("bills.labels.discrepancy")}
|
||||
valueStyle={{
|
||||
color: discrepancy.getAmount() === 0 ? 'green' : 'red',
|
||||
color: discrepancy.getAmount() === 0 ? "green" : "red"
|
||||
}}
|
||||
value={discrepancy.toFormat()}
|
||||
/>
|
||||
@@ -176,27 +172,27 @@ export default function JobBillsTotalComponent({
|
||||
title={
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t('jobs.labels.plitooltips.laboradj'),
|
||||
__html: t("jobs.labels.plitooltips.laboradj")
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Statistic title={t('bills.labels.dedfromlbr')} value={lbrAdjustments.toFormat()} />
|
||||
<Statistic title={t("bills.labels.dedfromlbr")} value={lbrAdjustments.toFormat()} />
|
||||
</Tooltip>
|
||||
<Typography.Title>=</Typography.Title>
|
||||
<Tooltip
|
||||
title={
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t('jobs.labels.plitooltips.discrep2'),
|
||||
__html: t("jobs.labels.plitooltips.discrep2")
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Statistic
|
||||
title={t('bills.labels.discrepancy')}
|
||||
title={t("bills.labels.discrepancy")}
|
||||
valueStyle={{
|
||||
color: discrepWithLbrAdj.getAmount() === 0 ? 'green' : 'red',
|
||||
color: discrepWithLbrAdj.getAmount() === 0 ? "green" : "red"
|
||||
}}
|
||||
value={discrepWithLbrAdj.toFormat()}
|
||||
/>
|
||||
@@ -206,27 +202,27 @@ export default function JobBillsTotalComponent({
|
||||
title={
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t('jobs.labels.plitooltips.creditmemos'),
|
||||
__html: t("jobs.labels.plitooltips.creditmemos")
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Statistic title={t('bills.labels.totalreturns')} value={totalReturns.toFormat()} />
|
||||
<Statistic title={t("bills.labels.totalreturns")} value={totalReturns.toFormat()} />
|
||||
</Tooltip>
|
||||
<Typography.Title>=</Typography.Title>
|
||||
<Tooltip
|
||||
title={
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t('jobs.labels.plitooltips.discrep3'),
|
||||
__html: t("jobs.labels.plitooltips.discrep3")
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Statistic
|
||||
title={t('bills.labels.discrepancy')}
|
||||
title={t("bills.labels.discrepancy")}
|
||||
valueStyle={{
|
||||
color: discrepWithCms.getAmount() === 0 ? 'green' : 'red',
|
||||
color: discrepWithCms.getAmount() === 0 ? "green" : "red"
|
||||
}}
|
||||
value={discrepWithCms.toFormat()}
|
||||
/>
|
||||
@@ -238,40 +234,40 @@ export default function JobBillsTotalComponent({
|
||||
discrepWithLbrAdj.getAmount() !== 0 ||
|
||||
discrepancy.getAmount() !== 0) && (
|
||||
<Alert
|
||||
style={{ margin: '8px 0px' }}
|
||||
style={{ margin: "8px 0px" }}
|
||||
type="warning"
|
||||
message={t('jobs.labels.outstanding_reconciliation_discrep')}
|
||||
message={t("jobs.labels.outstanding_reconciliation_discrep")}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
<Col md={24} lg={6}>
|
||||
<Card title={t('jobs.labels.returntotals')} style={{ height: '100%' }}>
|
||||
<Card title={t("jobs.labels.returntotals")} style={{ height: "100%" }}>
|
||||
<Space wrap>
|
||||
<Tooltip
|
||||
title={
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t('jobs.labels.plitooltips.totalreturns'),
|
||||
__html: t("jobs.labels.plitooltips.totalreturns")
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Statistic title={t('bills.labels.totalreturns')} value={totalReturns.toFormat()} />
|
||||
<Statistic title={t("bills.labels.totalreturns")} value={totalReturns.toFormat()} />
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
title={
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t('jobs.labels.plitooltips.calculatedcreditsnotreceived'),
|
||||
__html: t("jobs.labels.plitooltips.calculatedcreditsnotreceived")
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Statistic
|
||||
title={t('bills.labels.calculatedcreditsnotreceived')}
|
||||
title={t("bills.labels.calculatedcreditsnotreceived")}
|
||||
valueStyle={{
|
||||
color: calculatedCreditsNotReceived.getAmount() <= 0 ? 'green' : 'red',
|
||||
color: calculatedCreditsNotReceived.getAmount() <= 0 ? "green" : "red"
|
||||
}}
|
||||
value={
|
||||
calculatedCreditsNotReceived.getAmount() >= 0
|
||||
@@ -284,15 +280,15 @@ export default function JobBillsTotalComponent({
|
||||
title={
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: t('jobs.labels.plitooltips.creditsnotreceived'),
|
||||
__html: t("jobs.labels.plitooltips.creditsnotreceived")
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Statistic
|
||||
title={t('bills.labels.creditsnotreceived')}
|
||||
title={t("bills.labels.creditsnotreceived")}
|
||||
valueStyle={{
|
||||
color: totalReturnsMarkedNotReceived.getAmount() <= 0 ? 'green' : 'red',
|
||||
color: totalReturnsMarkedNotReceived.getAmount() <= 0 ? "green" : "red"
|
||||
}}
|
||||
value={
|
||||
totalReturnsMarkedNotReceived.getAmount() >= 0
|
||||
@@ -303,11 +299,7 @@ export default function JobBillsTotalComponent({
|
||||
</Tooltip>
|
||||
</Space>
|
||||
{showWarning && calculatedCreditsNotReceived.getAmount() > 0 && (
|
||||
<Alert
|
||||
style={{ margin: '8px 0px' }}
|
||||
type="warning"
|
||||
message={t('jobs.labels.outstanding_credit_memos')}
|
||||
/>
|
||||
<Alert style={{ margin: "8px 0px" }} type="warning" message={t("jobs.labels.outstanding_credit_memos")} />
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Card, Form, Input, notification, Switch } from "antd";
|
||||
import dayjs from "../../../../utils/day";
|
||||
import queryString from "query-string";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -14,6 +13,7 @@ import { UPDATE_OWNER } from "../../../../graphql/owners.queries";
|
||||
import { insertAuditTrail } from "../../../../redux/application/application.actions";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../../../utils/AuditTrailMappings";
|
||||
import dayjs from "../../../../utils/day";
|
||||
import ConfigFormComponents from "../../../config-form-components/config-form-components.component";
|
||||
import DateTimePicker from "../../../form-date-time-picker/form-date-time-picker.component";
|
||||
|
||||
@@ -275,7 +275,19 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
|
||||
>
|
||||
<DateTimePicker disabled={readOnly} />
|
||||
</Form.Item>
|
||||
<Form.Item name="actual_delivery" label={t("jobs.fields.actual_delivery")} disabled={readOnly}>
|
||||
<Form.Item
|
||||
name="actual_delivery"
|
||||
label={t("jobs.fields.actual_delivery")}
|
||||
rules={[
|
||||
{
|
||||
required: bodyshop.deliverchecklist.actual_delivery
|
||||
? bodyshop.deliverchecklist.actual_delivery
|
||||
: false
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
disabled={readOnly}
|
||||
>
|
||||
<DateTimePicker disabled={readOnly} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { useSplitTreatments } from '@splitsoftware/splitio-react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
import { GET_LINE_TICKET_BY_PK } from '../../graphql/jobs-lines.queries';
|
||||
import { selectJobReadOnly } from '../../redux/application/application.selectors';
|
||||
import { selectBodyshop } from '../../redux/user/user.selectors';
|
||||
import AlertComponent from '../alert/alert.component';
|
||||
import LaborAllocationsTableComponent from '../labor-allocations-table/labor-allocations-table.component';
|
||||
import PayrollLaborAllocationsTable from '../labor-allocations-table/labor-allocations-table.payroll.component';
|
||||
import LoadingSkeleton from '../loading-skeleton/loading-skeleton.component';
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { GET_LINE_TICKET_BY_PK } from "../../graphql/jobs-lines.queries";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import LaborAllocationsTableComponent from "../labor-allocations-table/labor-allocations-table.component";
|
||||
import PayrollLaborAllocationsTable from "../labor-allocations-table/labor-allocations-table.payroll.component";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
jobRO: selectJobReadOnly,
|
||||
jobRO: selectJobReadOnly
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
@@ -24,21 +24,21 @@ export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRoGuardLabor
|
||||
export function JobCloseRoGuardLabor({ job, jobRO, bodyshop, form, warningCallback }) {
|
||||
const { loading, error, data, refetch } = useQuery(GET_LINE_TICKET_BY_PK, {
|
||||
variables: { id: job.id },
|
||||
fetchPolicy: 'network-only',
|
||||
nextFetchPolicy: 'network-only',
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
const {
|
||||
treatments: { Enhanced_Payroll },
|
||||
treatments: { Enhanced_Payroll }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ['Enhanced_Payroll'],
|
||||
splitKey: bodyshop.imexshopid,
|
||||
names: ["Enhanced_Payroll"],
|
||||
splitKey: bodyshop.imexshopid
|
||||
});
|
||||
|
||||
if (loading) return <LoadingSkeleton />;
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
return Enhanced_Payroll.treatment === 'on' ? (
|
||||
return Enhanced_Payroll.treatment === "on" ? (
|
||||
<PayrollLaborAllocationsTable
|
||||
jobId={job.id}
|
||||
timetickets={data ? data.timetickets : []}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
import { Alert, Card } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
import { selectJobReadOnly } from '../../redux/application/application.selectors';
|
||||
import { selectBodyshop } from '../../redux/user/user.selectors';
|
||||
import { useMemo } from 'react';
|
||||
import Dinero from 'dinero.js';
|
||||
import DataLabel from '../data-label/data-label.component';
|
||||
import { Alert, Card } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { useMemo } from "react";
|
||||
import Dinero from "dinero.js";
|
||||
import DataLabel from "../data-label/data-label.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
jobRO: selectJobReadOnly,
|
||||
jobRO: selectJobReadOnly
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
@@ -41,25 +41,21 @@ export function JobCloseRoGuardProfit({ job, jobRO, bodyshop, form, warningCallb
|
||||
|
||||
useEffect(() => {
|
||||
if (balance.getAmount() !== 0) {
|
||||
warningCallback({ key: 'ar', warning: t('jobs.labels.outstanding_ar') });
|
||||
warningCallback({ key: "ar", warning: t("jobs.labels.outstanding_ar") });
|
||||
}
|
||||
}, [balance, t, warningCallback]);
|
||||
|
||||
return (
|
||||
<Card title={t('jobs.labels.accountsreceivable')} style={{ height: '100%' }}>
|
||||
<DataLabel label={t('payments.labels.totalpayments')}>{total.toFormat()}</DataLabel>
|
||||
<Card title={t("jobs.labels.accountsreceivable")} style={{ height: "100%" }}>
|
||||
<DataLabel label={t("payments.labels.totalpayments")}>{total.toFormat()}</DataLabel>
|
||||
<DataLabel
|
||||
valueStyle={{ color: balance.getAmount() !== 0 ? 'red' : 'green' }}
|
||||
label={t('payments.labels.balance')}
|
||||
valueStyle={{ color: balance.getAmount() !== 0 ? "red" : "green" }}
|
||||
label={t("payments.labels.balance")}
|
||||
>
|
||||
{balance.toFormat()}
|
||||
</DataLabel>
|
||||
{balance.getAmount() !== 0 && (
|
||||
<Alert
|
||||
style={{ margin: '8px 0px' }}
|
||||
type="warning"
|
||||
message={t('jobs.labels.outstanding_ar')}
|
||||
/>
|
||||
<Alert style={{ margin: "8px 0px" }} type="warning" message={t("jobs.labels.outstanding_ar")} />
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { connect } from 'react-redux';
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
import { QUERY_BILLS_BY_JOBID } from '../../graphql/bills.queries';
|
||||
import { selectJobReadOnly } from '../../redux/application/application.selectors';
|
||||
import { selectBodyshop } from '../../redux/user/user.selectors';
|
||||
import AlertComponent from '../alert/alert.component';
|
||||
import JobBillsTotalComponent from '../job-bills-total/job-bills-total.component';
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { QUERY_PARTS_BILLS_BY_JOBID } from "../../graphql/bills.queries";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import JobBillsTotalComponent from "../job-bills-total/job-bills-total.component";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
jobRO: selectJobReadOnly,
|
||||
jobRO: selectJobReadOnly
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
@@ -19,10 +19,10 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRoGuardBills);
|
||||
|
||||
export function JobCloseRoGuardBills({ job, jobRO, bodyshop, form, warningCallback }) {
|
||||
const { loading, error, data } = useQuery(QUERY_BILLS_BY_JOBID, {
|
||||
const { loading, error, data } = useQuery(QUERY_PARTS_BILLS_BY_JOBID, {
|
||||
variables: { jobid: job.id },
|
||||
fetchPolicy: 'network-only',
|
||||
nextFetchPolicy: 'network-only',
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { LockOutlined } from "@ant-design/icons";
|
||||
import { Badge, Card, Col, Collapse, Form, Input, Row, Space, Tooltip } from "antd";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -12,9 +12,8 @@ import JobCloseRoGuardBills from "./job-close-ro-guard.bills";
|
||||
import JobCloseRoGuardPpd from "./job-close-ro-guard.ppd";
|
||||
import JobCloseRoGuardProfit from "./job-close-ro-guard.profit";
|
||||
import "./job-close-ro-guard.styles.scss";
|
||||
import JobCloseRoGuardSublet from "./job-close-ro-guard.sublet";
|
||||
import JobCloseRoGuardTtLifecycle from "./job-close-ro-guard.tt-lifecycle";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import JobCloseRoGuardTtLifecycle from "./job-close-ro-guard.tt-lifecycle";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
@@ -76,20 +75,21 @@ export function JobCloseRoGuardContainer({ job, jobRO, bodyshop, form }) {
|
||||
<Col className="ro-guard-col-multiple" md={24} lg={6}>
|
||||
<Row gutter={[32, 32]} style={{ height: "100%" }}>
|
||||
<Col span={24}>
|
||||
<JobCloseRoGuardProfit
|
||||
job={job}
|
||||
form={form} //warningCallback={warningCallback}
|
||||
/>
|
||||
<JobCloseRoGuardProfit job={job} form={form} warningCallback={warningCallback} />
|
||||
</Col>
|
||||
<Col span={24}>
|
||||
<JobCloseRoGuardAr job={job} form={form} warningCallback={warningCallback} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col className="ro-guard-col-50" md={24} lg={8}>
|
||||
<JobCloseRoGuardSublet job={job} warningCallback={warningCallback} />
|
||||
{InstanceRenderManager({ rome: <JobCloseRoGuardPpd job={job} warningCallback={warningCallback} /> })}
|
||||
</Col>
|
||||
{InstanceRenderManager({
|
||||
rome: (
|
||||
<Col md={24} lg={8}>
|
||||
{/* <JobCloseRoGuardSublet job={job} warningCallback={warningCallback} /> */}
|
||||
<JobCloseRoGuardPpd job={job} warningCallback={warningCallback} />
|
||||
</Col>
|
||||
)
|
||||
})}
|
||||
<Col className="ro-guard-col" md={24} lg={10}>
|
||||
<JobCloseRoGuardLabor job={job} form={form} warningCallback={warningCallback} />
|
||||
</Col>
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { LockOutlined } from '@ant-design/icons';
|
||||
import { Card, Form, Input } from 'antd';
|
||||
import axios from 'axios';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
import { selectJobReadOnly } from '../../redux/application/application.selectors';
|
||||
import { selectBodyshop } from '../../redux/user/user.selectors';
|
||||
import JobCostingStatistics from '../job-costing-statistics/job-costing-statistics.component';
|
||||
import LoadingSkeleton from '../loading-skeleton/loading-skeleton.component';
|
||||
import { Alert, Card } from "antd";
|
||||
import axios from "axios";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import JobCostingStatistics from "../job-costing-statistics/job-costing-statistics.component";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
jobRO: selectJobReadOnly,
|
||||
jobRO: selectJobReadOnly
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
@@ -31,7 +30,7 @@ export function JobCloseRoGuardProfit({ job, jobRO, bodyshop, form, warningCallb
|
||||
try {
|
||||
if (job.id) {
|
||||
setLoading(true);
|
||||
const { data } = await axios.post('/job/costing', { jobid: job.id });
|
||||
const { data } = await axios.post("/job/costing", { jobid: job.id });
|
||||
setCostingData(data);
|
||||
}
|
||||
} catch (error) {}
|
||||
@@ -45,16 +44,19 @@ export function JobCloseRoGuardProfit({ job, jobRO, bodyshop, form, warningCallb
|
||||
parseFloat(costingData?.summaryData.gppercent) < bodyshop?.md_ro_guard?.totalgppercent_minimum;
|
||||
|
||||
useEffect(() => {
|
||||
if (enforceProfitPassword && typeof warningCallback === 'function') {
|
||||
warningCallback({ key: 'profit', warning: t('jobs.labels.profitbypassrequired') });
|
||||
if (enforceProfitPassword && typeof warningCallback === "function") {
|
||||
warningCallback({ key: "profit", warning: t("jobs.labels.profitbypassrequired") });
|
||||
}
|
||||
}, [enforceProfitPassword, t, warningCallback]);
|
||||
|
||||
if (loading || !costingData) return <LoadingSkeleton />;
|
||||
|
||||
return (
|
||||
<Card title={t('jobs.labels.profits')} style={{ height: '100%' }}>
|
||||
<Card title={t("jobs.labels.profits")} style={{ height: "100%" }}>
|
||||
<JobCostingStatistics summaryData={costingData?.summaryData} onlyGP />
|
||||
{enforceProfitPassword && (
|
||||
<Alert style={{ margin: "8px 0px" }} type="warning" message={t("jobs.labels.profitbypassrequired")} />
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
import { Alert, Card, Table } from 'antd';
|
||||
import { t } from 'i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
import { selectJobReadOnly } from '../../redux/application/application.selectors';
|
||||
import { selectBodyshop } from '../../redux/user/user.selectors';
|
||||
import CurrencyFormatter from '../../utils/CurrencyFormatter';
|
||||
import { alphaSort } from '../../utils/sorters';
|
||||
import { Alert, Card, Table } from "antd";
|
||||
import { t } from "i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
jobRO: selectJobReadOnly,
|
||||
jobRO: selectJobReadOnly
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
@@ -20,69 +20,56 @@ export default connect(mapStateToProps, mapDispatchToProps)(JobCloseRGuardSublet
|
||||
|
||||
export function JobCloseRGuardSublet({ job, jobRO, bodyshop, form, warningCallback }) {
|
||||
const subletsNotDone = job?.joblines.filter(
|
||||
(j) =>
|
||||
(j.part_type === 'PAS' || j.part_type === 'PASL') &&
|
||||
(!j.sublet_completed || !j.sublet_ignored)
|
||||
(j) => (j.part_type === "PAS" || j.part_type === "PASL") && (!j.sublet_completed || !j.sublet_ignored)
|
||||
);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t('joblines.fields.line_desc'),
|
||||
dataIndex: 'line_desc',
|
||||
fixed: 'left',
|
||||
key: 'line_desc',
|
||||
title: t("joblines.fields.line_desc"),
|
||||
dataIndex: "line_desc",
|
||||
fixed: "left",
|
||||
key: "line_desc",
|
||||
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
||||
onCell: (record) => ({
|
||||
className: record.manual_line && 'job-line-manual',
|
||||
className: record.manual_line && "job-line-manual",
|
||||
style: {
|
||||
...(record.critical ? { boxShadow: ' -.5em 0 0 #FFC107' } : {}),
|
||||
},
|
||||
...(record.critical ? { boxShadow: " -.5em 0 0 #FFC107" } : {})
|
||||
}
|
||||
}),
|
||||
ellipsis: true,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: t('joblines.fields.act_price'),
|
||||
dataIndex: 'act_price',
|
||||
key: 'act_price',
|
||||
title: t("joblines.fields.act_price"),
|
||||
dataIndex: "act_price",
|
||||
key: "act_price",
|
||||
sorter: (a, b) => a.act_price - b.act_price,
|
||||
|
||||
ellipsis: true,
|
||||
render: (text, record) => <CurrencyFormatter>{record.act_price}</CurrencyFormatter>,
|
||||
render: (text, record) => <CurrencyFormatter>{record.act_price}</CurrencyFormatter>
|
||||
},
|
||||
{
|
||||
title: t('joblines.fields.part_qty'),
|
||||
dataIndex: 'part_qty',
|
||||
key: 'part_qty',
|
||||
title: t("joblines.fields.part_qty"),
|
||||
dataIndex: "part_qty",
|
||||
key: "part_qty"
|
||||
},
|
||||
{
|
||||
title: t('joblines.fields.notes'),
|
||||
dataIndex: 'notes',
|
||||
key: 'notes',
|
||||
},
|
||||
title: t("joblines.fields.notes"),
|
||||
dataIndex: "notes",
|
||||
key: "notes"
|
||||
}
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (subletsNotDone.length > 0) {
|
||||
warningCallback({ key: 'sublet', warning: t('jobs.labels.outstanding_sublets') });
|
||||
warningCallback({ key: "sublet", warning: t("jobs.labels.outstanding_sublets") });
|
||||
}
|
||||
}, [subletsNotDone.length, warningCallback]);
|
||||
|
||||
return (
|
||||
<Card title={t('jobs.labels.subletsnotcompleted')}>
|
||||
<Table
|
||||
dataSource={subletsNotDone}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
rowKey="id"
|
||||
bordered
|
||||
size="small"
|
||||
/>
|
||||
<Card title={t("jobs.labels.subletsnotcompleted")}>
|
||||
<Table dataSource={subletsNotDone} columns={columns} pagination={false} rowKey="id" bordered size="small" />
|
||||
{subletsNotDone.length > 0 && (
|
||||
<Alert
|
||||
style={{ margin: '8px 0px' }}
|
||||
type="warning"
|
||||
message={t('jobs.labels.outstanding_sublets')}
|
||||
/>
|
||||
<Alert style={{ margin: "8px 0px" }} type="warning" message={t("jobs.labels.outstanding_sublets")} />
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import React from 'react';
|
||||
import React from "react";
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
import { selectJobReadOnly } from '../../redux/application/application.selectors';
|
||||
import { selectBodyshop } from '../../redux/user/user.selectors';
|
||||
import JobLifecycleComponent from '../job-lifecycle/job-lifecycle.component';
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import JobLifecycleComponent from "../job-lifecycle/job-lifecycle.component";
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop,
|
||||
jobRO: selectJobReadOnly,
|
||||
jobRO: selectJobReadOnly
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CardTemplate from "./job-detail-cards.template.component";
|
||||
import Car from "../job-damage-visual/job-damage-visual.component";
|
||||
import CardTemplate from "./job-detail-cards.template.component";
|
||||
|
||||
export default function JobDetailCardsDamageComponent({ loading, data }) {
|
||||
const { t } = useTranslation();
|
||||
const { area_of_damage } = data;
|
||||
return (
|
||||
<CardTemplate loading={loading} title={t("jobs.labels.cards.damage")}>
|
||||
{area_of_damage ? <Car dmg1={area_of_damage.impact1} dmg2={area_of_damage.impact2} /> : t("jobs.errors.nodamage")}
|
||||
{area_of_damage ? (
|
||||
<Car
|
||||
dmg1={area_of_damage.impact1 && area_of_damage.impact1.padStart(2, "0")}
|
||||
dmg2={area_of_damage.impact2 && area_of_damage.impact2.padStart(2, "0")}
|
||||
/>
|
||||
) : (
|
||||
t("jobs.errors.nodamage")
|
||||
)}
|
||||
</CardTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,16 @@ export function JobDetailCardsPartsComponent({ loading, data, jobRO }) {
|
||||
const { t } = useTranslation();
|
||||
const { joblines_status } = data;
|
||||
|
||||
const filteredJobLines = data.joblines.filter(
|
||||
(j) =>
|
||||
j.part_type !== null &&
|
||||
j.part_type !== "PAE" &&
|
||||
j.part_type !== "PAS" &&
|
||||
j.part_type !== "PASL" &&
|
||||
j.part_qty !== 0 &&
|
||||
j.act_price !== 0
|
||||
);
|
||||
//TODO: Correct jobline_statuses view by including the part_qty !== 0 and act_price !== 0
|
||||
const columns = [
|
||||
{
|
||||
title: t("joblines.fields.line_desc"),
|
||||
@@ -95,7 +105,7 @@ export function JobDetailCardsPartsComponent({ loading, data, jobRO }) {
|
||||
<div>
|
||||
<CardTemplate loading={loading} title={t("jobs.labels.cards.parts")}>
|
||||
<PartsStatusPie joblines_status={joblines_status} />
|
||||
<Table key="id" columns={columns} dataSource={data ? data.joblines : []} />
|
||||
<Table key="id" columns={columns} dataSource={filteredJobLines ? filteredJobLines : []} />
|
||||
</CardTemplate>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Col, Divider, Row, Skeleton, Space, Timeline, Typography } from "antd";
|
||||
import { Col, Row, Skeleton, Space, Timeline, Typography } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { GET_JOB_LINE_ORDERS } from "../../graphql/jobs.queries";
|
||||
import { QUERY_JOBLINE_TASKS_PAGINATED } from "../../graphql/tasks.queries.js";
|
||||
import { selectTechnician } from "../../redux/tech/tech.selectors.js";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import BillDetailEditcontainer from "../bill-detail-edit/bill-detail-edit.container.jsx";
|
||||
import FeatureWrapper from "../feature-wrapper/feature-wrapper.component.jsx";
|
||||
import TaskListContainer from "../task-list/task-list.container.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
bodyshop: selectBodyshop,
|
||||
technician: selectTechnician
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobLinesExpander);
|
||||
|
||||
export function JobLinesExpander({ jobline, jobid, bodyshop }) {
|
||||
export function JobLinesExpander({ jobline, jobid, bodyshop, technician }) {
|
||||
const { t } = useTranslation();
|
||||
const { loading, error, data } = useQuery(GET_JOB_LINE_ORDERS, {
|
||||
fetchPolicy: "network-only",
|
||||
@@ -43,13 +48,31 @@ export function JobLinesExpander({ jobline, jobid, bodyshop }) {
|
||||
? data.parts_order_lines.map((line) => ({
|
||||
key: line.id,
|
||||
children: (
|
||||
<Space split={<Divider type="vertical" />} wrap>
|
||||
<Link to={`/manage/jobs/${jobid}?partsorderid=${line.parts_order.id}`}>
|
||||
{line.parts_order.order_number}
|
||||
</Link>
|
||||
<DateFormatter>{line.parts_order.order_date}</DateFormatter>
|
||||
{line.parts_order.vendor.name}
|
||||
</Space>
|
||||
<Row wrap>
|
||||
<Col span={4}>
|
||||
{!technician ? (
|
||||
<>
|
||||
<Link to={`/manage/jobs/${jobid}?partsorderid=${line.parts_order.id}`}>
|
||||
{line.parts_order.order_number}
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
`${line.parts_order.order_number}`
|
||||
)}
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<DateFormatter>{line.parts_order.order_date}</DateFormatter>
|
||||
</Col>
|
||||
<Col span={4}>{line.parts_order.vendor.name}</Col>
|
||||
{line.backordered_eta ? (
|
||||
<Col span={4}>
|
||||
<span>
|
||||
{`${t("parts_orders.fields.backordered_eta")}: `}
|
||||
<DateFormatter>{line.backordered_eta}</DateFormatter>
|
||||
</span>
|
||||
</Col>
|
||||
) : null}
|
||||
</Row>
|
||||
)
|
||||
}))
|
||||
: [
|
||||
@@ -61,48 +84,6 @@ export function JobLinesExpander({ jobline, jobid, bodyshop }) {
|
||||
}
|
||||
/>{" "}
|
||||
</Col>
|
||||
<Col md={24} lg={8}>
|
||||
<Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title>
|
||||
<Timeline
|
||||
items={
|
||||
data.billlines.length > 0
|
||||
? data.billlines.map((line) => ({
|
||||
key: line.id,
|
||||
children: (
|
||||
<Row wrap>
|
||||
<Col span={4}>
|
||||
<Link to={`/manage/jobs/${jobid}?tab=partssublet&billid=${line.bill.id}`}>
|
||||
{line.bill.invoice_number}
|
||||
</Link>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<span>
|
||||
{`${t("billlines.fields.actual_price")}: `}
|
||||
<CurrencyFormatter>{line.actual_price}</CurrencyFormatter>
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<span>
|
||||
{`${t("billlines.fields.actual_cost")}: `}
|
||||
<CurrencyFormatter>{line.actual_cost}</CurrencyFormatter>
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<DateFormatter>{line.bill.date}</DateFormatter>
|
||||
</Col>
|
||||
<Col span={4}> {line.bill.vendor.name}</Col>
|
||||
</Row>
|
||||
)
|
||||
}))
|
||||
: [
|
||||
{
|
||||
key: "no-orders",
|
||||
children: t("bills.labels.nobilllines")
|
||||
}
|
||||
]
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={24} lg={8}>
|
||||
<Typography.Title level={4}>{t("parts_dispatch.labels.parts_dispatch")}</Typography.Title>
|
||||
<Timeline
|
||||
@@ -111,23 +92,91 @@ export function JobLinesExpander({ jobline, jobid, bodyshop }) {
|
||||
? data.parts_dispatch_lines.map((line) => ({
|
||||
key: line.id,
|
||||
children: (
|
||||
<Space split={<Divider type="vertical" />} wrap>
|
||||
<Link to={`/manage/jobs/${jobid}?partsorderid=${line.id}`}>{line.parts_dispatch.number}</Link>
|
||||
{bodyshop.employees.find((e) => e.id === line.parts_dispatch.employeeid)?.first_name}
|
||||
<Space>
|
||||
{t("parts_dispatch_lines.fields.accepted_at")}
|
||||
<DateFormatter>{line.accepted_at}</DateFormatter>
|
||||
</Space>
|
||||
</Space>
|
||||
<Row>
|
||||
<Col span={8}>{line.parts_dispatch.number}</Col>
|
||||
<Col span={8}>
|
||||
{bodyshop.employees.find((e) => e.id === line.parts_dispatch.employeeid)?.first_name}
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
{line.accepted_at ? (
|
||||
<Space>
|
||||
{t("parts_dispatch_lines.fields.accepted_at")}
|
||||
<DateFormatter>{line.accepted_at}</DateFormatter>
|
||||
</Space>
|
||||
) : null}
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}))
|
||||
: {
|
||||
key: "dispatch-lines",
|
||||
children: t("parts_orders.labels.notyetordered")
|
||||
}
|
||||
: [
|
||||
{
|
||||
key: "dispatch-lines",
|
||||
children: t("parts_dispatch.labels.notyetdispatched")
|
||||
}
|
||||
]
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
<FeatureWrapper featureName="bills" noauth={() => null}>
|
||||
<Col md={24} lg={8}>
|
||||
<Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title>
|
||||
<BillDetailEditcontainer />
|
||||
<Timeline
|
||||
items={
|
||||
data.billlines.length > 0
|
||||
? data.billlines.map((line) => ({
|
||||
key: line.id,
|
||||
children: (
|
||||
<Row wrap>
|
||||
<Col span={4}>
|
||||
{!technician ? (
|
||||
<>
|
||||
<Link to={`/manage/jobs/${jobid}?tab=partssublet&billid=${line.bill.id}`}>
|
||||
{line.bill.invoice_number}
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
`${line.bill.invoice_number}`
|
||||
)}
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<span>
|
||||
{`${t("billlines.fields.actual_price")}: `}
|
||||
<CurrencyFormatter>{line.actual_price}</CurrencyFormatter>
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<span>
|
||||
{`${t("billlines.fields.actual_cost")}: `}
|
||||
<CurrencyFormatter>{line.actual_cost}</CurrencyFormatter>
|
||||
</span>
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<DateFormatter>{line.bill.date}</DateFormatter>
|
||||
</Col>
|
||||
<Col span={4}> {line.bill.vendor.name}</Col>
|
||||
</Row>
|
||||
)
|
||||
}))
|
||||
: [
|
||||
{
|
||||
key: "no-orders",
|
||||
children: t("bills.labels.nobilllines")
|
||||
}
|
||||
]
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</FeatureWrapper>
|
||||
<Col md={24} lg={24}>
|
||||
<TaskListContainer
|
||||
parentJobId={jobid}
|
||||
relationshipType={"joblineid"}
|
||||
relationshipId={jobline.id}
|
||||
query={{ QUERY_JOBLINE_TASKS_PAGINATED }}
|
||||
titleTranslation="tasks.titles.job_tasks"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,13 +3,21 @@ import { Button, Form, notification, Popover, Tooltip } from "antd";
|
||||
import axios from "axios";
|
||||
import { t } from "i18next";
|
||||
import React, { useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { UPDATE_LINE_PPC } from "../../graphql/jobs-lines.queries";
|
||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import CurrencyFormItemComponent from "../form-items-formatted/currency-form-item.component";
|
||||
import JobLineConvertToLabor from "../job-line-convert-to-labor/job-line-convert-to-labor.component";
|
||||
|
||||
export default function JobLinesPartPriceChange({ job, line, refetch }) {
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
technician: selectTechnician
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
export function JobLinesPartPriceChange({ job, line, refetch, technician }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [updatePartPrice] = useMutation(UPDATE_LINE_PPC);
|
||||
|
||||
@@ -52,7 +60,7 @@ export default function JobLinesPartPriceChange({ job, line, refetch }) {
|
||||
}
|
||||
};
|
||||
|
||||
const popcontent = InstanceRenderManager({
|
||||
const popcontent = !technician && InstanceRenderManager({
|
||||
imex: null,
|
||||
rome: (
|
||||
<Form layout="vertical" onFinish={handleFinish} initialValues={{ act_price: line.act_price }}>
|
||||
@@ -95,3 +103,4 @@ export default function JobLinesPartPriceChange({ job, line, refetch }) {
|
||||
</JobLineConvertToLabor>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobLinesPartPriceChange);
|
||||
|
||||
@@ -31,14 +31,17 @@ import JobLinesBillRefernece from "../job-lines-bill-reference/job-lines-bill-re
|
||||
// import AllocationsEmployeeLabelContainer from "../allocations-employee-label/allocations-employee-label.container";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import _ from "lodash";
|
||||
import { FaTasks } from "react-icons/fa";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import dayjs from "../../utils/day";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import JobCreateIOU from "../job-create-iou/job-create-iou.component";
|
||||
import JobLineBulkAssignComponent from "../job-line-bulk-assign/job-line-bulk-assign.component";
|
||||
import JobLineDispatchButton from "../job-line-dispatch-button/job-line-dispatch-button.component";
|
||||
import JoblineTeamAssignment from "../job-line-team-assignment/job-line-team-assignmnent.component";
|
||||
import JobSendPartPriceChangeComponent from "../job-send-parts-price-change/job-send-parts-price-change.component";
|
||||
import PartsOrderDrawer from "../parts-order-list-table/parts-order-list-table-drawer.component";
|
||||
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
|
||||
import JobLinesExpander from "./job-lines-expander.component";
|
||||
import JobLinesPartPriceChange from "./job-lines-part-price-change.component";
|
||||
@@ -52,7 +55,9 @@ const mapStateToProps = createStructuredSelector({
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setJobLineEditContext: (context) => dispatch(setModalContext({ context: context, modal: "jobLineEdit" })),
|
||||
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
||||
setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" }))
|
||||
setPartsReceiveContext: (context) => dispatch(setModalContext({ context: context, modal: "partsReceive" })),
|
||||
setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" })),
|
||||
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
|
||||
});
|
||||
|
||||
export function JobLinesComponent({
|
||||
@@ -60,6 +65,7 @@ export function JobLinesComponent({
|
||||
jobRO,
|
||||
technician,
|
||||
setPartsOrderContext,
|
||||
setPartsReceiveContext,
|
||||
loading,
|
||||
refetch,
|
||||
jobLines,
|
||||
@@ -67,7 +73,12 @@ export function JobLinesComponent({
|
||||
job,
|
||||
setJobLineEditContext,
|
||||
form,
|
||||
setBillEnterContext
|
||||
setBillEnterContext,
|
||||
setTaskUpsertContext,
|
||||
billsQuery,
|
||||
handleBillOnRowClick,
|
||||
handlePartsOrderOnRowClick,
|
||||
handlePartsDispatchOnRowClick
|
||||
}) {
|
||||
const [deleteJobLine] = useMutation(DELETE_JOB_LINE_BY_PK);
|
||||
const {
|
||||
@@ -194,7 +205,6 @@ export function JobLinesComponent({
|
||||
onFilter: (value, record) => value.includes(record.part_type),
|
||||
render: (text, record) => (record.part_type ? t(`joblines.fields.part_types.${record.part_type}`) : null)
|
||||
},
|
||||
|
||||
{
|
||||
title: t("joblines.fields.act_price"),
|
||||
dataIndex: "act_price",
|
||||
@@ -209,7 +219,6 @@ export function JobLinesComponent({
|
||||
dataIndex: "part_qty",
|
||||
key: "part_qty"
|
||||
},
|
||||
|
||||
// {
|
||||
// title: t('joblines.fields.tax_part'),
|
||||
// dataIndex: 'tax_part',
|
||||
@@ -318,7 +327,7 @@ export function JobLinesComponent({
|
||||
key: "actions",
|
||||
render: (text, record) => (
|
||||
<Space>
|
||||
{(record.manual_line || jobIsPrivate) && (
|
||||
{(record.manual_line || jobIsPrivate) && !technician && (
|
||||
<>
|
||||
<Button
|
||||
disabled={jobRO}
|
||||
@@ -331,6 +340,23 @@ export function JobLinesComponent({
|
||||
>
|
||||
<EditFilled />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
title={t("tasks.buttons.create")}
|
||||
onClick={() => {
|
||||
setTaskUpsertContext({
|
||||
context: {
|
||||
jobid: job.id,
|
||||
joblineid: record.id
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FaTasks />
|
||||
</Button>
|
||||
{(record.manual_line || jobIsPrivate) && !technician && (
|
||||
<>
|
||||
<Button
|
||||
disabled={jobRO}
|
||||
onClick={async () => {
|
||||
@@ -415,6 +441,15 @@ export function JobLinesComponent({
|
||||
return (
|
||||
<div>
|
||||
<PartsOrderModalContainer />
|
||||
{!technician && (
|
||||
<PartsOrderDrawer
|
||||
job={job}
|
||||
billsQuery={billsQuery}
|
||||
handleOnRowClick={handlePartsOrderOnRowClick}
|
||||
setPartsReceiveContext={setPartsReceiveContext}
|
||||
setTaskUpsertContext={setTaskUpsertContext}
|
||||
/>
|
||||
)}
|
||||
<PageHeader
|
||||
title={t("jobs.labels.estimatelines")}
|
||||
extra={
|
||||
@@ -452,7 +487,7 @@ export function JobLinesComponent({
|
||||
vendorid: bodyshop.inhousevendorid,
|
||||
invoice_number: "ih",
|
||||
isinhouse: true,
|
||||
date: new dayjs(),
|
||||
date: dayjs(),
|
||||
total: 0,
|
||||
billlines: selectedLines.map((p) => {
|
||||
return {
|
||||
@@ -531,7 +566,7 @@ export function JobLinesComponent({
|
||||
>
|
||||
{t("joblines.actions.new")}
|
||||
</Button>
|
||||
{bodyshop.region_config.toLowerCase().startsWith("us") && <JobSendPartPriceChangeComponent job={job} />}
|
||||
{InstanceRenderManager({ rome: <JobSendPartPriceChangeComponent job={job} disabled={technician} /> })}
|
||||
<JobCreateIOU job={job} selectedJobLines={selectedLines} />
|
||||
<Input.Search
|
||||
placeholder={t("general.labels.search")}
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
import JobLinesComponent from "./job-lines.component";
|
||||
|
||||
function JobLinesContainer({ job, joblines, refetch, form, ...rest }) {
|
||||
function JobLinesContainer({
|
||||
job,
|
||||
joblines,
|
||||
billsQuery,
|
||||
handleBillOnRowClick,
|
||||
handlePartsOrderOnRowClick,
|
||||
handlePartsDispatchOnRowClick,
|
||||
refetch,
|
||||
form,
|
||||
...rest
|
||||
}) {
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const jobLines = useMemo(() => {
|
||||
@@ -22,7 +32,19 @@ function JobLinesContainer({ job, joblines, refetch, form, ...rest }) {
|
||||
}, [joblines, searchText]);
|
||||
|
||||
return (
|
||||
<JobLinesComponent refetch={refetch} jobLines={jobLines} setSearchText={setSearchText} job={job} form={form} />
|
||||
<div>
|
||||
<JobLinesComponent
|
||||
refetch={refetch}
|
||||
jobLines={jobLines}
|
||||
billsQuery={billsQuery}
|
||||
handleBillOnRowClick={handleBillOnRowClick}
|
||||
handlePartsOrderOnRowClick={handlePartsOrderOnRowClick}
|
||||
handlePartsDispatchOnRowClick={handlePartsDispatchOnRowClick}
|
||||
setSearchText={setSearchText}
|
||||
job={job}
|
||||
form={form}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,17 +11,18 @@ import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { UPDATE_JOB_LINE } from "../../graphql/jobs-lines.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
technician: selectTechnician
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(JobLineConvertToLabor);
|
||||
|
||||
export function JobLineConvertToLabor({ children, jobline, job, insertAuditTrail, ...otherBtnProps }) {
|
||||
export function JobLineConvertToLabor({ children, jobline, job, insertAuditTrail, technician, ...otherBtnProps }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -154,7 +155,7 @@ export function JobLineConvertToLabor({ children, jobline, job, insertAuditTrail
|
||||
setLoading(true);
|
||||
|
||||
form.setFieldsValue({
|
||||
// date: new dayjs(),
|
||||
// date: dayjs(),
|
||||
// bodyhrs: Math.round(v.bodyhrs * 10) / 10,
|
||||
// painthrs: Math.round(v.painthrs * 10) / 10,
|
||||
});
|
||||
@@ -165,7 +166,7 @@ export function JobLineConvertToLabor({ children, jobline, job, insertAuditTrail
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
{jobline.act_price !== 0 && (
|
||||
{jobline.act_price !== 0 && !technician && (
|
||||
<Popover disabled={jobline.convertedtolbr} content={overlay} open={visibility} placement="bottom">
|
||||
<Tooltip title={t("joblines.actions.converttolabor")}>
|
||||
<Button
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
import { useLazyQuery } from "@apollo/client";
|
||||
import { Select, Space, Spin, Tag } from "antd";
|
||||
import _ from "lodash";
|
||||
@@ -6,8 +7,6 @@ import { useTranslation } from "react-i18next";
|
||||
import { SEARCH_JOBS_BY_ID_FOR_AUTOCOMPLETE, SEARCH_JOBS_FOR_AUTOCOMPLETE } from "../../graphql/jobs.queries";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
import { SearchOutlined } from "@ant-design/icons";
|
||||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
@@ -75,7 +74,7 @@ const JobSearchSelect = (
|
||||
filterOption={false}
|
||||
onSearch={handleSearch}
|
||||
//loading={loading || idLoading}
|
||||
suffixIcon={loading && <Spin />}
|
||||
suffixIcon={(loading || idLoading) && <Spin />}
|
||||
notFoundContent={loading ? <LoadingOutlined /> : null}
|
||||
{...restProps}
|
||||
>
|
||||
@@ -86,9 +85,9 @@ const JobSearchSelect = (
|
||||
<span>
|
||||
{`${clm_no && o.clm_no ? `${o.clm_no} | ` : ""}${
|
||||
o.ro_number || t("general.labels.na")
|
||||
} | ${OwnerNameDisplayFunction(o)} | ${
|
||||
o.v_model_yr || ""
|
||||
} ${o.v_make_desc || ""} ${o.v_model_desc || ""}`}
|
||||
} | ${OwnerNameDisplayFunction(o)} | ${o.v_model_yr || ""} ${o.v_make_desc || ""} ${
|
||||
o.v_model_desc || ""
|
||||
}`}
|
||||
</span>
|
||||
<Tag>
|
||||
<strong>{o.status}</strong>
|
||||
|
||||
@@ -3,7 +3,7 @@ import axios from "axios";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export default function JobSendPartPriceChangeComponent({ job }) {
|
||||
export default function JobSendPartPriceChangeComponent({ job, disabled }) {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const handleClick = async () => {
|
||||
@@ -24,7 +24,7 @@ export default function JobSendPartPriceChangeComponent({ job }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Button onClick={handleClick} loading={loading}>
|
||||
<Button onClick={handleClick} loading={loading} disabled={disabled}>
|
||||
{t("jobs.actions.sendpartspricechange")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -3,8 +3,8 @@ import Dinero from "dinero.js";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
|
||||
export default function JobTotalsTableLabor({ job }) {
|
||||
const { t } = useTranslation();
|
||||
@@ -56,16 +56,49 @@ export default function JobTotalsTableLabor({ job }) {
|
||||
sortOrder: state.sortedInfo.columnKey === "mod_lb_hrs" && state.sortedInfo.order,
|
||||
render: (text, record) => record.hours.toFixed(1)
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.total"),
|
||||
dataIndex: "total",
|
||||
key: "total",
|
||||
align: "right",
|
||||
sorter: (a, b) => a.total.amount - b.total.amount,
|
||||
sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
||||
|
||||
render: (text, record) => Dinero(record.total).toFormat()
|
||||
}
|
||||
...InstanceRenderManager({
|
||||
imex: [
|
||||
{
|
||||
title: t("joblines.fields.total"),
|
||||
dataIndex: "total",
|
||||
key: "total",
|
||||
align: "right",
|
||||
sorter: (a, b) => a.total.amount - b.total.amount,
|
||||
sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
||||
render: (text, record) => Dinero(record.total).toFormat()
|
||||
}
|
||||
],
|
||||
rome: [
|
||||
{
|
||||
title: t("joblines.fields.amount"),
|
||||
dataIndex: "base",
|
||||
key: "base",
|
||||
align: "right",
|
||||
sorter: (a, b) => a.base.amount - b.base.amount,
|
||||
sortOrder: state.sortedInfo.columnKey === "base" && state.sortedInfo.order,
|
||||
render: (text, record) => Dinero(record.base).toFormat()
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.adjustment"),
|
||||
dataIndex: "adjustment",
|
||||
key: "adjustment",
|
||||
align: "right",
|
||||
sorter: (a, b) => a.adjustment.amount - b.adjustment.amount,
|
||||
sortOrder: state.sortedInfo.columnKey === "adjustment" && state.sortedInfo.order,
|
||||
render: (text, record) => Dinero(record.adjustment).toFormat()
|
||||
},
|
||||
{
|
||||
title: t("joblines.fields.total"),
|
||||
dataIndex: "total",
|
||||
key: "total",
|
||||
align: "right",
|
||||
sorter: (a, b) => a.total.amount - b.total.amount,
|
||||
sortOrder: state.sortedInfo.columnKey === "total" && state.sortedInfo.order,
|
||||
render: (text, record) => Dinero(record.total).toFormat()
|
||||
}
|
||||
],
|
||||
promanager: "USE_ROME"
|
||||
})
|
||||
];
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
@@ -91,6 +124,16 @@ export default function JobTotalsTableLabor({ job }) {
|
||||
<Table.Summary.Cell>
|
||||
{(job.job_totals.rates.mapa.hours + job.job_totals.rates.mash.hours).toFixed(1)}
|
||||
</Table.Summary.Cell>
|
||||
{InstanceRenderManager({
|
||||
imex: null,
|
||||
rome: (
|
||||
<>
|
||||
<Table.Summary.Cell />
|
||||
<Table.Summary.Cell />
|
||||
</>
|
||||
),
|
||||
promanager: "USE_ROME"
|
||||
})}
|
||||
<Table.Summary.Cell align="right">
|
||||
<strong>{Dinero(job.job_totals.rates.rates_subtotal).toFormat()}</strong>
|
||||
</Table.Summary.Cell>
|
||||
@@ -122,7 +165,29 @@ export default function JobTotalsTableLabor({ job }) {
|
||||
<CurrencyFormatter>{job.job_totals.rates.mapa.rate}</CurrencyFormatter>
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell>{job.job_totals.rates.mapa.hours.toFixed(1)}</Table.Summary.Cell>
|
||||
<Table.Summary.Cell align="right">{Dinero(job.job_totals.rates.mapa.total).toFormat()}</Table.Summary.Cell>
|
||||
{InstanceRenderManager({
|
||||
imex: (
|
||||
<>
|
||||
<Table.Summary.Cell align="right">
|
||||
{Dinero(job.job_totals.rates.mapa.total).toFormat()}
|
||||
</Table.Summary.Cell>
|
||||
</>
|
||||
),
|
||||
rome: (
|
||||
<>
|
||||
<Table.Summary.Cell align="right">
|
||||
{Dinero(job.job_totals.rates.mapa.base).toFormat()}
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell align="right">
|
||||
{Dinero(job.job_totals.rates.mapa.adjustment).toFormat()}
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell align="right">
|
||||
{Dinero(job.job_totals.rates.mapa.total).toFormat()}
|
||||
</Table.Summary.Cell>
|
||||
</>
|
||||
),
|
||||
promanager: "USE_ROME"
|
||||
})}
|
||||
</Table.Summary.Row>
|
||||
<Table.Summary.Row>
|
||||
<Table.Summary.Cell>
|
||||
@@ -151,7 +216,29 @@ export default function JobTotalsTableLabor({ job }) {
|
||||
<CurrencyFormatter>{job.job_totals.rates.mash.rate}</CurrencyFormatter>
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell>{job.job_totals.rates.mash.hours.toFixed(1)}</Table.Summary.Cell>
|
||||
<Table.Summary.Cell align="right">{Dinero(job.job_totals.rates.mash.total).toFormat()}</Table.Summary.Cell>
|
||||
{InstanceRenderManager({
|
||||
imex: (
|
||||
<>
|
||||
<Table.Summary.Cell align="right">
|
||||
{Dinero(job.job_totals.rates.mash.total).toFormat()}
|
||||
</Table.Summary.Cell>
|
||||
</>
|
||||
),
|
||||
rome: (
|
||||
<>
|
||||
<Table.Summary.Cell align="right">
|
||||
{Dinero(job.job_totals.rates.mash.base).toFormat()}
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell align="right">
|
||||
{Dinero(job.job_totals.rates.mash.adjustment).toFormat()}
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell align="right">
|
||||
{Dinero(job.job_totals.rates.mash.total).toFormat()}
|
||||
</Table.Summary.Cell>
|
||||
</>
|
||||
),
|
||||
promanager: "USE_ROME"
|
||||
})}
|
||||
</Table.Summary.Row>
|
||||
<Table.Summary.Row>
|
||||
<Table.Summary.Cell>
|
||||
@@ -159,6 +246,16 @@ export default function JobTotalsTableLabor({ job }) {
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell />
|
||||
<Table.Summary.Cell />
|
||||
{InstanceRenderManager({
|
||||
imex: null,
|
||||
rome: (
|
||||
<>
|
||||
<Table.Summary.Cell />
|
||||
<Table.Summary.Cell />
|
||||
</>
|
||||
),
|
||||
promanager: "USE_ROME"
|
||||
})}
|
||||
<Table.Summary.Cell align="right">
|
||||
<strong>{Dinero(job.job_totals.rates.subtotal).toFormat()}</strong>
|
||||
</Table.Summary.Cell>
|
||||
|
||||
@@ -165,6 +165,8 @@ export function JobTotalsTableTotals({ bodyshop, job }) {
|
||||
bold: true
|
||||
}
|
||||
];
|
||||
// TODO: was removed by Patrick during a CI bug fix.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [job.job_totals, job.cieca_pft, t, bodyshop.md_responsibility_centers]);
|
||||
|
||||
const columns = [
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Button, Space, notification } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DELETE_DELIVERY_CHECKLIST, DELETE_INTAKE_CHECKLIST } from "../../graphql/jobs.queries";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
|
||||
export default function JobAdminDeleteIntake({ job }) {
|
||||
const { t } = useTranslation();
|
||||
@@ -47,16 +48,22 @@ export default function JobAdminDeleteIntake({ job }) {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
const InstanceRender = InstanceRenderManager({
|
||||
imex: true,
|
||||
rome: "USE_IMEX",
|
||||
promanager: false
|
||||
});
|
||||
|
||||
return InstanceRender ? (
|
||||
<>
|
||||
<Space wrap>
|
||||
<Button loading={loading} onClick={handleDelete} disabled={!job.intakechecklist}>
|
||||
{t("jobs.labels.deleteintake")}
|
||||
</Button>
|
||||
<Button loading={loading} onClick={handleDeleteDelivery} disabled={!job.deliverychecklist}>
|
||||
<Button loading={loading} onClick={handleDeleteDelivery} disabled={!job.deliverchecklist}>
|
||||
{t("jobs.labels.deletedelivery")}
|
||||
</Button>
|
||||
</Space>
|
||||
</>
|
||||
);
|
||||
) : null;
|
||||
}
|
||||
|
||||
@@ -15,23 +15,43 @@ export const GetSupplementDelta = async (client, jobId, newLines) => {
|
||||
const linesToUpdate = [];
|
||||
|
||||
newLines.forEach((newLine) => {
|
||||
const matchingIndex = existingLines.findIndex((eL) => eL.unq_seq === newLine.unq_seq);
|
||||
const matchingIndexLines = [];
|
||||
existingLines.forEach((eL, index) => {
|
||||
if (eL.unq_seq === newLine.unq_seq) {
|
||||
matchingIndexLines.push({ index, record: eL });
|
||||
}
|
||||
});
|
||||
|
||||
//Should do a check to make sure there is only 1 matching unq sequence number.
|
||||
|
||||
if (matchingIndex >= 0) {
|
||||
//Found a relevant matching line. Add it to lines to update.
|
||||
linesToUpdate.push({
|
||||
id: existingLines[matchingIndex].id,
|
||||
newData: { ...newLine, removed: false, act_price_before_ppc: null }
|
||||
});
|
||||
|
||||
//Splice out item we found for performance.
|
||||
existingLines.splice(matchingIndex, 1);
|
||||
} else {
|
||||
if (matchingIndexLines.length === 0) {
|
||||
//Didn't find a match. Must be a new line.
|
||||
linesToInsert.push(newLine);
|
||||
}
|
||||
//If we find only 1, we can use the old logic.
|
||||
else if (matchingIndexLines.length === 1) {
|
||||
//Found a relevant matching line. Add it to lines to update.
|
||||
linesToUpdate.push({
|
||||
id: matchingIndexLines[0].record.id,
|
||||
newData: { ...newLine, removed: false, act_price_before_ppc: null }
|
||||
});
|
||||
//Splice out item we found for performance.
|
||||
existingLines.splice(matchingIndexLines[0].index, 1);
|
||||
} else if (matchingIndexLines.length === 2) {
|
||||
//if we find 2, we need to separate it out by the non-refinish lines and splice one out.
|
||||
//Find the mathcing one depending on whether this is the refiniish one or not.
|
||||
const matchingLine = matchingIndexLines.find((i) =>
|
||||
newLine.mod_lbr_ty === "LAR" ? i.record.mod_lbr_ty === "LAR" : i.record.mod_lbr_ty !== "LAR"
|
||||
);
|
||||
|
||||
linesToUpdate.push({
|
||||
id: matchingLine.record.id,
|
||||
newData: { ...newLine, removed: false, act_price_before_ppc: null }
|
||||
});
|
||||
//Splice out item we found for performance.
|
||||
existingLines.splice(matchingLine.index, 1);
|
||||
} else {
|
||||
//We found more than 2 matching lines. We should never get here. Throw a warning and back out!
|
||||
throw new Error("Too many matching lines found. Ensure EMS file is valid.");
|
||||
}
|
||||
});
|
||||
|
||||
//Wahtever is left in the existing lines, are lines that should be removed.
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Col, Row, notification } from "antd";
|
||||
import Axios from "axios";
|
||||
import _ from "lodash";
|
||||
import dayjs from "../../utils/day";
|
||||
import queryString from "query-string";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -24,6 +23,8 @@ import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selecto
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import confirmDialog from "../../utils/asyncConfirm";
|
||||
import CriticalPartsScan from "../../utils/criticalPartsScan";
|
||||
import dayjs from "../../utils/day";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import JobsAvailableScan from "../jobs-available-scan/jobs-available-scan.component";
|
||||
import JobsFindModalContainer from "../jobs-find-modal/jobs-find-modal.container";
|
||||
@@ -32,7 +33,6 @@ import OwnerFindModalContainer from "../owner-find-modal/owner-find-modal.contai
|
||||
import { GetSupplementDelta } from "./jobs-available-supplement.estlines.util";
|
||||
import HeaderFields from "./jobs-available-supplement.headerfields";
|
||||
import JobsAvailableTableComponent from "./jobs-available-table.component";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -237,7 +237,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
|
||||
executeFunction: true,
|
||||
rome: ResolveCCCLineIssues,
|
||||
promanager: ResolveCCCLineIssues,
|
||||
args: [(supp, bodyshop)]
|
||||
args: [supp, bodyshop]
|
||||
});
|
||||
|
||||
await InstanceRenderManager({
|
||||
@@ -580,12 +580,13 @@ function ResolveCCCLineIssues(estData, bodyshop) {
|
||||
InstanceRenderManager({
|
||||
executeFunction: true,
|
||||
args: [],
|
||||
promanager: () => {
|
||||
rome: () => {
|
||||
if (line.mod_lbr_ty === "LAET" || line.mod_lbr_ty === "LAUT") {
|
||||
// line.notes += ` | ET/UT Update (prev = ${line.mod_lbr_ty})`;
|
||||
line.mod_lbr_ty = "LAR";
|
||||
}
|
||||
}
|
||||
},
|
||||
promanager: "USE_ROME"
|
||||
});
|
||||
});
|
||||
|
||||
@@ -608,13 +609,12 @@ function ResolveCCCLineIssues(estData, bodyshop) {
|
||||
//Web Est seems to have additional costs with UNQ_SEQ 0. Keep them all?
|
||||
if (line.unq_seq === 0) return;
|
||||
if (index0ActPrice !== line.act_price) {
|
||||
line.notes += ` | Price override.`;
|
||||
// line.notes += ` | Price override.`;
|
||||
return;
|
||||
}
|
||||
const indexInEstData = estData.joblines.data.findIndex((l) => l.unq_seq === line.unq_seq);
|
||||
estData.joblines.data[
|
||||
indexInEstData
|
||||
].notes += ` | Scrubbed due to the line_ref issue. (prev act price = ${estData.joblines.data[indexInEstData].act_price})`;
|
||||
//estData.joblines.data[indexInEstData].notes +=
|
||||
// ` | Act Price delete. (prev act price = ${estData.joblines.data[indexInEstData].act_price})`;
|
||||
estData.joblines.data[indexInEstData].act_price = 0;
|
||||
estData.joblines.data[indexInEstData].db_price = 0;
|
||||
});
|
||||
|
||||
@@ -189,7 +189,10 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
|
||||
</Col>
|
||||
<Col {...lossColDamage}>
|
||||
{job.area_of_damage ? (
|
||||
<Car dmg1={job.area_of_damage.impact1} dmg2={job.area_of_damage.impact2} />
|
||||
<Car
|
||||
dmg1={job.area_of_damage.impact1 && job.area_of_damage.impact1.padStart(2, "0")}
|
||||
dmg2={job.area_of_damage.impact2 && job.area_of_damage.impact2.padStart(2, "0")}
|
||||
/>
|
||||
) : (
|
||||
t("jobs.errors.nodamage")
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { DownCircleFilled } from "@ant-design/icons";
|
||||
import { useApolloClient, useMutation } from "@apollo/client";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { Button, Card, Dropdown, Form, Input, Modal, notification, Popconfirm, Popover, Select, Space } from "antd";
|
||||
import axios from "axios";
|
||||
import parsePhoneNumber from "libphonenumber-js";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -8,27 +11,24 @@ import { Link, useNavigate } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
|
||||
import { GET_CURRENT_QUESTIONSET_ID, INSERT_CSI } from "../../graphql/csi.queries";
|
||||
import { DELETE_JOB, UPDATE_JOB, VOID_JOB } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { setEmailOptions } from "../../redux/email/email.actions";
|
||||
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import dayjs from "../../utils/day";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
||||
import RbacWrapper from "../rbac-wrapper/rbac-wrapper.component";
|
||||
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
|
||||
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
|
||||
import axios from "axios";
|
||||
import { setEmailOptions } from "../../redux/email/email.actions";
|
||||
import { openChatByPhone, setMessage } from "../../redux/messaging/messaging.actions";
|
||||
import { GET_CURRENT_QUESTIONSET_ID, INSERT_CSI } from "../../graphql/csi.queries";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import parsePhoneNumber from "libphonenumber-js";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import FormDateTimePickerComponent from "../form-date-time-picker/form-date-time-picker.component";
|
||||
import dayjs from "../../utils/day";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -39,13 +39,57 @@ const mapStateToProps = createStructuredSelector({
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setScheduleContext: (context) => dispatch(setModalContext({ context: context, modal: "schedule" })),
|
||||
setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" })),
|
||||
setBillEnterContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "billEnter"
|
||||
})
|
||||
),
|
||||
setPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "payment" })),
|
||||
setJobCostingContext: (context) => dispatch(setModalContext({ context: context, modal: "jobCosting" })),
|
||||
setTimeTicketContext: (context) => dispatch(setModalContext({ context: context, modal: "timeTicket" })),
|
||||
setCardPaymentContext: (context) => dispatch(setModalContext({ context: context, modal: "cardPayment" })),
|
||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type })),
|
||||
setTimeTicketTaskContext: (context) => dispatch(setModalContext({ context: context, modal: "timeTicketTask" })),
|
||||
setJobCostingContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "jobCosting"
|
||||
})
|
||||
),
|
||||
setTimeTicketContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "timeTicket"
|
||||
})
|
||||
),
|
||||
setCardPaymentContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "cardPayment"
|
||||
})
|
||||
),
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(
|
||||
insertAuditTrail({
|
||||
jobid,
|
||||
operation,
|
||||
type
|
||||
})
|
||||
),
|
||||
setTimeTicketTaskContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "timeTicketTask"
|
||||
})
|
||||
),
|
||||
setTaskUpsertContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "taskUpsert"
|
||||
})
|
||||
),
|
||||
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
||||
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
||||
setMessage: (text) => dispatch(setMessage(text))
|
||||
@@ -67,7 +111,8 @@ export function JobsDetailHeaderActions({
|
||||
setEmailOptions,
|
||||
openChatByPhone,
|
||||
setMessage,
|
||||
setTimeTicketTaskContext
|
||||
setTimeTicketTaskContext,
|
||||
setTaskUpsertContext
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const client = useApolloClient();
|
||||
@@ -596,6 +641,7 @@ export function JobsDetailHeaderActions({
|
||||
const menuItems = [
|
||||
{
|
||||
key: "schedule",
|
||||
id: "job-actions-schedule",
|
||||
disabled: !jobInPreProduction || !job.converted || jobRO,
|
||||
label: t("jobs.actions.schedule"),
|
||||
onClick: () => {
|
||||
@@ -612,6 +658,7 @@ export function JobsDetailHeaderActions({
|
||||
},
|
||||
{
|
||||
key: "cancelallappointments",
|
||||
id: "job-actions-cancelallappointments",
|
||||
onClick: () => {
|
||||
if (job.status !== bodyshop.md_ro_statuses.default_scheduled) {
|
||||
return;
|
||||
@@ -625,6 +672,7 @@ export function JobsDetailHeaderActions({
|
||||
imex: [
|
||||
{
|
||||
key: "intake",
|
||||
id: "job-actions-intake",
|
||||
disabled: !!job.intakechecklist || !jobInPreProduction || !job.converted || jobRO,
|
||||
label:
|
||||
!!job.intakechecklist || !jobInPreProduction || !job.converted || jobRO ? (
|
||||
@@ -635,6 +683,7 @@ export function JobsDetailHeaderActions({
|
||||
},
|
||||
{
|
||||
key: "deliver",
|
||||
id: "job-actions-deliver",
|
||||
disabled: !jobInProduction || jobRO,
|
||||
label: !jobInProduction ? (
|
||||
t("jobs.actions.deliver")
|
||||
@@ -644,6 +693,7 @@ export function JobsDetailHeaderActions({
|
||||
},
|
||||
{
|
||||
key: "checklist",
|
||||
id: "job-actions-checklist",
|
||||
disabled: !job.converted,
|
||||
label: <Link to={`/manage/jobs/${job.id}/checklist`}>{t("jobs.actions.viewchecklist")}</Link>
|
||||
}
|
||||
@@ -652,6 +702,7 @@ export function JobsDetailHeaderActions({
|
||||
promanager: [
|
||||
{
|
||||
key: "toggleproduction",
|
||||
id: "job-actions-toggleproduction",
|
||||
disabled: !job.converted || jobRO,
|
||||
label: <JobsDetailHeaderActionsToggleProduction job={job} refetch={refetch} />
|
||||
}
|
||||
@@ -665,6 +716,7 @@ export function JobsDetailHeaderActions({
|
||||
? [
|
||||
{
|
||||
key: "entertimetickets",
|
||||
id: "job-actions-entertimetickets",
|
||||
disabled: !job.converted || (!bodyshop.tt_allow_post_to_invoiced && job.date_invoiced),
|
||||
label: t("timetickets.actions.enter"),
|
||||
onClick: () => {
|
||||
@@ -688,6 +740,7 @@ export function JobsDetailHeaderActions({
|
||||
if (bodyshop.md_tasks_presets.enable_tasks) {
|
||||
menuItems.push({
|
||||
key: "claimtimetickettasks",
|
||||
id: "job-actions-claimtimetickettasks",
|
||||
disabled: !job.converted || (!bodyshop.tt_allow_post_to_invoiced && job.date_invoiced),
|
||||
onClick: () => {
|
||||
setTimeTicketTaskContext({
|
||||
@@ -701,6 +754,7 @@ export function JobsDetailHeaderActions({
|
||||
|
||||
menuItems.push({
|
||||
key: "enterpayments",
|
||||
id: "job-actions-enterpayments",
|
||||
disabled: !job.converted,
|
||||
label: t("menus.header.enterpayment"),
|
||||
onClick: () => {
|
||||
@@ -716,22 +770,24 @@ export function JobsDetailHeaderActions({
|
||||
if (ImEXPay.treatment === "on") {
|
||||
menuItems.push({
|
||||
key: "entercardpayments",
|
||||
id: "job-actions-entercardpayments",
|
||||
disabled: !job.converted,
|
||||
label: t("menus.header.entercardpayment"),
|
||||
onClick: () => {
|
||||
logImEXEvent("job_header_enter_card_payment");
|
||||
|
||||
setCardPaymentContext({
|
||||
actions: {},
|
||||
actions: { refetch },
|
||||
context: { jobid: job.id }
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (HasFeatureAccess({ featureName: "courtesycars" })) {
|
||||
if (HasFeatureAccess({ featureName: "courtesycars", bodyshop })) {
|
||||
menuItems.push({
|
||||
key: "cccontract",
|
||||
id: "job-actions-cccontract",
|
||||
disabled: jobRO || !job.converted,
|
||||
label: (
|
||||
<Link state={{ jobId: job.id }} to="/manage/courtesycars/contracts/new">
|
||||
@@ -741,16 +797,29 @@ export function JobsDetailHeaderActions({
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.push({
|
||||
key: "createtask",
|
||||
id: "job-actions-createtask",
|
||||
label: t("menus.header.create_task"),
|
||||
onClick: () =>
|
||||
setTaskUpsertContext({
|
||||
actions: {},
|
||||
context: { jobid: job.id }
|
||||
})
|
||||
});
|
||||
|
||||
menuItems.push(
|
||||
job.inproduction
|
||||
? {
|
||||
key: "addtoproduction",
|
||||
key: "removefromproduction",
|
||||
id: "job-actions-removefromproduction",
|
||||
disabled: !job.converted,
|
||||
label: t("jobs.actions.removefromproduction"),
|
||||
onClick: () => AddToProduction(client, job.id, refetch, true)
|
||||
}
|
||||
: {
|
||||
key: "addtoproduction",
|
||||
id: "job-actions-addtoproduction",
|
||||
disabled: !job.converted,
|
||||
label: t("jobs.actions.addtoproduction"),
|
||||
onClick: () => AddToProduction(client, job.id, refetch)
|
||||
@@ -760,12 +829,14 @@ export function JobsDetailHeaderActions({
|
||||
menuItems.push(
|
||||
{
|
||||
key: "togglesuspend",
|
||||
id: "job-actions-togglesuspend",
|
||||
onClick: handleSuspend,
|
||||
label: job.suspended ? t("production.actions.unsuspend") : t("production.actions.suspend")
|
||||
},
|
||||
{
|
||||
key: "toggleAlert",
|
||||
onClick: handleAlertToggle,
|
||||
id: "job-actions-togglealert",
|
||||
label:
|
||||
job.production_vars && job.production_vars.alert
|
||||
? t("production.labels.alertoff")
|
||||
@@ -777,6 +848,7 @@ export function JobsDetailHeaderActions({
|
||||
children: [
|
||||
{
|
||||
key: "duplicate",
|
||||
id: "job-actions-duplicate",
|
||||
label: (
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.duplicateconfirm")}
|
||||
@@ -792,6 +864,7 @@ export function JobsDetailHeaderActions({
|
||||
},
|
||||
{
|
||||
key: "duplicatenolines",
|
||||
id: "job-actions-duplicatenolines",
|
||||
label: (
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.duplicateconfirm")}
|
||||
@@ -815,6 +888,7 @@ export function JobsDetailHeaderActions({
|
||||
? [
|
||||
{
|
||||
key: "postbills",
|
||||
id: "job-actions-postbills",
|
||||
disabled: !job.converted,
|
||||
label: t("jobs.actions.postbills"),
|
||||
onClick: () => {
|
||||
@@ -833,6 +907,7 @@ export function JobsDetailHeaderActions({
|
||||
|
||||
{
|
||||
key: "addtopartsqueue",
|
||||
id: "job-actions-addtopartsqueue",
|
||||
disabled: !job.converted || !jobInProduction || jobRO,
|
||||
label: t("jobs.actions.addtopartsqueue"),
|
||||
onClick: async () => {
|
||||
@@ -858,6 +933,7 @@ export function JobsDetailHeaderActions({
|
||||
},
|
||||
{
|
||||
key: "closejob",
|
||||
id: "job-actions-closejob",
|
||||
disabled: !jobInPostProduction,
|
||||
label: !jobInPostProduction ? (
|
||||
t("menus.jobsactions.closejob")
|
||||
@@ -873,6 +949,7 @@ export function JobsDetailHeaderActions({
|
||||
},
|
||||
{
|
||||
key: "admin",
|
||||
id: "job-actions-admin",
|
||||
label: (
|
||||
<Link
|
||||
to={{
|
||||
@@ -894,6 +971,7 @@ export function JobsDetailHeaderActions({
|
||||
) {
|
||||
menuItems.push({
|
||||
key: "exportcustdata",
|
||||
id: "job-actions-exportcustdata",
|
||||
disabled: !job.converted,
|
||||
label: t("jobs.actions.exportcustdata"),
|
||||
onClick: handleExportCustData
|
||||
@@ -904,18 +982,21 @@ export function JobsDetailHeaderActions({
|
||||
const children = [
|
||||
{
|
||||
key: "email",
|
||||
id: "job-actions-email",
|
||||
disabled: !!!job.ownr_ea,
|
||||
label: t("general.labels.email"),
|
||||
onClick: handleCreateCsi
|
||||
},
|
||||
{
|
||||
key: "text",
|
||||
id: "job-actions-text",
|
||||
disabled: !!!job.ownr_ph1,
|
||||
label: t("general.labels.text"),
|
||||
onClick: handleCreateCsi
|
||||
},
|
||||
{
|
||||
key: "generate",
|
||||
id: "job-actions-generate",
|
||||
disabled: job.csiinvites && job.csiinvites.length > 0,
|
||||
label: t("jobs.actions.generatecsi"),
|
||||
onClick: handleCreateCsi
|
||||
@@ -949,6 +1030,7 @@ export function JobsDetailHeaderActions({
|
||||
}
|
||||
menuItems.push({
|
||||
key: "sendcsi",
|
||||
id: "job-actions-sendcsi",
|
||||
label: t("jobs.actions.sendcsi"),
|
||||
disabled: !job.converted,
|
||||
children
|
||||
@@ -957,6 +1039,7 @@ export function JobsDetailHeaderActions({
|
||||
|
||||
menuItems.push({
|
||||
key: "jobcosting",
|
||||
id: "job-actions-jobcosting",
|
||||
disabled: !job.converted,
|
||||
label: t("jobs.labels.jobcosting"),
|
||||
onClick: () => {
|
||||
@@ -974,6 +1057,7 @@ export function JobsDetailHeaderActions({
|
||||
if (job && !job.converted) {
|
||||
menuItems.push({
|
||||
key: "deletejob",
|
||||
id: "job-actions-deletejob",
|
||||
label: (
|
||||
<Popconfirm
|
||||
title={t("jobs.labels.deleteconfirm")}
|
||||
@@ -990,6 +1074,7 @@ export function JobsDetailHeaderActions({
|
||||
|
||||
menuItems.push({
|
||||
key: "manualevent",
|
||||
id: "job-actions-manualevent",
|
||||
onClick: (e) => {
|
||||
setVisibility(true);
|
||||
},
|
||||
@@ -999,6 +1084,7 @@ export function JobsDetailHeaderActions({
|
||||
if (!jobRO && job.converted) {
|
||||
menuItems.push({
|
||||
key: "voidjob",
|
||||
id: "job-actions-voidjob",
|
||||
label: (
|
||||
<RbacWrapper action="jobs:void" noauth>
|
||||
<Popconfirm
|
||||
|
||||
@@ -1,55 +1,13 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import queryString from "query-string";
|
||||
import React from "react";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { QUERY_BILLS_BY_JOBID } from "../../graphql/bills.queries";
|
||||
import JobsDetailPliComponent from "./jobs-detail-pli.component";
|
||||
|
||||
export default function JobsDetailPliContainer({ job }) {
|
||||
const billsQuery = useQuery(QUERY_BILLS_BY_JOBID, {
|
||||
variables: { jobid: job.id },
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const history = useNavigate();
|
||||
|
||||
const handleBillOnRowClick = (record) => {
|
||||
if (record) {
|
||||
if (record.id) {
|
||||
search.billid = record.id;
|
||||
history({ search: queryString.stringify(search) });
|
||||
}
|
||||
} else {
|
||||
delete search.billid;
|
||||
history({ search: queryString.stringify(search) });
|
||||
}
|
||||
};
|
||||
|
||||
const handlePartsOrderOnRowClick = (record) => {
|
||||
if (record) {
|
||||
if (record.id) {
|
||||
search.partsorderid = record.id;
|
||||
history({ search: queryString.stringify(search) });
|
||||
}
|
||||
} else {
|
||||
delete search.partsorderid;
|
||||
history({ search: queryString.stringify(search) });
|
||||
}
|
||||
};
|
||||
|
||||
const handlePartsDispatchOnRowClick = (record) => {
|
||||
if (record) {
|
||||
if (record.id) {
|
||||
search.partsdispatchid = record.id;
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
}
|
||||
} else {
|
||||
delete search.partsdispatchid;
|
||||
history.push({ search: queryString.stringify(search) });
|
||||
}
|
||||
};
|
||||
export default function JobsDetailPliContainer({
|
||||
job,
|
||||
billsQuery,
|
||||
handleBillOnRowClick,
|
||||
handlePartsOrderOnRowClick,
|
||||
handlePartsDispatchOnRowClick
|
||||
}) {
|
||||
return (
|
||||
<JobsDetailPliComponent
|
||||
job={job}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Collapse, Form, Switch } from "antd";
|
||||
import { Collapse, Form, InputNumber, Switch } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -17,6 +17,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
<Collapse defaultActiveKey={expanded && "rates"}>
|
||||
<Collapse.Panel forceRender header={t("jobs.labels.cieca_pfl")} key="cieca_pfl">
|
||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAB")}>
|
||||
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAB", "lbr_adjp"]}>
|
||||
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||
name={["cieca_pfl", "LAB", "lbr_tax_in"]}
|
||||
@@ -24,6 +27,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
>
|
||||
<Switch disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||
name={["cieca_pfl", "LAB", "lbr_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["cieca_pfl", "LAB", "lbr_tax_in"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["cieca_pfl", "LAB", "lbr_tx_in1"]}
|
||||
@@ -61,6 +82,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAD")}>
|
||||
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAD", "lbr_adjp"]}>
|
||||
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||
name={["cieca_pfl", "LAD", "lbr_tax_in"]}
|
||||
@@ -68,6 +92,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
>
|
||||
<Switch disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||
name={["cieca_pfl", "LAD", "lbr_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["cieca_pfl", "LAD", "lbr_tax_in"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["cieca_pfl", "LAD", "lbr_tx_in1"]}
|
||||
@@ -105,6 +147,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAE")}>
|
||||
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAE", "lbr_adjp"]}>
|
||||
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||
name={["cieca_pfl", "LAE", "lbr_tax_in"]}
|
||||
@@ -112,6 +157,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
>
|
||||
<Switch disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||
name={["cieca_pfl", "LAE", "lbr_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["cieca_pfl", "LAE", "lbr_tax_in"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["cieca_pfl", "LAE", "lbr_tx_in1"]}
|
||||
@@ -149,6 +212,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAF")}>
|
||||
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAF", "lbr_adjp"]}>
|
||||
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||
name={["cieca_pfl", "LAF", "lbr_tax_in"]}
|
||||
@@ -156,6 +222,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
>
|
||||
<Switch disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||
name={["cieca_pfl", "LAF", "lbr_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["cieca_pfl", "LAF", "lbr_tax_in"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["cieca_pfl", "LAF", "lbr_tx_in1"]}
|
||||
@@ -193,6 +277,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAG")}>
|
||||
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAG", "lbr_adjp"]}>
|
||||
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||
name={["cieca_pfl", "LAG", "lbr_tax_in"]}
|
||||
@@ -200,6 +287,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
>
|
||||
<Switch disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||
name={["cieca_pfl", "LAG", "lbr_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["cieca_pfl", "LAG", "lbr_tax_in"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["cieca_pfl", "LAG", "lbr_tx_in1"]}
|
||||
@@ -237,6 +342,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAM")}>
|
||||
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAM", "lbr_adjp"]}>
|
||||
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||
name={["cieca_pfl", "LAM", "lbr_tax_in"]}
|
||||
@@ -244,6 +352,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
>
|
||||
<Switch disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||
name={["cieca_pfl", "LAM", "lbr_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["cieca_pfl", "LAM", "lbr_tax_in"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["cieca_pfl", "LAM", "lbr_tx_in1"]}
|
||||
@@ -281,6 +407,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAR")}>
|
||||
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAR", "lbr_adjp"]}>
|
||||
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||
name={["cieca_pfl", "LAR", "lbr_tax_in"]}
|
||||
@@ -288,6 +417,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
>
|
||||
<Switch disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||
name={["cieca_pfl", "LAR", "lbr_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["cieca_pfl", "LAR", "lbr_tax_in"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["cieca_pfl", "LAR", "lbr_tx_in1"]}
|
||||
@@ -325,6 +472,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAS")}>
|
||||
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAS", "lbr_adjp"]}>
|
||||
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||
name={["cieca_pfl", "LAS", "lbr_tax_in"]}
|
||||
@@ -332,6 +482,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
>
|
||||
<Switch disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||
name={["cieca_pfl", "LAS", "lbr_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["cieca_pfl", "LAS", "lbr_tax_in"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["cieca_pfl", "LAS", "lbr_tx_in1"]}
|
||||
@@ -369,6 +537,9 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow header={t("joblines.fields.lbr_types.LAU")}>
|
||||
<Form.Item label={t("jobs.fields.cieca_pfl.lbr_adjp")} name={["cieca_pfl", "LAU", "lbr_adjp"]}>
|
||||
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tax_in")}
|
||||
name={["cieca_pfl", "LAU", "lbr_tax_in"]}
|
||||
@@ -376,6 +547,24 @@ export function JobsDetailRatesLabor({ jobRO, expanded, required = true, form })
|
||||
>
|
||||
<Switch disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_taxp")}
|
||||
name={["cieca_pfl", "LAU", "lbr_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["cieca_pfl", "LAU", "lbr_tax_in"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.cieca_pfl.lbr_tx_in1")}
|
||||
name={["cieca_pfl", "LAU", "lbr_tx_in1"]}
|
||||
|
||||
@@ -23,7 +23,9 @@ export function JobsDetailRatesMaterials({ jobRO, expanded, required = true, for
|
||||
<Form.Item label={t("jobs.fields.materials.cal_opcode")} name={["materials", "MAPA", "cal_opcode"]}>
|
||||
<Input disabled={jobRO} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("jobs.fields.materials.mat_adjp")} name={["materials", "MAPA", "mat_adjp"]}>
|
||||
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.materials.tax_ind")}
|
||||
name={["materials", "MAPA", "tax_ind"]}
|
||||
@@ -31,6 +33,24 @@ export function JobsDetailRatesMaterials({ jobRO, expanded, required = true, for
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.materials.mat_taxp")}
|
||||
name={["materials", "MAPA", "mat_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["materials", "MAPA", "tax_ind"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.materials.mat_tx_in1")}
|
||||
name={["materials", "MAPA", "mat_tx_in1"]}
|
||||
@@ -74,7 +94,9 @@ export function JobsDetailRatesMaterials({ jobRO, expanded, required = true, for
|
||||
<Form.Item label={t("jobs.fields.materials.cal_opcode")} name={["materials", "MASH", "cal_opcode"]}>
|
||||
<Input disabled={jobRO} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("jobs.fields.materials.mat_adjp")} name={["materials", "MAPA", "mat_adjp"]}>
|
||||
<InputNumber min={-100} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.materials.tax_ind")}
|
||||
name={["materials", "MASH", "tax_ind"]}
|
||||
@@ -82,6 +104,24 @@ export function JobsDetailRatesMaterials({ jobRO, expanded, required = true, for
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
return (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.materials.mat_taxp")}
|
||||
name={["materials", "MASH", "mat_taxp"]}
|
||||
rules={[
|
||||
{
|
||||
required: form.getFieldValue(["materials", "MASH", "tax_ind"])
|
||||
//message: t("general.validation.required"),
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.materials.mat_tx_in1")}
|
||||
name={["materials", "MASH", "mat_tx_in1"]}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -988,18 +989,24 @@ export function JobsDetailRatesParts({ jobRO, expanded, required = true, form })
|
||||
<Form.Item label={t("jobs.fields.tax_str_rt")} name="tax_str_rt">
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.tax_paint_mat_rt")} name="tax_paint_mat_rt">
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.tax_shop_mat_rt")} name="tax_shop_mat_rt">
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
{InstanceRenderManager({ imex: true, rome: false, promanager: "USE_ROME" }) ? (
|
||||
<>
|
||||
<Form.Item label={t("jobs.fields.tax_paint_mat_rt")} name="tax_paint_mat_rt">
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.tax_shop_mat_rt")} name="tax_shop_mat_rt">
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>{" "}
|
||||
</>
|
||||
) : null}
|
||||
<Form.Item label={t("jobs.fields.tax_sub_rt")} name="tax_sub_rt">
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("jobs.fields.tax_lbr_rt")} name="tax_lbr_rt">
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
{InstanceRenderManager({ imex: true, rome: false, promanager: "USE_ROME" }) ? (
|
||||
<Form.Item label={t("jobs.fields.tax_lbr_rt")} name="tax_lbr_rt">
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
) : null}
|
||||
<Form.Item label={t("jobs.fields.tax_levies_rt")} name="tax_levies_rt">
|
||||
<InputNumber min={0} max={100} precision={4} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -96,7 +96,6 @@ export function JobsList({ bodyshop, refetch, loading, jobs, total }) {
|
||||
}),
|
||||
onFilter: (value, record) => value.includes(record.status)
|
||||
},
|
||||
|
||||
{
|
||||
title: t("jobs.fields.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
|
||||
@@ -16,19 +16,15 @@ import useLocalStorage from "../../utils/useLocalStorage";
|
||||
import AlertComponent from "../alert/alert.component";
|
||||
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import { setJoyRideSteps } from "../../redux/application/application.actions";
|
||||
import { OwnerNameDisplayFunction } from "./../owner-name-display/owner-name-display.component";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setJoyRideSteps: (steps) => dispatch(setJoyRideSteps(steps))
|
||||
});
|
||||
const mapDispatchToProps = (dispatch) => ({});
|
||||
|
||||
export function JobsList({ bodyshop, setJoyRideSteps }) {
|
||||
export function JobsList({ bodyshop }) {
|
||||
const searchParams = queryString.parse(useLocation().search);
|
||||
const { selected } = searchParams;
|
||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||
@@ -176,7 +172,6 @@ export function JobsList({ bodyshop, setJoyRideSteps }) {
|
||||
[],
|
||||
onFilter: (value, record) => value.includes(record.status)
|
||||
},
|
||||
|
||||
{
|
||||
title: t("jobs.fields.vehicle"),
|
||||
dataIndex: "vehicle",
|
||||
@@ -316,34 +311,6 @@ export function JobsList({ bodyshop, setJoyRideSteps }) {
|
||||
title={t("titles.bc.jobs-active")}
|
||||
extra={
|
||||
<Space wrap>
|
||||
{InstanceRenderManager({
|
||||
promanager: (
|
||||
<Button
|
||||
onClick={() =>
|
||||
setJoyRideSteps([
|
||||
{
|
||||
target: "#active-jobs-list",
|
||||
content: "This is where you will see all work coming in and currently here."
|
||||
},
|
||||
{
|
||||
target: "#header-jobs",
|
||||
spotlightClicks: true,
|
||||
disableOverlayClose: true,
|
||||
content:
|
||||
"The jobs menu lets you access additional pages to see more information. You can import new jobs, search all jobs, or manage your current production."
|
||||
},
|
||||
{
|
||||
target: "#header-jobs-available",
|
||||
content: "You can find jobs to import here."
|
||||
}
|
||||
])
|
||||
}
|
||||
>
|
||||
Start Walk Through
|
||||
</Button>
|
||||
)
|
||||
})}
|
||||
|
||||
<Button onClick={() => refetch()}>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
|
||||
@@ -6,7 +6,8 @@ import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { alphaSort, statusSort } from "../../utils/sorters";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort, dateSort, statusSort } from "../../utils/sorters";
|
||||
import OwnerDetailUpdateJobsComponent from "../owner-detail-update-jobs/owner-detail-update-jobs.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -75,7 +76,18 @@ function OwnerDetailJobsComponent({ bodyshop, owner }) {
|
||||
})),
|
||||
onFilter: (value, record) => value.includes(record.status)
|
||||
},
|
||||
|
||||
{
|
||||
title: t("jobs.fields.actual_completion"),
|
||||
dataIndex: "actual_completion",
|
||||
key: "actual_completion",
|
||||
render: (text, record) => (
|
||||
<DateTimeFormatter>{record.actual_completion}</DateTimeFormatter>
|
||||
),
|
||||
sorter: (a, b) => dateSort(a.actual_completion, b.actual_completion),
|
||||
sortOrder:
|
||||
state.sortedInfo.columnKey === "actual_completion" &&
|
||||
state.sortedInfo.order,
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.clm_total"),
|
||||
dataIndex: "clm_total",
|
||||
|
||||
@@ -0,0 +1,411 @@
|
||||
import { DeleteFilled, EyeFilled } from "@ant-design/icons";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||
import { Button, Drawer, Grid, Popconfirm, Space, Table } from "antd";
|
||||
|
||||
import queryString from "query-string";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FaTasks } from "react-icons/fa";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { QUERY_BILL_BY_PK } from "../../graphql/bills.queries";
|
||||
import { DELETE_PARTS_ORDER } from "../../graphql/parts-orders.queries";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import DataLabel from "../data-label/data-label.component";
|
||||
import FeatureWrapperComponent from "../feature-wrapper/feature-wrapper.component";
|
||||
import PartsOrderBackorderEta from "../parts-order-backorder-eta/parts-order-backorder-eta.component";
|
||||
import PartsOrderCmReceived from "../parts-order-cm-received/parts-order-cm-received.component";
|
||||
import PartsOrderDeleteLine from "../parts-order-delete-line/parts-order-delete-line.component";
|
||||
import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component";
|
||||
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
|
||||
import PrintWrapper from "../print-wrapper/print-wrapper.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBillEnterContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "billEnter"
|
||||
})
|
||||
),
|
||||
setPartsReceiveContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "partsReceive"
|
||||
})
|
||||
),
|
||||
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
|
||||
});
|
||||
|
||||
export function PartsOrderListTableDrawerComponent({
|
||||
setBillEnterContext,
|
||||
bodyshop,
|
||||
jobRO,
|
||||
job,
|
||||
billsQuery,
|
||||
handleOnRowClick,
|
||||
setPartsReceiveContext,
|
||||
setTaskUpsertContext
|
||||
}) {
|
||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||
.filter((screen) => !!screen[1])
|
||||
.slice(-1)[0];
|
||||
|
||||
const bpoints = {
|
||||
xs: "100%",
|
||||
sm: "100%",
|
||||
md: "100%",
|
||||
lg: "75%",
|
||||
xl: "75%",
|
||||
xxl: "65%"
|
||||
};
|
||||
const drawerPercentage = selectedBreakpoint ? bpoints[selectedBreakpoint[0]] : "100%";
|
||||
const responsibilityCenters = bodyshop.md_responsibility_centers;
|
||||
const Templates = TemplateList("partsorder", { job });
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {}
|
||||
});
|
||||
|
||||
const [returnfrombill, setReturnFromBill] = useState();
|
||||
const [billData, setBillData] = useState();
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const selectedpartsorder = search.partsorderid;
|
||||
|
||||
const [billQuery] = useLazyQuery(QUERY_BILL_BY_PK);
|
||||
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
|
||||
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
|
||||
const { refetch } = billsQuery;
|
||||
|
||||
useEffect(() => {
|
||||
if (returnfrombill === null) {
|
||||
setBillData(null);
|
||||
} else {
|
||||
const fetchData = async () => {
|
||||
const result = await billQuery({
|
||||
variables: { billid: returnfrombill }
|
||||
});
|
||||
setBillData(result.data);
|
||||
};
|
||||
fetchData();
|
||||
}
|
||||
}, [returnfrombill, billQuery]);
|
||||
|
||||
const recordActions = (record, showView = false) => (
|
||||
<Space direction="horizontal" wrap>
|
||||
{showView && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (record.returnfrombill) {
|
||||
setReturnFromBill(record.returnfrombill);
|
||||
} else {
|
||||
setReturnFromBill(null);
|
||||
}
|
||||
handleOnRowClick(record);
|
||||
}}
|
||||
>
|
||||
<EyeFilled />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
disabled={jobRO || record.return || record.vendor.id === bodyshop.inhousevendorid}
|
||||
onClick={() => {
|
||||
logImEXEvent("parts_order_receive_bill");
|
||||
setPartsReceiveContext({
|
||||
actions: { refetch: refetch },
|
||||
context: {
|
||||
jobId: job.id,
|
||||
job: job,
|
||||
partsorderlines: record.parts_order_lines.map((pol) => {
|
||||
return {
|
||||
joblineid: pol.job_line_id,
|
||||
id: pol.id,
|
||||
line_desc: pol.line_desc,
|
||||
quantity: pol.quantity,
|
||||
act_price: pol.act_price,
|
||||
oem_partno: pol.oem_partno
|
||||
};
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("parts_orders.actions.receive")}
|
||||
</Button>
|
||||
<Button
|
||||
title={t("tasks.buttons.create")}
|
||||
onClick={() => {
|
||||
setTaskUpsertContext({
|
||||
context: {
|
||||
jobid: job.id,
|
||||
partsorderid: record.id
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FaTasks />
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title={t("parts_orders.labels.confirmdelete")}
|
||||
disabled={jobRO}
|
||||
onConfirm={async () => {
|
||||
//Delete the parts return.!
|
||||
|
||||
await deletePartsOrder({
|
||||
variables: { partsOrderId: record.id },
|
||||
update(cache) {
|
||||
cache.modify({
|
||||
fields: {
|
||||
parts_orders(existingPartsOrders, { readField }) {
|
||||
return existingPartsOrders.filter((billref) => record.id !== readField("id", billref));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button disabled={jobRO}>
|
||||
<DeleteFilled />
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<FeatureWrapperComponent featureName="bills" noauth={() => null}>
|
||||
<Button
|
||||
disabled={(jobRO ? !record.return : jobRO) || record.vendor.id === bodyshop.inhousevendorid}
|
||||
onClick={() => {
|
||||
logImEXEvent("parts_order_receive_bill");
|
||||
|
||||
setBillEnterContext({
|
||||
actions: { refetch: refetch },
|
||||
context: {
|
||||
job: job,
|
||||
bill: {
|
||||
vendorid: record.vendor.id,
|
||||
is_credit_memo: record.return,
|
||||
billlines: record.parts_order_lines.map((pol) => {
|
||||
return {
|
||||
joblineid: pol.job_line_id || "noline",
|
||||
line_desc: pol.line_desc,
|
||||
quantity: pol.quantity,
|
||||
|
||||
actual_price: pol.act_price,
|
||||
|
||||
cost_center: pol.jobline?.part_type
|
||||
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
|
||||
? pol.jobline.part_type !== "PAE"
|
||||
? pol.jobline.part_type
|
||||
: null
|
||||
: responsibilityCenters.defaults &&
|
||||
(responsibilityCenters.defaults.costs[pol.jobline.part_type] || null)
|
||||
: null
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("parts_orders.actions.receivebill")}
|
||||
</Button>
|
||||
</FeatureWrapperComponent>
|
||||
<PrintWrapper
|
||||
templateObject={{
|
||||
name: record.return ? Templates.parts_return_slip.key : Templates.parts_order.key,
|
||||
variables: { id: record.id }
|
||||
}}
|
||||
messageObject={{
|
||||
subject: record.return ? Templates.parts_return_slip.subject : Templates.parts_order.subject,
|
||||
to: record.vendor.email
|
||||
}}
|
||||
id={job.id}
|
||||
/>
|
||||
</Space>
|
||||
);
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
const selectedPartsOrderRecord = parts_orders.find((r) => r.id === selectedpartsorder);
|
||||
|
||||
const rowExpander = (record) => {
|
||||
const columns = [
|
||||
{
|
||||
title: t("parts_orders.fields.line_desc"),
|
||||
dataIndex: "line_desc",
|
||||
key: "line_desc",
|
||||
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
||||
sortOrder: state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.quantity"),
|
||||
dataIndex: "quantity",
|
||||
key: "quantity",
|
||||
sorter: (a, b) => a.quantity - b.quantity,
|
||||
sortOrder: state.sortedInfo.columnKey === "quantity" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.act_price"),
|
||||
dataIndex: "act_price",
|
||||
key: "act_price",
|
||||
sorter: (a, b) => a.act_price - b.act_price,
|
||||
sortOrder: state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
|
||||
render: (text, record) => <CurrencyFormatter>{record.act_price}</CurrencyFormatter>
|
||||
},
|
||||
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
|
||||
? [
|
||||
{
|
||||
title: t("parts_orders.fields.cost"),
|
||||
dataIndex: "cost",
|
||||
key: "cost",
|
||||
sorter: (a, b) => a.cost - b.cost,
|
||||
sortOrder: state.sortedInfo.columnKey === "cost" && state.sortedInfo.order,
|
||||
render: (text, record) => <CurrencyFormatter>{record.cost}</CurrencyFormatter>
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: t("parts_orders.fields.part_type"),
|
||||
dataIndex: "part_type",
|
||||
key: "part_type",
|
||||
render: (text, record) => (record.part_type ? t(`joblines.fields.part_types.${record.part_type}`) : null)
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.oem_partno"),
|
||||
dataIndex: "oem_partno",
|
||||
key: "oem_partno",
|
||||
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
|
||||
sortOrder: state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.line_remarks"),
|
||||
dataIndex: "line_remarks",
|
||||
key: "line_remarks"
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status"
|
||||
},
|
||||
|
||||
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
|
||||
? [
|
||||
{
|
||||
title: t("parts_orders.fields.cm_received"),
|
||||
dataIndex: "cm_received",
|
||||
key: "cm_received",
|
||||
render: (text, record) => (
|
||||
<PartsOrderCmReceived
|
||||
orderLineId={record.id}
|
||||
checked={record.cm_received}
|
||||
partsorderid={selectedPartsOrderRecord.id}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: t("parts_orders.fields.backordered_on"),
|
||||
dataIndex: "backordered_on",
|
||||
key: "backordered_on",
|
||||
render: (text, record) => <DateFormatter>{text}</DateFormatter>
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.backordered_eta"),
|
||||
dataIndex: "backordered_eta",
|
||||
key: "backordered_eta",
|
||||
render: (text, record) => (
|
||||
<PartsOrderBackorderEta
|
||||
backordered_eta={record.backordered_eta}
|
||||
disabled={jobRO}
|
||||
partsOrderStatus={record.status}
|
||||
partsLineId={record.id}
|
||||
jobLineId={record.job_line_id}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
render: (text, record) => (
|
||||
<Space wrap>
|
||||
<PartsOrderDeleteLine
|
||||
disabled={jobRO}
|
||||
partsOrderStatus={record.status}
|
||||
partsLineId={record.id}
|
||||
partsOrderId={selectedpartsorder}
|
||||
jobLineId={record.job_line_id}
|
||||
/>
|
||||
<PartsOrderLineBackorderButton
|
||||
disabled={jobRO}
|
||||
partsOrderStatus={record.status}
|
||||
partsLineId={record.id}
|
||||
jobLineId={record.job_line_id}
|
||||
/>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader
|
||||
title={
|
||||
billData
|
||||
? `${record.vendor.name} - ${record.order_number} - ${t("bills.labels.returnfrombill")}: ${billData.bills_by_pk.invoice_number}`
|
||||
: `${record.vendor.name} - ${record.order_number}`
|
||||
}
|
||||
extra={recordActions(record)}
|
||||
/>
|
||||
<Table
|
||||
scroll={{
|
||||
x: true //y: "50rem"
|
||||
}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={record.parts_order_lines}
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
<DataLabel label={t("parts_orders.fields.comments")}>
|
||||
<div style={{ whiteSpace: "pre" }}>{record.comments}</div>
|
||||
</DataLabel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PartsReceiveModalContainer />
|
||||
<Drawer
|
||||
placement="right"
|
||||
onClose={() => handleOnRowClick(null)}
|
||||
open={selectedpartsorder}
|
||||
closable
|
||||
width={drawerPercentage}
|
||||
>
|
||||
{selectedPartsOrderRecord && rowExpander(selectedPartsOrderRecord)}
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PartsOrderListTableDrawerComponent);
|
||||
@@ -1,31 +1,23 @@
|
||||
import { DeleteFilled, EyeFilled, SyncOutlined } from "@ant-design/icons";
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Card, Checkbox, Drawer, Grid, Input, Popconfirm, Space, Table } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
|
||||
import queryString from "query-string";
|
||||
import { Button, Card, Checkbox, Input, Popconfirm, Space, Table } from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FaTasks } from "react-icons/fa";
|
||||
import { connect } from "react-redux";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { DELETE_PARTS_ORDER } from "../../graphql/parts-orders.queries";
|
||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter } from "../../utils/DateFormatter";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import DataLabel from "../data-label/data-label.component";
|
||||
import PartsOrderBackorderEta from "../parts-order-backorder-eta/parts-order-backorder-eta.component";
|
||||
import PartsOrderCmReceived from "../parts-order-cm-received/parts-order-cm-received.component";
|
||||
import PartsOrderDeleteLine from "../parts-order-delete-line/parts-order-delete-line.component";
|
||||
import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import FeatureWrapperComponent from "../feature-wrapper/feature-wrapper.component";
|
||||
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
|
||||
import PrintWrapper from "../print-wrapper/print-wrapper.component";
|
||||
import FeatureWrapperComponent from "../feature-wrapper/feature-wrapper.component";
|
||||
import PartsOrderDrawer from "./parts-order-list-table-drawer.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
@@ -33,8 +25,21 @@ const mapStateToProps = createStructuredSelector({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBillEnterContext: (context) => dispatch(setModalContext({ context: context, modal: "billEnter" })),
|
||||
setPartsReceiveContext: (context) => dispatch(setModalContext({ context: context, modal: "partsReceive" }))
|
||||
setBillEnterContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "billEnter"
|
||||
})
|
||||
),
|
||||
setPartsReceiveContext: (context) =>
|
||||
dispatch(
|
||||
setModalContext({
|
||||
context: context,
|
||||
modal: "partsReceive"
|
||||
})
|
||||
),
|
||||
setTaskUpsertContext: (context) => dispatch(setModalContext({ context, modal: "taskUpsert" }))
|
||||
});
|
||||
|
||||
export function PartsOrderListTableComponent({
|
||||
@@ -44,21 +49,9 @@ export function PartsOrderListTableComponent({
|
||||
job,
|
||||
billsQuery,
|
||||
handleOnRowClick,
|
||||
setPartsReceiveContext
|
||||
setPartsReceiveContext,
|
||||
setTaskUpsertContext
|
||||
}) {
|
||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||
.filter((screen) => !!screen[1])
|
||||
.slice(-1)[0];
|
||||
|
||||
const bpoints = {
|
||||
xs: "100%",
|
||||
sm: "100%",
|
||||
md: "100%",
|
||||
lg: "75%",
|
||||
xl: "75%",
|
||||
xxl: "65%"
|
||||
};
|
||||
const drawerPercentage = selectedBreakpoint ? bpoints[selectedBreakpoint[0]] : "100%";
|
||||
const responsibilityCenters = bodyshop.md_responsibility_centers;
|
||||
const Templates = TemplateList("partsorder", { job });
|
||||
|
||||
@@ -66,19 +59,21 @@ export function PartsOrderListTableComponent({
|
||||
const [state, setState] = useState({
|
||||
sortedInfo: {}
|
||||
});
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const selectedpartsorder = search.partsorderid;
|
||||
const [searchText, setSearchText] = useState("");
|
||||
|
||||
const [searchText, setSearchText] = useState("");
|
||||
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
|
||||
|
||||
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
|
||||
const { refetch } = billsQuery;
|
||||
|
||||
const recordActions = (record, showView = false) => (
|
||||
<Space wrap>
|
||||
<Space direction="horizontal" wrap>
|
||||
{showView && (
|
||||
<Button onClick={() => handleOnRowClick(record)}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleOnRowClick(record);
|
||||
}}
|
||||
>
|
||||
<EyeFilled />
|
||||
</Button>
|
||||
)}
|
||||
@@ -108,7 +103,19 @@ export function PartsOrderListTableComponent({
|
||||
>
|
||||
{t("parts_orders.actions.receive")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
title={t("tasks.buttons.create")}
|
||||
onClick={() => {
|
||||
setTaskUpsertContext({
|
||||
context: {
|
||||
jobid: job.id,
|
||||
partsorderid: record.id
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
<FaTasks />
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title={t("parts_orders.labels.confirmdelete")}
|
||||
disabled={jobRO}
|
||||
@@ -148,7 +155,7 @@ export function PartsOrderListTableComponent({
|
||||
is_credit_memo: record.return,
|
||||
billlines: record.parts_order_lines.map((pol) => {
|
||||
return {
|
||||
joblineid: pol.job_line_id,
|
||||
joblineid: pol.job_line_id || "noline",
|
||||
line_desc: pol.line_desc,
|
||||
quantity: pol.quantity,
|
||||
|
||||
@@ -243,147 +250,6 @@ export function PartsOrderListTableComponent({
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
};
|
||||
|
||||
const selectedPartsOrderRecord = parts_orders.find((r) => r.id === selectedpartsorder);
|
||||
|
||||
const rowExpander = (record) => {
|
||||
const columns = [
|
||||
{
|
||||
title: t("parts_orders.fields.line_desc"),
|
||||
dataIndex: "line_desc",
|
||||
key: "line_desc",
|
||||
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
||||
sortOrder: state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.quantity"),
|
||||
dataIndex: "quantity",
|
||||
key: "quantity",
|
||||
sorter: (a, b) => a.quantity - b.quantity,
|
||||
sortOrder: state.sortedInfo.columnKey === "quantity" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.act_price"),
|
||||
dataIndex: "act_price",
|
||||
key: "act_price",
|
||||
sorter: (a, b) => a.act_price - b.act_price,
|
||||
sortOrder: state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
|
||||
render: (text, record) => <CurrencyFormatter>{record.act_price}</CurrencyFormatter>
|
||||
},
|
||||
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
|
||||
? [
|
||||
{
|
||||
title: t("parts_orders.fields.cost"),
|
||||
dataIndex: "cost",
|
||||
key: "cost",
|
||||
sorter: (a, b) => a.cost - b.cost,
|
||||
sortOrder: state.sortedInfo.columnKey === "cost" && state.sortedInfo.order,
|
||||
render: (text, record) => <CurrencyFormatter>{record.cost}</CurrencyFormatter>
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: t("parts_orders.fields.part_type"),
|
||||
dataIndex: "part_type",
|
||||
key: "part_type",
|
||||
render: (text, record) => (record.part_type ? t(`joblines.fields.part_types.${record.part_type}`) : null)
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.oem_partno"),
|
||||
dataIndex: "oem_partno",
|
||||
key: "oem_partno",
|
||||
sorter: (a, b) => alphaSort(a.oem_partno, b.oem_partno),
|
||||
sortOrder: state.sortedInfo.columnKey === "oem_partno" && state.sortedInfo.order
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.line_remarks"),
|
||||
dataIndex: "line_remarks",
|
||||
key: "line_remarks"
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.status"),
|
||||
dataIndex: "status",
|
||||
key: "status"
|
||||
},
|
||||
|
||||
...(selectedPartsOrderRecord && selectedPartsOrderRecord.return
|
||||
? [
|
||||
{
|
||||
title: t("parts_orders.fields.cm_received"),
|
||||
dataIndex: "cm_received",
|
||||
key: "cm_received",
|
||||
render: (text, record) => (
|
||||
<PartsOrderCmReceived
|
||||
orderLineId={record.id}
|
||||
checked={record.cm_received}
|
||||
partsorderid={selectedPartsOrderRecord.id}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
title: t("parts_orders.fields.backordered_on"),
|
||||
dataIndex: "backordered_on",
|
||||
key: "backordered_on",
|
||||
render: (text, record) => <DateFormatter>{text}</DateFormatter>
|
||||
},
|
||||
{
|
||||
title: t("parts_orders.fields.backordered_eta"),
|
||||
dataIndex: "backordered_eta",
|
||||
key: "backordered_eta",
|
||||
render: (text, record) => (
|
||||
<PartsOrderBackorderEta
|
||||
backordered_eta={record.backordered_eta}
|
||||
disabled={jobRO}
|
||||
partsOrderStatus={record.status}
|
||||
partsLineId={record.id}
|
||||
jobLineId={record.job_line_id}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
||||
{
|
||||
title: t("general.labels.actions"),
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
render: (text, record) => (
|
||||
<Space wrap>
|
||||
<PartsOrderDeleteLine
|
||||
disabled={jobRO}
|
||||
partsOrderStatus={record.status}
|
||||
partsLineId={record.id}
|
||||
partsOrderId={selectedpartsorder}
|
||||
jobLineId={record.job_line_id}
|
||||
/>
|
||||
<PartsOrderLineBackorderButton
|
||||
disabled={jobRO}
|
||||
partsOrderStatus={record.status}
|
||||
partsLineId={record.id}
|
||||
jobLineId={record.job_line_id}
|
||||
/>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader title={record && `${record.vendor.name} - ${record.order_number}`} extra={recordActions(record)} />
|
||||
<Table
|
||||
scroll={{
|
||||
x: true //y: "50rem"
|
||||
}}
|
||||
columns={columns}
|
||||
rowKey="id"
|
||||
dataSource={record.parts_order_lines}
|
||||
/>
|
||||
<DataLabel label={t("parts_orders.fields.comments")}>
|
||||
<div style={{ whiteSpace: "pre" }}>{record.comments}</div>
|
||||
</DataLabel>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const filteredPartsOrders = parts_orders
|
||||
? searchText === ""
|
||||
? parts_orders
|
||||
@@ -414,15 +280,13 @@ export function PartsOrderListTableComponent({
|
||||
}
|
||||
>
|
||||
<PartsReceiveModalContainer />
|
||||
<Drawer
|
||||
placement="right"
|
||||
onClose={() => handleOnRowClick(null)}
|
||||
open={selectedpartsorder}
|
||||
closable
|
||||
width={drawerPercentage}
|
||||
>
|
||||
{selectedPartsOrderRecord && rowExpander(selectedPartsOrderRecord)}
|
||||
</Drawer>
|
||||
<PartsOrderDrawer
|
||||
job={job}
|
||||
billsQuery={billsQuery}
|
||||
handleOnRowClick={handleOnRowClick}
|
||||
setPartsReceiveContext={setPartsReceiveContext}
|
||||
setTaskUpsertContext={setTaskUpsertContext}
|
||||
/>
|
||||
<Table
|
||||
loading={billsQuery.loading}
|
||||
scroll={{
|
||||
|
||||
@@ -182,7 +182,7 @@ export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState,
|
||||
}
|
||||
]}
|
||||
>
|
||||
<InputNumber />
|
||||
<InputNumber min={1} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t("parts_orders.fields.act_price")}
|
||||
|
||||
@@ -158,7 +158,7 @@ export function PartsOrderModalContainer({
|
||||
vendorid: bodyshop.inhousevendorid,
|
||||
invoice_number: "ih",
|
||||
isinhouse: true,
|
||||
date: new dayjs(),
|
||||
date: dayjs(),
|
||||
total: 0,
|
||||
billlines: values.parts_order_lines.data.map((p) => {
|
||||
return {
|
||||
|
||||
@@ -119,8 +119,14 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Descriptions title={t("job_payments.titles.descriptions")} contentStyle={{ fontWeight: "600" }} column={4}>
|
||||
<Descriptions.Item label={t("job_payments.titles.payer")}>{record.payer}</Descriptions.Item>
|
||||
<Descriptions
|
||||
title={t("job_payments.titles.descriptions")}
|
||||
contentStyle={{ fontWeight: "600" }}
|
||||
column={4}
|
||||
>
|
||||
<Descriptions.Item label={t("job_payments.titles.hint")}>
|
||||
{payment_response?.response?.methodhint}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t("job_payments.titles.payername")}>
|
||||
{payment_response?.response?.nameOnCard ?? ""}
|
||||
</Descriptions.Item>
|
||||
@@ -132,7 +138,7 @@ const PaymentExpandedRowComponent = ({ record, bodyshop }) => {
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t("job_payments.titles.transactionid")}>{record.transactionid}</Descriptions.Item>
|
||||
<Descriptions.Item label={t("job_payments.titles.paymentid")}>
|
||||
{payment_response?.response?.paymentreferenceid ?? ""}
|
||||
{payment_response?.ext_paymentid ?? ""}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label={t("job_payments.titles.paymenttype")}>{record.type}</Descriptions.Item>
|
||||
<Descriptions.Item label={t("job_payments.titles.paymentnum")}>{record.paymentnum}</Descriptions.Item>
|
||||
|
||||
@@ -7,8 +7,8 @@ import { createStructuredSelector } from "reselect";
|
||||
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||
import { UPDATE_PAYMENT } from "../../graphql/payments.queries";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import { selectPayment } from "../../redux/modals/modals.selectors";
|
||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
@@ -58,8 +58,10 @@ const PaymentMarkForExportButton = ({ bodyshop, payment, refetch, setPaymentCont
|
||||
refetch
|
||||
},
|
||||
context: {
|
||||
...paymentModal.context,
|
||||
...paymentModal.context,
|
||||
...payment,
|
||||
smartRefetch: true,
|
||||
exportedat: today
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,32 +1,30 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Form, Modal, notification, Space } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { INSERT_NEW_PAYMENT, UPDATE_PAYMENT } from "../../graphql/payments.queries";
|
||||
import { setEmailOptions } from "../../redux/email/email.actions";
|
||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||
import { selectPayment } from "../../redux/modals/modals.selectors";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import {useMutation} from "@apollo/client";
|
||||
import {Button, Form, Modal, notification, Space} from "antd";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {useTranslation} from "react-i18next";
|
||||
import {connect} from "react-redux";
|
||||
import {createStructuredSelector} from "reselect";
|
||||
import {INSERT_NEW_PAYMENT, UPDATE_PAYMENT} from "../../graphql/payments.queries";
|
||||
import {toggleModalVisible} from "../../redux/modals/modals.actions";
|
||||
import {selectPayment} from "../../redux/modals/modals.selectors";
|
||||
import {selectBodyshop} from "../../redux/user/user.selectors";
|
||||
import {GenerateDocument} from "../../utils/RenderTemplate";
|
||||
import {TemplateList} from "../../utils/TemplateConstants";
|
||||
import PaymentForm from "../payment-form/payment-form.component";
|
||||
import PaymentMarkForExportButton from "../payment-mark-export-button/payment-mark-export-button-component";
|
||||
import PaymentMarkForExportButton
|
||||
from "../payment-mark-export-button/payment-mark-export-button-component";
|
||||
import PaymentReexportButton from "../payment-reexport-button/payment-reexport-button.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
paymentModal: selectPayment,
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
||||
toggleModalVisible: () => dispatch(toggleModalVisible("payment"))
|
||||
});
|
||||
|
||||
function PaymentModalContainer({ paymentModal, toggleModalVisible, bodyshop, currentUser, setEmailOptions }) {
|
||||
function PaymentModalContainer({paymentModal, toggleModalVisible, bodyshop }) {
|
||||
const [form] = Form.useForm();
|
||||
const [enterAgain, setEnterAgain] = useState(false);
|
||||
const [insertPayment] = useMutation(INSERT_NEW_PAYMENT);
|
||||
@@ -34,7 +32,6 @@ function PaymentModalContainer({ paymentModal, toggleModalVisible, bodyshop, cur
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { context, actions, open } = paymentModal;
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
@@ -84,9 +81,9 @@ function PaymentModalContainer({ paymentModal, toggleModalVisible, bodyshop, cur
|
||||
});
|
||||
|
||||
if (!!!updatedPayment.errors) {
|
||||
notification["success"]({ message: t("payments.successes.payment") });
|
||||
notification["success"]({ message: t("payments.successes.paymentupdate") });
|
||||
} else {
|
||||
notification["error"]({ message: t("payments.errors.payment") });
|
||||
notification["error"]({ message: t("payments.errors.paymentupdate") });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ import { Button, notification } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { UPDATE_PAYMENT } from "../../graphql/payments.queries";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
import { selectPayment } from "../../redux/modals/modals.selectors";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
paymentModal: selectPayment
|
||||
@@ -40,6 +40,7 @@ const PaymentReexportButton = ({ paymentModal, payment, refetch, setPaymentConte
|
||||
refetch
|
||||
},
|
||||
context: {
|
||||
...paymentModal.context,
|
||||
...paymentModal.context,
|
||||
...payment,
|
||||
exportedat: null
|
||||
|
||||
@@ -14,11 +14,11 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
import CaBcEtfTableModalContainer from "../ca-bc-etf-table-modal/ca-bc-etf-table-modal.container";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import { Button, Card, Form, InputNumber, notification, Popover, Radio } from "antd";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Form,
|
||||
InputNumber,
|
||||
notification,
|
||||
Popover,
|
||||
Radio,
|
||||
Space,
|
||||
} from "antd";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -95,10 +104,16 @@ export function PrintCenterJobsLabels({ bodyshop, jobId }) {
|
||||
>
|
||||
<InputNumber min={1} precision={0} max={99} />
|
||||
</Form.Item>
|
||||
<Button type="primary" loading={loading} onClick={handleOk}>
|
||||
{t("general.actions.print")}
|
||||
</Button>
|
||||
<Button onClick={handleCancel}>{t("general.actions.cancel")}</Button>
|
||||
<div style={{ display: "flex", justifyContent: "flex-end" }}>
|
||||
<Space>
|
||||
<Button type="primary" loading={loading} onClick={handleOk}>
|
||||
{t("general.actions.print")}
|
||||
</Button>
|
||||
<Button onClick={handleCancel}>
|
||||
{t("general.actions.cancel")}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,6 @@ export default connect(mapStateToProps, mapDispatchToProps)(ProductionBoardFilte
|
||||
|
||||
export function ProductionBoardFilters({ bodyshop, filter, setFilter, loading }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Space wrap>
|
||||
{loading && <Spin />}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
.imex-kanban-card {
|
||||
padding: 0px !important;
|
||||
|
||||
.ant-card-body {
|
||||
padding: 0.8rem;
|
||||
}
|
||||
|
||||
.ant-card-head {
|
||||
padding: 0rem 0.8rem;
|
||||
}
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
import {
|
||||
BranchesOutlined,
|
||||
CalendarOutlined,
|
||||
DownloadOutlined,
|
||||
EyeFilled,
|
||||
PauseCircleOutlined
|
||||
} from "@ant-design/icons";
|
||||
import { Card, Col, Row, Space, Tooltip } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import ProductionAlert from "../production-list-columns/production-list-columns.alert.component";
|
||||
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
||||
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
|
||||
import "./production-board-card.styles.scss";
|
||||
import dayjs from "../../utils/day";
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
||||
|
||||
const cardColor = (ssbuckets, totalHrs) => {
|
||||
const bucket = ssbuckets.filter((bucket) => bucket.gte <= totalHrs && (!!bucket.lt ? bucket.lt > totalHrs : true))[0];
|
||||
|
||||
let color = { r: 255, g: 255, b: 255 };
|
||||
|
||||
if (bucket && bucket.color) {
|
||||
color = bucket.color;
|
||||
|
||||
if (bucket.color.rgb) {
|
||||
color = bucket.color.rgb;
|
||||
}
|
||||
}
|
||||
|
||||
return color;
|
||||
};
|
||||
|
||||
function getContrastYIQ(bgColor) {
|
||||
const yiq = (bgColor.r * 299 + bgColor.g * 587 + bgColor.b * 114) / 1000;
|
||||
|
||||
return yiq >= 128 ? "black" : "white";
|
||||
}
|
||||
|
||||
export default function ProductionBoardCard(technician, card, bodyshop, cardSettings) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
let employee_body, employee_prep, employee_refinish, employee_csr;
|
||||
if (card.employee_body) {
|
||||
employee_body = bodyshop.employees.find((e) => e.id === card.employee_body);
|
||||
}
|
||||
if (card.employee_prep) {
|
||||
employee_prep = bodyshop.employees.find((e) => e.id === card.employee_prep);
|
||||
}
|
||||
if (card.employee_refinish) {
|
||||
employee_refinish = bodyshop.employees.find((e) => e.id === card.employee_refinish);
|
||||
}
|
||||
if (card.employee_csr) {
|
||||
employee_csr = bodyshop.employees.find((e) => e.id === card.employee_csr);
|
||||
}
|
||||
// if (card.employee_csr) {
|
||||
// employee_csr = bodyshop.employees.find((e) => e.id === card.employee_csr);
|
||||
// }
|
||||
|
||||
const pastDueAlert =
|
||||
!!card.scheduled_completion &&
|
||||
((dayjs().isSameOrAfter(dayjs(card.scheduled_completion), "day") && "production-completion-past") ||
|
||||
(dayjs().add(1, "day").isSame(dayjs(card.scheduled_completion), "day") && "production-completion-soon"));
|
||||
|
||||
const totalHrs = card.labhrs.aggregate.sum.mod_lb_hrs + card.larhrs.aggregate.sum.mod_lb_hrs;
|
||||
const bgColor = cardColor(bodyshop.ssbuckets, totalHrs);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="react-kanban-card imex-kanban-card"
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor:
|
||||
cardSettings && cardSettings.cardcolor && `rgba(${bgColor.r},${bgColor.g},${bgColor.b},${bgColor.a})`,
|
||||
color: cardSettings && cardSettings.cardcolor && getContrastYIQ(bgColor)
|
||||
}}
|
||||
title={
|
||||
<Space>
|
||||
<ProductionAlert record={card} key="alert" />
|
||||
{card.suspended && <PauseCircleOutlined style={{ color: "orangered" }} />}
|
||||
{card.iouparent && (
|
||||
<Tooltip title={t("jobs.labels.iou")}>
|
||||
<BranchesOutlined style={{ color: "orangered" }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
<span style={{ fontWeight: "bolder" }}>
|
||||
<Link to={technician ? `/tech/joblookup?selected=${card.id}` : `/manage/jobs/${card.id}`}>
|
||||
{card.ro_number || t("general.labels.na")}
|
||||
</Link>
|
||||
</span>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Link to={{ search: `?selected=${card.id}` }}>
|
||||
<EyeFilled />
|
||||
</Link>
|
||||
}
|
||||
>
|
||||
<Row>
|
||||
{cardSettings && cardSettings.ownr_nm && (
|
||||
<Col span={24}>
|
||||
{cardSettings && cardSettings.compact ? (
|
||||
<div className="ellipses">{`${card.ownr_ln || ""} ${card.ownr_co_nm || ""}`}</div>
|
||||
) : (
|
||||
<div className="ellipses">
|
||||
<OwnerNameDisplay ownerObject={card} />
|
||||
</div>
|
||||
)}
|
||||
</Col>
|
||||
)}
|
||||
<Col span={24}>
|
||||
<div className="ellipses">{`${card.v_model_yr || ""} ${
|
||||
card.v_make_desc || ""
|
||||
} ${card.v_model_desc || ""}`}</div>
|
||||
</Col>
|
||||
{cardSettings && cardSettings.ins_co_nm && card.ins_co_nm && (
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
|
||||
<div className="ellipses">{card.ins_co_nm || ""}</div>
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings && cardSettings.clm_no && card.clm_no && (
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
|
||||
<div className="ellipses">{card.clm_no || ""}</div>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
{cardSettings && cardSettings.employeeassignments && (
|
||||
<Col span={24}>
|
||||
<Row>
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`B: ${
|
||||
employee_body ? `${employee_body.first_name.substr(0, 3)} ${employee_body.last_name.charAt(0)}` : ""
|
||||
} ${card.labhrs.aggregate.sum.mod_lb_hrs || "?"}h`}</Col>
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`P: ${
|
||||
employee_prep ? `${employee_prep.first_name.substr(0, 3)} ${employee_prep.last_name.charAt(0)}` : ""
|
||||
}`}</Col>
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`R: ${
|
||||
employee_refinish
|
||||
? `${employee_refinish.first_name.substr(0, 3)} ${employee_refinish.last_name.charAt(0)}`
|
||||
: ""
|
||||
} ${card.larhrs.aggregate.sum.mod_lb_hrs || "?"}h`}</Col>
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`C: ${
|
||||
employee_csr ? `${employee_csr.first_name} ${employee_csr.last_name}` : ""
|
||||
}`}</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
)}
|
||||
{/* {cardSettings && cardSettings.laborhrs && (
|
||||
<Col span={24}>
|
||||
<Row>
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`B: ${
|
||||
card.labhrs.aggregate.sum.mod_lb_hrs || "?"
|
||||
} hrs`}</Col>
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>{`R: ${
|
||||
card.larhrs.aggregate.sum.mod_lb_hrs || "?"
|
||||
} hrs`}</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
)} */}
|
||||
{cardSettings && cardSettings.actual_in && card.actual_in && (
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
|
||||
<Space>
|
||||
<DownloadOutlined />
|
||||
<DateTimeFormatter format="MM/DD">{card.actual_in}</DateTimeFormatter>
|
||||
</Space>
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings && cardSettings.scheduled_completion && card.scheduled_completion && (
|
||||
<Col span={cardSettings && cardSettings.compact ? 24 : 12}>
|
||||
<Space className={pastDueAlert}>
|
||||
<CalendarOutlined />
|
||||
<DateTimeFormatter format="MM/DD">{card.scheduled_completion}</DateTimeFormatter>
|
||||
</Space>
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings && cardSettings.ats && card.alt_transport && (
|
||||
<Col span={12}>
|
||||
<div>{card.alt_transport || ""}</div>
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings && cardSettings.sublets && (
|
||||
<Col span={12}>
|
||||
<ProductionSubletsManageComponent subletJobLines={card.subletLines} />
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings && cardSettings.production_note && (
|
||||
<Col span={24}>
|
||||
{cardSettings && cardSettings.production_note && <ProductionListColumnProductionNote record={card} />}
|
||||
</Col>
|
||||
)}
|
||||
{cardSettings && cardSettings.partsstatus && (
|
||||
<Col span={24}>
|
||||
<JobPartsQueueCount parts={card.joblines_status} />
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
import {
|
||||
BranchesOutlined,
|
||||
CalendarOutlined,
|
||||
DownloadOutlined,
|
||||
EyeFilled,
|
||||
PauseCircleOutlined
|
||||
} from "@ant-design/icons";
|
||||
import { Card, Col, Row, Space, Tooltip } from "antd";
|
||||
import React, { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||
import Dinero from "dinero.js";
|
||||
|
||||
import ProductionAlert from "../production-list-columns/production-list-columns.alert.component";
|
||||
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
|
||||
import ProductionSubletsManageComponent from "../production-sublets-manage/production-sublets-manage.component";
|
||||
|
||||
import dayjs from "../../utils/day";
|
||||
|
||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
||||
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
|
||||
|
||||
const cardColor = (ssbuckets, totalHrs) => {
|
||||
const bucket = ssbuckets.find((bucket) => bucket.gte <= totalHrs && (!bucket.lt || bucket.lt > totalHrs));
|
||||
return bucket && bucket.color ? bucket.color.rgb || bucket.color : { r: 255, g: 255, b: 255 };
|
||||
};
|
||||
|
||||
const getContrastYIQ = (bgColor) =>
|
||||
(bgColor.r * 299 + bgColor.g * 587 + bgColor.b * 114) / 1000 >= 128 ? "black" : "white";
|
||||
|
||||
const findEmployeeById = (employees, id) => employees.find((e) => e.id === id);
|
||||
|
||||
const EllipsesToolTip = React.memo(({ title, children, kiosk }) => {
|
||||
if (kiosk || !title) {
|
||||
return <div className="ellipses no-select">{children}</div>;
|
||||
}
|
||||
return (
|
||||
<Tooltip title={title}>
|
||||
<div className="ellipses">{children}</div>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
const OwnerNameToolTip = ({ metadata, cardSettings }) =>
|
||||
cardSettings?.ownr_nm && (
|
||||
<Col span={24}>
|
||||
<EllipsesToolTip
|
||||
title={metadata.ownr_ln || metadata.ownr_co_nm ? <OwnerNameDisplay ownerObject={metadata} /> : null}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{metadata.ownr_ln || metadata.ownr_co_nm ? (
|
||||
cardSettings.compact ? (
|
||||
`${metadata.ownr_ln || ""} ${metadata.ownr_co_nm || ""}`
|
||||
) : (
|
||||
<OwnerNameDisplay ownerObject={metadata} />
|
||||
)
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const ModelInfoToolTip = ({ metadata, cardSettings }) =>
|
||||
cardSettings?.model_info && (
|
||||
<Col span={24}>
|
||||
<EllipsesToolTip
|
||||
title={
|
||||
metadata.v_model_yr || metadata.v_make_desc || metadata.v_model_desc
|
||||
? `${metadata.v_model_yr || ""} ${metadata.v_make_desc || ""} ${metadata.v_model_desc || ""}`
|
||||
: null
|
||||
}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{metadata.v_model_yr || metadata.v_make_desc || metadata.v_model_desc ? (
|
||||
`${metadata.v_model_yr || ""} ${metadata.v_make_desc || ""} ${metadata.v_model_desc || ""}`
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const InsuranceCompanyToolTip = ({ metadata, cardSettings }) =>
|
||||
cardSettings?.ins_co_nm && (
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip title={metadata.ins_co_nm || null} kiosk={cardSettings.kiosk}>
|
||||
{metadata.ins_co_nm ? metadata.ins_co_nm : <span> </span>}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const ClaimNumberToolTip = ({ metadata, cardSettings }) =>
|
||||
cardSettings?.clm_no && (
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip title={metadata.clm_no || null} kiosk={cardSettings.kiosk}>
|
||||
{metadata.clm_no ? metadata.clm_no : <span> </span>}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const EmployeeAssignmentsToolTip = ({
|
||||
metadata,
|
||||
cardSettings,
|
||||
employee_body,
|
||||
employee_prep,
|
||||
employee_refinish,
|
||||
employee_csr
|
||||
}) =>
|
||||
cardSettings?.employeeassignments && (
|
||||
<Col span={24}>
|
||||
<Row>
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip
|
||||
title={
|
||||
employee_body || metadata.labhrs.aggregate.sum.mod_lb_hrs
|
||||
? `B: ${employee_body ? `${employee_body.first_name.substring(0, 3)} ${employee_body.last_name.charAt(0)}` : ""} ${metadata.labhrs.aggregate.sum.mod_lb_hrs || "?"}h`
|
||||
: null
|
||||
}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{employee_body || metadata.labhrs.aggregate.sum.mod_lb_hrs ? (
|
||||
`B: ${employee_body ? `${employee_body.first_name.substring(0, 3)} ${employee_body.last_name.charAt(0)}` : ""} ${metadata.labhrs.aggregate.sum.mod_lb_hrs || "?"}h`
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip
|
||||
title={
|
||||
employee_prep
|
||||
? `P: ${employee_prep ? `${employee_prep.first_name.substring(0, 3)} ${employee_prep.last_name.charAt(0)}` : ""}`
|
||||
: null
|
||||
}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{employee_prep ? (
|
||||
`P: ${employee_prep ? `${employee_prep.first_name.substring(0, 3)} ${employee_prep.last_name.charAt(0)}` : ""}`
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip
|
||||
title={
|
||||
employee_refinish || metadata.larhrs.aggregate.sum.mod_lb_hrs
|
||||
? `R: ${employee_refinish ? `${employee_refinish.first_name.substring(0, 3)} ${employee_refinish.last_name.charAt(0)}` : ""} ${metadata.larhrs.aggregate.sum.mod_lb_hrs || "?"}h`
|
||||
: null
|
||||
}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{employee_refinish || metadata.larhrs.aggregate.sum.mod_lb_hrs ? (
|
||||
`R: ${employee_refinish ? `${employee_refinish.first_name.substring(0, 3)} ${employee_refinish.last_name.charAt(0)}` : ""} ${metadata.larhrs.aggregate.sum.mod_lb_hrs || "?"}h`
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip
|
||||
title={
|
||||
employee_csr ? `C: ${employee_csr ? `${employee_csr.first_name} ${employee_csr.last_name}` : ""}` : null
|
||||
}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{employee_csr ? (
|
||||
`C: ${employee_csr ? `${employee_csr.first_name} ${employee_csr.last_name}` : ""}`
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const ActualInToolTip = ({ metadata, cardSettings }) =>
|
||||
cardSettings?.actual_in && (
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip title={metadata.actual_in || null} kiosk={cardSettings.kiosk}>
|
||||
{metadata.actual_in ? (
|
||||
<Space>
|
||||
<DownloadOutlined />
|
||||
<DateTimeFormatter format="MM/DD">{metadata.actual_in}</DateTimeFormatter>
|
||||
</Space>
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const EstimatorToolTip = ({ metadata, cardSettings }) => {
|
||||
return (
|
||||
cardSettings?.estimator && (
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip
|
||||
title={metadata.est_ct_fn && metadata.est_ct_ln ? `${metadata.est_ct_fn} ${metadata.est_ct_ln}` : null}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{metadata.est_ct_fn && metadata.est_ct_ln ? (
|
||||
<span>E: {`${metadata.est_ct_fn} ${metadata.est_ct_ln}`}</span>
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const SubtotalTooltip = ({ metadata, cardSettings, t }) => {
|
||||
const amount = metadata?.job_totals?.totals?.subtotal?.amount;
|
||||
const dineroAmount = amount ? Dinero({ amount: parseInt(amount * 100) }).toFormat("0,0.00") : null;
|
||||
|
||||
return (
|
||||
cardSettings?.subtotal && (
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip
|
||||
title={!!amount ? `${t("production.statistics.currency_symbol")}${dineroAmount}` : null}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{!!amount ? (
|
||||
<span>{`${t("production.statistics.currency_symbol")}${dineroAmount}`}</span>
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const ScheduledCompletionToolTip = ({ metadata, cardSettings, pastDueAlert }) =>
|
||||
cardSettings?.scheduled_completion && (
|
||||
<Col span={cardSettings.compact ? 24 : 12}>
|
||||
<EllipsesToolTip title={metadata.scheduled_completion || null} kiosk={cardSettings.kiosk}>
|
||||
{metadata.scheduled_completion ? (
|
||||
<Space className={pastDueAlert}>
|
||||
<CalendarOutlined />
|
||||
<DateTimeFormatter format="MM/DD">{metadata.scheduled_completion}</DateTimeFormatter>
|
||||
</Space>
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const AltTransportToolTip = ({ metadata, cardSettings }) =>
|
||||
cardSettings?.ats && (
|
||||
<Col span={12}>
|
||||
<EllipsesToolTip title={metadata.alt_transport || null} kiosk={cardSettings.kiosk}>
|
||||
{metadata.alt_transport ? metadata.alt_transport : <span> </span>}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const SubletsComponent = ({ metadata, cardSettings }) =>
|
||||
cardSettings?.sublets && (
|
||||
<Col span={12}>
|
||||
{metadata.subletLines ? (
|
||||
<ProductionSubletsManageComponent subletJobLines={metadata.subletLines} />
|
||||
) : (
|
||||
<span> </span>
|
||||
)}
|
||||
</Col>
|
||||
);
|
||||
|
||||
const ProductionNoteComponent = ({ metadata, cardSettings, card }) =>
|
||||
cardSettings?.production_note && (
|
||||
<Col span={24} style={{ margin: "2px 0" }}>
|
||||
<ProductionListColumnProductionNote
|
||||
record={{
|
||||
production_vars: metadata?.production_vars,
|
||||
id: card?.id,
|
||||
refetch: card?.refetch
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
);
|
||||
|
||||
const PartsStatusComponent = ({ metadata, cardSettings }) =>
|
||||
cardSettings?.partsstatus && (
|
||||
<Col span={24} style={{ textAlign: "center" }}>
|
||||
{metadata.joblines_status ? <JobPartsQueueCount parts={metadata.joblines_status} /> : <span> </span>}
|
||||
</Col>
|
||||
);
|
||||
|
||||
const TasksToolTip = ({ metadata, cardSettings, t }) =>
|
||||
cardSettings?.tasks && (
|
||||
<Col span={12}>
|
||||
<EllipsesToolTip
|
||||
title={`${t("production.labels.tasks")}: ${metadata.tasks_aggregate?.aggregate?.count || 0}`}
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
{metadata.tasks_aggregate?.aggregate?.count ? (
|
||||
`T: ${metadata.tasks_aggregate.aggregate.count}`
|
||||
) : (
|
||||
<span>T: 0</span>
|
||||
)}
|
||||
</EllipsesToolTip>
|
||||
</Col>
|
||||
);
|
||||
|
||||
export default function ProductionBoardCard({ technician, card, bodyshop, cardSettings, clone }) {
|
||||
const { t } = useTranslation();
|
||||
const { metadata } = card;
|
||||
|
||||
const employees = useMemo(() => bodyshop.employees, [bodyshop.employees]);
|
||||
|
||||
const { employee_body, employee_prep, employee_refinish, employee_csr } = useMemo(() => {
|
||||
return {
|
||||
employee_body: metadata?.employee_body && findEmployeeById(employees, metadata.employee_body),
|
||||
employee_prep: metadata?.employee_prep && findEmployeeById(employees, metadata.employee_prep),
|
||||
employee_refinish: metadata?.employee_refinish && findEmployeeById(employees, metadata.employee_refinish),
|
||||
employee_csr: metadata?.employee_csr && findEmployeeById(employees, metadata.employee_csr)
|
||||
};
|
||||
}, [metadata, employees]);
|
||||
|
||||
const pastDueAlert = useMemo(() => {
|
||||
if (!metadata?.scheduled_completion) return null;
|
||||
const completionDate = dayjs(metadata.scheduled_completion);
|
||||
if (dayjs().isSameOrAfter(completionDate, "day")) return "production-completion-past";
|
||||
if (dayjs().add(1, "day").isSame(completionDate, "day")) return "production-completion-soon";
|
||||
return null;
|
||||
}, [metadata?.scheduled_completion]);
|
||||
|
||||
const totalHrs = useMemo(() => {
|
||||
return metadata?.labhrs && metadata?.larhrs
|
||||
? metadata.labhrs.aggregate.sum.mod_lb_hrs + metadata.larhrs.aggregate.sum.mod_lb_hrs
|
||||
: 0;
|
||||
}, [metadata?.labhrs, metadata?.larhrs]);
|
||||
|
||||
const bgColor = useMemo(() => cardColor(bodyshop.ssbuckets, totalHrs), [bodyshop.ssbuckets, totalHrs]);
|
||||
const contrastYIQ = useMemo(() => getContrastYIQ(bgColor), [bgColor]);
|
||||
|
||||
const isBodyEmpty = useMemo(() => {
|
||||
return !(
|
||||
cardSettings?.ownr_nm ||
|
||||
cardSettings?.model_info ||
|
||||
cardSettings?.ins_co_nm ||
|
||||
cardSettings?.clm_no ||
|
||||
cardSettings?.employeeassignments ||
|
||||
cardSettings?.actual_in ||
|
||||
cardSettings?.scheduled_completion ||
|
||||
cardSettings?.ats ||
|
||||
cardSettings?.sublets ||
|
||||
cardSettings?.production_note ||
|
||||
cardSettings?.partsstatus ||
|
||||
cardSettings?.estimator ||
|
||||
cardSettings?.subtotal ||
|
||||
cardSettings?.tasks
|
||||
);
|
||||
}, [cardSettings]);
|
||||
|
||||
const headerContent = (
|
||||
<div className="header-content-container">
|
||||
<div className="inner-container">
|
||||
<ProductionAlert id={card.id} productionVars={metadata?.production_vars} refetch={card?.refetch} key="alert" />
|
||||
{metadata?.suspended && <PauseCircleOutlined className="circle-outline" key="suspended" />}
|
||||
{metadata?.iouparent && (
|
||||
<EllipsesToolTip
|
||||
title={t("jobs.labels.iou")}
|
||||
key="iouparent"
|
||||
className="iouparent"
|
||||
kiosk={cardSettings.kiosk}
|
||||
>
|
||||
<BranchesOutlined className="branches-outlined" />
|
||||
</EllipsesToolTip>
|
||||
)}
|
||||
</div>
|
||||
<span className="tech-container">
|
||||
<Link to={technician ? `/tech/joblookup?selected=${card.id}` : `/manage/jobs/${card.id}`}>
|
||||
{metadata?.ro_number || t("general.labels.na")}
|
||||
</Link>
|
||||
</span>
|
||||
{isBodyEmpty && (
|
||||
<div className="body-empty-container">
|
||||
<Link to={{ search: `?selected=${card.id}` }}>
|
||||
<EyeFilled />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const bodyContent = (
|
||||
<Row>
|
||||
<OwnerNameToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||
<ModelInfoToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||
<InsuranceCompanyToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||
<ClaimNumberToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||
<EmployeeAssignmentsToolTip
|
||||
metadata={metadata}
|
||||
cardSettings={cardSettings}
|
||||
employee_body={employee_body}
|
||||
employee_prep={employee_prep}
|
||||
employee_refinish={employee_refinish}
|
||||
employee_csr={employee_csr}
|
||||
/>
|
||||
<EstimatorToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||
<TasksToolTip metadata={metadata} cardSettings={cardSettings} t={t} />
|
||||
<SubtotalTooltip metadata={metadata} cardSettings={cardSettings} t={t} />
|
||||
<ActualInToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||
<ScheduledCompletionToolTip metadata={metadata} cardSettings={cardSettings} pastDueAlert={pastDueAlert} />
|
||||
<AltTransportToolTip metadata={metadata} cardSettings={cardSettings} />
|
||||
<SubletsComponent metadata={metadata} cardSettings={cardSettings} />
|
||||
<ProductionNoteComponent metadata={metadata} cardSettings={cardSettings} card={card} />
|
||||
<PartsStatusComponent metadata={metadata} cardSettings={cardSettings} />
|
||||
</Row>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={`react-trello-card ${cardSettings.kiosk ? "kiosk-mode" : ""}`}
|
||||
size="small"
|
||||
style={{
|
||||
backgroundColor: cardSettings?.cardcolor && `rgba(${bgColor.r},${bgColor.g},${bgColor.b},${bgColor.a})`,
|
||||
color: cardSettings?.cardcolor && contrastYIQ
|
||||
}}
|
||||
title={!isBodyEmpty ? headerContent : null}
|
||||
extra={
|
||||
!isBodyEmpty && (
|
||||
<Link to={{ search: `?selected=${card.id}` }}>
|
||||
<EyeFilled />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
>
|
||||
{isBodyEmpty ? headerContent : bodyContent}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Card, Col, Form, notification, Popover, Row, Switch } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UPDATE_KANBAN_SETTINGS } from "../../graphql/user.queries";
|
||||
|
||||
export default function ProductionBoardKanbanCardSettings({ associationSettings }) {
|
||||
const [form] = Form.useForm();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [updateKbSettings] = useMutation(UPDATE_KANBAN_SETTINGS);
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue(associationSettings && associationSettings.kanban_settings);
|
||||
}, [form, associationSettings, open]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
setLoading(true);
|
||||
const result = await updateKbSettings({
|
||||
variables: {
|
||||
id: associationSettings && associationSettings.id,
|
||||
ks: values
|
||||
}
|
||||
});
|
||||
if (result.errors) {
|
||||
notification.open({
|
||||
type: "error",
|
||||
message: t("production.errors.settings", {
|
||||
error: JSON.stringify(result.errors)
|
||||
})
|
||||
});
|
||||
}
|
||||
setOpen(false);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const overlay = (
|
||||
<div>
|
||||
<Card>
|
||||
<Form form={form} onFinish={handleFinish} layout="vertical">
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={12}>
|
||||
<Form.Item label={t("production.labels.compact")} name="compact" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.ownr_nm")} name="ownr_nm">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.clm_no")} name="clm_no">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.ins_co_nm")} name="ins_co_nm">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
{/* <Form.Item
|
||||
valuePropName="checked"
|
||||
label={t("production.labels.laborhrs")}
|
||||
name="laborhrs"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item> */}
|
||||
<Form.Item
|
||||
valuePropName="checked"
|
||||
label={t("production.labels.employeeassignments")}
|
||||
name="employeeassignments"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.actual_in")} name="actual_in">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.cardcolor")} name="cardcolor">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
valuePropName="checked"
|
||||
label={t("production.labels.scheduled_completion")}
|
||||
name="scheduled_completion"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.ats")} name="ats">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.production_note")} name="production_note">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
{/* <Form.Item
|
||||
valuePropName='checked' label={t("production.labels.alert")} name="alert">
|
||||
<Switch/>
|
||||
</Form.Item> */}
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.sublets")} name="sublets">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.partsstatus")} name="partsstatus">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item valuePropName="checked" label={t("production.labels.stickyheader")} name="stickyheader">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
<Button
|
||||
onClick={() => {
|
||||
form.submit();
|
||||
}}
|
||||
>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<Popover content={overlay} open={open} placement="topRight">
|
||||
<Button loading={loading} onClick={() => setOpen(true)}>
|
||||
{t("production.labels.cardsettings")}
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -1,265 +1,237 @@
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { useApolloClient } from "@apollo/client";
|
||||
import Board, { moveCard } from "@asseinfo/react-kanban";
|
||||
import { Button, Grid, notification, Space, Statistic } from "antd";
|
||||
import Board from "./trello-board/index";
|
||||
import { Button, notification, Skeleton, Space } from "antd";
|
||||
import { PageHeader } from "@ant-design/pro-layout";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { Sticky, StickyContainer } from "react-sticky";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import styled from "styled-components";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { generate_UPDATE_JOB_KANBAN } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { selectTechnician } from "../../redux/tech/tech.selectors";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import IndefiniteLoading from "../indefinite-loading/indefinite-loading.component";
|
||||
import ProductionBoardFilters from "../production-board-filters/production-board-filters.component";
|
||||
import ProductionBoardCard from "../production-board-kanban-card/production-board-kanban-card.component";
|
||||
import ProductionListDetailComponent from "../production-list-detail/production-list-detail.component";
|
||||
import ProductionBoardKanbanCardSettings from "./production-board-kanban.card-settings.component";
|
||||
//import "@asseinfo/react-kanban/dist/styles.css";
|
||||
import CardColorLegend from "../production-board-kanban-card/production-board-kanban-card-color-legend.component";
|
||||
import CardColorLegend from "./production-board-kanban-card-color-legend.component.jsx";
|
||||
import "./production-board-kanban.styles.scss";
|
||||
import { createBoardData } from "./production-board-kanban.utils.js";
|
||||
import ProductionBoardKanbanSettings from "./settings/production-board-kanban.settings.component.jsx";
|
||||
import cloneDeep from "lodash/cloneDeep";
|
||||
import isEqual from "lodash/isEqual";
|
||||
import { mergeWithDefaults } from "./settings/defaultKanbanSettings.js";
|
||||
import NoteUpsertModal from "../../components/note-upsert-modal/note-upsert-modal.container";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
technician: selectTechnician
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||
dispatch(
|
||||
insertAuditTrail({
|
||||
jobid,
|
||||
operation,
|
||||
type
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
export function ProductionBoardKanbanComponent({
|
||||
data,
|
||||
bodyshop,
|
||||
refetch,
|
||||
technician,
|
||||
insertAuditTrail,
|
||||
associationSettings
|
||||
}) {
|
||||
const [boardLanes, setBoardLanes] = useState({
|
||||
columns: [{ id: "Loading...", title: "Loading...", cards: [] }]
|
||||
});
|
||||
|
||||
function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTrail, associationSettings, statuses }) {
|
||||
const [boardLanes, setBoardLanes] = useState({ lanes: [] });
|
||||
const [filter, setFilter] = useState({ search: "", employeeId: null });
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isMoving, setIsMoving] = useState(false);
|
||||
const [orientation, setOrientation] = useState("vertical");
|
||||
|
||||
const { t } = useTranslation();
|
||||
useEffect(() => {
|
||||
const boardData = createBoardData(
|
||||
[...bodyshop.md_ro_statuses.production_statuses, ...(bodyshop.md_ro_statuses.additional_board_statuses || [])],
|
||||
data,
|
||||
filter
|
||||
);
|
||||
|
||||
boardData.columns = boardData.columns.map((d) => {
|
||||
return { ...d, title: `${d.title} (${d.cards.length})` };
|
||||
});
|
||||
setBoardLanes(boardData);
|
||||
setIsMoving(false);
|
||||
}, [data, setBoardLanes, setIsMoving, bodyshop.md_ro_statuses, filter]);
|
||||
|
||||
const client = useApolloClient();
|
||||
|
||||
const handleDragEnd = async (card, source, destination) => {
|
||||
logImEXEvent("kanban_drag_end");
|
||||
useEffect(() => {
|
||||
if (associationSettings) {
|
||||
setLoading(true);
|
||||
setOrientation(associationSettings?.kanban_settings?.orientation ? "vertical" : "horizontal");
|
||||
setLoading(false);
|
||||
}
|
||||
}, [associationSettings]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMoving(true);
|
||||
setBoardLanes(moveCard(boardLanes, source, destination));
|
||||
const sameColumnTransfer = source.fromColumnId === destination.toColumnId;
|
||||
const sourceColumn = boardLanes.columns.find((x) => x.id === source.fromColumnId);
|
||||
const destinationColumn = boardLanes.columns.find((x) => x.id === destination.toColumnId);
|
||||
|
||||
const movedCardWillBeFirst = destination.toPosition === 0;
|
||||
|
||||
const movedCardWillBeLast = destinationColumn.cards.length - destination.toPosition < 1;
|
||||
|
||||
const lastCardInDestinationColumn = destinationColumn.cards[destinationColumn.cards.length - 1];
|
||||
|
||||
const oldChildCard = sourceColumn.cards[source.fromPosition + 1];
|
||||
|
||||
const newChildCard = movedCardWillBeLast
|
||||
? null
|
||||
: destinationColumn.cards[
|
||||
sameColumnTransfer
|
||||
? source.fromPosition - destination.toPosition > 0
|
||||
? destination.toPosition
|
||||
: destination.toPosition + 1
|
||||
: destination.toPosition
|
||||
];
|
||||
|
||||
const oldChildCardNewParent = oldChildCard ? card.kanbanparent : null;
|
||||
|
||||
let movedCardNewKanbanParent;
|
||||
if (movedCardWillBeFirst) {
|
||||
//console.log("==> New Card is first.");
|
||||
movedCardNewKanbanParent = "-1";
|
||||
} else if (movedCardWillBeLast) {
|
||||
// console.log("==> New Card is last.");
|
||||
movedCardNewKanbanParent = lastCardInDestinationColumn.id;
|
||||
} else if (!!newChildCard) {
|
||||
// console.log("==> New Card is somewhere in the middle");
|
||||
movedCardNewKanbanParent = newChildCard.kanbanparent;
|
||||
} else {
|
||||
console.log("==> !!!!!!Couldn't find a parent.!!!! <==");
|
||||
}
|
||||
const newChildCardNewParent = newChildCard ? card.id : null;
|
||||
const update = await client.mutate({
|
||||
mutation: generate_UPDATE_JOB_KANBAN(
|
||||
oldChildCard ? oldChildCard.id : null,
|
||||
oldChildCardNewParent,
|
||||
card.id,
|
||||
movedCardNewKanbanParent,
|
||||
destination.toColumnId,
|
||||
newChildCard ? newChildCard.id : null,
|
||||
newChildCardNewParent
|
||||
)
|
||||
});
|
||||
insertAuditTrail({
|
||||
jobid: card.id,
|
||||
operation: AuditTrailMapping.jobstatuschange(destination.toColumnId),
|
||||
type: "jobstatuschange"
|
||||
const newBoardData = createBoardData({
|
||||
statuses,
|
||||
data,
|
||||
filter,
|
||||
cardSettings: associationSettings?.kanban_settings
|
||||
});
|
||||
|
||||
if (update.errors) {
|
||||
notification["error"]({
|
||||
message: t("production.errors.boardupdate", {
|
||||
message: JSON.stringify(update.errors)
|
||||
})
|
||||
});
|
||||
newBoardData.lanes = newBoardData.lanes.map((lane) => ({
|
||||
...lane,
|
||||
title: `${lane.title} (${lane.cards.length})`
|
||||
}));
|
||||
|
||||
setBoardLanes((prevBoardLanes) => {
|
||||
const deepClonedData = cloneDeep(newBoardData);
|
||||
if (!isEqual(prevBoardLanes, deepClonedData)) {
|
||||
return deepClonedData;
|
||||
}
|
||||
return prevBoardLanes;
|
||||
});
|
||||
setIsMoving(false);
|
||||
}, [data, bodyshop.md_ro_statuses, filter, statuses, associationSettings?.kanban_settings]);
|
||||
|
||||
const getCardByID = useCallback((data, cardId) => {
|
||||
for (const lane of data.lanes) {
|
||||
for (const card of lane.cards) {
|
||||
if (card.id === cardId) {
|
||||
return card;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
const totalHrs = data
|
||||
.reduce(
|
||||
(acc, val) => acc + (val.labhrs?.aggregate?.sum?.mod_lb_hrs || 0) + (val.larhrs?.aggregate?.sum?.mod_lb_hrs || 0),
|
||||
0
|
||||
)
|
||||
.toFixed(1);
|
||||
const totalLAB = data.reduce((acc, val) => acc + (val.labhrs?.aggregate?.sum?.mod_lb_hrs || 0), 0).toFixed(1);
|
||||
const totalLAR = data.reduce((acc, val) => acc + (val.larhrs?.aggregate?.sum?.mod_lb_hrs || 0), 0).toFixed(1);
|
||||
const selectedBreakpoint = Object.entries(Grid.useBreakpoint())
|
||||
.filter((screen) => !!screen[1])
|
||||
.slice(-1)[0];
|
||||
const onDragEnd = useCallback(
|
||||
async ({ type, source, destination, draggableId }) => {
|
||||
logImEXEvent("kanban_drag_end");
|
||||
|
||||
const standardSizes = {
|
||||
xs: "250",
|
||||
sm: "250",
|
||||
md: "250",
|
||||
lg: "250",
|
||||
xl: "250",
|
||||
xxl: "250"
|
||||
};
|
||||
const compactSizes = {
|
||||
xs: "150",
|
||||
sm: "150",
|
||||
md: "150",
|
||||
lg: "150",
|
||||
xl: "155",
|
||||
xxl: "155"
|
||||
};
|
||||
if (!type || type !== "lane" || !source || !destination || isMoving) return;
|
||||
|
||||
const width = selectedBreakpoint
|
||||
? associationSettings && associationSettings.kanban_settings && associationSettings.kanban_settings.compact
|
||||
? compactSizes[selectedBreakpoint[0]]
|
||||
: standardSizes[selectedBreakpoint[0]]
|
||||
: "250";
|
||||
setIsMoving(true);
|
||||
|
||||
const stickyHeader = {
|
||||
renderColumnHeader: ({ title }) => (
|
||||
<Sticky>
|
||||
{({
|
||||
style,
|
||||
const targetLane = boardLanes.lanes.find((lane) => lane.id === destination.droppableId);
|
||||
const sourceLane = boardLanes.lanes.find((lane) => lane.id === source.droppableId);
|
||||
|
||||
// the following are also available but unused in this example
|
||||
isSticky,
|
||||
wasSticky,
|
||||
distanceFromTop,
|
||||
distanceFromBottom,
|
||||
calculatedHeight
|
||||
}) => (
|
||||
<div className="react-kanban-column-header" style={{ ...style, zIndex: "99", backgroundColor: "#ddd" }}>
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
</Sticky>
|
||||
)
|
||||
};
|
||||
if (!targetLane || !sourceLane) {
|
||||
setIsMoving(false);
|
||||
console.error("Invalid source or destination lane");
|
||||
return;
|
||||
}
|
||||
|
||||
const cardSettings =
|
||||
associationSettings &&
|
||||
associationSettings.kanban_settings &&
|
||||
Object.keys(associationSettings.kanban_settings).length > 0
|
||||
? associationSettings.kanban_settings
|
||||
: {
|
||||
ats: true,
|
||||
clm_no: true,
|
||||
compact: false,
|
||||
ownr_nm: true,
|
||||
sublets: true,
|
||||
ins_co_nm: true,
|
||||
production_note: true,
|
||||
employeeassignments: true,
|
||||
scheduled_completion: true,
|
||||
stickyheader: false,
|
||||
cardcolor: false
|
||||
};
|
||||
const sameColumnTransfer = source.droppableId === destination.droppableId;
|
||||
const sourceCard = getCardByID(boardLanes, draggableId);
|
||||
|
||||
const movedCardWillBeFirst = destination.index === 0;
|
||||
const movedCardWillBeLast = destination.index >= targetLane.cards.length - 1;
|
||||
|
||||
const lastCardInTargetLane = targetLane.cards[targetLane.cards.length - 1];
|
||||
const oldChildCard = sourceLane.cards[source.index + 1];
|
||||
|
||||
const newChildCard = movedCardWillBeLast
|
||||
? null
|
||||
: targetLane.cards[
|
||||
sameColumnTransfer
|
||||
? source.index < destination.index
|
||||
? destination.index + 1
|
||||
: destination.index
|
||||
: destination.index
|
||||
];
|
||||
|
||||
const oldChildCardNewParent = oldChildCard ? sourceCard.metadata.kanbanparent : null;
|
||||
|
||||
let movedCardNewKanbanParent;
|
||||
if (movedCardWillBeFirst) {
|
||||
movedCardNewKanbanParent = "-1";
|
||||
} else if (movedCardWillBeLast) {
|
||||
movedCardNewKanbanParent = lastCardInTargetLane.id;
|
||||
} else if (newChildCard) {
|
||||
movedCardNewKanbanParent = newChildCard.metadata.kanbanparent;
|
||||
} else {
|
||||
console.error("==> !!!!!!Couldn't find a parent.!!!! <==");
|
||||
}
|
||||
|
||||
const newChildCardNewParent = newChildCard ? draggableId : null;
|
||||
|
||||
try {
|
||||
const update = await client.mutate({
|
||||
mutation: generate_UPDATE_JOB_KANBAN(
|
||||
oldChildCard ? oldChildCard.id : null,
|
||||
oldChildCardNewParent,
|
||||
draggableId,
|
||||
movedCardNewKanbanParent,
|
||||
targetLane.id,
|
||||
newChildCard ? newChildCard.id : null,
|
||||
newChildCardNewParent
|
||||
)
|
||||
});
|
||||
|
||||
insertAuditTrail({
|
||||
jobid: draggableId,
|
||||
operation: AuditTrailMapping.jobstatuschange(targetLane.id),
|
||||
type: "jobstatuschange"
|
||||
});
|
||||
|
||||
if (update.errors) {
|
||||
notification["error"]({
|
||||
message: t("production.errors.boardupdate", {
|
||||
message: JSON.stringify(update.errors)
|
||||
})
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notification["error"]({
|
||||
message: t("production.errors.boardupdate", {
|
||||
message: error.message
|
||||
})
|
||||
});
|
||||
} finally {
|
||||
setIsMoving(false);
|
||||
}
|
||||
},
|
||||
[boardLanes, client, getCardByID, isMoving, t, insertAuditTrail]
|
||||
);
|
||||
|
||||
const cardSettings = useMemo(() => {
|
||||
const kanbanSettings = associationSettings?.kanban_settings;
|
||||
return mergeWithDefaults(kanbanSettings);
|
||||
}, [associationSettings]);
|
||||
|
||||
const handleSettingsChange = useCallback((newSettings) => {
|
||||
setLoading(true);
|
||||
setOrientation(newSettings.orientation ? "vertical" : "horizontal");
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return <Skeleton active />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container width={width}>
|
||||
<div>
|
||||
<IndefiniteLoading loading={isMoving} />
|
||||
|
||||
<PageHeader
|
||||
title={
|
||||
<Space>
|
||||
<Statistic title={t("dashboard.titles.productionhours")} value={totalHrs} />
|
||||
<Statistic title={t("dashboard.titles.labhours")} value={totalLAB} />
|
||||
<Statistic title={t("dashboard.titles.larhours")} value={totalLAR} />
|
||||
<Statistic title={t("appointments.labels.inproduction")} value={data && data.length} />
|
||||
</Space>
|
||||
}
|
||||
title={cardSettings.cardcolor && <CardColorLegend cardSettings={cardSettings} bodyshop={bodyshop} />}
|
||||
style={{ paddingInline: 0, paddingBlock: 0 }}
|
||||
extra={
|
||||
<Space wrap>
|
||||
<Button onClick={() => refetch && refetch()}>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
<ProductionBoardFilters filter={filter} setFilter={setFilter} loading={isMoving} />
|
||||
<ProductionBoardKanbanCardSettings associationSettings={associationSettings} />
|
||||
<ProductionBoardKanbanSettings
|
||||
parentLoading={setLoading}
|
||||
associationSettings={associationSettings}
|
||||
onSettingsChange={handleSettingsChange}
|
||||
bodyshop={bodyshop}
|
||||
data={data}
|
||||
/>
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
|
||||
{cardSettings.cardcolor && <CardColorLegend cardSettings={cardSettings} bodyshop={bodyshop} />}
|
||||
|
||||
<NoteUpsertModal />
|
||||
<ProductionListDetailComponent jobs={data} />
|
||||
<StickyContainer>
|
||||
<Board
|
||||
style={{ height: "100%" }}
|
||||
children={boardLanes}
|
||||
disableCardDrag={isMoving}
|
||||
{...(cardSettings.stickyheader && stickyHeader)}
|
||||
renderCard={(card) => ProductionBoardCard(technician, card, bodyshop, cardSettings)}
|
||||
onCardDragEnd={handleDragEnd}
|
||||
/>
|
||||
</StickyContainer>
|
||||
</Container>
|
||||
|
||||
<Board
|
||||
queryData={data}
|
||||
data={boardLanes}
|
||||
onDragEnd={onDragEnd}
|
||||
orientation={orientation}
|
||||
cardSettings={cardSettings}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ProductionBoardKanbanComponent);
|
||||
|
||||
const Container = styled.div`
|
||||
.react-kanban-card-skeleton,
|
||||
.react-kanban-card,
|
||||
.react-kanban-card-adder-form {
|
||||
box-sizing: border-box;
|
||||
max-width: ${(props) => props.width}px;
|
||||
min-width: ${(props) => props.width}px;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import { useApolloClient, useQuery, useSubscription } from "@apollo/client";
|
||||
import _ from "lodash";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import { useQuery, useSubscription } from "@apollo/client";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import {
|
||||
QUERY_EXACT_JOB_IN_PRODUCTION,
|
||||
QUERY_EXACT_JOBS_IN_PRODUCTION,
|
||||
QUERY_JOBS_IN_PRODUCTION,
|
||||
SUBSCRIPTION_JOBS_IN_PRODUCTION
|
||||
} from "../../graphql/jobs.queries";
|
||||
import { QUERY_JOBS_IN_PRODUCTION, SUBSCRIPTION_JOBS_IN_PRODUCTION } from "../../graphql/jobs.queries";
|
||||
import { QUERY_KANBAN_SETTINGS } from "../../graphql/user.queries";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import ProductionBoardKanbanComponent from "./production-board-kanban.component";
|
||||
@@ -18,76 +12,53 @@ const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
|
||||
export function ProductionBoardKanbanContainer({ bodyshop, currentUser }) {
|
||||
function ProductionBoardKanbanContainer({ bodyshop, currentUser }) {
|
||||
const combinedStatuses = useMemo(
|
||||
() => [
|
||||
...bodyshop.md_ro_statuses.production_statuses,
|
||||
...(bodyshop.md_ro_statuses.additional_board_statuses || [])
|
||||
],
|
||||
[bodyshop.md_ro_statuses.production_statuses, bodyshop.md_ro_statuses.additional_board_statuses]
|
||||
);
|
||||
|
||||
const { refetch, loading, data } = useQuery(QUERY_JOBS_IN_PRODUCTION, {
|
||||
pollInterval: 3600000,
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
nextFetchPolicy: "network-only",
|
||||
onError: (error) => console.error(`Error fetching jobs in production: ${error.message}`)
|
||||
});
|
||||
const client = useApolloClient();
|
||||
const [joblist, setJoblist] = useState([]);
|
||||
const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION);
|
||||
|
||||
useEffect(() => {
|
||||
if (!(data && data.jobs)) return;
|
||||
setJoblist(
|
||||
data.jobs.map((j) => {
|
||||
return { id: j.id, updated_at: j.updated_at };
|
||||
})
|
||||
);
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!updatedJobs || joblist.length === 0) return;
|
||||
|
||||
const jobDiff = _.differenceWith(
|
||||
joblist,
|
||||
updatedJobs.jobs,
|
||||
(a, b) => a.id === b.id && a.updated_at === b.updated_at
|
||||
);
|
||||
|
||||
jobDiff.forEach((job) => {
|
||||
getUpdatedJobData(job.id);
|
||||
});
|
||||
if (jobDiff.length > 1) {
|
||||
getUpdatedJobsData(jobDiff.map((j) => j.id));
|
||||
} else if (jobDiff.length === 1) {
|
||||
jobDiff.forEach((job) => {
|
||||
getUpdatedJobData(job.id);
|
||||
});
|
||||
}
|
||||
|
||||
setJoblist(updatedJobs.jobs);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [updatedJobs]);
|
||||
|
||||
const getUpdatedJobData = async (jobId) => {
|
||||
client.query({
|
||||
query: QUERY_EXACT_JOB_IN_PRODUCTION,
|
||||
variables: { id: jobId }
|
||||
});
|
||||
};
|
||||
const getUpdatedJobsData = async (jobIds) => {
|
||||
client.query({
|
||||
query: QUERY_EXACT_JOBS_IN_PRODUCTION,
|
||||
variables: { ids: jobIds }
|
||||
});
|
||||
};
|
||||
const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION, {
|
||||
onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`)
|
||||
});
|
||||
|
||||
const { loading: associationSettingsLoading, data: associationSettings } = useQuery(QUERY_KANBAN_SETTINGS, {
|
||||
variables: { email: currentUser.email }
|
||||
variables: { email: currentUser.email },
|
||||
onError: (error) => console.error(`Error fetching Kanban settings: ${error.message}`)
|
||||
});
|
||||
|
||||
// const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {}));
|
||||
|
||||
useEffect(() => {
|
||||
if (updatedJobs && data) {
|
||||
refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`));
|
||||
}
|
||||
}, [updatedJobs, data, refetch]);
|
||||
|
||||
const filteredAssociationSettings = useMemo(() => {
|
||||
return associationSettings?.associations[0] || null;
|
||||
}, [associationSettings]);
|
||||
|
||||
return (
|
||||
<ProductionBoardKanbanComponent
|
||||
loading={loading || associationSettingsLoading}
|
||||
data={data ? data.jobs : []}
|
||||
refetch={refetch}
|
||||
associationSettings={
|
||||
associationSettings && associationSettings.associations[0] ? associationSettings.associations[0] : null
|
||||
}
|
||||
associationSettings={filteredAssociationSettings}
|
||||
bodyshop={bodyshop}
|
||||
statuses={combinedStatuses}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, null)(ProductionBoardKanbanContainer);
|
||||
export default connect(mapStateToProps)(ProductionBoardKanbanContainer);
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { Card, Statistic } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PropTypes from "prop-types";
|
||||
import { defaultKanbanSettings, statisticsItems } from "./settings/defaultKanbanSettings.js";
|
||||
|
||||
export const StatisticType = {
|
||||
HOURS: "hours",
|
||||
AMOUNT: "amount",
|
||||
JOBS: "jobs",
|
||||
TASKS: "tasks"
|
||||
};
|
||||
|
||||
const mergeStatistics = (items, values) => {
|
||||
const valuesMap = values.reduce((acc, value) => {
|
||||
acc[value.id] = value;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return items.map((item) => ({
|
||||
...item,
|
||||
value: valuesMap[item.id]?.value,
|
||||
type: valuesMap[item.id]?.type
|
||||
}));
|
||||
};
|
||||
|
||||
const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const calculateTotal = (items, key, subKey) => {
|
||||
return items.reduce((acc, item) => acc + (item[key]?.aggregate?.sum?.[subKey] || 0), 0);
|
||||
};
|
||||
|
||||
const calculateTotalAmount = (items, key) => {
|
||||
return items.reduce((acc, item) => acc + (item[key]?.totals?.subtotal?.amount || 0), 0);
|
||||
};
|
||||
|
||||
const calculateReducerTotal = (lanes, key, subKey) => {
|
||||
return lanes.reduce((acc, lane) => {
|
||||
return (
|
||||
acc + lane.cards.reduce((laneAcc, card) => laneAcc + (card.metadata[key]?.aggregate?.sum?.[subKey] || 0), 0)
|
||||
);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const calculateReducerTotalAmount = (lanes, key) => {
|
||||
return lanes.reduce((acc, lane) => {
|
||||
return (
|
||||
acc + lane.cards.reduce((laneAcc, card) => laneAcc + (card.metadata[key]?.totals?.subtotal?.amount || 0), 0)
|
||||
);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const formatValue = (value, type) => {
|
||||
if (type === StatisticType.JOBS) {
|
||||
return value.toFixed(0);
|
||||
}
|
||||
if (type === StatisticType.HOURS) {
|
||||
return value.toFixed(2);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const totalHrs = useMemo(() => {
|
||||
if (!cardSettings.totalHrs) return null;
|
||||
const total = calculateTotal(data, "labhrs", "mod_lb_hrs") + calculateTotal(data, "larhrs", "mod_lb_hrs");
|
||||
return parseFloat(total.toFixed(2));
|
||||
}, [data, cardSettings.totalHrs]);
|
||||
|
||||
const totalLAB = useMemo(() => {
|
||||
if (!cardSettings.totalLAB) return null;
|
||||
const total = calculateTotal(data, "labhrs", "mod_lb_hrs");
|
||||
return parseFloat(total.toFixed(2));
|
||||
}, [data, cardSettings.totalLAB]);
|
||||
|
||||
const totalLAR = useMemo(() => {
|
||||
if (!cardSettings.totalLAR) return null;
|
||||
const total = calculateTotal(data, "larhrs", "mod_lb_hrs");
|
||||
return parseFloat(total.toFixed(2));
|
||||
}, [data, cardSettings.totalLAR]);
|
||||
|
||||
const jobsInProduction = useMemo(
|
||||
() => (cardSettings.jobsInProduction ? data.length : null),
|
||||
[data, cardSettings.jobsInProduction]
|
||||
);
|
||||
|
||||
const totalAmountInProduction = useMemo(() => {
|
||||
if (!cardSettings.totalAmountInProduction) return null;
|
||||
const total = calculateTotalAmount(data, "job_totals");
|
||||
return parseFloat(total.toFixed(2));
|
||||
}, [data, cardSettings.totalAmountInProduction]);
|
||||
|
||||
const totalHrsOnBoard = useMemo(() => {
|
||||
if (!reducerData || !cardSettings.totalHrsOnBoard) return null;
|
||||
const total =
|
||||
calculateReducerTotal(reducerData.lanes, "labhrs", "mod_lb_hrs") +
|
||||
calculateReducerTotal(reducerData.lanes, "larhrs", "mod_lb_hrs");
|
||||
return parseFloat(total.toFixed(2));
|
||||
}, [reducerData, cardSettings.totalHrsOnBoard]);
|
||||
|
||||
const totalLABOnBoard = useMemo(() => {
|
||||
if (!reducerData || !cardSettings.totalLABOnBoard) return null;
|
||||
const total = calculateReducerTotal(reducerData.lanes, "labhrs", "mod_lb_hrs");
|
||||
return parseFloat(total.toFixed(2));
|
||||
}, [reducerData, cardSettings.totalLABOnBoard]);
|
||||
|
||||
const totalLAROnBoard = useMemo(() => {
|
||||
if (!reducerData || !cardSettings.totalLAROnBoard) return null;
|
||||
const total = calculateReducerTotal(reducerData.lanes, "larhrs", "mod_lb_hrs");
|
||||
return parseFloat(total.toFixed(2));
|
||||
}, [reducerData, cardSettings.totalLAROnBoard]);
|
||||
|
||||
const jobsOnBoard = useMemo(
|
||||
() =>
|
||||
reducerData && cardSettings.jobsOnBoard
|
||||
? reducerData.lanes.reduce((acc, lane) => acc + lane.cards.length, 0)
|
||||
: null,
|
||||
[reducerData, cardSettings.jobsOnBoard]
|
||||
);
|
||||
|
||||
const totalAmountOnBoard = useMemo(() => {
|
||||
if (!reducerData || !cardSettings.totalAmountOnBoard) return null;
|
||||
const total = calculateReducerTotalAmount(reducerData.lanes, "job_totals");
|
||||
return parseFloat(total.toFixed(2));
|
||||
}, [reducerData, cardSettings.totalAmountOnBoard]);
|
||||
|
||||
const tasksInProduction = useMemo(() => {
|
||||
if (!data || !cardSettings.tasksInProduction) return null;
|
||||
return data.reduce((acc, item) => acc + (item.tasks_aggregate?.aggregate?.count || 0), 0);
|
||||
}, [data, cardSettings.tasksInProduction]);
|
||||
|
||||
const tasksOnBoard = useMemo(() => {
|
||||
if (!reducerData || !cardSettings.tasksOnBoard) return null;
|
||||
return reducerData.lanes.reduce((acc, lane) => {
|
||||
return (
|
||||
acc + lane.cards.reduce((laneAcc, card) => laneAcc + (card.metadata.tasks_aggregate?.aggregate?.count || 0), 0)
|
||||
);
|
||||
}, 0);
|
||||
}, [reducerData, cardSettings.tasksOnBoard]);
|
||||
|
||||
const statistics = useMemo(
|
||||
() =>
|
||||
mergeStatistics(statisticsItems, [
|
||||
{ id: 0, value: totalHrs, type: StatisticType.HOURS },
|
||||
{ id: 1, value: totalAmountInProduction, type: StatisticType.AMOUNT },
|
||||
{ id: 2, value: totalLAB, type: StatisticType.HOURS },
|
||||
{ id: 3, value: totalLAR, type: StatisticType.HOURS },
|
||||
{ id: 4, value: jobsInProduction, type: StatisticType.JOBS },
|
||||
{ id: 5, value: totalHrsOnBoard, type: StatisticType.HOURS },
|
||||
{ id: 6, value: totalAmountOnBoard, type: StatisticType.AMOUNT },
|
||||
{ id: 7, value: totalLABOnBoard, type: StatisticType.HOURS },
|
||||
{ id: 8, value: totalLAROnBoard, type: StatisticType.HOURS },
|
||||
{ id: 9, value: jobsOnBoard, type: StatisticType.JOBS },
|
||||
{ id: 10, value: tasksOnBoard, type: StatisticType.TASKS },
|
||||
{ id: 11, value: tasksInProduction, type: StatisticType.TASKS }
|
||||
]),
|
||||
[
|
||||
totalHrs,
|
||||
totalAmountInProduction,
|
||||
totalLAB,
|
||||
totalLAR,
|
||||
jobsInProduction,
|
||||
totalHrsOnBoard,
|
||||
totalAmountOnBoard,
|
||||
totalLABOnBoard,
|
||||
totalLAROnBoard,
|
||||
jobsOnBoard,
|
||||
tasksOnBoard,
|
||||
tasksInProduction
|
||||
]
|
||||
);
|
||||
|
||||
const sortedStatistics = useMemo(() => {
|
||||
const statisticsMap = new Map(statistics.map((stat) => [stat.id, stat]));
|
||||
|
||||
return (
|
||||
cardSettings?.statisticsOrder ? cardSettings.statisticsOrder : defaultKanbanSettings.statisticsOrder
|
||||
).reduce((sorted, orderId) => {
|
||||
const value = statisticsMap.get(orderId);
|
||||
if (value && value.value !== null) {
|
||||
sorted.push(value);
|
||||
}
|
||||
return sorted;
|
||||
}, []);
|
||||
}, [statistics, cardSettings.statisticsOrder]);
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", gap: "5px", flexWrap: "wrap", marginBottom: "5px" }}>
|
||||
{sortedStatistics.map((stat) => (
|
||||
<Card styles={{ body: { padding: "8px" } }} key={stat.id}>
|
||||
<Statistic
|
||||
title={t(`production.statistics.${stat.label}`)}
|
||||
value={formatValue(stat.value, stat.type)}
|
||||
prefix={stat.type === StatisticType.AMOUNT ? t("production.statistics.currency_symbol") : undefined}
|
||||
suffix={
|
||||
stat.type === StatisticType.HOURS
|
||||
? t("production.statistics.hours")
|
||||
: stat.type === StatisticType.JOBS
|
||||
? t("production.statistics.jobs")
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ProductionStatistics.propTypes = {
|
||||
data: PropTypes.array.isRequired,
|
||||
cardSettings: PropTypes.object.isRequired,
|
||||
reducerData: PropTypes.object
|
||||
};
|
||||
|
||||
export default ProductionStatistics;
|
||||
@@ -1,145 +1,72 @@
|
||||
.react-kanban-board {
|
||||
.react-trello-board {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.react-kanban-card {
|
||||
border-radius: 3px;
|
||||
background-color: #fff;
|
||||
padding: 4px;
|
||||
margin-bottom: 7px;
|
||||
.height-preserving-container:empty {
|
||||
min-height: calc(var(--child-height));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// .react-kanban-card-skeleton,
|
||||
// .react-kanban-card,
|
||||
// .react-kanban-card-adder-form {
|
||||
// box-sizing: border-box;
|
||||
// max-width: 145px;
|
||||
// min-width: 145px;
|
||||
// }
|
||||
|
||||
.react-kanban-card--dragging {
|
||||
box-shadow: 2px 2px grey;
|
||||
.height-preserving-container {
|
||||
}
|
||||
|
||||
.react-kanban-card__description {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.react-kanban-card__title {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 5px;
|
||||
.react-trello-column-header {
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.react-kanban-column {
|
||||
padding: 10px;
|
||||
border-radius: 2px;
|
||||
background-color: #eee;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.react-kanban-column input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-form {
|
||||
border-radius: 3px;
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
margin-bottom: 7px;
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-form input {
|
||||
border: 0px;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-button {
|
||||
width: 100%;
|
||||
margin-top: 5px;
|
||||
background-color: transparent;
|
||||
cursor: pointer;
|
||||
border: 1px solid #ccc;
|
||||
transition: 0.3s;
|
||||
border-radius: 3px;
|
||||
font-size: 20px;
|
||||
margin-bottom: 10px;
|
||||
font-weight: bold;
|
||||
background-color: #d0d0d0;
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-button:hover {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-form__title {
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 5px;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-form__title:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-form__description {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-form__description:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-form__button {
|
||||
background-color: #eee;
|
||||
.production-alert {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 5px;
|
||||
width: 45%;
|
||||
margin-top: 5px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.react-trello-footer {
|
||||
background-color: #d0d0d0;
|
||||
border-radius: 0 0 5px 5px;
|
||||
}
|
||||
|
||||
.react-kanban-card-adder-form__button:hover {
|
||||
transition: 0.3s;
|
||||
cursor: pointer;
|
||||
background-color: #ccc;
|
||||
.grid-item {
|
||||
margin: 1px; // TODO: (Note) THis is where we set the margin for vertical
|
||||
}
|
||||
|
||||
.react-kanban-column-header {
|
||||
padding-bottom: 10px;
|
||||
font-weight: bold;
|
||||
.lane-title {
|
||||
vertical-align: middle;
|
||||
|
||||
.icon {
|
||||
margin-right: 8px; /* Adjust the spacing as needed */
|
||||
}
|
||||
}
|
||||
|
||||
.react-kanban-column-header input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.react-kanban-column-header__button {
|
||||
color: #333333;
|
||||
background-color: #ffffff;
|
||||
border-color: #cccccc;
|
||||
}
|
||||
|
||||
.react-kanban-column-header__button:hover,
|
||||
.react-kanban-column-header__button:focus,
|
||||
.react-kanban-column-header__button:active {
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
|
||||
.react-kanban-column-adder-button {
|
||||
border: 2px dashed #eee;
|
||||
height: 132px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.react-kanban-column-adder-button:hover {
|
||||
cursor: pointer;
|
||||
.header-content-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
.body-empty-container {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
.tech-container {
|
||||
font-weight: bolder;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
.branches-outlined {
|
||||
color: orangered;
|
||||
}
|
||||
}
|
||||
.inner-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
.circle-outline {
|
||||
color: orangered;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.iou-parent {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,121 +1,102 @@
|
||||
// Function to sort an array of objects by parentId
|
||||
import { groupBy } from "lodash";
|
||||
|
||||
const sortByParentId = (arr) => {
|
||||
// return arr.reduce((accumulator, currentValue) => {
|
||||
// //Find the parent item.
|
||||
// let item = accumulator.find((x) => x.id === currentValue.kanbanparent);
|
||||
// //Get index of parent item
|
||||
// let index = accumulator.indexOf(item);
|
||||
|
||||
// index = index !== -1 ? index + 1 : 0;
|
||||
// accumulator.splice(index, 0, currentValue);
|
||||
// return accumulator;
|
||||
// }, []);
|
||||
|
||||
let parentId = "-1";
|
||||
const sortedList = [];
|
||||
const byParentsIdsList = groupBy(arr, "kanbanparent"); // Create a new array with objects indexed by parentId
|
||||
//console.log("sortByParentId -> byParentsIdsList", byParentsIdsList);
|
||||
const byParentsIdsList = groupBy(arr, "kanbanparent");
|
||||
|
||||
while (byParentsIdsList[parentId]) {
|
||||
sortedList.push(...byParentsIdsList[parentId]); //Spread in the whole list in case several items have the same parents.
|
||||
parentId = byParentsIdsList[parentId][byParentsIdsList[parentId].length -1].id; //Grab the ID from the last one.
|
||||
sortedList.push(...byParentsIdsList[parentId]);
|
||||
parentId = byParentsIdsList[parentId][byParentsIdsList[parentId].length - 1].id;
|
||||
}
|
||||
|
||||
if (byParentsIdsList["null"]) byParentsIdsList["null"].map((i) => sortedList.push(i));
|
||||
if (byParentsIdsList["null"]) {
|
||||
sortedList.push(...byParentsIdsList["null"]);
|
||||
}
|
||||
|
||||
//Validate that the 2 arrays are of the same length and no children are missing.
|
||||
// Ensure all items are included in the sorted list
|
||||
if (arr.length !== sortedList.length) {
|
||||
arr.map((origItem) => {
|
||||
if (!!!sortedList.find((s) => s.id === origItem.id)) {
|
||||
arr.forEach((origItem) => {
|
||||
if (!sortedList.some((s) => s.id === origItem.id)) {
|
||||
sortedList.push(origItem);
|
||||
console.log("DATA CONSISTENCY ERROR: ", origItem.ro_number);
|
||||
}
|
||||
return 1;
|
||||
});
|
||||
}
|
||||
|
||||
return sortedList;
|
||||
};
|
||||
|
||||
export const createBoardData = (AllStatuses, Jobs, filter) => {
|
||||
// Function to create board data based on statuses and jobs, with optional filtering
|
||||
export const createBoardData = ({ statuses, data, filter, cardSettings }) => {
|
||||
const { search, employeeId } = filter;
|
||||
const boardLanes = {
|
||||
columns: AllStatuses.map((s) => {
|
||||
return {
|
||||
id: s,
|
||||
title: s,
|
||||
cards: []
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
const filteredJobs =
|
||||
(search === "" || !search) && !employeeId
|
||||
? Jobs
|
||||
: Jobs.filter((j) => {
|
||||
let include = false;
|
||||
if (search && search !== "") {
|
||||
include = CheckSearch(search, j);
|
||||
}
|
||||
const lanes = statuses.map((status) => ({
|
||||
id: status,
|
||||
title: status,
|
||||
cards: []
|
||||
}));
|
||||
|
||||
if (!!employeeId) {
|
||||
include =
|
||||
include ||
|
||||
j.employee_body === employeeId ||
|
||||
j.employee_prep === employeeId ||
|
||||
j.employee_csr === employeeId ||
|
||||
j.employee_refinish === employeeId;
|
||||
}
|
||||
let filteredJobs =
|
||||
(search === "" || !search) && !employeeId ? data : data.filter((job) => checkFilter(search, employeeId, job));
|
||||
|
||||
return include;
|
||||
});
|
||||
// Filter jobs by selectedMdInsCos if it has values
|
||||
if (cardSettings?.selectedMdInsCos?.length > 0) {
|
||||
filteredJobs = filteredJobs.filter((job) => cardSettings.selectedMdInsCos.includes(job.ins_co_nm));
|
||||
}
|
||||
|
||||
const DataGroupedByStatus = groupBy(filteredJobs, (d) => d.status);
|
||||
// Filter jobs by selectedEstimators if it has values
|
||||
if (cardSettings?.selectedEstimators?.length > 0) {
|
||||
filteredJobs = filteredJobs.filter((job) =>
|
||||
cardSettings.selectedEstimators.includes(`${job.est_ct_fn} ${job.est_ct_ln}`)
|
||||
);
|
||||
}
|
||||
|
||||
Object.keys(DataGroupedByStatus).map((statusGroupKey) => {
|
||||
const DataGroupedByStatus = groupBy(filteredJobs, "status");
|
||||
|
||||
Object.keys(DataGroupedByStatus).forEach((statusGroupKey) => {
|
||||
try {
|
||||
const needle = boardLanes.columns.find((l) => l.id === statusGroupKey);
|
||||
if (!needle?.cards) return null;
|
||||
needle.cards = sortByParentId(DataGroupedByStatus[statusGroupKey]);
|
||||
const lane = lanes.find((l) => l.id === statusGroupKey);
|
||||
if (!lane) return;
|
||||
|
||||
lane.cards = sortByParentId(DataGroupedByStatus[statusGroupKey]).map((job) => {
|
||||
const { id, title, description, due_date, ...metadata } = job;
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
label: due_date || "",
|
||||
metadata
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Error while creating board card", error);
|
||||
console.error("Error while creating board card", error);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
return boardLanes;
|
||||
return { lanes };
|
||||
};
|
||||
|
||||
const CheckSearch = (search, job) => {
|
||||
return (
|
||||
(job.ro_number || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
(job.ownr_fn || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
(job.ownr_co_nm || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
(job.ownr_ln || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
(job.status || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
(job.v_make_desc || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
(job.v_model_desc || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
(job.clm_no || "").toLowerCase().includes(search.toLowerCase()) ||
|
||||
(job.plate_no || "").toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
// Function to check if a job matches the search and/or employeeId filter
|
||||
const checkFilter = (search, employeeId, job) => {
|
||||
const lowerSearch = search?.toLowerCase() ?? "";
|
||||
|
||||
const matchesSearch =
|
||||
lowerSearch &&
|
||||
[
|
||||
job.ro_number,
|
||||
job.ownr_fn,
|
||||
job.ownr_co_nm,
|
||||
job.ownr_ln,
|
||||
job.status,
|
||||
job.v_make_desc,
|
||||
job.v_model_desc,
|
||||
job.clm_no,
|
||||
job.plate_no
|
||||
].some((field) => field?.toLowerCase().includes(lowerSearch));
|
||||
|
||||
const matchesEmployeeId =
|
||||
employeeId && [job.employee_body, job.employee_prep, job.employee_csr, job.employee_refinish].includes(employeeId);
|
||||
|
||||
return matchesSearch || matchesEmployeeId;
|
||||
};
|
||||
|
||||
// export const updateBoardOnMove = (board, card, source, destination) => {
|
||||
// //Slice from source
|
||||
|
||||
// const sourceCardList = board.columns.find((x) => x.id === source.fromColumnId)
|
||||
// .cards;
|
||||
// sourceCardList.slice(source.fromPosition, 0);
|
||||
|
||||
// //Splice into destination.
|
||||
// const destCardList = board.columns.find(
|
||||
// (x) => x.id === destination.toColumnId
|
||||
// ).cards;
|
||||
// console.log("updateBoardOnMove -> destCardList", destCardList);
|
||||
|
||||
// destCardList.splice(destination.toPosition, 0, card);
|
||||
// console.log("updateBoardOnMove -> destCardList", destCardList);
|
||||
// console.log("board", board);
|
||||
// return board;
|
||||
// };
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import React from "react";
|
||||
import { Card, Form, Select } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const FilterSettings = ({
|
||||
selectedMdInsCos,
|
||||
setSelectedMdInsCos,
|
||||
selectedEstimators,
|
||||
setSelectedEstimators,
|
||||
setHasChanges,
|
||||
bodyshop,
|
||||
data
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const extractNames = (source, firstNameKey, lastNameKey) =>
|
||||
source.map((item) => ({
|
||||
firstName: item[firstNameKey],
|
||||
lastName: item[lastNameKey]
|
||||
}));
|
||||
|
||||
const bodyshopNames = extractNames(bodyshop.md_estimators, "est_ct_fn", "est_ct_ln");
|
||||
const dataNames = extractNames(data, "est_ct_fn", "est_ct_ln");
|
||||
|
||||
const combinedNames = [...bodyshopNames, ...dataNames];
|
||||
|
||||
const uniqueNames = Array.from(
|
||||
new Map(combinedNames.map((item) => [`${item.firstName} ${item.lastName}`, item])).values()
|
||||
);
|
||||
|
||||
return (
|
||||
<Card title={t("production.settings.filters_title")}>
|
||||
<Form.Item label={t("production.settings.filters.md_ins_cos")}>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder={t("production.settings.filters.md_ins_cos")}
|
||||
value={selectedMdInsCos}
|
||||
onChange={(value) => {
|
||||
setSelectedMdInsCos(value);
|
||||
setHasChanges(true);
|
||||
}}
|
||||
options={bodyshop.md_ins_cos.map((item) => ({
|
||||
value: item.name,
|
||||
label: item.name
|
||||
}))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label={t("production.settings.filters.md_estimators")}>
|
||||
<Select
|
||||
mode="multiple"
|
||||
placeholder={t("production.settings.filters.md_estimators")}
|
||||
value={selectedEstimators}
|
||||
onChange={(value) => {
|
||||
setSelectedEstimators(value);
|
||||
setHasChanges(true);
|
||||
}}
|
||||
options={uniqueNames.map((item) => {
|
||||
const name = `${item.firstName} ${item.lastName}`.trim();
|
||||
return {
|
||||
value: name,
|
||||
label: name
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
FilterSettings.propTypes = {
|
||||
selectedMdInsCos: PropTypes.array.isRequired,
|
||||
setSelectedMdInsCos: PropTypes.func.isRequired,
|
||||
setHasChanges: PropTypes.func.isRequired,
|
||||
selectedEstimators: PropTypes.array.isRequired,
|
||||
setSelectedEstimators: PropTypes.func,
|
||||
bodyshop: PropTypes.object.isRequired,
|
||||
data: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
export default FilterSettings;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Card, Checkbox, Col, Form, Row } from "antd";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const InformationSettings = ({ t }) => (
|
||||
<Card title={t("production.settings.information")}>
|
||||
<Row gutter={[16, 16]}>
|
||||
{[
|
||||
"model_info",
|
||||
"ownr_nm",
|
||||
"clm_no",
|
||||
"ins_co_nm",
|
||||
"employeeassignments",
|
||||
"actual_in",
|
||||
"scheduled_completion",
|
||||
"ats",
|
||||
"production_note",
|
||||
"sublets",
|
||||
"partsstatus",
|
||||
"estimator",
|
||||
"subtotal",
|
||||
"tasks"
|
||||
].map((item) => (
|
||||
<Col span={4} key={item}>
|
||||
<Form.Item name={item} valuePropName="checked">
|
||||
<Checkbox>{t(`production.labels.${item}`)}</Checkbox>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
|
||||
InformationSettings.propTypes = {
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default InformationSettings;
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Card, Col, Form, Radio, Row } from "antd";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const LayoutSettings = ({ t }) => (
|
||||
<Card title={t("production.settings.layout")}>
|
||||
<Row gutter={[16, 16]}>
|
||||
{[
|
||||
{
|
||||
name: "orientation",
|
||||
label: t("production.labels.orientation"),
|
||||
options: [
|
||||
{ value: true, label: t("production.labels.vertical") },
|
||||
{ value: false, label: t("production.labels.horizontal") }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "cardSize",
|
||||
label: t("production.labels.card_size"),
|
||||
options: [
|
||||
{ value: "small", label: t("production.options.small") },
|
||||
{ value: "medium", label: t("production.options.medium") },
|
||||
{ value: "large", label: t("production.options.large") }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "compact",
|
||||
label: t("production.labels.compact"),
|
||||
options: [
|
||||
{ value: true, label: t("production.labels.tall") },
|
||||
{ value: false, label: t("production.labels.wide") }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "cardcolor",
|
||||
label: t("production.labels.cardcolor"),
|
||||
options: [
|
||||
{ value: true, label: t("production.labels.on") },
|
||||
{ value: false, label: t("production.labels.off") }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "kiosk",
|
||||
label: t("production.labels.kiosk_mode"),
|
||||
options: [
|
||||
{ value: true, label: t("production.labels.on") },
|
||||
{ value: false, label: t("production.labels.off") }
|
||||
]
|
||||
}
|
||||
].map(({ name, label, options }) => (
|
||||
<Col span={4} key={name}>
|
||||
<Form.Item name={name} label={label}>
|
||||
<Radio.Group>
|
||||
{options.map((option) => (
|
||||
<Radio.Button key={option.value.toString()} value={option.value}>
|
||||
{option.label}
|
||||
</Radio.Button>
|
||||
))}
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Card>
|
||||
);
|
||||
|
||||
LayoutSettings.propTypes = {
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default LayoutSettings;
|
||||
@@ -0,0 +1,59 @@
|
||||
import { DragDropContext, Draggable, Droppable } from "../trello-board/dnd/lib/index.js";
|
||||
import { statisticsItems } from "./defaultKanbanSettings.js";
|
||||
import { Card, Checkbox, Form } from "antd";
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
const StatisticsSettings = ({ t, statisticsOrder, setStatisticsOrder, setHasChanges }) => {
|
||||
const onDragEnd = (result) => {
|
||||
if (!result.destination) return;
|
||||
const newOrder = Array.from(statisticsOrder);
|
||||
const [movedItem] = newOrder.splice(result.source.index, 1);
|
||||
newOrder.splice(result.destination.index, 0, movedItem);
|
||||
setStatisticsOrder(newOrder);
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card title={t("production.settings.statistics_title")}>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable direction="grid" droppableId="statistics">
|
||||
{(provided) => (
|
||||
<div
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
style={{ display: "flex", flexWrap: "wrap", gap: "8px" }}
|
||||
>
|
||||
{statisticsOrder.map((itemId, index) => {
|
||||
const item = statisticsItems.find((stat) => stat.id === itemId);
|
||||
return (
|
||||
<Draggable key={itemId} draggableId={itemId.toString()} index={index}>
|
||||
{(provided) => (
|
||||
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
|
||||
<Card styles={{ body: { padding: "5px" } }} style={{ marginBottom: 8, flex: "0 1 auto" }}>
|
||||
<Form.Item style={{ marginBottom: 0 }} name={item.name} valuePropName="checked">
|
||||
<Checkbox>{t(`production.settings.statistics.${item.label}`)}</Checkbox>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
})}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
StatisticsSettings.propTypes = {
|
||||
t: PropTypes.func.isRequired,
|
||||
statisticsOrder: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
setStatisticsOrder: PropTypes.func.isRequired,
|
||||
setHasChanges: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default StatisticsSettings;
|
||||
@@ -0,0 +1,67 @@
|
||||
const statisticsItems = [
|
||||
{ id: 0, name: "totalHrs", label: "total_hours_in_production" },
|
||||
{ id: 1, name: "totalAmountInProduction", label: "total_amount_in_production" },
|
||||
{ id: 2, name: "totalLAB", label: "total_lab_in_production" },
|
||||
{ id: 3, name: "totalLAR", label: "total_lar_in_production" },
|
||||
{ id: 4, name: "jobsInProduction", label: "jobs_in_production" },
|
||||
{ id: 5, name: "totalHrsOnBoard", label: "total_hours_on_board" },
|
||||
{ id: 6, name: "totalAmountOnBoard", label: "total_amount_on_board" },
|
||||
{ id: 7, name: "totalLABOnBoard", label: "total_lab_on_board" },
|
||||
{ id: 8, name: "totalLAROnBoard", label: "total_lar_on_board" },
|
||||
{ id: 9, name: "jobsOnBoard", label: "total_jobs_on_board" },
|
||||
{ id: 10, name: "tasksOnBoard", label: "tasks_on_board" },
|
||||
{ id: 11, name: "tasksInProduction", label: "tasks_in_production" }
|
||||
];
|
||||
|
||||
const defaultKanbanSettings = {
|
||||
ats: true,
|
||||
clm_no: true,
|
||||
compact: false,
|
||||
ownr_nm: true,
|
||||
sublets: true,
|
||||
ins_co_nm: true,
|
||||
production_note: true,
|
||||
employeeassignments: true,
|
||||
scheduled_completion: true,
|
||||
cardcolor: false,
|
||||
orientation: false,
|
||||
tasks: false,
|
||||
cardSize: "small",
|
||||
model_info: true,
|
||||
kiosk: false,
|
||||
totalHrs: true,
|
||||
totalAmountInProduction: false,
|
||||
totalLAB: true,
|
||||
totalLAR: true,
|
||||
jobsInProduction: true,
|
||||
totalHrsOnBoard: false,
|
||||
totalLABOnBoard: false,
|
||||
totalLAROnBoard: false,
|
||||
jobsOnBoard: false,
|
||||
tasksOnBoard: false,
|
||||
tasksInProduction: false,
|
||||
totalAmountOnBoard: true,
|
||||
estimator: false,
|
||||
subtotal: false,
|
||||
statisticsOrder: statisticsItems.map((item) => item.id),
|
||||
selectedMdInsCos: [],
|
||||
selectedEstimators: []
|
||||
};
|
||||
|
||||
const mergeWithDefaults = (settings) => {
|
||||
// Create a new object that starts with the default settings
|
||||
const mergedSettings = { ...defaultKanbanSettings };
|
||||
|
||||
// Override with the provided settings, if any
|
||||
if (settings) {
|
||||
for (const key in settings) {
|
||||
if (settings.hasOwnProperty(key)) {
|
||||
mergedSettings[key] = settings[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mergedSettings;
|
||||
};
|
||||
|
||||
export { defaultKanbanSettings, statisticsItems, mergeWithDefaults };
|
||||
@@ -0,0 +1,162 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Button, Card, Col, Form, notification, Popover, Row, Tabs } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UPDATE_KANBAN_SETTINGS } from "../../../graphql/user.queries.js";
|
||||
import { defaultKanbanSettings, mergeWithDefaults } from "./defaultKanbanSettings.js";
|
||||
import LayoutSettings from "./LayoutSettings.jsx";
|
||||
import InformationSettings from "./InformationSettings.jsx";
|
||||
import StatisticsSettings from "./StatisticsSettings.jsx";
|
||||
import FilterSettings from "./FilterSettings.jsx";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
function ProductionBoardKanbanSettings({ associationSettings, parentLoading, bodyshop, data }) {
|
||||
const [form] = Form.useForm();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [statisticsOrder, setStatisticsOrder] = useState(defaultKanbanSettings.statisticsOrder);
|
||||
const [selectedMdInsCos, setSelectedMdInsCos] = useState(defaultKanbanSettings.selectedMdInsCos);
|
||||
const [selectedEstimators, setSelectedEstimators] = useState(defaultKanbanSettings.selectedEstimators);
|
||||
|
||||
const [updateKbSettings] = useMutation(UPDATE_KANBAN_SETTINGS);
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
if (associationSettings?.kanban_settings) {
|
||||
const finalSettings = mergeWithDefaults(associationSettings.kanban_settings);
|
||||
form.setFieldsValue(finalSettings);
|
||||
setStatisticsOrder(finalSettings.statisticsOrder);
|
||||
setSelectedMdInsCos(finalSettings.selectedMdInsCos);
|
||||
setSelectedEstimators(finalSettings.selectedEstimators);
|
||||
}
|
||||
}, [form, associationSettings]);
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
setLoading(true);
|
||||
parentLoading(true);
|
||||
|
||||
const result = await updateKbSettings({
|
||||
variables: {
|
||||
id: associationSettings?.id,
|
||||
ks: {
|
||||
...associationSettings.kanban_settings,
|
||||
...values,
|
||||
statisticsOrder,
|
||||
selectedMdInsCos,
|
||||
selectedEstimators
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (result.errors) {
|
||||
notification.open({
|
||||
type: "error",
|
||||
message: t("production.errors.settings", {
|
||||
error: JSON.stringify(result.errors)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
setLoading(false);
|
||||
parentLoading(false);
|
||||
setHasChanges(false);
|
||||
};
|
||||
|
||||
const handleValuesChange = () => setHasChanges(true);
|
||||
|
||||
const handleRestoreDefaults = () => {
|
||||
form.setFieldsValue({
|
||||
...defaultKanbanSettings,
|
||||
statisticsOrder: defaultKanbanSettings.statisticsOrder
|
||||
});
|
||||
setStatisticsOrder(defaultKanbanSettings.statisticsOrder);
|
||||
setSelectedMdInsCos(defaultKanbanSettings.selectedMdInsCos);
|
||||
setSelectedEstimators(defaultKanbanSettings.selectedEstimators);
|
||||
setHasChanges(true);
|
||||
};
|
||||
|
||||
const overlay = (
|
||||
<Card style={{ minWidth: "80vw" }}>
|
||||
<Form form={form} onFinish={handleFinish} layout="vertical" onValuesChange={handleValuesChange}>
|
||||
<Tabs
|
||||
defaultActiveKey="1"
|
||||
items={[
|
||||
{
|
||||
key: "1",
|
||||
label: t("production.settings.layout"),
|
||||
children: <LayoutSettings t={t} />
|
||||
},
|
||||
{
|
||||
key: "2",
|
||||
label: t("production.settings.information"),
|
||||
children: <InformationSettings t={t} />
|
||||
},
|
||||
{
|
||||
key: "3",
|
||||
label: t("production.settings.statistics_title"),
|
||||
children: (
|
||||
<StatisticsSettings
|
||||
t={t}
|
||||
statisticsOrder={statisticsOrder}
|
||||
setStatisticsOrder={setStatisticsOrder}
|
||||
setHasChanges={setHasChanges}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: "4",
|
||||
label: t("production.settings.filters_title"),
|
||||
children: (
|
||||
<FilterSettings
|
||||
selectedMdInsCos={selectedMdInsCos}
|
||||
setSelectedMdInsCos={setSelectedMdInsCos}
|
||||
selectedEstimators={selectedEstimators}
|
||||
setSelectedEstimators={setSelectedEstimators}
|
||||
setHasChanges={setHasChanges}
|
||||
bodyshop={bodyshop}
|
||||
data={data}
|
||||
/>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<Row justify="center" style={{ marginTop: 15 }} gutter={16}>
|
||||
<Col span={8}>
|
||||
<Button block onClick={() => setOpen(false)}>
|
||||
{t("general.actions.cancel")}
|
||||
</Button>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Button block onClick={handleRestoreDefaults}>
|
||||
{t("general.actions.defaults")}
|
||||
</Button>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Button block onClick={form.submit} loading={loading} type="primary" disabled={!hasChanges}>
|
||||
{t("general.actions.save")}
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover content={overlay} open={open} placement="topRight">
|
||||
<Button loading={loading} onClick={() => setOpen(!open)}>
|
||||
{t("production.settings.board_settings")}
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
ProductionBoardKanbanSettings.propTypes = {
|
||||
associationSettings: PropTypes.object,
|
||||
parentLoading: PropTypes.func.isRequired,
|
||||
bodyshop: PropTypes.object.isRequired,
|
||||
data: PropTypes.array
|
||||
};
|
||||
|
||||
export default ProductionBoardKanbanSettings;
|
||||
@@ -0,0 +1,83 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
/**
|
||||
* Height Memory Wrapper
|
||||
* @param children
|
||||
* @param maxHeight
|
||||
* @param setMaxHeight
|
||||
* @param override - Override the minHeight style from being set
|
||||
* @param itemKey - Unique key to preserve height for items with the same key
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const HeightMemoryWrapper = ({ children, maxHeight, setMaxHeight, override, itemKey }) => {
|
||||
const ref = useRef(null);
|
||||
const heightMapRef = useRef(new Map());
|
||||
const [localMaxHeight, setLocalMaxHeight] = useState(maxHeight);
|
||||
const [devicePixelRatio, setDevicePixelRatio] = useState(window.devicePixelRatio);
|
||||
|
||||
useEffect(() => {
|
||||
const currentRef = ref.current;
|
||||
const updateHeight = () => {
|
||||
const currentHeight = currentRef?.firstChild?.clientHeight || 0;
|
||||
if (itemKey) {
|
||||
const keyHeight = heightMapRef.current.get(itemKey) || 0;
|
||||
const newHeight = Math.max(keyHeight, currentHeight);
|
||||
heightMapRef.current.set(itemKey, newHeight);
|
||||
setLocalMaxHeight(newHeight);
|
||||
} else {
|
||||
setLocalMaxHeight((prevHeight) => Math.max(prevHeight, currentHeight));
|
||||
}
|
||||
setMaxHeight((prevHeight) => Math.max(prevHeight, currentHeight));
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(updateHeight);
|
||||
|
||||
if (currentRef?.firstChild) {
|
||||
resizeObserver.observe(currentRef.firstChild);
|
||||
}
|
||||
|
||||
const resizeHandler = () => {
|
||||
if (Math.abs(window.devicePixelRatio - devicePixelRatio) > 0.1) {
|
||||
// Threshold to detect significant zoom level change
|
||||
heightMapRef.current.clear(); // Clearing the height memory as zoom level has changed significantly
|
||||
setLocalMaxHeight(0); // Reset local max height
|
||||
setDevicePixelRatio(window.devicePixelRatio); // Update the recorded device pixel ratio
|
||||
}
|
||||
updateHeight();
|
||||
};
|
||||
|
||||
window.addEventListener("resize", resizeHandler);
|
||||
|
||||
return () => {
|
||||
if (currentRef?.firstChild) {
|
||||
resizeObserver.unobserve(currentRef.firstChild);
|
||||
}
|
||||
window.removeEventListener("resize", resizeHandler);
|
||||
};
|
||||
}, [itemKey, setMaxHeight, devicePixelRatio]);
|
||||
|
||||
useEffect(() => {
|
||||
if (itemKey && heightMapRef.current.has(itemKey)) {
|
||||
setLocalMaxHeight(heightMapRef.current.get(itemKey));
|
||||
}
|
||||
}, [itemKey]);
|
||||
|
||||
const style = override ? {} : { minHeight: localMaxHeight };
|
||||
|
||||
return (
|
||||
<div ref={ref} style={style}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
HeightMemoryWrapper.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
maxHeight: PropTypes.number.isRequired,
|
||||
setMaxHeight: PropTypes.func.isRequired,
|
||||
override: PropTypes.bool,
|
||||
itemKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
||||
};
|
||||
|
||||
export default HeightMemoryWrapper;
|
||||
@@ -0,0 +1,24 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
const HeightPreservingItem = ({ children, ...props }) => {
|
||||
const [size, setSize] = useState(0);
|
||||
const knownSize = props["data-known-size"];
|
||||
useEffect(() => {
|
||||
setSize((prevSize) => {
|
||||
return knownSize === 0 ? prevSize : knownSize;
|
||||
});
|
||||
}, [setSize, knownSize]);
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className="height-preserving-container"
|
||||
style={{
|
||||
"--child-height": `${size}px`
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeightPreservingItem;
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from "react";
|
||||
|
||||
const ItemComponent = ({ children, maxCardHeight, maxCardWidth, ...props }) => (
|
||||
<div style={{ minWidth: maxCardWidth, minHeight: maxCardHeight }} {...props}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default ItemComponent;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user