Compare commits
478 Commits
feature/cd
...
feature/pb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09187bdb7f | ||
|
|
2aed392fbe | ||
|
|
1a0054a911 | ||
|
|
6e6f3d3d3e | ||
|
|
29df140680 | ||
|
|
f37c67a122 | ||
|
|
e53c9aab72 | ||
|
|
385ad06adc | ||
|
|
d1e2d943a9 | ||
|
|
90aa3557b7 | ||
|
|
8891167183 | ||
|
|
6631e645df | ||
|
|
b7f202969b | ||
|
|
1dc3353ecc | ||
|
|
b1a3f1a7b8 | ||
|
|
89f3a26635 | ||
|
|
506fe9b1af | ||
|
|
37c898d3ce | ||
|
|
e4eac5714a | ||
|
|
9d4a59ca16 | ||
|
|
8ec524061a | ||
|
|
434ed46b5a | ||
|
|
3ece5e0ba2 | ||
|
|
52a383ffb7 | ||
|
|
8ad1d5929a | ||
|
|
445c01499b | ||
|
|
0b05be841d | ||
|
|
6bcb5f2af5 | ||
|
|
7482751c5b | ||
|
|
7a025fff42 | ||
|
|
602fe36638 | ||
|
|
da08fc74f1 | ||
|
|
14af45baf0 | ||
|
|
420a88c505 | ||
|
|
f3c44f8dd1 | ||
|
|
c72111e18b | ||
|
|
1ca2870912 | ||
|
|
db9744e1e5 | ||
|
|
e700095551 | ||
|
|
99196a77ed | ||
|
|
289a8222a0 | ||
|
|
dc10f8d35b | ||
|
|
f448232fe7 | ||
|
|
4baf4b4afa | ||
|
|
1785093023 | ||
|
|
0d65f8d894 | ||
|
|
3d8c390291 | ||
|
|
f0a13856bc | ||
|
|
ad6394783d | ||
|
|
16e9843298 | ||
|
|
a4c949c376 | ||
|
|
0e0d5316b7 | ||
|
|
60cb6ee8fb | ||
|
|
7d3279d21a | ||
|
|
402d13ad99 | ||
|
|
e803f5a2d4 | ||
|
|
3989d0f1e2 | ||
|
|
161d476ab3 | ||
|
|
ce84a89cf3 | ||
|
|
1d210a9e52 | ||
|
|
660f463aea | ||
|
|
ad9f01111c | ||
|
|
1b885e4114 | ||
|
|
b56742bcb2 | ||
|
|
40d4d69a9a | ||
|
|
b54d5beb76 | ||
|
|
5cbcd440f5 | ||
|
|
1cdc34249a | ||
|
|
8bbb218777 | ||
|
|
755ac7f657 | ||
|
|
d7b884ff86 | ||
|
|
fda3620ed0 | ||
|
|
61ad9f0d58 | ||
|
|
804c8ad40a | ||
|
|
0ddf009f8f | ||
|
|
14309b5c96 | ||
|
|
57d9de469a | ||
|
|
404ade396c | ||
|
|
bdad6da6d9 | ||
|
|
e7ef3b94c1 | ||
|
|
c19c92ab7e | ||
|
|
944229bae3 | ||
|
|
661b05d9e3 | ||
|
|
4d1d471a66 | ||
|
|
3e84fbbaf4 | ||
|
|
c4e59c1a5e | ||
|
|
9ee8e9007a | ||
|
|
4d52a5c44a | ||
|
|
fff9073f9d | ||
|
|
71f0b8a005 | ||
|
|
d2b965f79e | ||
|
|
a02aa71a95 | ||
|
|
7562bf5c95 | ||
|
|
f9521483e2 | ||
|
|
ca85858885 | ||
|
|
c7b3a94533 | ||
|
|
b010c9ecb0 | ||
|
|
0b98d04bac | ||
|
|
e25e388e59 | ||
|
|
10654b7916 | ||
|
|
ff049ad3e8 | ||
|
|
3572fff2c1 | ||
|
|
774e4cdf94 | ||
|
|
65ade5cab8 | ||
|
|
bf21a073fb | ||
|
|
851f1c265f | ||
|
|
51f3b5927b | ||
|
|
42c779f368 | ||
|
|
c104ee4fd9 | ||
|
|
0b2efa31b5 | ||
|
|
17baa8fcb2 | ||
|
|
e550baf59d | ||
|
|
fd53eb92e6 | ||
|
|
6521c0bfb8 | ||
|
|
50489dd682 | ||
|
|
220afa5add | ||
|
|
9d549b02fe | ||
|
|
ca2ded047b | ||
|
|
c423e61ce8 | ||
|
|
65550c7bf4 | ||
|
|
cdb4da9e5f | ||
|
|
b59a5303c6 | ||
|
|
9a72abbed0 | ||
|
|
762a5ff01b | ||
|
|
2f6571e703 | ||
|
|
80c90f1819 | ||
|
|
4b0c2c60a2 | ||
|
|
ee7d7d2f6a | ||
|
|
0ad670013b | ||
|
|
064a66aa66 | ||
|
|
f46c19b152 | ||
|
|
ea2c583b26 | ||
|
|
7bd2a90141 | ||
|
|
632549dd9d | ||
|
|
70ef274821 | ||
|
|
7d9759fda4 | ||
|
|
0fd06d5e4e | ||
|
|
4995e44e06 | ||
|
|
fd43d7d56d | ||
|
|
96f292f61c | ||
|
|
e899e4545f | ||
|
|
c62f5e3911 | ||
|
|
9f1d184081 | ||
|
|
29e596eedb | ||
|
|
6e28afda67 | ||
|
|
eca7e9fba1 | ||
|
|
aa2ac2b296 | ||
|
|
ce57752b95 | ||
|
|
63163c6459 | ||
|
|
2712ee5c0b | ||
|
|
2e28a4a790 | ||
|
|
4179943df6 | ||
|
|
8bb8eee384 | ||
|
|
85d79a8d7f | ||
|
|
d38dd67c2f | ||
|
|
e73d082eab | ||
|
|
2637538d9a | ||
|
|
0eacf9a840 | ||
|
|
eb58274f90 | ||
|
|
aa410d6847 | ||
|
|
97f1be9d6f | ||
|
|
a7c9dde5e3 | ||
|
|
8ec5bc049b | ||
|
|
1f2bec06ef | ||
|
|
1cad57bec2 | ||
|
|
10f3c01677 | ||
|
|
fc68b669db | ||
|
|
6e22091b81 | ||
|
|
545db54c14 | ||
|
|
aa69fef9ba | ||
|
|
1e30642d28 | ||
|
|
c1dfba949e | ||
|
|
dfa9592755 | ||
|
|
8f2b1f0f78 | ||
|
|
cf91ec14c0 | ||
|
|
891aa649e8 | ||
|
|
dfe0f99bea | ||
|
|
591022c097 | ||
|
|
a8a0167123 | ||
|
|
c76da54b93 | ||
|
|
186f6101ff | ||
|
|
e95cf0a026 | ||
|
|
3eaa3a0189 | ||
|
|
0ba445aad2 | ||
|
|
637c718b05 | ||
|
|
a01019b1b3 | ||
|
|
f6e2393a40 | ||
|
|
715857a587 | ||
|
|
186ad2d7a2 | ||
|
|
c64c49ab6e | ||
|
|
3e067e2103 | ||
|
|
4cc1f77b14 | ||
|
|
334dd5fe94 | ||
|
|
d73f37cacc | ||
|
|
c6a7e3dcb1 | ||
|
|
4bdc02d22c | ||
|
|
afa8e04008 | ||
|
|
990c1bb2bf | ||
|
|
a5a59c526c | ||
|
|
2d5bef4b7b | ||
|
|
5893b1e3b3 | ||
|
|
0af452b8a4 | ||
|
|
3d82198c90 | ||
|
|
01bca360c7 | ||
|
|
59e994ac29 | ||
|
|
d5f3105341 | ||
|
|
92920f69d4 | ||
|
|
61ac520192 | ||
|
|
45176cc2e2 | ||
|
|
6aacce5a58 | ||
|
|
7a515c35d2 | ||
|
|
80dc04669c | ||
|
|
a702c87986 | ||
|
|
1942103985 | ||
|
|
fd161fa9eb | ||
|
|
44a18f4ace | ||
|
|
3269c4f602 | ||
|
|
43cbf0084a | ||
|
|
28b1356f76 | ||
|
|
38456cb213 | ||
|
|
8c826eaaed | ||
|
|
f047556ab5 | ||
|
|
057b335e82 | ||
|
|
4b55719f86 | ||
|
|
30c7da2bf9 | ||
|
|
82fb4fc74c | ||
|
|
151f122b8c | ||
|
|
b198ca5051 | ||
|
|
d9abe84949 | ||
|
|
5387ff207c | ||
|
|
17f4d69e30 | ||
|
|
77d3fc359d | ||
|
|
86151f3337 | ||
|
|
ae4b5bca33 | ||
|
|
eb120a264e | ||
|
|
7e3f496ea1 | ||
|
|
4d33f16f13 | ||
|
|
d7ebefe7ab | ||
|
|
8dc2197677 | ||
|
|
e3804b103b | ||
|
|
fe993cba73 | ||
|
|
ef6cdf07d8 | ||
|
|
cdbde6f5fa | ||
|
|
4059aa9875 | ||
|
|
22e30ae5cb | ||
|
|
78c883743f | ||
|
|
e46307e715 | ||
|
|
f3e078b481 | ||
|
|
c03f54b3eb | ||
|
|
bb0234fb7d | ||
|
|
03f25c8c0e | ||
|
|
8e5005daa0 | ||
|
|
259458eec3 | ||
|
|
59e57aa274 | ||
|
|
de33bcd72b | ||
|
|
d72472ccc3 | ||
|
|
91efd170c8 | ||
|
|
6f1ddd51fd | ||
|
|
9f48a91a29 | ||
|
|
a7e2548e14 | ||
|
|
3294faaeaa | ||
|
|
06480159e3 | ||
|
|
6ce06ed5c0 | ||
|
|
71e535388a | ||
|
|
dbd265d368 | ||
|
|
c11f182f83 | ||
|
|
5f70cfd585 | ||
|
|
6e0675f28b | ||
|
|
96f45d2c80 | ||
|
|
091e44f471 | ||
|
|
00549d6a88 | ||
|
|
d940e0ee78 | ||
|
|
39a38d46ee | ||
|
|
71435ed75a | ||
|
|
e7ec408b98 | ||
|
|
9306064420 | ||
|
|
37a16edfb4 | ||
|
|
9ad5b9547f | ||
|
|
47edb0bdf4 | ||
|
|
5db47e879c | ||
|
|
54b46dd25e | ||
|
|
4cb92c8508 | ||
|
|
cb76e2dcde | ||
|
|
901c64ed85 | ||
|
|
f6f90d68fa | ||
|
|
05e295fcac | ||
|
|
c97df6dc61 | ||
|
|
61406aafa6 | ||
|
|
03210db711 | ||
|
|
b3a34c109a | ||
|
|
81daad35d8 | ||
|
|
529eb24d76 | ||
|
|
e81bf7b561 | ||
|
|
7a35dc9b38 | ||
|
|
c72ef97b82 | ||
|
|
5284ee2ef9 | ||
|
|
724c097d52 | ||
|
|
3c3da178ba | ||
|
|
db4e5d48af | ||
|
|
a7cf081ed5 | ||
|
|
db5b11f6d3 | ||
|
|
8d3d52485f | ||
|
|
edb58ebc81 | ||
|
|
971b518e8f | ||
|
|
8d9507dce1 | ||
|
|
748f8f472d | ||
|
|
db2b4739c2 | ||
|
|
6c12e5cb03 | ||
|
|
140e57a123 | ||
|
|
3ca6791939 | ||
|
|
1c473c95a2 | ||
|
|
7e145bdec7 | ||
|
|
44c17bd7a2 | ||
|
|
c493f6e31e | ||
|
|
5213b0b315 | ||
|
|
8bfd0a1c16 | ||
|
|
0541167cc5 | ||
|
|
834966ae96 | ||
|
|
e7d813c3f3 | ||
|
|
26e47ff203 | ||
|
|
d2b2a5399d | ||
|
|
5a4d6d3e8c | ||
|
|
61a5e180f4 | ||
|
|
d5b8ea3ac5 | ||
|
|
4e87ef179b | ||
|
|
3b7c31626d | ||
|
|
a57e35354d | ||
|
|
a269fd3ad8 | ||
|
|
86bee9ad0d | ||
|
|
f8b57ca9bd | ||
|
|
5f7b780195 | ||
|
|
ba5400d04a | ||
|
|
0190e737c1 | ||
|
|
db64d9b69f | ||
|
|
0d12ab36a9 | ||
|
|
8f52efbaca | ||
|
|
d6dc5db185 | ||
|
|
58d1859640 | ||
|
|
dba648aea2 | ||
|
|
12f6206f88 | ||
|
|
7d9fd06b6d | ||
|
|
d6fbf16272 | ||
|
|
b68de683b0 | ||
|
|
2f9d025fbe | ||
|
|
9c1ffaba17 | ||
|
|
142df39cd2 | ||
|
|
ef79ccc299 | ||
|
|
6d2d96be5c | ||
|
|
8ff2a6e6c4 | ||
|
|
b793aa3394 | ||
|
|
be2cfb908a | ||
|
|
e34084b146 | ||
|
|
c1d7168260 | ||
|
|
b3a3709b72 | ||
|
|
dd59f3d026 | ||
|
|
ccb00ff391 | ||
|
|
b37970a6df | ||
|
|
2cd7fcfbd8 | ||
|
|
77819b06fe | ||
|
|
4e936b4cff | ||
|
|
b2362a85fa | ||
|
|
f45f351678 | ||
|
|
311171fa0a | ||
|
|
7e05f03f4d | ||
|
|
76826c1e80 | ||
|
|
18618efdc0 | ||
|
|
d1952dfc25 | ||
|
|
fe5f4f2727 | ||
|
|
fd7c907b8f | ||
|
|
0273255c2c | ||
|
|
e86160e530 | ||
|
|
9f7e0d611a | ||
|
|
0b523efa95 | ||
|
|
6b64499e24 | ||
|
|
e78e628bec | ||
|
|
ed8eb51c2f | ||
|
|
124d68ef68 | ||
|
|
3e802c1465 | ||
|
|
ac18e78897 | ||
|
|
e37ee39e88 | ||
|
|
fed16a4aa3 | ||
|
|
8c94dfce9e | ||
|
|
69c13dd052 | ||
|
|
a5fe54164e | ||
|
|
5ecd5a5a5c | ||
|
|
ed45347c23 | ||
|
|
3e0385479f | ||
|
|
36f833be91 | ||
|
|
8fb39f9ea4 | ||
|
|
5b84ebbc25 | ||
|
|
b0ec7867b5 | ||
|
|
4e4c59ce4d | ||
|
|
e9bf1c05ad | ||
|
|
7ac1fa5abf | ||
|
|
a489ac1d26 | ||
|
|
4d7a7442ce | ||
|
|
a19bce5a37 | ||
|
|
35782244bf | ||
|
|
7407429344 | ||
|
|
55c532f6e2 | ||
|
|
d3c8b5d731 | ||
|
|
cf09f98d7e | ||
|
|
d8e8a8e4e9 | ||
|
|
65210dea2f | ||
|
|
0c9b850872 | ||
|
|
99486830b7 | ||
|
|
74a62a46d3 | ||
|
|
d306041bcf | ||
|
|
ae8a924cd6 | ||
|
|
6ab1b9f787 | ||
|
|
46ddc440fe | ||
|
|
6bf8eacfbd | ||
|
|
b2fa4f220d | ||
|
|
2f175c304c | ||
|
|
79714e5708 | ||
|
|
7c5aa9c913 | ||
|
|
59b8bae182 | ||
|
|
35323ba624 | ||
|
|
8ca3741a52 | ||
|
|
c3c021774e | ||
|
|
6b811d635b | ||
|
|
97aecd3ddc | ||
|
|
c4fdef445e | ||
|
|
e642087360 | ||
|
|
098754125b | ||
|
|
6ce2d2723b | ||
|
|
ae4a864533 | ||
|
|
27d9322ced | ||
|
|
f5003080db | ||
|
|
79e11dda4c | ||
|
|
3b992edc21 | ||
|
|
f75f88840f | ||
|
|
3e1663bf18 | ||
|
|
9a60149d75 | ||
|
|
11cfef904b | ||
|
|
a45dcd307b | ||
|
|
54b483333f | ||
|
|
7f4a36038e | ||
|
|
990ec1a553 | ||
|
|
12307cbd56 | ||
|
|
6bd49a461e | ||
|
|
c9ed8a9360 | ||
|
|
120d6f9f5f | ||
|
|
0d30fc0e32 | ||
|
|
6f64cb71f2 | ||
|
|
7ce4264309 | ||
|
|
5385e6918b | ||
|
|
a8ad65000d | ||
|
|
7a6a834998 | ||
|
|
ae9ca0ac3b | ||
|
|
9fd23e9181 | ||
|
|
a288a1a2a4 | ||
|
|
33e2201524 | ||
|
|
34422dfef7 | ||
|
|
7999895323 | ||
|
|
c6635845f5 | ||
|
|
e770232e1d | ||
|
|
51dcf3a7c6 | ||
|
|
afd745917d | ||
|
|
aa8e12ef58 | ||
|
|
41bbda7bcf | ||
|
|
f19289362d | ||
|
|
71ef3dadc5 | ||
|
|
2d546d92b5 | ||
|
|
1360a73028 | ||
|
|
7de224831f | ||
|
|
e9cda93898 | ||
|
|
2c1f5a9f34 | ||
|
|
d88d7ebebd | ||
|
|
17dcc2efd8 | ||
|
|
991df9c48f | ||
|
|
3391d7d3f4 | ||
|
|
bccb5e353b | ||
|
|
8cbef14ea3 | ||
|
|
8e05105917 | ||
|
|
81babca775 | ||
|
|
fe8dd2a920 | ||
|
|
3176cfcc56 |
@@ -1 +1 @@
|
|||||||
client_max_body_size 15M;
|
client_max_body_size 50M;
|
||||||
17689
_reference/CDK Testing Accounts
Normal file
@@ -1,9 +1,25 @@
|
|||||||
// craco.config.js
|
// craco.config.js
|
||||||
const TerserPlugin = require("terser-webpack-plugin");
|
const TerserPlugin = require("terser-webpack-plugin");
|
||||||
const CracoLessPlugin = require("craco-less");
|
const CracoLessPlugin = require("craco-less");
|
||||||
|
const SentryWebpackPlugin = require("@sentry/webpack-plugin");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
|
{
|
||||||
|
plugin: SentryWebpackPlugin,
|
||||||
|
options: {
|
||||||
|
// sentry-cli configuration
|
||||||
|
authToken:
|
||||||
|
"6b45b028a02342db97a9a2f92c0959058665443d379d4a3a876430009e744260",
|
||||||
|
org: "snapt-software",
|
||||||
|
project: "imexonline",
|
||||||
|
release: process.env.REACT_APP_GIT_SHA,
|
||||||
|
|
||||||
|
// webpack-specific configuration
|
||||||
|
include: ".",
|
||||||
|
ignore: ["node_modules", "webpack.config.js"],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
plugin: CracoLessPlugin,
|
plugin: CracoLessPlugin,
|
||||||
options: {
|
options: {
|
||||||
@@ -53,4 +69,5 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
devtool: "source-map",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,81 +4,88 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"proxy": "http://localhost:5000",
|
"proxy": "http://localhost:5000",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.3.17",
|
"@apollo/client": "^3.4.16",
|
||||||
"@craco/craco": "^5.9.0",
|
"@craco/craco": "^6.4.0",
|
||||||
"@fingerprintjs/fingerprintjs": "^3.1.2",
|
"@fingerprintjs/fingerprintjs": "^3.3.0",
|
||||||
"@lourenci/react-kanban": "^2.1.0",
|
"@lourenci/react-kanban": "^2.1.0",
|
||||||
"@sentry/react": "^6.3.6",
|
"@openreplay/tracker": "^3.4.4",
|
||||||
"@sentry/tracing": "^6.3.6",
|
"@openreplay/tracker-assist": "^3.4.4",
|
||||||
"@stripe/react-stripe-js": "^1.4.0",
|
"@openreplay/tracker-graphql": "^3.0.0",
|
||||||
"@stripe/stripe-js": "^1.14.0",
|
"@openreplay/tracker-redux": "^3.0.0",
|
||||||
"@tanem/react-nprogress": "^3.0.65",
|
"@sentry/react": "^6.13.3",
|
||||||
"antd": "^4.15.5",
|
"@sentry/tracing": "^6.13.3",
|
||||||
|
"@splitsoftware/splitio-react": "^1.3.0",
|
||||||
|
"@stripe/react-stripe-js": "^1.6.0",
|
||||||
|
"@stripe/stripe-js": "^1.20.2",
|
||||||
|
"@tanem/react-nprogress": "^3.0.81",
|
||||||
|
"antd": "^4.16.13",
|
||||||
"apollo-link-logger": "^2.0.0",
|
"apollo-link-logger": "^2.0.0",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.23.0",
|
||||||
"craco-less": "^1.17.1",
|
"craco-less": "^1.20.0",
|
||||||
"dinero.js": "^1.8.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^9.0.2",
|
"dotenv": "^10.0.0",
|
||||||
"enquire-js": "^0.2.1",
|
"enquire-js": "^0.2.1",
|
||||||
"env-cmd": "^10.1.0",
|
"env-cmd": "^10.1.0",
|
||||||
"exifr": "^7.0.0",
|
"exifr": "^7.1.3",
|
||||||
"firebase": "^8.6.0",
|
"firebase": "^9.1.3",
|
||||||
"graphql": "^15.5.0",
|
"graphql": "^15.6.1",
|
||||||
"i18next": "^20.2.2",
|
"i18next": "^21.3.3",
|
||||||
"i18next-browser-languagedetector": "^6.1.1",
|
"i18next-browser-languagedetector": "^6.1.2",
|
||||||
"jsoneditor": "^9.4.1",
|
"jsoneditor": "^9.5.6",
|
||||||
"jsreport-browser-client-dist": "^1.3.0",
|
"jsreport-browser-client-dist": "^1.3.0",
|
||||||
"libphonenumber-js": "^1.9.17",
|
"libphonenumber-js": "^1.9.38",
|
||||||
"logrocket": "^1.2.0",
|
"logrocket": "^2.1.1",
|
||||||
"markerjs2": "^2.8.1",
|
"markerjs2": "^2.15.0",
|
||||||
"moment-business-days": "^1.2.0",
|
"moment-business-days": "^1.2.0",
|
||||||
"phone": "^2.4.21",
|
"phone": "^3.1.8",
|
||||||
"preval.macro": "^5.0.0",
|
"preval.macro": "^5.0.0",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
"query-string": "^7.0.0",
|
"query-string": "^7.0.1",
|
||||||
"rc-queue-anim": "^1.8.5",
|
"rc-queue-anim": "^2.0.0",
|
||||||
"rc-scroll-anim": "^2.7.6",
|
"rc-scroll-anim": "^2.7.6",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"react-big-calendar": "^0.33.2",
|
"react-big-calendar": "^0.38.0",
|
||||||
"react-color": "^2.19.3",
|
"react-color": "^2.19.3",
|
||||||
|
"react-cookie": "^4.1.1",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-drag-listview": "^0.1.8",
|
"react-drag-listview": "^0.1.8",
|
||||||
"react-grid-gallery": "^0.5.5",
|
"react-grid-gallery": "^0.5.5",
|
||||||
"react-grid-layout": "^1.2.5",
|
"react-grid-layout": "^1.3.0",
|
||||||
"react-i18next": "^11.8.15",
|
"react-i18next": "^11.12.0",
|
||||||
"react-icons": "^4.2.0",
|
"react-icons": "^4.3.1",
|
||||||
"react-number-format": "^4.5.5",
|
"react-number-format": "^4.7.3",
|
||||||
"react-redux": "^7.2.4",
|
"react-redux": "^7.2.5",
|
||||||
"react-resizable": "^3.0.1",
|
"react-resizable": "^3.0.4",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.3.0",
|
||||||
"react-scripts": "^4.0.3",
|
"react-scripts": "^4.0.3",
|
||||||
"react-sublime-video": "^0.2.5",
|
"react-sublime-video": "^0.2.5",
|
||||||
"react-virtualized": "^9.22.3",
|
"react-virtualized": "^9.22.3",
|
||||||
"recharts": "^2.0.7",
|
"recharts": "^2.1.5",
|
||||||
"redux": "^4.1.0",
|
"redux": "^4.1.1",
|
||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"redux-saga": "^1.1.3",
|
"redux-saga": "^1.1.3",
|
||||||
"redux-state-sync": "^3.1.2",
|
"redux-state-sync": "^3.1.2",
|
||||||
"reselect": "^4.0.0",
|
"reselect": "^4.0.0",
|
||||||
"sass": "^1.32.13",
|
"sass": "^1.43.3",
|
||||||
"socket.io-client": "^4.1.2",
|
"socket.io-client": "^4.3.2",
|
||||||
"styled-components": "^5.3.0",
|
"styled-components": "^5.3.3",
|
||||||
"subscriptions-transport-ws": "^0.9.18",
|
"subscriptions-transport-ws": "^0.9.18",
|
||||||
"web-vitals": "^1.1.2",
|
"web-vitals": "^2.1.2",
|
||||||
"workbox-background-sync": "^6.1.5",
|
"workbox-background-sync": "^6.3.0",
|
||||||
"workbox-broadcast-update": "^6.1.5",
|
"workbox-broadcast-update": "^6.3.0",
|
||||||
"workbox-cacheable-response": "^6.1.5",
|
"workbox-cacheable-response": "^6.3.0",
|
||||||
"workbox-core": "^6.1.5",
|
"workbox-core": "^6.3.0",
|
||||||
"workbox-expiration": "^6.1.5",
|
"workbox-expiration": "^6.3.0",
|
||||||
"workbox-google-analytics": "^6.1.5",
|
"workbox-google-analytics": "^6.3.0",
|
||||||
"workbox-navigation-preload": "^6.1.5",
|
"workbox-navigation-preload": "^6.3.0",
|
||||||
"workbox-precaching": "^6.1.5",
|
"workbox-precaching": "^6.3.0",
|
||||||
"workbox-range-requests": "^6.1.5",
|
"workbox-range-requests": "^6.3.0",
|
||||||
"workbox-routing": "^6.1.5",
|
"workbox-routing": "^6.3.0",
|
||||||
"workbox-strategies": "^6.1.5",
|
"workbox-strategies": "^6.3.0",
|
||||||
"workbox-streams": "^6.1.5"
|
"workbox-streams": "^6.3.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"postinstall": "patch-package",
|
||||||
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
"analyze": "source-map-explorer 'build/static/js/*.js'",
|
||||||
"start": "craco start",
|
"start": "craco start",
|
||||||
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
|
"build": "REACT_APP_GIT_SHA=`git rev-parse --short HEAD` craco build",
|
||||||
@@ -108,6 +115,8 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@sentry/webpack-plugin": "^1.18.3",
|
||||||
|
"patch-package": "^6.4.7",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
"source-map-explorer": "^2.5.2"
|
"source-map-explorer": "^2.5.2"
|
||||||
}
|
}
|
||||||
|
|||||||
13087
client/patches/peerjs+1.3.2.patch
Normal file
@@ -1,4 +1,8 @@
|
|||||||
import { ApolloProvider } from "@apollo/client";
|
import { ApolloProvider } from "@apollo/client";
|
||||||
|
//import trackerRedux from "@openreplay/tracker-redux";
|
||||||
|
import Tracker from "@openreplay/tracker";
|
||||||
|
import trackerGraphQL from "@openreplay/tracker-graphql";
|
||||||
|
import { SplitFactory, SplitSdk } from "@splitsoftware/splitio-react";
|
||||||
import { ConfigProvider } from "antd";
|
import { ConfigProvider } from "antd";
|
||||||
import enLocale from "antd/es/locale/en_US";
|
import enLocale from "antd/es/locale/en_US";
|
||||||
import LogRocket from "logrocket";
|
import LogRocket from "logrocket";
|
||||||
@@ -6,12 +10,41 @@ import moment from "moment";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
import GlobalLoadingBar from "../components/global-loading-bar/global-loading-bar.component";
|
||||||
|
//import trackerAssist from "@openreplay/tracker-assist";
|
||||||
|
import { getCurrentUser } from "../firebase/firebase.utils";
|
||||||
import client from "../utils/GraphQLClient";
|
import client from "../utils/GraphQLClient";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
|
||||||
moment.locale("en-US");
|
moment.locale("en-US");
|
||||||
|
|
||||||
|
export const tracker = new Tracker({
|
||||||
|
projectKey: "trDmOZlEXUpjGsMtHroA",
|
||||||
|
ingestPoint: "https://replay.imex.online/ingest",
|
||||||
|
...(process.env.NODE_ENV === null || process.env.NODE_ENV === "development"
|
||||||
|
? { __DISABLE_SECURE_MODE: true }
|
||||||
|
: {}),
|
||||||
|
// beaconSize: 10485760,
|
||||||
|
onStart: async ({ sessionID }) => {
|
||||||
|
const user = await getCurrentUser();
|
||||||
|
if (user) tracker.setUserID(user.email);
|
||||||
|
console.log("ORS SESSION ", sessionID, user && user.email);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// tracker.use(
|
||||||
|
// trackerAssist({ confirmText: "Technical support is about to assist you." })
|
||||||
|
// ); // check the list of available options below
|
||||||
|
export const recordGraphQL = tracker.use(trackerGraphQL());
|
||||||
|
tracker.start();
|
||||||
if (process.env.NODE_ENV === "production") LogRocket.init("gvfvfw/bodyshopapp");
|
if (process.env.NODE_ENV === "production") LogRocket.init("gvfvfw/bodyshopapp");
|
||||||
|
|
||||||
|
export const factory = SplitSdk({
|
||||||
|
core: {
|
||||||
|
authorizationKey: process.env.REACT_APP_SPLIT_API,
|
||||||
|
key: "anon",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default function AppContainer() {
|
export default function AppContainer() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -29,7 +62,9 @@ export default function AppContainer() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GlobalLoadingBar />
|
<GlobalLoadingBar />
|
||||||
<App />
|
<SplitFactory factory={factory}>
|
||||||
|
<App />
|
||||||
|
</SplitFactory>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</ApolloProvider>
|
</ApolloProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -124,3 +124,13 @@
|
|||||||
z-index: 2 !important;
|
z-index: 2 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.react-kanban-column {
|
||||||
|
background-color: #ddd !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.production-list-table {
|
||||||
|
td.ant-table-column-sort {
|
||||||
|
background: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
24
client/src/assets/C2QB_composite_English.svg
Normal file
|
After Width: | Height: | Size: 51 KiB |
24
client/src/assets/C2QB_transparent_English.svg
Normal file
|
After Width: | Height: | Size: 51 KiB |
4
client/src/assets/qbo/C2QB_green_btn_med_default.svg
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
5
client/src/assets/qbo/C2QB_green_btn_med_hover.svg
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
4
client/src/assets/qbo/C2QB_green_btn_short_default.svg
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
5
client/src/assets/qbo/C2QB_green_btn_short_hover.svg
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
4
client/src/assets/qbo/C2QB_green_btn_tall_default.svg
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
5
client/src/assets/qbo/C2QB_green_btn_tall_hover.svg
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
5
client/src/assets/qbo/C2QB_transparent_btn_med_hover.svg
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
@@ -1,25 +1,9 @@
|
|||||||
import Axios from "axios";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||||
export default function Test() {
|
export default function Test() {
|
||||||
const handleQbSignIn = async () => {
|
|
||||||
const result = await Axios.post("/qbo/authorize", { userId: "1234" });
|
|
||||||
console.log("handleQbSignIn -> result", result.data);
|
|
||||||
// window.open(result.data, "_blank", "toolbar=0,location=0,menubar=0");
|
|
||||||
|
|
||||||
var parameters = "location=1,width=800,height=650";
|
|
||||||
parameters +=
|
|
||||||
",left=" +
|
|
||||||
(window.screen.width - 800) / 2 +
|
|
||||||
",top=" +
|
|
||||||
(window.screen.height - 650) / 2;
|
|
||||||
|
|
||||||
// Launch Popup
|
|
||||||
window.open(result.data, "connectPopup", parameters);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button onClick={handleQbSignIn}>Sign Into Qb.</button>
|
<QboAuthorizeComponent />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,25 @@ import PayableExportAll from "../payable-export-all-button/payable-export-all-bu
|
|||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
export default function AccountingPayablesTableComponent({ loading, bills }) {
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(AccountingPayablesTableComponent);
|
||||||
|
|
||||||
|
export function AccountingPayablesTableComponent({ bodyshop, loading, bills }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [selectedBills, setSelectedBills] = useState([]);
|
const [selectedBills, setSelectedBills] = useState([]);
|
||||||
const [transInProgress, setTransInProgress] = useState(false);
|
const [transInProgress, setTransInProgress] = useState(false);
|
||||||
@@ -109,6 +126,17 @@ export default function AccountingPayablesTableComponent({ loading, bills }) {
|
|||||||
<Checkbox disabled checked={record.is_credit_memo} />
|
<Checkbox disabled checked={record.is_credit_memo} />
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t("exportlogs.labels.attempts"),
|
||||||
|
dataIndex: "attempts",
|
||||||
|
key: "attempts",
|
||||||
|
|
||||||
|
render: (text, record) => {
|
||||||
|
const success = record.exportlogs.filter((e) => e.successful).length;
|
||||||
|
const attempts = record.exportlogs.length;
|
||||||
|
return `${success}/${attempts}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t("general.labels.actions"),
|
title: t("general.labels.actions"),
|
||||||
dataIndex: "actions",
|
dataIndex: "actions",
|
||||||
@@ -121,6 +149,7 @@ export default function AccountingPayablesTableComponent({ loading, bills }) {
|
|||||||
billId={record.id}
|
billId={record.id}
|
||||||
disabled={transInProgress || !!record.exported}
|
disabled={transInProgress || !!record.exported}
|
||||||
loadingCallback={setTransInProgress}
|
loadingCallback={setTransInProgress}
|
||||||
|
setSelectedBills={setSelectedBills}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -154,6 +183,9 @@ export default function AccountingPayablesTableComponent({ loading, bills }) {
|
|||||||
loadingCallback={setTransInProgress}
|
loadingCallback={setTransInProgress}
|
||||||
completedCallback={setSelectedBills}
|
completedCallback={setSelectedBills}
|
||||||
/>
|
/>
|
||||||
|
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
|
||||||
|
<QboAuthorizeComponent />
|
||||||
|
)}
|
||||||
<Input
|
<Input
|
||||||
value={state.search}
|
value={state.search}
|
||||||
onChange={handleSearch}
|
onChange={handleSearch}
|
||||||
|
|||||||
@@ -8,8 +8,26 @@ import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
|
|||||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||||
import PaymentExportButton from "../payment-export-button/payment-export-button.component";
|
import PaymentExportButton from "../payment-export-button/payment-export-button.component";
|
||||||
import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component";
|
import PaymentsExportAllButton from "../payments-export-all-button/payments-export-all-button.component";
|
||||||
|
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
export default function AccountingPayablesTableComponent({
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(AccountingPayablesTableComponent);
|
||||||
|
|
||||||
|
export function AccountingPayablesTableComponent({
|
||||||
|
bodyshop,
|
||||||
loading,
|
loading,
|
||||||
payments,
|
payments,
|
||||||
}) {
|
}) {
|
||||||
@@ -108,7 +126,17 @@ export default function AccountingPayablesTableComponent({
|
|||||||
<DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
|
<DateTimeFormatter>{record.exportedat}</DateTimeFormatter>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t("exportlogs.labels.attempts"),
|
||||||
|
dataIndex: "attempts",
|
||||||
|
key: "attempts",
|
||||||
|
|
||||||
|
render: (text, record) => {
|
||||||
|
const success = record.exportlogs.filter((e) => e.successful).length;
|
||||||
|
const attempts = record.exportlogs.length;
|
||||||
|
return `${success}/${attempts}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t("general.labels.actions"),
|
title: t("general.labels.actions"),
|
||||||
dataIndex: "actions",
|
dataIndex: "actions",
|
||||||
@@ -120,6 +148,7 @@ export default function AccountingPayablesTableComponent({
|
|||||||
paymentId={record.id}
|
paymentId={record.id}
|
||||||
disabled={transInProgress || !!record.exportedat}
|
disabled={transInProgress || !!record.exportedat}
|
||||||
loadingCallback={setTransInProgress}
|
loadingCallback={setTransInProgress}
|
||||||
|
setSelectedPayments={setSelectedPayments}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -152,6 +181,9 @@ export default function AccountingPayablesTableComponent({
|
|||||||
loadingCallback={setTransInProgress}
|
loadingCallback={setTransInProgress}
|
||||||
completedCallback={setSelectedPayments}
|
completedCallback={setSelectedPayments}
|
||||||
/>
|
/>
|
||||||
|
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
|
||||||
|
<QboAuthorizeComponent />
|
||||||
|
)}
|
||||||
<Input
|
<Input
|
||||||
value={state.search}
|
value={state.search}
|
||||||
onChange={handleSearch}
|
onChange={handleSearch}
|
||||||
|
|||||||
@@ -8,7 +8,26 @@ import { alphaSort } from "../../utils/sorters";
|
|||||||
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
|
import JobExportButton from "../jobs-close-export-button/jobs-close-export-button.component";
|
||||||
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
|
import JobsExportAllButton from "../jobs-export-all-button/jobs-export-all-button.component";
|
||||||
|
|
||||||
export default function AccountingReceivablesTableComponent({ loading, jobs }) {
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import QboAuthorizeComponent from "../qbo-authorize/qbo-authorize.component";
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
|
});
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(AccountingReceivablesTableComponent);
|
||||||
|
|
||||||
|
export function AccountingReceivablesTableComponent({
|
||||||
|
bodyshop,
|
||||||
|
loading,
|
||||||
|
jobs,
|
||||||
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [selectedJobs, setSelectedJobs] = useState([]);
|
const [selectedJobs, setSelectedJobs] = useState([]);
|
||||||
const [transInProgress, setTransInProgress] = useState(false);
|
const [transInProgress, setTransInProgress] = useState(false);
|
||||||
@@ -114,17 +133,28 @@ export default function AccountingReceivablesTableComponent({ loading, jobs }) {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t("exportlogs.labels.attempts"),
|
||||||
|
dataIndex: "attempts",
|
||||||
|
key: "attempts",
|
||||||
|
|
||||||
|
render: (text, record) => {
|
||||||
|
const success = record.exportlogs.filter((e) => e.successful).length;
|
||||||
|
const attempts = record.exportlogs.length;
|
||||||
|
return `${success}/${attempts}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t("general.labels.actions"),
|
title: t("general.labels.actions"),
|
||||||
dataIndex: "actions",
|
dataIndex: "actions",
|
||||||
key: "actions",
|
key: "actions",
|
||||||
sorter: (a, b) => a.clm_total - b.clm_total,
|
|
||||||
|
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<JobExportButton
|
<JobExportButton
|
||||||
jobId={record.id}
|
jobId={record.id}
|
||||||
disabled={!!record.date_exported}
|
disabled={!!record.date_exported}
|
||||||
|
setSelectedJobs={setSelectedJobs}
|
||||||
/>
|
/>
|
||||||
<Link to={`/manage/jobs/${record.id}/close`}>
|
<Link to={`/manage/jobs/${record.id}/close`}>
|
||||||
<Button>{t("jobs.labels.viewallocations")}</Button>
|
<Button>{t("jobs.labels.viewallocations")}</Button>
|
||||||
@@ -169,12 +199,17 @@ export default function AccountingReceivablesTableComponent({ loading, jobs }) {
|
|||||||
<Card
|
<Card
|
||||||
extra={
|
extra={
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<JobsExportAllButton
|
{!bodyshop.cdk_dealerid && !bodyshop.pbs_serialnumber && (
|
||||||
jobIds={selectedJobs}
|
<JobsExportAllButton
|
||||||
disabled={transInProgress || selectedJobs.length === 0}
|
jobIds={selectedJobs}
|
||||||
loadingCallback={setTransInProgress}
|
disabled={transInProgress || selectedJobs.length === 0}
|
||||||
completedCallback={setSelectedJobs}
|
loadingCallback={setTransInProgress}
|
||||||
/>
|
completedCallback={setSelectedJobs}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && (
|
||||||
|
<QboAuthorizeComponent />
|
||||||
|
)}
|
||||||
<Input.Search
|
<Input.Search
|
||||||
value={state.search}
|
value={state.search}
|
||||||
onChange={handleSearch}
|
onChange={handleSearch}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import BillReeportButtonComponent from "../bill-reexport-button/bill-reexport-bu
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
@@ -33,6 +35,8 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setPartsOrderContext: (context) =>
|
setPartsOrderContext: (context) =>
|
||||||
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
||||||
|
insertAuditTrail: ({ jobid, operation }) =>
|
||||||
|
dispatch(insertAuditTrail({ jobid, operation })),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
@@ -40,7 +44,10 @@ export default connect(
|
|||||||
mapDispatchToProps
|
mapDispatchToProps
|
||||||
)(BillDetailEditcontainer);
|
)(BillDetailEditcontainer);
|
||||||
|
|
||||||
export function BillDetailEditcontainer({ setPartsOrderContext }) {
|
export function BillDetailEditcontainer({
|
||||||
|
setPartsOrderContext,
|
||||||
|
insertAuditTrail,
|
||||||
|
}) {
|
||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -134,6 +141,12 @@ export function BillDetailEditcontainer({ setPartsOrderContext }) {
|
|||||||
});
|
});
|
||||||
await Promise.all(updates);
|
await Promise.all(updates);
|
||||||
|
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: bill.jobid,
|
||||||
|
billid: search.billid,
|
||||||
|
operation: AuditTrailMapping.billupdated(bill.invoice_number),
|
||||||
|
});
|
||||||
|
|
||||||
await refetch();
|
await refetch();
|
||||||
form.setFieldsValue(transformData(data));
|
form.setFieldsValue(transformData(data));
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
|
|||||||
@@ -11,13 +11,17 @@ import {
|
|||||||
QUERY_JOB_LBR_ADJUSTMENTS,
|
QUERY_JOB_LBR_ADJUSTMENTS,
|
||||||
UPDATE_JOB,
|
UPDATE_JOB,
|
||||||
} from "../../graphql/jobs.queries";
|
} from "../../graphql/jobs.queries";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
import { toggleModalVisible } from "../../redux/modals/modals.actions";
|
||||||
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
|
import { selectBillEnterModal } from "../../redux/modals/modals.selectors";
|
||||||
import {
|
import {
|
||||||
selectBodyshop,
|
selectBodyshop,
|
||||||
selectCurrentUser,
|
selectCurrentUser,
|
||||||
} from "../../redux/user/user.selectors";
|
} from "../../redux/user/user.selectors";
|
||||||
|
import confirmDialog from "../../utils/asyncConfirm";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import BillFormContainer from "../bill-form/bill-form.container";
|
import BillFormContainer from "../bill-form/bill-form.container";
|
||||||
|
import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
|
||||||
import { handleUpload } from "../documents-upload/documents-upload.utility";
|
import { handleUpload } from "../documents-upload/documents-upload.utility";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
@@ -27,6 +31,8 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
|
toggleModalVisible: () => dispatch(toggleModalVisible("billEnter")),
|
||||||
|
insertAuditTrail: ({ jobid, operation }) =>
|
||||||
|
dispatch(insertAuditTrail({ jobid, operation })),
|
||||||
});
|
});
|
||||||
|
|
||||||
function BillEnterModalContainer({
|
function BillEnterModalContainer({
|
||||||
@@ -34,6 +40,7 @@ function BillEnterModalContainer({
|
|||||||
toggleModalVisible,
|
toggleModalVisible,
|
||||||
bodyshop,
|
bodyshop,
|
||||||
currentUser,
|
currentUser,
|
||||||
|
insertAuditTrail,
|
||||||
}) {
|
}) {
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -43,7 +50,31 @@ function BillEnterModalContainer({
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
|
|
||||||
|
const formValues = useMemo(() => {
|
||||||
|
return {
|
||||||
|
...billEnterModal.context.bill,
|
||||||
|
jobid:
|
||||||
|
(billEnterModal.context.job && billEnterModal.context.job.id) || null,
|
||||||
|
federal_tax_rate:
|
||||||
|
(bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.federal_tax_rate) ||
|
||||||
|
0,
|
||||||
|
state_tax_rate:
|
||||||
|
(bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.state_tax_rate) ||
|
||||||
|
0,
|
||||||
|
local_tax_rate:
|
||||||
|
(bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.local_tax_rate) ||
|
||||||
|
0,
|
||||||
|
};
|
||||||
|
}, [billEnterModal, bodyshop]);
|
||||||
|
|
||||||
const handleFinish = async (values) => {
|
const handleFinish = async (values) => {
|
||||||
|
let totals = CalculateBillTotal(values);
|
||||||
|
if (totals.discrepancy.getAmount() !== 0) {
|
||||||
|
if (!(await confirmDialog(t("bills.labels.savewithdiscrepancy")))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const { upload, location, ...remainingValues } = values;
|
const { upload, location, ...remainingValues } = values;
|
||||||
|
|
||||||
@@ -81,8 +112,9 @@ function BillEnterModalContainer({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
refetchQueries: ["QUERY_PARTS_BILLS_BY_JOBID"],
|
||||||
});
|
});
|
||||||
console.log("adjustmentsToInsert", adjustmentsToInsert);
|
|
||||||
const adjKeys = Object.keys(adjustmentsToInsert);
|
const adjKeys = Object.keys(adjustmentsToInsert);
|
||||||
if (adjKeys.length > 0) {
|
if (adjKeys.length > 0) {
|
||||||
//Query the adjustments, merge, and update them.
|
//Query the adjustments, merge, and update them.
|
||||||
@@ -115,7 +147,12 @@ function BillEnterModalContainer({
|
|||||||
message: JSON.stringify(jobUpdate.errors),
|
message: JSON.stringify(jobUpdate.errors),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: values.jobid,
|
||||||
|
operation: AuditTrailMapping.jobmodifylbradj(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!!r1.errors) {
|
if (!!r1.errors) {
|
||||||
@@ -171,9 +208,15 @@ function BillEnterModalContainer({
|
|||||||
});
|
});
|
||||||
if (billEnterModal.actions.refetch) billEnterModal.actions.refetch();
|
if (billEnterModal.actions.refetch) billEnterModal.actions.refetch();
|
||||||
|
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: values.jobid,
|
||||||
|
billid: billId,
|
||||||
|
operation: AuditTrailMapping.billposted(remainingValues.invoice_number),
|
||||||
|
});
|
||||||
|
|
||||||
if (enterAgain) {
|
if (enterAgain) {
|
||||||
form.resetFields();
|
form.resetFields();
|
||||||
form.setFieldsValue({ billlines: [] });
|
form.setFieldsValue({ ...form.getFieldsValue(), billlines: [] });
|
||||||
} else {
|
} else {
|
||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
}
|
}
|
||||||
@@ -191,23 +234,6 @@ function BillEnterModalContainer({
|
|||||||
if (enterAgain) form.submit();
|
if (enterAgain) form.submit();
|
||||||
}, [enterAgain, form]);
|
}, [enterAgain, form]);
|
||||||
|
|
||||||
const formValues = useMemo(() => {
|
|
||||||
return {
|
|
||||||
...billEnterModal.context.bill,
|
|
||||||
jobid:
|
|
||||||
(billEnterModal.context.job && billEnterModal.context.job.id) || null,
|
|
||||||
federal_tax_rate:
|
|
||||||
(bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.federal_tax_rate) ||
|
|
||||||
0,
|
|
||||||
state_tax_rate:
|
|
||||||
(bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.state_tax_rate) ||
|
|
||||||
0,
|
|
||||||
local_tax_rate:
|
|
||||||
(bodyshop.bill_tax_rates && bodyshop.bill_tax_rates.local_tax_rate) ||
|
|
||||||
0,
|
|
||||||
};
|
|
||||||
}, [billEnterModal, bodyshop]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (billEnterModal.visible) {
|
if (billEnterModal.visible) {
|
||||||
form.setFieldsValue(formValues);
|
form.setFieldsValue(formValues);
|
||||||
|
|||||||
@@ -72,9 +72,11 @@ export function BillEnterModalLinesComponent({
|
|||||||
quantity: opt.part_qty || 1,
|
quantity: opt.part_qty || 1,
|
||||||
actual_price: opt.cost,
|
actual_price: opt.cost,
|
||||||
cost_center: opt.part_type
|
cost_center: opt.part_type
|
||||||
? responsibilityCenters.defaults.costs[
|
? responsibilityCenters.defaults &&
|
||||||
|
(responsibilityCenters.defaults.costs[
|
||||||
opt.part_type
|
opt.part_type
|
||||||
] || null
|
] ||
|
||||||
|
null)
|
||||||
: null,
|
: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -154,7 +156,6 @@ export function BillEnterModalLinesComponent({
|
|||||||
setFieldsValue({
|
setFieldsValue({
|
||||||
billlines: getFieldsValue("billlines").billlines.map(
|
billlines: getFieldsValue("billlines").billlines.map(
|
||||||
(item, idx) => {
|
(item, idx) => {
|
||||||
console.log("Checking", index, idx);
|
|
||||||
if (idx === index) {
|
if (idx === index) {
|
||||||
console.log(
|
console.log(
|
||||||
"Found and setting.",
|
"Found and setting.",
|
||||||
@@ -500,9 +501,9 @@ const EditableCell = ({
|
|||||||
labelCol={{ span: 0 }}
|
labelCol={{ span: 0 }}
|
||||||
{...(formItemProps && formItemProps(record))}
|
{...(formItemProps && formItemProps(record))}
|
||||||
>
|
>
|
||||||
{(formInput && formInput(record, record.key)) || children}
|
{(formInput && formInput(record, record.name)) || children}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
{additional && additional(record, record.key)}
|
{additional && additional(record, record.name)}
|
||||||
</Space>
|
</Space>
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
@@ -514,7 +515,7 @@ const EditableCell = ({
|
|||||||
name={dataIndex}
|
name={dataIndex}
|
||||||
{...(formItemProps && formItemProps(record))}
|
{...(formItemProps && formItemProps(record))}
|
||||||
>
|
>
|
||||||
{(formInput && formInput(record, record.key)) || children}
|
{(formInput && formInput(record, record.name)) || children}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
import Dinero from "dinero.js";
|
import Dinero from "dinero.js";
|
||||||
|
|
||||||
export const CalculateBillTotal = (invoice) => {
|
export const CalculateBillTotal = (invoice) => {
|
||||||
const {
|
const { total, billlines, federal_tax_rate, local_tax_rate, state_tax_rate } =
|
||||||
total,
|
invoice;
|
||||||
billlines,
|
|
||||||
federal_tax_rate,
|
|
||||||
local_tax_rate,
|
|
||||||
state_tax_rate,
|
|
||||||
} = invoice;
|
|
||||||
|
|
||||||
//TODO Determine why this recalculates so many times.
|
//TODO Determine why this recalculates so many times.
|
||||||
let subtotal = Dinero({ amount: 0 });
|
let subtotal = Dinero({ amount: 0 });
|
||||||
@@ -20,8 +15,7 @@ export const CalculateBillTotal = (invoice) => {
|
|||||||
billlines.forEach((i) => {
|
billlines.forEach((i) => {
|
||||||
if (!!i) {
|
if (!!i) {
|
||||||
const itemTotal = Dinero({
|
const itemTotal = Dinero({
|
||||||
amount:
|
amount: Math.round((i.actual_cost || 0) * 100),
|
||||||
Math.round(((i.actual_cost || 0) * 100 + Number.EPSILON) * 100) / 100,
|
|
||||||
}).multiply(i.quantity || 1);
|
}).multiply(i.quantity || 1);
|
||||||
|
|
||||||
subtotal = subtotal.add(itemTotal);
|
subtotal = subtotal.add(itemTotal);
|
||||||
|
|||||||
@@ -12,7 +12,19 @@ const BillLineSearchSelect = ({ options, disabled, ...restProps }, ref) => {
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
showSearch
|
showSearch
|
||||||
optionFilterProp="line_desc"
|
// optionFilterProp="line_desc"
|
||||||
|
filterOption={(inputValue, option) => {
|
||||||
|
console.log(inputValue);
|
||||||
|
return (
|
||||||
|
(option.line_desc &&
|
||||||
|
option.line_desc
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(inputValue.toLowerCase())) ||
|
||||||
|
(option.oem_partno &&
|
||||||
|
option.oem_partno.toLowerCase().includes(inputValue.toLowerCase()))
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
notFoundContent={"Removed."}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
>
|
>
|
||||||
<Select.Option key={null} value={"noline"} cost={0} line_desc={""}>
|
<Select.Option key={null} value={"noline"} cost={0} line_desc={""}>
|
||||||
@@ -21,14 +33,19 @@ const BillLineSearchSelect = ({ options, disabled, ...restProps }, ref) => {
|
|||||||
{options
|
{options
|
||||||
? options.map((item) => (
|
? options.map((item) => (
|
||||||
<Option
|
<Option
|
||||||
|
disabled={item.removed}
|
||||||
key={item.id}
|
key={item.id}
|
||||||
value={item.id}
|
value={item.id}
|
||||||
cost={item.act_price ? item.act_price : 0}
|
cost={item.act_price ? item.act_price : 0}
|
||||||
part_type={item.part_type}
|
part_type={item.part_type}
|
||||||
line_desc={item.line_desc}
|
line_desc={item.line_desc}
|
||||||
part_qty={item.part_qty}
|
part_qty={item.part_qty}
|
||||||
|
oem_partno={item.oem_partno}
|
||||||
|
style={{
|
||||||
|
...(item.removed ? { textDecoration: "line-through" } : {}),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{`${item.line_desc}${
|
{`${item.removed ? `(REMOVED) ` : ""}${item.line_desc}${
|
||||||
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
item.oem_partno ? ` - ${item.oem_partno}` : ""
|
||||||
}`}
|
}`}
|
||||||
</Option>
|
</Option>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import { DateFormatter } from "../../utils/DateFormatter";
|
||||||
import { alphaSort, dateSort } from "../../utils/sorters";
|
import { alphaSort, dateSort } from "../../utils/sorters";
|
||||||
@@ -14,6 +15,7 @@ import PrintWrapperComponent from "../print-wrapper/print-wrapper.component";
|
|||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//jobRO: selectJobReadOnly,
|
//jobRO: selectJobReadOnly,
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
@@ -26,6 +28,7 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export function BillsListTableComponent({
|
export function BillsListTableComponent({
|
||||||
|
bodyshop,
|
||||||
job,
|
job,
|
||||||
billsQuery,
|
billsQuery,
|
||||||
handleOnRowClick,
|
handleOnRowClick,
|
||||||
@@ -52,7 +55,9 @@ export function BillsListTableComponent({
|
|||||||
)}
|
)}
|
||||||
<BillDeleteButton bill={record} />
|
<BillDeleteButton bill={record} />
|
||||||
<Button
|
<Button
|
||||||
disabled={record.is_credit_memo}
|
disabled={
|
||||||
|
record.is_credit_memo || record.vendorid === bodyshop.inhousevendorid
|
||||||
|
}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setPartsOrderContext({
|
setPartsOrderContext({
|
||||||
actions: {},
|
actions: {},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { selectSelectedConversation } from "../../redux/messaging/messaging.sele
|
|||||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||||
import "./chat-conversation-list.styles.scss";
|
import "./chat-conversation-list.styles.scss";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { TimeAgoFormatter } from "../../utils/DateFormatter";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
selectedConversation: selectSelectedConversation,
|
selectedConversation: selectSelectedConversation,
|
||||||
@@ -60,13 +61,18 @@ export function ChatConversationListComponent({
|
|||||||
) : (
|
) : (
|
||||||
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
|
<PhoneFormatter>{item.phone_num}</PhoneFormatter>
|
||||||
)}
|
)}
|
||||||
{item.job_conversations.length > 0
|
<div sryle={{ display: "inline-block" }}>
|
||||||
? item.job_conversations.map((j, idx) => (
|
<div>
|
||||||
<Tag key={idx} className="ro-number-tag">
|
{item.job_conversations.length > 0
|
||||||
{j.job.ro_number}
|
? item.job_conversations.map((j, idx) => (
|
||||||
</Tag>
|
<Tag key={idx} className="ro-number-tag">
|
||||||
))
|
{j.job.ro_number}
|
||||||
: null}
|
</Tag>
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
<TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>
|
||||||
|
</div>
|
||||||
<Badge count={item.messages_aggregate.aggregate.count || 0} />
|
<Badge count={item.messages_aggregate.aggregate.count || 0} />
|
||||||
</List.Item>
|
</List.Item>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Icon from "@ant-design/icons";
|
import Icon from "@ant-design/icons";
|
||||||
|
import { Tooltip } from "antd";
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef } from "react";
|
||||||
@@ -9,6 +10,7 @@ import {
|
|||||||
CellMeasurerCache,
|
CellMeasurerCache,
|
||||||
List,
|
List,
|
||||||
} from "react-virtualized";
|
} from "react-virtualized";
|
||||||
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
import "./chat-message-list.styles.scss";
|
import "./chat-message-list.styles.scss";
|
||||||
|
|
||||||
export default function ChatMessageListComponent({ messages }) {
|
export default function ChatMessageListComponent({ messages }) {
|
||||||
@@ -85,17 +87,22 @@ export default function ChatMessageListComponent({ messages }) {
|
|||||||
|
|
||||||
const MessageRender = (message) => {
|
const MessageRender = (message) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<Tooltip title={DateTimeFormatter({ children: message.created_at })}>
|
||||||
{message.image_path &&
|
<div>
|
||||||
message.image_path.map((i, idx) => (
|
{message.image_path &&
|
||||||
<div key={idx} style={{ display: "flex", justifyContent: "center" }}>
|
message.image_path.map((i, idx) => (
|
||||||
<a href={i} target="__blank">
|
<div
|
||||||
<img alt="Received" className="message-img" src={i} />
|
key={idx}
|
||||||
</a>
|
style={{ display: "flex", justifyContent: "center" }}
|
||||||
</div>
|
>
|
||||||
))}
|
<a href={i} target="__blank">
|
||||||
<div>{message.text}</div>
|
<img alt="Received" className="message-img" src={i} />
|
||||||
</div>
|
</a>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div>{message.text}</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export function ContractConvertToRo({
|
|||||||
const billingLines = [];
|
const billingLines = [];
|
||||||
if (contractLength > 0)
|
if (contractLength > 0)
|
||||||
billingLines.push({
|
billingLines.push({
|
||||||
|
manual_line:true,
|
||||||
unq_seq: 1,
|
unq_seq: 1,
|
||||||
line_no: 1,
|
line_no: 1,
|
||||||
line_ref: 1,
|
line_ref: 1,
|
||||||
@@ -70,6 +71,7 @@ export function ContractConvertToRo({
|
|||||||
contract.kmend - contract.kmstart - contract.dailyfreekm * contractLength;
|
contract.kmend - contract.kmstart - contract.dailyfreekm * contractLength;
|
||||||
if (mileageDiff > 0) {
|
if (mileageDiff > 0) {
|
||||||
billingLines.push({
|
billingLines.push({
|
||||||
|
manual_line:true,
|
||||||
unq_seq: 2,
|
unq_seq: 2,
|
||||||
line_no: 2,
|
line_no: 2,
|
||||||
line_ref: 2,
|
line_ref: 2,
|
||||||
@@ -86,6 +88,7 @@ export function ContractConvertToRo({
|
|||||||
|
|
||||||
if (values.refuelqty > 0) {
|
if (values.refuelqty > 0) {
|
||||||
billingLines.push({
|
billingLines.push({
|
||||||
|
manual_line:true,
|
||||||
unq_seq: 3,
|
unq_seq: 3,
|
||||||
line_no: 3,
|
line_no: 3,
|
||||||
line_ref: 3,
|
line_ref: 3,
|
||||||
@@ -101,6 +104,7 @@ export function ContractConvertToRo({
|
|||||||
}
|
}
|
||||||
if (values.applyCleanupCharge) {
|
if (values.applyCleanupCharge) {
|
||||||
billingLines.push({
|
billingLines.push({
|
||||||
|
manual_line:true,
|
||||||
unq_seq: 4,
|
unq_seq: 4,
|
||||||
line_no: 4,
|
line_no: 4,
|
||||||
line_ref: 4,
|
line_ref: 4,
|
||||||
@@ -117,6 +121,7 @@ export function ContractConvertToRo({
|
|||||||
if (contract.damagewaiver) {
|
if (contract.damagewaiver) {
|
||||||
//Add for cleanup fee.
|
//Add for cleanup fee.
|
||||||
billingLines.push({
|
billingLines.push({
|
||||||
|
manual_line:true,
|
||||||
unq_seq: 5,
|
unq_seq: 5,
|
||||||
line_no: 5,
|
line_no: 5,
|
||||||
line_ref: 5,
|
line_ref: 5,
|
||||||
|
|||||||
@@ -83,8 +83,10 @@ export function ContractsList({
|
|||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<Link to={`/manage/courtesycars/${record.courtesycar.id}`}>{`${
|
<Link to={`/manage/courtesycars/${record.courtesycar.id}`}>{`${
|
||||||
record.courtesycar.year
|
record.courtesycar.year
|
||||||
} ${record.courtesycar.make} ${record.courtesycar.model} ${
|
} ${record.courtesycar.make} ${record.courtesycar.model}${
|
||||||
record.courtesycar.plate ? `(${record.courtesycar.plate})` : ""
|
record.courtesycar.plate ? ` (${record.courtesycar.plate})` : ""
|
||||||
|
}${
|
||||||
|
record.courtesycar.fleetnumber ? ` (${record.courtesycar.fleetnumber})` : ""
|
||||||
}`}</Link>
|
}`}</Link>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ const CourtesyCarStatusComponent = ({ value, onChange }, ref) => {
|
|||||||
<Option value="courtesycars.status.out">
|
<Option value="courtesycars.status.out">
|
||||||
{t("courtesycars.status.out")}
|
{t("courtesycars.status.out")}
|
||||||
</Option>
|
</Option>
|
||||||
|
<Option value="courtesycars.status.sold">
|
||||||
|
{t("courtesycars.status.sold")}
|
||||||
|
</Option>
|
||||||
</Select>
|
</Select>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ export default function CourtesyCarsList({ loading, courtesycars, refetch }) {
|
|||||||
text: t("courtesycars.status.out"),
|
text: t("courtesycars.status.out"),
|
||||||
value: "courtesycars.status.out",
|
value: "courtesycars.status.out",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: t("courtesycars.status.sold"),
|
||||||
|
value: "courtesycars.status.sold",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
onFilter: (value, record) => value.includes(record.status),
|
onFilter: (value, record) => value.includes(record.status),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ export default function DashboardMonthlyRevenueGraph({ data, ...cardProps }) {
|
|||||||
let dailySales;
|
let dailySales;
|
||||||
if (!!jobsByDate[val]) {
|
if (!!jobsByDate[val]) {
|
||||||
dailySales = jobsByDate[val].reduce((dayAcc, dayVal) => {
|
dailySales = jobsByDate[val].reduce((dayAcc, dayVal) => {
|
||||||
return dayAcc.add(Dinero(dayVal.job_totals.totals.subtotal));
|
return dayAcc.add(
|
||||||
|
Dinero((dayVal.job_totals && dayVal.job_totals.totals.subtotal) || 0)
|
||||||
|
);
|
||||||
}, Dinero());
|
}, Dinero());
|
||||||
} else {
|
} else {
|
||||||
dailySales = Dinero();
|
dailySales = Dinero();
|
||||||
|
|||||||
@@ -13,7 +13,14 @@ export default function DashboardProjectedMonthlySales({ data, ...cardProps }) {
|
|||||||
const dollars =
|
const dollars =
|
||||||
data.projected_monthly_sales &&
|
data.projected_monthly_sales &&
|
||||||
data.projected_monthly_sales.reduce(
|
data.projected_monthly_sales.reduce(
|
||||||
(acc, val) => acc.add(Dinero(val.job_totals.totals.subtotal)),
|
(acc, val) =>
|
||||||
|
acc.add(
|
||||||
|
Dinero(
|
||||||
|
val.job_totals &&
|
||||||
|
val.job_totals.totals &&
|
||||||
|
val.job_totals.totals.subtotal
|
||||||
|
)
|
||||||
|
),
|
||||||
Dinero()
|
Dinero()
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ export default function DashboardTotalProductionDollars({
|
|||||||
const dollars =
|
const dollars =
|
||||||
data.production_jobs &&
|
data.production_jobs &&
|
||||||
data.production_jobs.reduce(
|
data.production_jobs.reduce(
|
||||||
(acc, val) => acc.add(Dinero(val.job_totals.totals.subtotal)),
|
(acc, val) =>
|
||||||
|
acc.add(Dinero(val.job_totals && val.job_totals.totals.subtotal)),
|
||||||
Dinero()
|
Dinero()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ const componentList = {
|
|||||||
h: 3,
|
h: 3,
|
||||||
},
|
},
|
||||||
MonthlyPartsSales: {
|
MonthlyPartsSales: {
|
||||||
label: i18next.t("dashboard.titles.productiondollars"),
|
label: i18next.t("dashboard.titles.monthlypartssales"),
|
||||||
component: DashboardMonthlyPartsSales,
|
component: DashboardMonthlyPartsSales,
|
||||||
gqlFragment: null,
|
gqlFragment: null,
|
||||||
minW: 2,
|
minW: 2,
|
||||||
@@ -253,7 +253,7 @@ const componentList = {
|
|||||||
h: 2,
|
h: 2,
|
||||||
},
|
},
|
||||||
MonthlyLaborSales: {
|
MonthlyLaborSales: {
|
||||||
label: i18next.t("dashboard.titles.monthlypartssales"),
|
label: i18next.t("dashboard.titles.monthlylaborsales"),
|
||||||
component: DashboardMonthlyLaborSales,
|
component: DashboardMonthlyLaborSales,
|
||||||
gqlFragment: null,
|
gqlFragment: null,
|
||||||
minW: 2,
|
minW: 2,
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { Button, Card, Table, Typography } from "antd";
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import Dinero from "dinero.js";
|
||||||
|
import { SyncOutlined } from "@ant-design/icons";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
//currentUser: selectCurrentUser
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(DmsAllocationsSummary);
|
||||||
|
|
||||||
|
export function DmsAllocationsSummary({ socket, bodyshop, jobId, title }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [allocationsSummary, setAllocationsSummary] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (socket.connected) {
|
||||||
|
socket.emit("cdk-calculate-allocations", jobId, (ack) => {
|
||||||
|
setAllocationsSummary(ack);
|
||||||
|
socket.allocationsSummary = ack;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [socket, socket.connected, jobId]);
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.dms.center"),
|
||||||
|
dataIndex: "center",
|
||||||
|
key: "center",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.dms.sale"),
|
||||||
|
dataIndex: "sale",
|
||||||
|
key: "sale",
|
||||||
|
render: (text, record) => Dinero(record.sale).toFormat(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.dms.cost"),
|
||||||
|
dataIndex: "cost",
|
||||||
|
key: "cost",
|
||||||
|
render: (text, record) => Dinero(record.cost).toFormat(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.dms.sale_dms_acctnumber"),
|
||||||
|
dataIndex: "sale_dms_acctnumber",
|
||||||
|
key: "sale_dms_acctnumber",
|
||||||
|
render: (text, record) =>
|
||||||
|
record.profitCenter && record.profitCenter.dms_acctnumber,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.dms.cost_dms_acctnumber"),
|
||||||
|
dataIndex: "cost_dms_acctnumber",
|
||||||
|
key: "cost_dms_acctnumber",
|
||||||
|
render: (text, record) =>
|
||||||
|
record.costCenter && record.costCenter.dms_acctnumber,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.dms.dms_wip_acctnumber"),
|
||||||
|
dataIndex: "dms_wip_acctnumber",
|
||||||
|
key: "dms_wip_acctnumber",
|
||||||
|
render: (text, record) =>
|
||||||
|
record.costCenter && record.costCenter.dms_wip_acctnumber,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
title={title}
|
||||||
|
extra={
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
socket.emit("cdk-calculate-allocations", jobId, (ack) =>
|
||||||
|
setAllocationsSummary(ack)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SyncOutlined />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Table
|
||||||
|
pagination={{ position: "top", defaultPageSize: 50 }}
|
||||||
|
columns={columns}
|
||||||
|
rowKey="center"
|
||||||
|
dataSource={allocationsSummary}
|
||||||
|
summary={() => {
|
||||||
|
const totals = allocationsSummary.reduce(
|
||||||
|
(acc, val) => {
|
||||||
|
return {
|
||||||
|
totalSale: acc.totalSale.add(Dinero(val.sale)),
|
||||||
|
totalCost: acc.totalCost.add(Dinero(val.cost)),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
totalSale: Dinero(),
|
||||||
|
totalCost: Dinero(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table.Summary.Row>
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
<Typography.Title level={4}>
|
||||||
|
{t("general.labels.totals")}
|
||||||
|
</Typography.Title>
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
{totals.totalSale.toFormat()}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell>
|
||||||
|
{
|
||||||
|
// totals.totalCost.toFormat()
|
||||||
|
}
|
||||||
|
</Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell></Table.Summary.Cell>
|
||||||
|
<Table.Summary.Cell></Table.Summary.Cell>
|
||||||
|
</Table.Summary.Row>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
105
client/src/components/dms-cdk-makes/dms-cdk-makes.component.jsx
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { useLazyQuery } from "@apollo/client";
|
||||||
|
import { Button, Input, Modal, Table } from "antd";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { SEARCH_DMS_VEHICLES } from "../../graphql/dms.queries";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import AlertComponent from "../alert/alert.component";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
//currentUser: selectCurrentUser
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
|
});
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(DmsCdkVehicles);
|
||||||
|
|
||||||
|
export function DmsCdkVehicles({ bodyshop, form, socket, job }) {
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [selectedModel, setSelectedModel] = useState(null);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [callSearch, { loading, error, data }] =
|
||||||
|
useLazyQuery(SEARCH_DMS_VEHICLES);
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("vehicles.fields.v_make_desc"),
|
||||||
|
dataIndex: "make",
|
||||||
|
key: "make",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("vehicles.fields.v_model_desc"),
|
||||||
|
dataIndex: "model",
|
||||||
|
key: "model",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.dms.dms_make"),
|
||||||
|
dataIndex: "makecode",
|
||||||
|
key: "makecode",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.dms.dms_model"),
|
||||||
|
dataIndex: "modelcode",
|
||||||
|
key: "modelcode",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Modal
|
||||||
|
width={"90%"}
|
||||||
|
visible={visible}
|
||||||
|
onCancel={() => setVisible(false)}
|
||||||
|
onOk={() => {
|
||||||
|
form.setFieldsValue({
|
||||||
|
dms_make: selectedModel.makecode,
|
||||||
|
dms_model: selectedModel.modelcode,
|
||||||
|
});
|
||||||
|
setVisible(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{error && <AlertComponent error={error.message} />}
|
||||||
|
<Table
|
||||||
|
title={() => (
|
||||||
|
<Input.Search
|
||||||
|
onSearch={(val) => callSearch({ variables: { search: val } })}
|
||||||
|
placeholder={t("general.labels.search")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
columns={columns}
|
||||||
|
loading={loading}
|
||||||
|
rowKey="id"
|
||||||
|
dataSource={data ? data.search_dms_vehicles : []}
|
||||||
|
onRow={(record) => {
|
||||||
|
return {
|
||||||
|
onClick: () => setSelectedModel(record),
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
rowSelection={{
|
||||||
|
onSelect: (record) => {
|
||||||
|
setSelectedModel(record);
|
||||||
|
},
|
||||||
|
|
||||||
|
type: "radio",
|
||||||
|
selectedRowKeys: [selectedModel && selectedModel.id],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setVisible(true);
|
||||||
|
callSearch({
|
||||||
|
variables: {
|
||||||
|
search: job && job.v_model_desc && job.v_model_desc.substr(0, 3),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("jobs.actions.dms.findmakemodelcode")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Button } from "antd";
|
||||||
|
import axios from "axios";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
//currentUser: selectCurrentUser
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
|
});
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(DmsCdkMakesRefetch);
|
||||||
|
|
||||||
|
export function DmsCdkMakesRefetch({ bodyshop, form, socket }) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const handleRefetch = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await axios.post("/cdk/getvehicles", {
|
||||||
|
cdk_dealerid: bodyshop.cdk_dealerid,
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
});
|
||||||
|
console.log(response);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Button loading={loading} onClick={handleRefetch}>
|
||||||
|
{t("jobs.actions.dms.refetchmakesmodels")}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,10 +1,24 @@
|
|||||||
import { Button, Table } from "antd";
|
import { Button, Table, Col , Checkbox} from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
import { socket } from "../../pages/dms/dms.container";
|
import { socket } from "../../pages/dms/dms.container";
|
||||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { alphaSort } from "../../utils/sorters";
|
import { alphaSort } from "../../utils/sorters";
|
||||||
export default function DmsCustomerSelector() {
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
|
});
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(DmsCustomerSelector);
|
||||||
|
|
||||||
|
export function DmsCustomerSelector({ bodyshop }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [customerList, setcustomerList] = useState([]);
|
const [customerList, setcustomerList] = useState([]);
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
@@ -15,63 +29,93 @@ export default function DmsCustomerSelector() {
|
|||||||
setcustomerList(customerList);
|
setcustomerList(customerList);
|
||||||
});
|
});
|
||||||
|
|
||||||
const onOk = () => {
|
const onUseSelected = () => {
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
socket.emit("cdk-selected-customer", selectedCustomer);
|
socket.emit("cdk-selected-customer", selectedCustomer);
|
||||||
|
setSelectedCustomer(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUseGeneric = () => {
|
||||||
|
setVisible(false);
|
||||||
|
socket.emit(
|
||||||
|
"cdk-selected-customer",
|
||||||
|
bodyshop.cdk_configuration.generic_customer_number
|
||||||
|
);
|
||||||
|
setSelectedCustomer(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCreateNew = () => {
|
||||||
|
setVisible(false);
|
||||||
|
socket.emit("cdk-selected-customer", null);
|
||||||
|
setSelectedCustomer(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: t("dms.fields.name1"),
|
title: t("jobs.fields.dms.id"),
|
||||||
|
dataIndex: ["id", "value"],
|
||||||
|
key: "id",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.dms.vinowner"),
|
||||||
|
dataIndex: "vinOwner",
|
||||||
|
key: "vinOwner",
|
||||||
|
render: (text, record) => <Checkbox disabled checked={record.vinOwner}/>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("jobs.fields.dms.name1"),
|
||||||
dataIndex: ["name1", "fullName"],
|
dataIndex: ["name1", "fullName"],
|
||||||
key: "name1",
|
key: "name1",
|
||||||
sorter: (a, b) => alphaSort(a.name1?.fullName, b.name1?.fullName),
|
sorter: (a, b) => alphaSort(a.name1?.fullName, b.name1?.fullName),
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
title: t("dms.fields.name2"),
|
title: t("jobs.fields.dms.address"),
|
||||||
dataIndex: ["name2", "fullName"],
|
|
||||||
key: "name2",
|
|
||||||
sorter: (a, b) => alphaSort(a.name2?.fullName, b.name2?.fullName),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("dms.fields.phone"),
|
|
||||||
dataIndex: ["contactInfo", "mainTelephoneNumber", "value"],
|
|
||||||
key: "phone",
|
|
||||||
render: (record, value) => (
|
|
||||||
<PhoneFormatter>
|
|
||||||
{record.contactInfo?.mainTelephoneNumber?.value}
|
|
||||||
</PhoneFormatter>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t("dms.fields.address"),
|
|
||||||
//dataIndex: ["name2", "fullName"],
|
//dataIndex: ["name2", "fullName"],
|
||||||
key: "address",
|
key: "address",
|
||||||
render: (record, value) =>
|
render: (record, value) =>
|
||||||
`${record.address?.addressLine[0]}, ${record.address?.city} ${record.address?.stateOrProvince} ${record.address?.postalCode}`,
|
`${record?.address?.addressLine[0]}, ${record.address?.city} ${record.address?.stateOrProvince} ${record.address?.postalCode}`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (!visible) return <></>;
|
if (!visible) return <></>;
|
||||||
return (
|
return (
|
||||||
<Table
|
<Col span={24}>
|
||||||
title={() => (
|
<Table
|
||||||
<div>
|
title={() => (
|
||||||
<Button onClick={onOk}>Select</Button>
|
<div>
|
||||||
</div>
|
<Button onClick={onUseSelected} disabled={!selectedCustomer}>
|
||||||
)}
|
{t("jobs.actions.dms.useselected")}
|
||||||
pagination={{ position: "top" }}
|
</Button>
|
||||||
columns={columns}
|
<Button
|
||||||
rowKey={(record) => record.id.value}
|
onClick={onUseGeneric}
|
||||||
dataSource={customerList}
|
disabled={
|
||||||
//onChange={handleTableChange}
|
!(
|
||||||
rowSelection={{
|
bodyshop.cdk_configuration &&
|
||||||
onSelect: (props) => {
|
bodyshop.cdk_configuration.generic_customer_number
|
||||||
setSelectedCustomer(props.id.value);
|
)
|
||||||
},
|
}
|
||||||
type: "radio",
|
>
|
||||||
selectedRowKeys: [selectedCustomer],
|
{t("jobs.actions.dms.usegeneric")}
|
||||||
}}
|
</Button>
|
||||||
/>
|
<Button onClick={onCreateNew}>
|
||||||
|
{t("jobs.actions.dms.createnewcustomer")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
pagination={{ position: "top" }}
|
||||||
|
columns={columns}
|
||||||
|
rowKey={(record) => record.id.value}
|
||||||
|
dataSource={customerList}
|
||||||
|
//onChange={handleTableChange}
|
||||||
|
rowSelection={{
|
||||||
|
onSelect: (props) => {
|
||||||
|
setSelectedCustomer(props.id.value);
|
||||||
|
},
|
||||||
|
type: "radio",
|
||||||
|
selectedRowKeys: [selectedCustomer],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Divider, Space, Tag, Timeline } from "antd";
|
||||||
|
import moment from "moment";
|
||||||
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import {
|
||||||
|
setBreadcrumbs,
|
||||||
|
setSelectedHeader,
|
||||||
|
} from "../../redux/application/application.actions";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
||||||
|
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(DmsLogEvents);
|
||||||
|
|
||||||
|
export function DmsLogEvents({ socket, logs, bodyshop }) {
|
||||||
|
return (
|
||||||
|
<Timeline pending reverse={true}>
|
||||||
|
{logs.map((log, idx) => (
|
||||||
|
<Timeline.Item key={idx} color={LogLevelHierarchy(log.level)}>
|
||||||
|
<Space wrap align="start" style={{}}>
|
||||||
|
<Tag color={LogLevelHierarchy(log.level)}>{log.level}</Tag>
|
||||||
|
<span>{moment(log.timestamp).format("MM/DD/YYYY HH:MM:ss")}</span>
|
||||||
|
<Divider type="vertical" />
|
||||||
|
<span>{log.message}</span>
|
||||||
|
</Space>
|
||||||
|
</Timeline.Item>
|
||||||
|
))}
|
||||||
|
</Timeline>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function LogLevelHierarchy(level) {
|
||||||
|
switch (level) {
|
||||||
|
case "TRACE":
|
||||||
|
return "pink";
|
||||||
|
case "DEBUG":
|
||||||
|
return "orange";
|
||||||
|
case "INFO":
|
||||||
|
return "blue";
|
||||||
|
case "WARNING":
|
||||||
|
return "yellow";
|
||||||
|
case "ERROR":
|
||||||
|
return "red";
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
339
client/src/components/dms-post-form/dms-post-form.component.jsx
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
import { DeleteFilled } from "@ant-design/icons";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Divider,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Statistic,
|
||||||
|
Typography,
|
||||||
|
} from "antd";
|
||||||
|
import Dinero from "dinero.js";
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { determineDmsType } from "../../pages/dms/dms.container";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component";
|
||||||
|
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||||
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
|
});
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(DmsPostForm);
|
||||||
|
|
||||||
|
export function DmsPostForm({ bodyshop, socket, job }) {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handlePayerSelect = (value, index) => {
|
||||||
|
form.setFieldsValue({
|
||||||
|
payers: form.getFieldValue("payers").map((payer, mapIndex) => {
|
||||||
|
if (index !== mapIndex) return payer;
|
||||||
|
const cdkPayer =
|
||||||
|
bodyshop.cdk_configuration.payers &&
|
||||||
|
bodyshop.cdk_configuration.payers.find((i) => i.name === value);
|
||||||
|
|
||||||
|
if (!cdkPayer) return payer;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...cdkPayer,
|
||||||
|
dms_acctnumber: cdkPayer.dms_acctnumber,
|
||||||
|
controlnumber: job && job[cdkPayer.control_type],
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFinish = (values) => {
|
||||||
|
socket.emit(`${determineDmsType(bodyshop)}-export-job`, {
|
||||||
|
jobid: job.id,
|
||||||
|
txEnvelope: values,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title={t("jobs.labels.dms.postingform")}>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={handleFinish}
|
||||||
|
initialValues={{
|
||||||
|
story: t("jobs.labels.dms.defaultstory", {
|
||||||
|
ro_number: job.ro_number,
|
||||||
|
area_of_damage: job.area_of_damage && job.area_of_damage.impact1,
|
||||||
|
}).substr(0, 239),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LayoutFormRow grow>
|
||||||
|
<Form.Item
|
||||||
|
name="journal"
|
||||||
|
label={t("jobs.fields.dms.journal")}
|
||||||
|
initialValue={
|
||||||
|
bodyshop.cdk_configuration &&
|
||||||
|
bodyshop.cdk_configuration.default_journal
|
||||||
|
}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="kmin"
|
||||||
|
label={t("jobs.fields.kmin")}
|
||||||
|
initialValue={job && job.kmin}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber disabled />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="kmout"
|
||||||
|
label={t("jobs.fields.kmout")}
|
||||||
|
initialValue={job && job.kmout}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber disabled />
|
||||||
|
</Form.Item>
|
||||||
|
</LayoutFormRow>
|
||||||
|
|
||||||
|
<LayoutFormRow style={{ justifyContent: "center" }} grow>
|
||||||
|
<Form.Item
|
||||||
|
name="dms_make"
|
||||||
|
label={t("jobs.fields.dms.dms_make")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input disabled />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="dms_model"
|
||||||
|
label={t("jobs.fields.dms.dms_model")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input disabled />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<DmsCdkMakes form={form} socket={socket} job={job} />
|
||||||
|
<DmsCdkMakesRefetch />
|
||||||
|
</LayoutFormRow>
|
||||||
|
<Form.Item
|
||||||
|
name="story"
|
||||||
|
label={t("jobs.fields.dms.story")}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.TextArea maxLength={240} />
|
||||||
|
</Form.Item>
|
||||||
|
<Divider />
|
||||||
|
<Form.List name={["payers"]}>
|
||||||
|
{(fields, { add, remove }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{fields.map((field, index) => (
|
||||||
|
<Form.Item key={field.key}>
|
||||||
|
<Space wrap>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.dms.payer.name")}
|
||||||
|
key={`${index}name`}
|
||||||
|
name={[field.name, "name"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
style={{ minWidth: "15rem" }}
|
||||||
|
onSelect={(value) => handlePayerSelect(value, index)}
|
||||||
|
>
|
||||||
|
{bodyshop.cdk_configuration &&
|
||||||
|
bodyshop.cdk_configuration.payers &&
|
||||||
|
bodyshop.cdk_configuration.payers.map((payer) => (
|
||||||
|
<Select.Option key={payer.name}>
|
||||||
|
{payer.name}
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.dms.payer.dms_acctnumber")}
|
||||||
|
key={`${index}dms_acctnumber`}
|
||||||
|
name={[field.name, "dms_acctnumber"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input disabled />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.dms.payer.amount")}
|
||||||
|
key={`${index}amount`}
|
||||||
|
name={[field.name, "amount"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<CurrencyInput min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.dms.payer.controlnumber")}
|
||||||
|
key={`${index}controlnumber`}
|
||||||
|
name={[field.name, "controlnumber"]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item shouldUpdate>
|
||||||
|
{() => {
|
||||||
|
const payers = form.getFieldValue("payers");
|
||||||
|
|
||||||
|
const row = payers && payers[index];
|
||||||
|
|
||||||
|
const cdkPayer =
|
||||||
|
bodyshop.cdk_configuration.payers &&
|
||||||
|
bodyshop.cdk_configuration.payers.find(
|
||||||
|
(i) => i && row && i.name === row.name
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{cdkPayer &&
|
||||||
|
t(`jobs.fields.${cdkPayer.control_type}`)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<DeleteFilled
|
||||||
|
onClick={() => {
|
||||||
|
remove(field.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
))}
|
||||||
|
<Form.Item>
|
||||||
|
<Button
|
||||||
|
disabled={!(fields.length < 3)}
|
||||||
|
onClick={() => {
|
||||||
|
if (fields.length < 3) add();
|
||||||
|
}}
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
>
|
||||||
|
{t("jobs.actions.dms.addpayer")}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.List>
|
||||||
|
<Form.Item shouldUpdate>
|
||||||
|
{() => {
|
||||||
|
//Perform Calculation to determine discrepancy.
|
||||||
|
let totalAllocated = Dinero();
|
||||||
|
|
||||||
|
const payers = form.getFieldValue("payers");
|
||||||
|
payers &&
|
||||||
|
payers.forEach((payer) => {
|
||||||
|
totalAllocated = totalAllocated.add(
|
||||||
|
Dinero({ amount: Math.round((payer?.amount || 0) * 100) })
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const totals =
|
||||||
|
socket.allocationsSummary &&
|
||||||
|
socket.allocationsSummary.reduce(
|
||||||
|
(acc, val) => {
|
||||||
|
return {
|
||||||
|
totalSale: acc.totalSale.add(Dinero(val.sale)),
|
||||||
|
totalCost: acc.totalCost.add(Dinero(val.cost)),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{
|
||||||
|
totalSale: Dinero(),
|
||||||
|
totalCost: Dinero(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const discrep = totals
|
||||||
|
? totals.totalSale.subtract(totalAllocated)
|
||||||
|
: Dinero();
|
||||||
|
return (
|
||||||
|
<Space size="large" wrap align="center">
|
||||||
|
<Statistic
|
||||||
|
title={t("jobs.labels.subtotal")}
|
||||||
|
value={(totals ? totals.totalSale : Dinero()).toFormat()}
|
||||||
|
/>
|
||||||
|
<Typography.Title>-</Typography.Title>
|
||||||
|
<Statistic
|
||||||
|
title={t("jobs.labels.dms.totalallocated")}
|
||||||
|
value={totalAllocated.toFormat()}
|
||||||
|
/>
|
||||||
|
<Typography.Title>=</Typography.Title>
|
||||||
|
<Statistic
|
||||||
|
title={t("jobs.labels.dms.notallocated")}
|
||||||
|
valueStyle={{
|
||||||
|
color: discrep.getAmount() === 0 ? "green" : "red",
|
||||||
|
}}
|
||||||
|
value={discrep.toFormat()}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
disabled={
|
||||||
|
!socket.allocationsSummary || discrep.getAmount() !== 0
|
||||||
|
}
|
||||||
|
htmlType="submit"
|
||||||
|
>
|
||||||
|
{t("jobs.actions.dms.post")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -37,6 +37,8 @@ export default function EmailOverlayComponent({ form, selectedMediaState }) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Divider>{t("emails.labels.preview")}</Divider>
|
<Divider>{t("emails.labels.preview")}</Divider>
|
||||||
|
<strong>{t("emails.labels.pdfcopywillbeattached")}</strong>
|
||||||
|
|
||||||
<Form.Item shouldUpdate>
|
<Form.Item shouldUpdate>
|
||||||
{() => {
|
{() => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -43,11 +43,17 @@ export function EmailOverlayContainer({
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [sending, setSending] = useState(false);
|
const [sending, setSending] = useState(false);
|
||||||
const [rawHtml, setRawHtml] = useState("");
|
const [rawHtml, setRawHtml] = useState("");
|
||||||
|
const [pdfCopytoAttach, setPdfCopytoAttach] = useState({
|
||||||
|
filename: null,
|
||||||
|
pdf: null,
|
||||||
|
});
|
||||||
const [selectedMedia, setSelectedMedia] = useState([]);
|
const [selectedMedia, setSelectedMedia] = useState([]);
|
||||||
|
|
||||||
const defaultEmailFrom = {
|
const defaultEmailFrom = {
|
||||||
from: {
|
from: {
|
||||||
name: `${currentUser.displayName} @ ${bodyshop.shopname}`,
|
name: currentUser.displayName
|
||||||
|
? `${currentUser.displayName} @ ${bodyshop.shopname}`
|
||||||
|
: bodyshop.shopname,
|
||||||
address: EmailSettings.fromAddress,
|
address: EmailSettings.fromAddress,
|
||||||
},
|
},
|
||||||
ReplyTo: {
|
ReplyTo: {
|
||||||
@@ -59,17 +65,17 @@ export function EmailOverlayContainer({
|
|||||||
const handleFinish = async (values) => {
|
const handleFinish = async (values) => {
|
||||||
logImEXEvent("email_send_from_modal");
|
logImEXEvent("email_send_from_modal");
|
||||||
|
|
||||||
const attachments = [];
|
//const attachments = [];
|
||||||
|
|
||||||
if (values.fileList)
|
// if (values.fileList)
|
||||||
await asyncForEach(values.fileList, async (f) => {
|
// await asyncForEach(values.fileList, async (f) => {
|
||||||
const t = {
|
// const t = {
|
||||||
ContentType: f.type,
|
// ContentType: f.type,
|
||||||
Filename: f.name,
|
// Filename: f.name,
|
||||||
Base64Content: (await toBase64(f.originFileObj)).split(",")[1],
|
// Base64Content: (await toBase64(f.originFileObj)).split(",")[1],
|
||||||
};
|
// };
|
||||||
attachments.push(t);
|
// attachments.push(t);
|
||||||
});
|
// });
|
||||||
|
|
||||||
setSending(true);
|
setSending(true);
|
||||||
try {
|
try {
|
||||||
@@ -77,11 +83,28 @@ export function EmailOverlayContainer({
|
|||||||
...defaultEmailFrom,
|
...defaultEmailFrom,
|
||||||
...values,
|
...values,
|
||||||
html: rawHtml,
|
html: rawHtml,
|
||||||
attachments:
|
attachments: [
|
||||||
values.fileList &&
|
...(values.fileList
|
||||||
(await Promise.all(
|
? await Promise.all(
|
||||||
values.fileList.map(async (f) => await toBase64(f.originFileObj))
|
values.fileList.map(async (f) => {
|
||||||
)),
|
return {
|
||||||
|
filename: f.name,
|
||||||
|
path: await toBase64(f.originFileObj),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
)
|
||||||
|
: []),
|
||||||
|
...(pdfCopytoAttach.pdf
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
path: pdfCopytoAttach.pdf,
|
||||||
|
filename:
|
||||||
|
pdfCopytoAttach.filename &&
|
||||||
|
`${pdfCopytoAttach.filename}.pdf`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
media: selectedMedia.filter((m) => m.isSelected).map((m) => m.src),
|
media: selectedMedia.filter((m) => m.isSelected).map((m) => m.src),
|
||||||
//attachments,
|
//attachments,
|
||||||
});
|
});
|
||||||
@@ -99,13 +122,22 @@ export function EmailOverlayContainer({
|
|||||||
const render = async () => {
|
const render = async () => {
|
||||||
logImEXEvent("email_render_template", { template: emailConfig.template });
|
logImEXEvent("email_render_template", { template: emailConfig.template });
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
let html = await RenderTemplate(emailConfig.template, bodyshop, true);
|
let { html, pdf, filename } = await RenderTemplate(
|
||||||
|
emailConfig.template,
|
||||||
|
bodyshop,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
const response = await axios.post("/render/inlinecss", {
|
const response = await axios.post("/render/inlinecss", {
|
||||||
html: html,
|
html: html,
|
||||||
url: `${window.location.protocol}://${window.location.host}/`,
|
url: `${window.location.protocol}://${window.location.host}/`,
|
||||||
});
|
});
|
||||||
setRawHtml(response.data);
|
setRawHtml(response.data);
|
||||||
|
|
||||||
|
if (pdf) {
|
||||||
|
setPdfCopytoAttach({ pdf, filename });
|
||||||
|
}
|
||||||
|
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
...emailConfig.messageOptions,
|
...emailConfig.messageOptions,
|
||||||
cc:
|
cc:
|
||||||
@@ -166,8 +198,8 @@ const toBase64 = (file) =>
|
|||||||
reader.onerror = (error) => reject(error);
|
reader.onerror = (error) => reject(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
const asyncForEach = async (array, callback) => {
|
// const asyncForEach = async (array, callback) => {
|
||||||
for (let index = 0; index < array.length; index++) {
|
// for (let index = 0; index < array.length; index++) {
|
||||||
await callback(array[index], index, array);
|
// await callback(array[index], index, array);
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
selectBodyshop,
|
selectBodyshop,
|
||||||
selectCurrentUser,
|
selectCurrentUser,
|
||||||
} from "../../redux/user/user.selectors";
|
} from "../../redux/user/user.selectors";
|
||||||
|
import { tracker } from "../../App/App.container";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
@@ -36,6 +37,7 @@ class ErrorBoundary extends React.Component {
|
|||||||
componentDidCatch(error, info) {
|
componentDidCatch(error, info) {
|
||||||
console.log("Exception Caught by Error Boundary.", error, info);
|
console.log("Exception Caught by Error Boundary.", error, info);
|
||||||
this.setState({ ...this.state, error, info });
|
this.setState({ ...this.state, error, info });
|
||||||
|
tracker.event("error_boundary", error, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleErrorSubmit = () => {
|
handleErrorSubmit = () => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { withApollo } from "@apollo/client/react/hoc";
|
|||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { logImEXEvent, messaging } from "../../firebase/firebase.utils";
|
//import { logImEXEvent, messaging } from "../../firebase/firebase.utils";
|
||||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
@@ -15,21 +15,20 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
class FcmNotificationComponent extends Component {
|
class FcmNotificationComponent extends Component {
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
//const { client, currentUser } = this.props;
|
//const { client, currentUser } = this.props;
|
||||||
if (!!!messaging) return; //Skip all of the notification functionality if the firebase SDK could not start.
|
// if (!!!messaging) return; //Skip all of the notification functionality if the firebase SDK could not start.
|
||||||
|
// messaging
|
||||||
messaging
|
// .requestPermission()
|
||||||
.requestPermission()
|
// .then(async function () {
|
||||||
.then(async function () {
|
// // const token = await messaging.getToken();
|
||||||
// const token = await messaging.getToken();
|
// // client.mutate({
|
||||||
// client.mutate({
|
// // mutation: UPDATE_FCM_TOKEN,
|
||||||
// mutation: UPDATE_FCM_TOKEN,
|
// // variables: { authEmail: currentUser.email, token: { [token]: true } },
|
||||||
// variables: { authEmail: currentUser.email, token: { [token]: true } },
|
// // });
|
||||||
// });
|
// })
|
||||||
})
|
// .catch(function (err) {
|
||||||
.catch(function (err) {
|
// console.log("Unable to get permission to notify.", err);
|
||||||
console.log("Unable to get permission to notify.", err);
|
// logImEXEvent("fcm_permission_denied", { message: err });
|
||||||
logImEXEvent("fcm_permission_denied", { message: err });
|
// });
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export const PhoneItemFormatterValidation = (getFieldValue, name) => ({
|
|||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
} else {
|
} else {
|
||||||
const p = parsePhoneNumber(value, "CA");
|
const p = parsePhoneNumber(value, "CA");
|
||||||
if (p.isValid()) {
|
if (p && p.isValid()) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
} else {
|
} else {
|
||||||
return Promise.reject(i18n.t("general.validation.invalidphone"));
|
return Promise.reject(i18n.t("general.validation.invalidphone"));
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export default function GlobalSearch() {
|
|||||||
<span>{`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${
|
<span>{`${job.v_model_yr || ""} ${job.v_make_desc || ""} ${
|
||||||
job.v_model_desc || ""
|
job.v_model_desc || ""
|
||||||
}`}</span>
|
}`}</span>
|
||||||
<span>{`${job.clm_no}`}</span>
|
<span>{`${job.clm_no || ""}`}</span>
|
||||||
</Space>
|
</Space>
|
||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
@@ -62,13 +62,16 @@ export default function GlobalSearch() {
|
|||||||
}`,
|
}`,
|
||||||
label: (
|
label: (
|
||||||
<Link to={`/manage/owners/${owner.id}`}>
|
<Link to={`/manage/owners/${owner.id}`}>
|
||||||
<Space size="small" split={<Divider type="vertical" />}>
|
<Space size="small" split={<Divider type="vertical" />} wrap>
|
||||||
<span>{`${owner.ownr_fn || ""} ${owner.ownr_ln || ""} ${
|
<span>{`${owner.ownr_fn || ""} ${owner.ownr_ln || ""} ${
|
||||||
owner.ownr_co_nm || ""
|
owner.ownr_co_nm || ""
|
||||||
}`}</span>
|
}`}</span>
|
||||||
<PhoneNumberFormatter>
|
<PhoneNumberFormatter>
|
||||||
{owner.ownr_ph1}
|
{owner.ownr_ph1}
|
||||||
</PhoneNumberFormatter>
|
</PhoneNumberFormatter>
|
||||||
|
<PhoneNumberFormatter>
|
||||||
|
{owner.ownr_ph2}
|
||||||
|
</PhoneNumberFormatter>
|
||||||
</Space>
|
</Space>
|
||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
@@ -91,8 +94,8 @@ export default function GlobalSearch() {
|
|||||||
vehicle.v_make_desc || ""
|
vehicle.v_make_desc || ""
|
||||||
} ${vehicle.v_model_desc || ""}`}
|
} ${vehicle.v_model_desc || ""}`}
|
||||||
</span>
|
</span>
|
||||||
<span>{vehicle.plate_no}</span>
|
<span>{vehicle.plate_no || ""}</span>
|
||||||
<span> {vehicle.v_vin}</span>
|
<span> {vehicle.v_vin || ""}</span>
|
||||||
</Space>
|
</Space>
|
||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
@@ -108,10 +111,11 @@ export default function GlobalSearch() {
|
|||||||
label: (
|
label: (
|
||||||
<Link to={`/manage/jobs/${payment.job.id}`}>
|
<Link to={`/manage/jobs/${payment.job.id}`}>
|
||||||
<Space size="small" split={<Divider type="vertical" />}>
|
<Space size="small" split={<Divider type="vertical" />}>
|
||||||
|
<span>{payment.paymentnum}</span>
|
||||||
<span>{payment.job.ro_number}</span>
|
<span>{payment.job.ro_number}</span>
|
||||||
<span>{payment.job.memo}</span>
|
<span>{payment.memo || ""}</span>
|
||||||
<span>{payment.job.amount}</span>
|
<span>{payment.amount || ""}</span>
|
||||||
<span>{payment.job.transactionid}</span>
|
<span>{payment.transactionid || ""}</span>
|
||||||
</Space>
|
</Space>
|
||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
@@ -167,7 +171,6 @@ export default function GlobalSearch() {
|
|||||||
<AutoComplete
|
<AutoComplete
|
||||||
options={options}
|
options={options}
|
||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
allowClear
|
|
||||||
placeholder={t("general.labels.globalsearch")}
|
placeholder={t("general.labels.globalsearch")}
|
||||||
>
|
>
|
||||||
<Input.Search loading={loading} />
|
<Input.Search loading={loading} />
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ export function Jobd3RdPartyModal({ bodyshop, jobId }) {
|
|||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("printcenter.jobs.3rdpartyfields.ponumber")}
|
label={t("printcenter.jobs.3rdpartyfields.refnumber")}
|
||||||
name="ponumber"
|
name="ponumber"
|
||||||
>
|
>
|
||||||
<Input />
|
<Input />
|
||||||
|
|||||||
@@ -1,24 +1,50 @@
|
|||||||
import { Button, Popover, Space } from "antd";
|
import { AlertFilled } from "@ant-design/icons";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
Dropdown,
|
||||||
|
Menu,
|
||||||
|
notification,
|
||||||
|
Popover,
|
||||||
|
Space,
|
||||||
|
} from "antd";
|
||||||
|
import parsePhoneNumber from "libphonenumber-js";
|
||||||
|
import moment from "moment";
|
||||||
|
import queryString from "query-string";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Link, useHistory, useLocation } from "react-router-dom";
|
import { Link, useHistory, useLocation } from "react-router-dom";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import {
|
||||||
|
openChatByPhone,
|
||||||
|
setMessage,
|
||||||
|
} from "../../redux/messaging/messaging.actions";
|
||||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
import PhoneFormatter from "../../utils/PhoneFormatter";
|
|
||||||
import { GenerateDocument } from "../../utils/RenderTemplate";
|
import { GenerateDocument } from "../../utils/RenderTemplate";
|
||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
|
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
|
||||||
import DataLabel from "../data-label/data-label.component";
|
import DataLabel from "../data-label/data-label.component";
|
||||||
import ScheduleAtChange from "./job-at-change.component";
|
import ScheduleAtChange from "./job-at-change.component";
|
||||||
import ScheduleEventColor from "./schedule-event.color.component";
|
import ScheduleEventColor from "./schedule-event.color.component";
|
||||||
import queryString from "query-string";
|
import ScheduleEventNote from "./schedule-event.note.component";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setScheduleContext: (context) =>
|
setScheduleContext: (context) =>
|
||||||
dispatch(setModalContext({ context: context, modal: "schedule" })),
|
dispatch(setModalContext({ context: context, modal: "schedule" })),
|
||||||
|
openChatByPhone: (phone) => dispatch(openChatByPhone(phone)),
|
||||||
|
setMessage: (text) => dispatch(setMessage(text)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function ScheduleEventComponent({
|
export function ScheduleEventComponent({
|
||||||
|
bodyshop,
|
||||||
|
setMessage,
|
||||||
|
openChatByPhone,
|
||||||
event,
|
event,
|
||||||
refetch,
|
refetch,
|
||||||
handleCancel,
|
handleCancel,
|
||||||
@@ -38,7 +64,7 @@ export function ScheduleEventComponent({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const popoverContent = (
|
const popoverContent = (
|
||||||
<div>
|
<div style={{ maxWidth: "40vw" }}>
|
||||||
{!event.isintake ? (
|
{!event.isintake ? (
|
||||||
<strong>{event.title}</strong>
|
<strong>{event.title}</strong>
|
||||||
) : (
|
) : (
|
||||||
@@ -75,17 +101,25 @@ export function ScheduleEventComponent({
|
|||||||
{(event.job && event.job.ownr_ea) || ""}
|
{(event.job && event.job.ownr_ea) || ""}
|
||||||
</DataLabel>
|
</DataLabel>
|
||||||
<DataLabel label={t("jobs.fields.ownr_ph1")}>
|
<DataLabel label={t("jobs.fields.ownr_ph1")}>
|
||||||
<PhoneFormatter>
|
<ChatOpenButton
|
||||||
{(event.job && event.job.ownr_ph1) || ""}
|
phone={event.job && event.job.ownr_ph1}
|
||||||
</PhoneFormatter>
|
jobid={event.job.id}
|
||||||
|
/>
|
||||||
|
</DataLabel>
|
||||||
|
<DataLabel label={t("jobs.fields.ownr_ph2")}>
|
||||||
|
<ChatOpenButton
|
||||||
|
phone={event.job && event.job.ownr_ph2}
|
||||||
|
jobid={event.job.id}
|
||||||
|
/>
|
||||||
</DataLabel>
|
</DataLabel>
|
||||||
<DataLabel label={t("jobs.fields.alt_transport")}>
|
<DataLabel label={t("jobs.fields.alt_transport")}>
|
||||||
{(event.job && event.job.alt_transport) || ""}
|
{(event.job && event.job.alt_transport) || ""}
|
||||||
<ScheduleAtChange job={event && event.job} />
|
<ScheduleAtChange job={event && event.job} />
|
||||||
</DataLabel>
|
</DataLabel>
|
||||||
|
<ScheduleEventNote event={event} />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
<Divider />
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
{event.job ? (
|
{event.job ? (
|
||||||
<Link to={`/manage/jobs/${event.job && event.job.id}`}>
|
<Link to={`/manage/jobs/${event.job && event.job.id}`}>
|
||||||
@@ -106,23 +140,62 @@ export function ScheduleEventComponent({
|
|||||||
{t("appointments.actions.preview")}
|
{t("appointments.actions.preview")}
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
<Button
|
|
||||||
onClick={() => {
|
<Dropdown
|
||||||
const Template = TemplateList("job").appointment_reminder;
|
overlay={
|
||||||
GenerateDocument(
|
<Menu>
|
||||||
{
|
<Menu.Item
|
||||||
name: Template.key,
|
onClick={() => {
|
||||||
variables: { id: event.job.id },
|
const Template = TemplateList("job").appointment_reminder;
|
||||||
},
|
GenerateDocument(
|
||||||
{ to: event.job && event.job.ownr_ea, subject: Template.subject },
|
{
|
||||||
"e",
|
name: Template.key,
|
||||||
event.job && event.job.id
|
variables: { id: event.job.id },
|
||||||
);
|
},
|
||||||
}}
|
{
|
||||||
disabled={event.arrived}
|
to: event.job && event.job.ownr_ea,
|
||||||
|
subject: Template.subject,
|
||||||
|
},
|
||||||
|
"e",
|
||||||
|
event.job && event.job.id
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
disabled={event.arrived}
|
||||||
|
>
|
||||||
|
{t("general.labels.email")}
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
onClick={() => {
|
||||||
|
const p = parsePhoneNumber(event.job.ownr_ph1, "CA");
|
||||||
|
if (p && p.isValid()) {
|
||||||
|
openChatByPhone({
|
||||||
|
phone_num: p.formatInternational(),
|
||||||
|
jobid: event.job.id,
|
||||||
|
});
|
||||||
|
setMessage(
|
||||||
|
t("appointments.labels.reminder", {
|
||||||
|
shopname: bodyshop.shopname,
|
||||||
|
date: moment(event.start).format("MM/DD/YYYY"),
|
||||||
|
time: moment(event.start).format("HH:MM a"),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setVisible(false);
|
||||||
|
} else {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("messaging.error.invalidphone"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={event.arrived || !bodyshop.messagingservicesid}
|
||||||
|
>
|
||||||
|
{t("general.labels.sms")}
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{t("appointments.actions.sendreminder")}
|
<Button>{t("appointments.actions.sendreminder")}</Button>
|
||||||
</Button>
|
</Dropdown>
|
||||||
|
|
||||||
<Button onClick={() => handleCancel(event.id)} disabled={event.arrived}>
|
<Button onClick={() => handleCancel(event.id)} disabled={event.arrived}>
|
||||||
{t("appointments.actions.cancel")}
|
{t("appointments.actions.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -136,6 +209,9 @@ export function ScheduleEventComponent({
|
|||||||
jobId: event.job.id,
|
jobId: event.job.id,
|
||||||
job: event.job,
|
job: event.job,
|
||||||
previousEvent: event.id,
|
previousEvent: event.id,
|
||||||
|
color: event.color,
|
||||||
|
alt_transport: event.job && event.job.alt_transport,
|
||||||
|
note: event.note,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -161,6 +237,7 @@ export function ScheduleEventComponent({
|
|||||||
const RegularEvent = event.isintake ? (
|
const RegularEvent = event.isintake ? (
|
||||||
<div style={{ display: "flex", flexWrap: "wrap" }}>
|
<div style={{ display: "flex", flexWrap: "wrap" }}>
|
||||||
<Space>
|
<Space>
|
||||||
|
{event.note && <AlertFilled className="production-alert" />}
|
||||||
<strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
|
<strong>{`${event.job.ro_number || t("general.labels.na")}`}</strong>
|
||||||
<span>{`${(event.job && event.job.ownr_fn) || ""} ${
|
<span>{`${(event.job && event.job.ownr_fn) || ""} ${
|
||||||
(event.job && event.job.ownr_ln) || ""
|
(event.job && event.job.ownr_ln) || ""
|
||||||
@@ -202,4 +279,7 @@ export function ScheduleEventComponent({
|
|||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default connect(null, mapDispatchToProps)(ScheduleEventComponent);
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(ScheduleEventComponent);
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { EditFilled, SaveFilled } from "@ant-design/icons";
|
||||||
|
import { useMutation } from "@apollo/client";
|
||||||
|
import { Button, Input, notification, Space } from "antd";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { UPDATE_APPOINTMENT } from "../../graphql/appointments.queries";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import DataLabel from "../data-label/data-label.component";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
|
});
|
||||||
|
|
||||||
|
export function ScheduleEventNote({ event }) {
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
|
const [note, setNote] = useState(event.note || "");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [updateAppointment] = useMutation(UPDATE_APPOINTMENT);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const toggleEdit = async () => {
|
||||||
|
if (editing) {
|
||||||
|
//Await the update
|
||||||
|
setLoading(true);
|
||||||
|
const result = await updateAppointment({
|
||||||
|
variables: {
|
||||||
|
appid: event.id,
|
||||||
|
app: { note },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!!!result.errors) {
|
||||||
|
// notification["success"]({ message: t("appointments.successes.saved") });
|
||||||
|
} else {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("jobs.errors.saving", {
|
||||||
|
error: JSON.stringify(result.errors),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setEditing(false);
|
||||||
|
} else {
|
||||||
|
setEditing(true);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataLabel label={t("appointments.fields.note")}>
|
||||||
|
<Space flex>
|
||||||
|
{!editing ? (
|
||||||
|
event.note || ""
|
||||||
|
) : (
|
||||||
|
<Input.TextArea
|
||||||
|
rows={3}
|
||||||
|
value={note}
|
||||||
|
onChange={(e) => setNote(e.target.value)}
|
||||||
|
style={{ maxWidth: "8vw" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button onClick={toggleEdit} loading={loading}>
|
||||||
|
{editing ? <SaveFilled /> : <EditFilled />}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</DataLabel>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ScheduleEventNote);
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { useQuery } from "@apollo/client";
|
||||||
|
import { Card, Table } from "antd";
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { QUERY_AUDIT_TRAIL } from "../../graphql/audit_trail.queries";
|
||||||
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
|
|
||||||
|
export default function JobAuditTrail({ jobId }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { loading, data } = useQuery(QUERY_AUDIT_TRAIL, {
|
||||||
|
variables: { jobid: jobId },
|
||||||
|
skip: !jobId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("audit.fields.created"),
|
||||||
|
dataIndex: "created",
|
||||||
|
key: "created",
|
||||||
|
render: (text, record) => (
|
||||||
|
<DateTimeFormatter>{record.created}</DateTimeFormatter>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("audit.fields.useremail"),
|
||||||
|
dataIndex: "useremail",
|
||||||
|
key: "useremail",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("audit.fields.operation"),
|
||||||
|
dataIndex: "operation",
|
||||||
|
key: "operation",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card title={t("jobs.labels.audit")}>
|
||||||
|
<Table
|
||||||
|
loading={loading}
|
||||||
|
columns={columns}
|
||||||
|
rowKey="id"
|
||||||
|
dataSource={data ? data.audit_trail : []}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -16,16 +16,21 @@ import {
|
|||||||
import ConfigFormComponents from "../../../config-form-components/config-form-components.component";
|
import ConfigFormComponents from "../../../config-form-components/config-form-components.component";
|
||||||
import DateTimePicker from "../../../form-date-time-picker/form-date-time-picker.component";
|
import DateTimePicker from "../../../form-date-time-picker/form-date-time-picker.component";
|
||||||
import moment from "moment-business-days";
|
import moment from "moment-business-days";
|
||||||
|
import { insertAuditTrail } from "../../../../redux/application/application.actions";
|
||||||
|
import AuditTrailMapping from "../../../../utils/AuditTrailMappings";
|
||||||
|
import { UPDATE_OWNER } from "../../../../graphql/owners.queries";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
insertAuditTrail: ({ jobid, operation }) =>
|
||||||
|
dispatch(insertAuditTrail({ jobid, operation })),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function JobChecklistForm({
|
export function JobChecklistForm({
|
||||||
|
insertAuditTrail,
|
||||||
formItems,
|
formItems,
|
||||||
bodyshop,
|
bodyshop,
|
||||||
currentUser,
|
currentUser,
|
||||||
@@ -37,6 +42,8 @@ export function JobChecklistForm({
|
|||||||
const [intakeJob] = useMutation(UPDATE_JOB);
|
const [intakeJob] = useMutation(UPDATE_JOB);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [markAptArrived] = useMutation(MARK_LATEST_APPOINTMENT_AS_ARRIVED);
|
const [markAptArrived] = useMutation(MARK_LATEST_APPOINTMENT_AS_ARRIVED);
|
||||||
|
const [updateOwner] = useMutation(UPDATE_OWNER);
|
||||||
|
|
||||||
const { jobId } = useParams();
|
const { jobId } = useParams();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const search = queryString.parse(useLocation().search);
|
const search = queryString.parse(useLocation().search);
|
||||||
@@ -57,8 +64,16 @@ export function JobChecklistForm({
|
|||||||
...(type === "intake" && { actual_in: new Date() }),
|
...(type === "intake" && { actual_in: new Date() }),
|
||||||
...(type === "intake" && {
|
...(type === "intake" && {
|
||||||
production_vars: {
|
production_vars: {
|
||||||
...job.production_vars,
|
...(job ? job.production_vars : {}),
|
||||||
...values.production_vars,
|
|
||||||
|
note:
|
||||||
|
values.production_vars &&
|
||||||
|
values.production_vars.note &&
|
||||||
|
values.production_vars.note !== ""
|
||||||
|
? values &&
|
||||||
|
values.production_vars &&
|
||||||
|
values.production_vars.note
|
||||||
|
: job && job.production_vars && job.production_vars.note,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
...(type === "intake" && {
|
...(type === "intake" && {
|
||||||
@@ -104,11 +119,40 @@ export function JobChecklistForm({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (type === "intake" && job.owner && job.owner.id) {
|
||||||
|
//Updae Owner Allow to Text
|
||||||
|
const updateOwnerResult = await updateOwner({
|
||||||
|
variables: {
|
||||||
|
ownerId: job.owner.id,
|
||||||
|
owner: { allow_text_message: values.allow_text_message },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!!updateOwnerResult.errors) {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("checklist.errors.complete", {
|
||||||
|
error: JSON.stringify(result.errors),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
if (!!!result.errors) {
|
if (!!!result.errors) {
|
||||||
notification["success"]({ message: t("checklist.successes.completed") });
|
notification["success"]({ message: t("checklist.successes.completed") });
|
||||||
history.push(`/manage/jobs/${jobId}`);
|
history.push(`/manage/jobs/${jobId}`);
|
||||||
|
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: jobId,
|
||||||
|
operation: AuditTrailMapping.jobchecklist(
|
||||||
|
type,
|
||||||
|
(type === "deliver" && values.removeFromProduction && false) ||
|
||||||
|
(type === "intake" && values.addToProduction),
|
||||||
|
(type === "intake" && bodyshop.md_ro_statuses.default_arrived) ||
|
||||||
|
(type === "deliver" && bodyshop.md_ro_statuses.default_delivered)
|
||||||
|
),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
message: t("checklist.errors.complete", {
|
message: t("checklist.errors.complete", {
|
||||||
@@ -123,7 +167,11 @@ export function JobChecklistForm({
|
|||||||
title={t("checklist.labels.checklist")}
|
title={t("checklist.labels.checklist")}
|
||||||
extra={
|
extra={
|
||||||
!readOnly && (
|
!readOnly && (
|
||||||
<Button loading={loading} onClick={() => form.submit()}>
|
<Button
|
||||||
|
loading={loading}
|
||||||
|
type="primary"
|
||||||
|
onClick={() => form.submit()}
|
||||||
|
>
|
||||||
{t("general.actions.submit")}
|
{t("general.actions.submit")}
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
@@ -135,14 +183,17 @@ export function JobChecklistForm({
|
|||||||
initialValues={{
|
initialValues={{
|
||||||
...(type === "intake" && {
|
...(type === "intake" && {
|
||||||
addToProduction: true,
|
addToProduction: true,
|
||||||
|
allow_text_message: job.owner && job.owner.allow_text_message,
|
||||||
scheduled_completion:
|
scheduled_completion:
|
||||||
(job && job.scheduled_completion) ||
|
(job && job.scheduled_completion) ||
|
||||||
moment().businessAdd(
|
(job.labbrs && job.larhrs
|
||||||
(job.labhrs.aggregate.sum.mod_lb_hrs +
|
? moment().businessAdd(
|
||||||
job.larhrs.aggregate.sum.mod_lb_hrs) /
|
(job.labhrs.aggregate.sum.mod_lb_hrs +
|
||||||
bodyshop.target_touchtime,
|
job.larhrs.aggregate.sum.mod_lb_hrs) /
|
||||||
"days"
|
bodyshop.target_touchtime,
|
||||||
),
|
"days"
|
||||||
|
)
|
||||||
|
: null),
|
||||||
scheduled_delivery: job && job.scheduled_delivery,
|
scheduled_delivery: job && job.scheduled_delivery,
|
||||||
}),
|
}),
|
||||||
...(type === "deliver" && {
|
...(type === "deliver" && {
|
||||||
@@ -170,6 +221,14 @@ export function JobChecklistForm({
|
|||||||
>
|
>
|
||||||
<Switch disabled={readOnly} />
|
<Switch disabled={readOnly} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="allow_text_message"
|
||||||
|
valuePropName="checked"
|
||||||
|
label={t("checklist.labels.allow_text_message")}
|
||||||
|
disabled={readOnly}
|
||||||
|
>
|
||||||
|
<Switch disabled={readOnly} />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="scheduled_completion"
|
name="scheduled_completion"
|
||||||
label={t("jobs.fields.scheduled_completion")}
|
label={t("jobs.fields.scheduled_completion")}
|
||||||
@@ -194,6 +253,7 @@ export function JobChecklistForm({
|
|||||||
name={["production_vars", "note"]}
|
name={["production_vars", "note"]}
|
||||||
label={t("jobs.fields.production_vars.note")}
|
label={t("jobs.fields.production_vars.note")}
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
|
trigger="onChange"
|
||||||
>
|
>
|
||||||
<Input.TextArea rows={3} disabled={readOnly} />
|
<Input.TextArea rows={3} disabled={readOnly} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -112,60 +112,46 @@ export function JobDetailCards({ setPrintCenterContext }) {
|
|||||||
<Divider type="horizontal" />
|
<Divider type="horizontal" />
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col {...span}>
|
<Col {...span}>
|
||||||
<Card.Grid style={{ width: "100%", height: "100%" }}>
|
<JobDetailCardsInsuranceComponent
|
||||||
<JobDetailCardsInsuranceComponent
|
loading={loading}
|
||||||
loading={loading}
|
data={data ? data.jobs_by_pk : null}
|
||||||
data={data ? data.jobs_by_pk : null}
|
/>
|
||||||
/>
|
|
||||||
</Card.Grid>
|
|
||||||
</Col>
|
</Col>
|
||||||
<Col {...span}>
|
<Col {...span}>
|
||||||
<Card.Grid style={{ width: "100%", height: "100%" }}>
|
<JobDetailCardsTotalsComponent
|
||||||
<JobDetailCardsTotalsComponent
|
loading={loading}
|
||||||
loading={loading}
|
data={data ? data.jobs_by_pk : null}
|
||||||
data={data ? data.jobs_by_pk : null}
|
/>
|
||||||
/>
|
|
||||||
</Card.Grid>
|
|
||||||
</Col>
|
</Col>
|
||||||
<Col {...span}>
|
<Col {...span}>
|
||||||
<Card.Grid style={{ width: "100%", height: "100%" }}>
|
<JobDetailCardsDatesComponent
|
||||||
<JobDetailCardsDatesComponent
|
loading={loading}
|
||||||
loading={loading}
|
data={data ? data.jobs_by_pk : null}
|
||||||
data={data ? data.jobs_by_pk : null}
|
/>
|
||||||
/>
|
|
||||||
</Card.Grid>
|
|
||||||
</Col>
|
</Col>
|
||||||
<Col {...span}>
|
<Col {...span}>
|
||||||
<Card.Grid style={{ width: "100%", height: "100%" }}>
|
<JobDetailCardsPartsComponent
|
||||||
<JobDetailCardsPartsComponent
|
loading={loading}
|
||||||
loading={loading}
|
data={data ? data.jobs_by_pk : null}
|
||||||
data={data ? data.jobs_by_pk : null}
|
/>
|
||||||
/>
|
|
||||||
</Card.Grid>
|
|
||||||
</Col>
|
</Col>
|
||||||
<Col {...span}>
|
<Col {...span}>
|
||||||
<Card.Grid style={{ width: "100%", height: "100%" }}>
|
<JobDetailCardsNotesComponent
|
||||||
<JobDetailCardsNotesComponent
|
loading={loading}
|
||||||
loading={loading}
|
data={data ? data.jobs_by_pk : null}
|
||||||
data={data ? data.jobs_by_pk : null}
|
/>
|
||||||
/>
|
|
||||||
</Card.Grid>
|
|
||||||
</Col>
|
</Col>
|
||||||
<Col {...span}>
|
<Col {...span}>
|
||||||
<Card.Grid style={{ width: "100%", height: "100%" }}>
|
<JobDetailCardsDocumentsComponent
|
||||||
<JobDetailCardsDocumentsComponent
|
loading={loading}
|
||||||
loading={loading}
|
data={data ? data.jobs_by_pk : null}
|
||||||
data={data ? data.jobs_by_pk : null}
|
/>
|
||||||
/>
|
|
||||||
</Card.Grid>
|
|
||||||
</Col>
|
</Col>
|
||||||
<Col {...span}>
|
<Col {...span}>
|
||||||
<Card.Grid style={{ width: "100%", height: "100%" }}>
|
<JobDetailCardsDamageComponent
|
||||||
<JobDetailCardsDamageComponent
|
loading={loading}
|
||||||
loading={loading}
|
data={data ? data.jobs_by_pk : null}
|
||||||
data={data ? data.jobs_by_pk : null}
|
/>
|
||||||
/>
|
|
||||||
</Card.Grid>
|
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Timeline } from "antd";
|
import { Timeline } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { DateFormatter } from "../../utils/DateFormatter";
|
import { DateTimeFormatter } from "../../utils/DateFormatter";
|
||||||
import CardTemplate from "./job-detail-cards.template.component";
|
import CardTemplate from "./job-detail-cards.template.component";
|
||||||
|
|
||||||
export default function JobDetailCardsDatesComponent({ loading, data }) {
|
export default function JobDetailCardsDatesComponent({ loading, data }) {
|
||||||
@@ -26,80 +26,86 @@ export default function JobDetailCardsDatesComponent({ loading, data }) {
|
|||||||
) ? (
|
) ? (
|
||||||
<div>{t("jobs.errors.nodates")}</div>
|
<div>{t("jobs.errors.nodates")}</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{data.date_last_contacted ? (
|
||||||
|
<Timeline.Item>
|
||||||
|
<label>{t("jobs.fields.date_last_contacted")}: </label>
|
||||||
|
<DateTimeFormatter>{data.date_last_contacted}</DateTimeFormatter>
|
||||||
|
</Timeline.Item>
|
||||||
|
) : null}
|
||||||
{data.date_open ? (
|
{data.date_open ? (
|
||||||
<Timeline.Item>
|
<Timeline.Item>
|
||||||
<label>{t("jobs.fields.date_open")}: </label>
|
<label>{t("jobs.fields.date_open")}: </label>
|
||||||
<DateFormatter>{data.date_open}</DateFormatter>
|
<DateTimeFormatter>{data.date_open}</DateTimeFormatter>
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{data.date_estimated ? (
|
{data.date_estimated ? (
|
||||||
<Timeline.Item>
|
<Timeline.Item>
|
||||||
<label>{t("jobs.fields.date_estimated")}: </label>
|
<label>{t("jobs.fields.date_estimated")}: </label>
|
||||||
<DateFormatter>{data.date_estimated}</DateFormatter>
|
<DateTimeFormatter>{data.date_estimated}</DateTimeFormatter>
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{data.date_scheduled ? (
|
{data.date_scheduled ? (
|
||||||
<Timeline.Item>
|
<Timeline.Item>
|
||||||
<label>{t("jobs.fields.date_scheduled")}: </label>
|
<label>{t("jobs.fields.date_scheduled")}: </label>
|
||||||
<DateFormatter>{data.date_scheduled}</DateFormatter>
|
<DateTimeFormatter>{data.date_scheduled}</DateTimeFormatter>
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{data.scheduled_in ? (
|
{data.scheduled_in ? (
|
||||||
<Timeline.Item>
|
<Timeline.Item>
|
||||||
<label>{t("jobs.fields.scheduled_in")}: </label>
|
<label>{t("jobs.fields.scheduled_in")}: </label>
|
||||||
<DateFormatter>{data.scheduled_in}</DateFormatter>
|
<DateTimeFormatter>{data.scheduled_in}</DateTimeFormatter>
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{data.actual_in ? (
|
{data.actual_in ? (
|
||||||
<Timeline.Item>
|
<Timeline.Item>
|
||||||
<label>{t("jobs.fields.actual_in")}: </label>
|
<label>{t("jobs.fields.actual_in")}: </label>
|
||||||
<DateFormatter>{data.actual_in}</DateFormatter>
|
<DateTimeFormatter>{data.actual_in}</DateTimeFormatter>
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{data.scheduled_completion ? (
|
{data.scheduled_completion ? (
|
||||||
<Timeline.Item>
|
<Timeline.Item>
|
||||||
<label>{t("jobs.fields.scheduled_completion")}: </label>
|
<label>{t("jobs.fields.scheduled_completion")}: </label>
|
||||||
<DateFormatter>{data.scheduled_completion}</DateFormatter>
|
<DateTimeFormatter>{data.scheduled_completion}</DateTimeFormatter>
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{data.actual_completion ? (
|
{data.actual_completion ? (
|
||||||
<Timeline.Item>
|
<Timeline.Item>
|
||||||
<label>{t("jobs.fields.actual_completion")}: </label>
|
<label>{t("jobs.fields.actual_completion")}: </label>
|
||||||
<DateFormatter>{data.actual_completion}</DateFormatter>
|
<DateTimeFormatter>{data.actual_completion}</DateTimeFormatter>
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{data.scheduled_delivery ? (
|
{data.scheduled_delivery ? (
|
||||||
<Timeline.Item>
|
<Timeline.Item>
|
||||||
<label>{t("jobs.fields.scheduled_delivery")}: </label>
|
<label>{t("jobs.fields.scheduled_delivery")}: </label>
|
||||||
<DateFormatter>{data.scheduled_delivery}</DateFormatter>
|
<DateTimeFormatter>{data.scheduled_delivery}</DateTimeFormatter>
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{data.actual_delivery ? (
|
{data.actual_delivery ? (
|
||||||
<Timeline.Item>
|
<Timeline.Item>
|
||||||
<label>{t("jobs.fields.actual_delivery")}: </label>
|
<label>{t("jobs.fields.actual_delivery")}: </label>
|
||||||
<DateFormatter>{data.actual_delivery}</DateFormatter>
|
<DateTimeFormatter>{data.actual_delivery}</DateTimeFormatter>
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{data.date_invoiced ? (
|
{data.date_invoiced ? (
|
||||||
<Timeline.Item>
|
<Timeline.Item>
|
||||||
<label>{t("jobs.fields.date_invoiced")}: </label>
|
<label>{t("jobs.fields.date_invoiced")}: </label>
|
||||||
<DateFormatter>{data.date_invoiced}</DateFormatter>
|
<DateTimeFormatter>{data.date_invoiced}</DateTimeFormatter>
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{data.date_exported ? (
|
{data.date_exported ? (
|
||||||
<Timeline.Item>
|
<Timeline.Item>
|
||||||
<label>{t("jobs.fields.date_exported")}: </label>
|
<label>{t("jobs.fields.date_exported")}: </label>
|
||||||
<DateFormatter>{data.date_exported}</DateFormatter>
|
<DateTimeFormatter>{data.date_exported}</DateTimeFormatter>
|
||||||
</Timeline.Item>
|
</Timeline.Item>
|
||||||
) : null}
|
) : null}
|
||||||
</Timeline>
|
</Timeline>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Carousel } from "antd";
|
import { Carousel } from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { GenerateThumbUrl } from "../jobs-documents-gallery/job-documents.utility";
|
||||||
import CardTemplate from "./job-detail-cards.template.component";
|
import CardTemplate from "./job-detail-cards.template.component";
|
||||||
import { DetermineFileType } from "../documents-upload/documents-upload.utility";
|
|
||||||
export default function JobDetailCardsDocumentsComponent({ loading, data }) {
|
export default function JobDetailCardsDocumentsComponent({ loading, data }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -22,15 +22,7 @@ export default function JobDetailCardsDocumentsComponent({ loading, data }) {
|
|||||||
{data.documents.length > 0 ? (
|
{data.documents.length > 0 ? (
|
||||||
<Carousel autoplay>
|
<Carousel autoplay>
|
||||||
{data.documents.map((item) => (
|
{data.documents.map((item) => (
|
||||||
<img
|
<img key={item.id} src={GenerateThumbUrl(item)} alt={item.name} />
|
||||||
key={item.id}
|
|
||||||
src={`${
|
|
||||||
process.env.REACT_APP_CLOUDINARY_ENDPOINT
|
|
||||||
}/${DetermineFileType(item.type)}/upload/${
|
|
||||||
process.env.REACT_APP_CLOUDINARY_THUMB_TRANSFORMATIONS
|
|
||||||
}/${item.key}`}
|
|
||||||
alt={item.name}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</Carousel>
|
</Carousel>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ export default function JobDetailCardTemplate({
|
|||||||
if (extraLink) extra = { extra: <Link to={extraLink}>More</Link> };
|
if (extraLink) extra = { extra: <Link to={extraLink}>More</Link> };
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
size='small'
|
size="small"
|
||||||
className='job-card'
|
className="job-card"
|
||||||
title={title}
|
title={title}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
{...extra}>
|
style={{ height: "100%" }}
|
||||||
|
{...extra}
|
||||||
|
>
|
||||||
{otherProps.children}
|
{otherProps.children}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import JobLinesBillRefernece from "../job-lines-bill-reference/job-lines-bill-re
|
|||||||
// import AllocationsBulkAssignmentContainer from "../allocations-bulk-assignment/allocations-bulk-assignment.container";
|
// import AllocationsBulkAssignmentContainer from "../allocations-bulk-assignment/allocations-bulk-assignment.container";
|
||||||
// import AllocationsEmployeeLabelContainer from "../allocations-employee-label/allocations-employee-label.container";
|
// import AllocationsEmployeeLabelContainer from "../allocations-employee-label/allocations-employee-label.container";
|
||||||
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
|
import PartsOrderModalContainer from "../parts-order-modal/parts-order-modal.container";
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
@@ -161,7 +162,11 @@ export function JobLinesComponent({
|
|||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<>
|
<>
|
||||||
<CurrencyFormatter>{record.act_price}</CurrencyFormatter>
|
<CurrencyFormatter>
|
||||||
|
{record.db_ref === "900510" || record.db_ref === "900511"
|
||||||
|
? record.prt_dsmk_m
|
||||||
|
: record.act_price}
|
||||||
|
</CurrencyFormatter>
|
||||||
{record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? (
|
{record.prt_dsmk_p && record.prt_dsmk_p !== 0 ? (
|
||||||
<span
|
<span
|
||||||
style={{ marginLeft: ".2rem" }}
|
style={{ marginLeft: ".2rem" }}
|
||||||
@@ -297,7 +302,6 @@ export function JobLinesComponent({
|
|||||||
variables: { joblineId: record.id },
|
variables: { joblineId: record.id },
|
||||||
update(cache) {
|
update(cache) {
|
||||||
cache.modify({
|
cache.modify({
|
||||||
id: cache.identify(job),
|
|
||||||
fields: {
|
fields: {
|
||||||
joblines(existingJobLines, { readField }) {
|
joblines(existingJobLines, { readField }) {
|
||||||
return existingJobLines.filter(
|
return existingJobLines.filter(
|
||||||
@@ -334,10 +338,12 @@ export function JobLinesComponent({
|
|||||||
const markedTypes = [e.key];
|
const markedTypes = [e.key];
|
||||||
if (e.key === "PAN") markedTypes.push("PAP");
|
if (e.key === "PAN") markedTypes.push("PAP");
|
||||||
if (e.key === "PAS") markedTypes.push("PASL");
|
if (e.key === "PAS") markedTypes.push("PASL");
|
||||||
setSelectedLines([
|
setSelectedLines(
|
||||||
...selectedLines,
|
_.uniq([
|
||||||
...jobLines.filter((item) => markedTypes.includes(item.part_type)),
|
...selectedLines,
|
||||||
]);
|
...jobLines.filter((item) => markedTypes.includes(item.part_type)),
|
||||||
|
])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -400,7 +406,7 @@ export function JobLinesComponent({
|
|||||||
setState({
|
setState({
|
||||||
...state,
|
...state,
|
||||||
filteredInfo: {
|
filteredInfo: {
|
||||||
part_type: ["PAN,PAC,PAR,PAL,PAA,PAM,PAP,PAS,PASL"],
|
part_type: ["PAN,PAC,PAR,PAL,PAA,PAM,PAP,PAS,PASL,PAG"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -435,7 +441,7 @@ export function JobLinesComponent({
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={{ position: "top", defaultPageSize: 50 }}
|
pagination={false}
|
||||||
dataSource={jobLines}
|
dataSource={jobLines}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
scroll={{
|
scroll={{
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ export function JobEmployeeAssignments({
|
|||||||
});
|
});
|
||||||
const [visibility, setVisibility] = useState(false);
|
const [visibility, setVisibility] = useState(false);
|
||||||
|
|
||||||
const onChange = (e) => {
|
const onChange = (value, option) => {
|
||||||
setAssignment({ ...assignment, employeeid: e });
|
setAssignment({ ...assignment, employeeid: value, name: option.name });
|
||||||
};
|
};
|
||||||
|
|
||||||
const popContent = (
|
const popContent = (
|
||||||
@@ -56,7 +56,11 @@ export function JobEmployeeAssignments({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{bodyshop.employees.map((emp) => (
|
{bodyshop.employees.map((emp) => (
|
||||||
<Select.Option value={emp.id} key={emp.id}>
|
<Select.Option
|
||||||
|
value={emp.id}
|
||||||
|
key={emp.id}
|
||||||
|
name={`${emp.first_name} ${emp.last_name}`}
|
||||||
|
>
|
||||||
{`${emp.first_name} ${emp.last_name}`}
|
{`${emp.first_name} ${emp.last_name}`}
|
||||||
</Select.Option>
|
</Select.Option>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -6,14 +6,34 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
|
|||||||
import { UPDATE_JOB_ASSIGNMENTS } from "../../graphql/jobs.queries";
|
import { UPDATE_JOB_ASSIGNMENTS } from "../../graphql/jobs.queries";
|
||||||
import JobEmployeeAssignmentsComponent from "./job-employee-assignments.component";
|
import JobEmployeeAssignmentsComponent from "./job-employee-assignments.component";
|
||||||
|
|
||||||
export default function JobEmployeeAssignmentsContainer({ job, refetch }) {
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
//currentUser: selectCurrentUser
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
insertAuditTrail: ({ jobid, operation }) =>
|
||||||
|
dispatch(insertAuditTrail({ jobid, operation })),
|
||||||
|
});
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(JobEmployeeAssignmentsContainer);
|
||||||
|
|
||||||
|
export function JobEmployeeAssignmentsContainer({
|
||||||
|
job,
|
||||||
|
refetch,
|
||||||
|
insertAuditTrail,
|
||||||
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [updateJob] = useMutation(UPDATE_JOB_ASSIGNMENTS);
|
const [updateJob] = useMutation(UPDATE_JOB_ASSIGNMENTS);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const handleAdd = async (assignment) => {
|
const handleAdd = async (assignment) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const { operation, employeeid } = assignment;
|
const { operation, employeeid, name } = assignment;
|
||||||
logImEXEvent("job_assign_employee", { operation });
|
logImEXEvent("job_assign_employee", { operation });
|
||||||
|
|
||||||
let empAssignment = determineFieldName(operation);
|
let empAssignment = determineFieldName(operation);
|
||||||
@@ -23,6 +43,11 @@ export default function JobEmployeeAssignmentsContainer({ job, refetch }) {
|
|||||||
});
|
});
|
||||||
if (refetch) refetch();
|
if (refetch) refetch();
|
||||||
|
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: job.id,
|
||||||
|
operation: AuditTrailMapping.jobassignmentchange(operation, name),
|
||||||
|
});
|
||||||
|
|
||||||
if (!!result.errors) {
|
if (!!result.errors) {
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
message: t("jobs.errors.assigning", {
|
message: t("jobs.errors.assigning", {
|
||||||
@@ -48,6 +73,10 @@ export default function JobEmployeeAssignmentsContainer({ job, refetch }) {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: job.id,
|
||||||
|
operation: AuditTrailMapping.jobassignmentremoved(operation),
|
||||||
|
});
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export function JobLineStatusPopup({ bodyshop, jobline, disabled }) {
|
|||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{ width: "100%", minHeight: "2rem", cursor: "pointer" }}
|
style={{ width: "100%", minHeight: "1rem", cursor: "pointer" }}
|
||||||
onClick={() => !disabled && setEditing(true)}
|
onClick={() => !disabled && setEditing(true)}
|
||||||
>
|
>
|
||||||
{jobline.status}
|
{jobline.status}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Form, Input, InputNumber, Modal, Select } from "antd";
|
import { Form, Input, InputNumber, Modal, Select, Switch } from "antd";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import InputCurrency from "../form-items-formatted/currency-form-item.component";
|
import InputCurrency from "../form-items-formatted/currency-form-item.component";
|
||||||
@@ -115,18 +115,18 @@ export default function JobLinesUpsertModalComponent({
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("joblines.fields.mod_lb_hrs")}
|
label={t("joblines.fields.mod_lb_hrs")}
|
||||||
name="mod_lb_hrs"
|
name="mod_lb_hrs"
|
||||||
// rules={[
|
rules={[
|
||||||
// ({ getFieldValue }) => ({
|
({ getFieldValue }) => ({
|
||||||
// validator(rule, value) {
|
validator(rule, value) {
|
||||||
// if (!!getFieldValue("mod_lbr_ty") === !!value) {
|
if (!!getFieldValue("mod_lbr_ty") === !!value) {
|
||||||
// return Promise.resolve();
|
return Promise.resolve();
|
||||||
// }
|
}
|
||||||
// return Promise.reject(
|
return Promise.reject(
|
||||||
// t("joblines.validations.hrsrequirediflbrtyp")
|
t("joblines.validations.hrsrequirediflbrtyp")
|
||||||
// );
|
);
|
||||||
// },
|
},
|
||||||
// }),
|
}),
|
||||||
// ]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber precision={1} />
|
<InputNumber precision={1} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -169,49 +169,49 @@ export default function JobLinesUpsertModalComponent({
|
|||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("joblines.fields.part_qty")}
|
label={t("joblines.fields.part_qty")}
|
||||||
name="part_qty"
|
name="part_qty"
|
||||||
// rules={[
|
rules={[
|
||||||
// ({ getFieldValue }) => ({
|
({ getFieldValue }) => ({
|
||||||
// validator(rule, value) {
|
validator(rule, value) {
|
||||||
// if (!!getFieldValue("part_type") === !!value) {
|
if (!!getFieldValue("part_type") === !!value) {
|
||||||
// return Promise.resolve();
|
return Promise.resolve();
|
||||||
// }
|
}
|
||||||
// return Promise.reject(
|
return Promise.reject(
|
||||||
// t("joblines.validations.requiredifparttype")
|
t("joblines.validations.requiredifparttype")
|
||||||
// );
|
);
|
||||||
// },
|
},
|
||||||
// }),
|
}),
|
||||||
// ]}
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber precision={0} min={0} />
|
<InputNumber precision={0} min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("joblines.fields.db_price")} name="db_price">
|
{/* <Form.Item label={t("joblines.fields.db_price")} name="db_price">
|
||||||
<InputCurrency precision={2} min={0} />
|
<InputCurrency precision={2} min={0} />
|
||||||
</Form.Item>
|
</Form.Item> */}
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("joblines.fields.act_price")}
|
label={t("joblines.fields.act_price")}
|
||||||
name="act_price"
|
name="act_price"
|
||||||
// rules={[
|
rules={[
|
||||||
// ({ getFieldValue }) => ({
|
({ getFieldValue }) => ({
|
||||||
// validator(rule, value) {
|
validator(rule, value) {
|
||||||
// if (!value || getFieldValue("part_type") !== "PAE") {
|
if (!value || getFieldValue("part_type") !== "PAE") {
|
||||||
// return Promise.resolve();
|
return Promise.resolve();
|
||||||
// }
|
}
|
||||||
// return Promise.reject(
|
return Promise.reject(
|
||||||
// t("joblines.validations.zeropriceexistingpart")
|
t("joblines.validations.zeropriceexistingpart")
|
||||||
// );
|
);
|
||||||
// },
|
},
|
||||||
// }),
|
}),
|
||||||
// ({ getFieldValue }) => ({
|
({ getFieldValue }) => ({
|
||||||
// validator(rule, value) {
|
validator(rule, value) {
|
||||||
// if (!!getFieldValue("part_type") === !!value) {
|
if (!!getFieldValue("part_type") === !!value) {
|
||||||
// return Promise.resolve();
|
return Promise.resolve();
|
||||||
// }
|
}
|
||||||
// return Promise.reject(
|
return Promise.reject(
|
||||||
// t("joblines.validations.requiredifparttype")
|
t("joblines.validations.requiredifparttype")
|
||||||
// );
|
);
|
||||||
// },
|
},
|
||||||
// }),
|
}),
|
||||||
// ]}
|
]}
|
||||||
>
|
>
|
||||||
<InputCurrency precision={2} min={0} />
|
<InputCurrency precision={2} min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -222,6 +222,14 @@ export default function JobLinesUpsertModalComponent({
|
|||||||
>
|
>
|
||||||
<InputNumber precision={0} min={0} max={100} />
|
<InputNumber precision={0} min={0} max={100} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("joblines.fields.tax_part")}
|
||||||
|
name="tax_part"
|
||||||
|
valuePropName="checked"
|
||||||
|
initialValue={true}
|
||||||
|
>
|
||||||
|
<Switch />
|
||||||
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ function JobLinesUpsertModalContainer({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
refetchQueries: ["GET_LINE_TICKET_BY_PK"],
|
||||||
});
|
});
|
||||||
if (!r.errors) {
|
if (!r.errors) {
|
||||||
await Axios.post("/job/totalsssu", {
|
await Axios.post("/job/totalsssu", {
|
||||||
@@ -69,6 +70,7 @@ function JobLinesUpsertModalContainer({
|
|||||||
lineId: jobLineEditModal.context.id,
|
lineId: jobLineEditModal.context.id,
|
||||||
line: values,
|
line: values,
|
||||||
},
|
},
|
||||||
|
refetchQueries: ["GET_LINE_TICKET_BY_PK"],
|
||||||
});
|
});
|
||||||
if (!r.errors) {
|
if (!r.errors) {
|
||||||
notification["success"]({
|
notification["success"]({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Button, Card, Space, Table } from "antd";
|
import { Button, Card, Space, Table } from "antd";
|
||||||
|
import { EditFilled } from "@ant-design/icons";
|
||||||
import Dinero from "dinero.js";
|
import Dinero from "dinero.js";
|
||||||
import React, { useMemo, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -115,16 +116,29 @@ export function JobPayments({
|
|||||||
dataIndex: "actions",
|
dataIndex: "actions",
|
||||||
key: "actions",
|
key: "actions",
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<PrintWrapperComponent
|
<Space wrap>
|
||||||
templateObject={{
|
<Button
|
||||||
name: TemplateList("payment").payment_receipt.key,
|
disabled={record.exportedat}
|
||||||
variables: { id: record.id },
|
onClick={() => {
|
||||||
}}
|
setPaymentContext({
|
||||||
messageObject={{
|
actions: { refetch: refetch },
|
||||||
to: job.ownr_ea,
|
context: record,
|
||||||
}}
|
});
|
||||||
id={job.id}
|
}}
|
||||||
/>
|
>
|
||||||
|
<EditFilled />
|
||||||
|
</Button>
|
||||||
|
<PrintWrapperComponent
|
||||||
|
templateObject={{
|
||||||
|
name: TemplateList("payment").payment_receipt.key,
|
||||||
|
variables: { id: record.id },
|
||||||
|
}}
|
||||||
|
messageObject={{
|
||||||
|
to: job.ownr_ea,
|
||||||
|
}}
|
||||||
|
id={job.id}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Checkbox, PageHeader, Table } from "antd";
|
import { Checkbox, Table, Typography } from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
@@ -21,6 +21,8 @@ export default function JobReconciliationBillsTable({
|
|||||||
title: t("billlines.fields.line_desc"),
|
title: t("billlines.fields.line_desc"),
|
||||||
dataIndex: "line_desc",
|
dataIndex: "line_desc",
|
||||||
key: "line_desc",
|
key: "line_desc",
|
||||||
|
ellipsis: true,
|
||||||
|
minWidth: "65rem",
|
||||||
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
|
||||||
@@ -29,6 +31,8 @@ export default function JobReconciliationBillsTable({
|
|||||||
title: t("billlines.labels.from"),
|
title: t("billlines.labels.from"),
|
||||||
dataIndex: "from",
|
dataIndex: "from",
|
||||||
key: "from",
|
key: "from",
|
||||||
|
|
||||||
|
ellipsis: true,
|
||||||
render: (text, record) =>
|
render: (text, record) =>
|
||||||
`${record.bill.vendor && record.bill.vendor.name} / ${
|
`${record.bill.vendor && record.bill.vendor.name} / ${
|
||||||
record.bill.invoice_number
|
record.bill.invoice_number
|
||||||
@@ -39,6 +43,7 @@ export default function JobReconciliationBillsTable({
|
|||||||
dataIndex: "actual_price",
|
dataIndex: "actual_price",
|
||||||
key: "actual_price",
|
key: "actual_price",
|
||||||
sorter: (a, b) => a.actual_price - b.actual_price,
|
sorter: (a, b) => a.actual_price - b.actual_price,
|
||||||
|
width: "7rem",
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "actual_price" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "actual_price" && state.sortedInfo.order,
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
@@ -50,6 +55,7 @@ export default function JobReconciliationBillsTable({
|
|||||||
dataIndex: "actual_cost",
|
dataIndex: "actual_cost",
|
||||||
key: "actual_cost",
|
key: "actual_cost",
|
||||||
sorter: (a, b) => a.actual_cost - b.actual_cost,
|
sorter: (a, b) => a.actual_cost - b.actual_cost,
|
||||||
|
width: "7rem",
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "actual_cost" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "actual_cost" && state.sortedInfo.order,
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
@@ -57,10 +63,11 @@ export default function JobReconciliationBillsTable({
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("billlines.fields.quantity"),
|
title: t("joblines.fields.part_qty"),
|
||||||
dataIndex: "quantity",
|
dataIndex: "quantity",
|
||||||
key: "quantity",
|
key: "quantity",
|
||||||
sorter: (a, b) => a.quantity - b.quantity,
|
sorter: (a, b) => a.quantity - b.quantity,
|
||||||
|
width: "4rem",
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "quantity" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "quantity" && state.sortedInfo.order,
|
||||||
},
|
},
|
||||||
@@ -69,9 +76,11 @@ export default function JobReconciliationBillsTable({
|
|||||||
dataIndex: "is_credit_memo",
|
dataIndex: "is_credit_memo",
|
||||||
key: "is_credit_memo",
|
key: "is_credit_memo",
|
||||||
sorter: (a, b) => a.bill.is_credit_memo - b.bill.is_credit_memo,
|
sorter: (a, b) => a.bill.is_credit_memo - b.bill.is_credit_memo,
|
||||||
|
width: "8rem",
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "is_credit_memo" &&
|
state.sortedInfo.columnKey === "is_credit_memo" &&
|
||||||
state.sortedInfo.order,
|
state.sortedInfo.order,
|
||||||
|
|
||||||
render: (text, record) => (
|
render: (text, record) => (
|
||||||
<Checkbox disabled checked={record.bill.is_credit_memo} />
|
<Checkbox disabled checked={record.bill.is_credit_memo} />
|
||||||
),
|
),
|
||||||
@@ -86,10 +95,12 @@ export default function JobReconciliationBillsTable({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageHeader title={t("bills.labels.bills")}>
|
<div>
|
||||||
|
<Typography.Title level={4}>{t("bills.labels.bills")}</Typography.Title>
|
||||||
<Table
|
<Table
|
||||||
pagination={false}
|
pagination={false}
|
||||||
scroll={{ y: "40vh", x: true }}
|
size="small"
|
||||||
|
scroll={{ y: "60vh" }}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
dataSource={invoiceLineData}
|
dataSource={invoiceLineData}
|
||||||
@@ -97,8 +108,11 @@ export default function JobReconciliationBillsTable({
|
|||||||
rowSelection={{
|
rowSelection={{
|
||||||
onChange: handleOnRowClick,
|
onChange: handleOnRowClick,
|
||||||
selectedRowKeys: selectedLines,
|
selectedRowKeys: selectedLines,
|
||||||
|
getCheckboxProps: (record) => {
|
||||||
|
return { disabled: record.deductedfromlbr };
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</PageHeader>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,21 +22,23 @@ export default function JobReconciliationModalComponent({ job, bills }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div style={{ flex: 1, display: "flex", flexDirection: "column" }}>
|
||||||
<Row gutter={[16, 16]}>
|
<div style={{ flex: 1 }}>
|
||||||
<Col span={12}>
|
<Row gutter={8}>
|
||||||
<JobReconciliationPartsTable
|
<Col span={12}>
|
||||||
jobLineData={jobLineData}
|
<JobReconciliationPartsTable
|
||||||
jobLineState={jobLineState}
|
jobLineData={jobLineData}
|
||||||
/>
|
jobLineState={jobLineState}
|
||||||
</Col>
|
/>
|
||||||
<Col span={12}>
|
</Col>
|
||||||
<JobReconciliationBillsTable
|
<Col span={12}>
|
||||||
invoiceLineData={invoiceLineData}
|
<JobReconciliationBillsTable
|
||||||
billLineState={billLineState}
|
invoiceLineData={invoiceLineData}
|
||||||
/>
|
billLineState={billLineState}
|
||||||
</Col>
|
/>
|
||||||
</Row>
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
<Row>
|
<Row>
|
||||||
<JobReconciliationTotals
|
<JobReconciliationTotals
|
||||||
jobLines={jobLineData}
|
jobLines={jobLineData}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
.imex-reconciliation-modal {
|
||||||
|
top: 20px;
|
||||||
|
// .ant-modal-content {
|
||||||
|
// height: 95vh;
|
||||||
|
// display: flex;
|
||||||
|
// flex-direction: column;
|
||||||
|
// .ant-modal-body {
|
||||||
|
// display: flex;
|
||||||
|
// flex: 1;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import { selectReconciliation } from "../../redux/modals/modals.selectors";
|
|||||||
import JobReconciliationModalComponent from "./job-reconciliation-modal.component";
|
import JobReconciliationModalComponent from "./job-reconciliation-modal.component";
|
||||||
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
|
import "./job-reconciliation-modal.styles.scss";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
reconciliationModal: selectReconciliation,
|
reconciliationModal: selectReconciliation,
|
||||||
@@ -38,23 +39,23 @@ function JobReconciliationModalContainer({
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={t("jobs.labels.reconciliationheader")}
|
title={t("jobs.labels.reconciliationheader")}
|
||||||
width={"90%"}
|
width={"95%"}
|
||||||
visible={visible}
|
visible={visible}
|
||||||
okText={t("general.actions.close")}
|
okText={t("general.actions.close")}
|
||||||
onOk={handleCancel}
|
onOk={handleCancel}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
cancelButtonProps={{ display: "none" }}
|
cancelButtonProps={{ display: "none" }}
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
|
className="imex-reconciliation-modal"
|
||||||
>
|
>
|
||||||
<LoadingSpinner loading={loading}>
|
{loading && <LoadingSpinner loading={loading} />}
|
||||||
{error && <AlertComponent message={error.message} type="error" />}
|
{error && <AlertComponent message={error.message} type="error" />}
|
||||||
{data && (
|
{data && (
|
||||||
<JobReconciliationModalComponent
|
<JobReconciliationModalComponent
|
||||||
job={data && data.jobs_by_pk}
|
job={data && data.jobs_by_pk}
|
||||||
bills={data && data.bills}
|
bills={data && data.bills}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</LoadingSpinner>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { PageHeader, Table } from "antd";
|
import { Table, Typography } from "antd";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
import CurrencyFormatter from "../../utils/CurrencyFormatter";
|
||||||
@@ -23,6 +23,7 @@ export default function JobReconcilitionPartsTable({
|
|||||||
dataIndex: "line_desc",
|
dataIndex: "line_desc",
|
||||||
key: "line_desc",
|
key: "line_desc",
|
||||||
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
sorter: (a, b) => alphaSort(a.line_desc, b.line_desc),
|
||||||
|
ellipses: true,
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "line_desc" && state.sortedInfo.order,
|
||||||
},
|
},
|
||||||
@@ -57,6 +58,7 @@ export default function JobReconcilitionPartsTable({
|
|||||||
dataIndex: "act_price",
|
dataIndex: "act_price",
|
||||||
key: "act_price",
|
key: "act_price",
|
||||||
sorter: (a, b) => a.act_price - b.act_price,
|
sorter: (a, b) => a.act_price - b.act_price,
|
||||||
|
width: "7rem",
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "act_price" && state.sortedInfo.order,
|
||||||
|
|
||||||
@@ -68,10 +70,12 @@ export default function JobReconcilitionPartsTable({
|
|||||||
title: t("joblines.fields.part_qty"),
|
title: t("joblines.fields.part_qty"),
|
||||||
dataIndex: "part_qty",
|
dataIndex: "part_qty",
|
||||||
key: "part_qty",
|
key: "part_qty",
|
||||||
|
width: "4rem",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("joblines.fields.total"),
|
title: t("joblines.fields.total"),
|
||||||
dataIndex: "total",
|
dataIndex: "total",
|
||||||
|
width: "7rem",
|
||||||
key: "total",
|
key: "total",
|
||||||
sorter: (a, b) => a.act_price * a.part_qty - b.act_price * b.part_qty,
|
sorter: (a, b) => a.act_price * a.part_qty - b.act_price * b.part_qty,
|
||||||
sortOrder:
|
sortOrder:
|
||||||
@@ -89,6 +93,7 @@ export default function JobReconcilitionPartsTable({
|
|||||||
dataIndex: "status",
|
dataIndex: "status",
|
||||||
key: "status",
|
key: "status",
|
||||||
sorter: (a, b) => alphaSort(a.status, b.status),
|
sorter: (a, b) => alphaSort(a.status, b.status),
|
||||||
|
width: "6rem",
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "status" && state.sortedInfo.order,
|
||||||
},
|
},
|
||||||
@@ -102,11 +107,13 @@ export default function JobReconcilitionPartsTable({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageHeader title={t("jobs.labels.lines")}>
|
<div>
|
||||||
|
<Typography.Title level={4}>{t("jobs.labels.lines")}</Typography.Title>
|
||||||
<Table
|
<Table
|
||||||
pagination={false}
|
pagination={false}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
scroll={{ y: "40vh", x: true }}
|
size="small"
|
||||||
|
scroll={{ y: "60vh" }}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
dataSource={jobLineData}
|
dataSource={jobLineData}
|
||||||
onChange={handleTableChange}
|
onChange={handleTableChange}
|
||||||
@@ -122,6 +129,6 @@ export default function JobReconcilitionPartsTable({
|
|||||||
<div style={{ fontStyle: "italic", margin: "4px" }}>
|
<div style={{ fontStyle: "italic", margin: "4px" }}>
|
||||||
{t("jobs.labels.reconciliation.removedpartsstrikethrough")}
|
{t("jobs.labels.reconciliation.removedpartsstrikethrough")}
|
||||||
</div>
|
</div>
|
||||||
</PageHeader>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const reconcileByAssocLine = (
|
|||||||
const [selectedJobLines, setSelectedJobLines] = jobLineState;
|
const [selectedJobLines, setSelectedJobLines] = jobLineState;
|
||||||
|
|
||||||
const allJoblinesFromBills = billLines
|
const allJoblinesFromBills = billLines
|
||||||
.filter((bl) => bl.joblineid && !(bl.jobline && bl.jobline.removed))
|
.filter((bl) => bl.joblineid && bl.jobline && !bl.jobline.removed)
|
||||||
.map((bl) => bl.joblineid);
|
.map((bl) => bl.joblineid);
|
||||||
|
|
||||||
const duplicatedJobLinesbyInvoiceId = _.filter(
|
const duplicatedJobLinesbyInvoiceId = _.filter(
|
||||||
|
|||||||
@@ -1,12 +1,24 @@
|
|||||||
import { useMutation } from "@apollo/client";
|
import { useMutation, useLazyQuery } from "@apollo/client";
|
||||||
import { Button, Card, Form, notification, Popover } from "antd";
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Form,
|
||||||
|
InputNumber,
|
||||||
|
notification,
|
||||||
|
Popover,
|
||||||
|
Space,
|
||||||
|
} from "antd";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import React, { useState } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { INSERT_SCOREBOARD_ENTRY } from "../../graphql/scoreboard.queries";
|
import {
|
||||||
|
INSERT_SCOREBOARD_ENTRY,
|
||||||
|
QUERY_SCOREBOARD_ENTRY,
|
||||||
|
UPDATE_SCOREBOARD_ENTRY,
|
||||||
|
} from "../../graphql/scoreboard.queries";
|
||||||
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
import FormDatePicker from "../form-date-picker/form-date-picker.component";
|
||||||
import InputNumberCalculator from "../form-input-number-calculator/form-input-number-calculator.component";
|
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
|
||||||
|
|
||||||
export default function ScoreboardAddButton({
|
export default function ScoreboardAddButton({
|
||||||
job,
|
job,
|
||||||
@@ -15,17 +27,46 @@ export default function ScoreboardAddButton({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [insertScoreboardEntry] = useMutation(INSERT_SCOREBOARD_ENTRY);
|
const [insertScoreboardEntry] = useMutation(INSERT_SCOREBOARD_ENTRY);
|
||||||
|
const [updateScoreboardEntry] = useMutation(UPDATE_SCOREBOARD_ENTRY);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [visibility, setVisibility] = useState(false);
|
const [visibility, setVisibility] = useState(false);
|
||||||
|
const [callQuery, { loading: entryLoading, data: entryData }] = useLazyQuery(
|
||||||
|
QUERY_SCOREBOARD_ENTRY
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visibility) {
|
||||||
|
callQuery({ variables: { jobid: job.id } });
|
||||||
|
}
|
||||||
|
}, [visibility, job.id, callQuery]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("UE", entryData);
|
||||||
|
if (entryData && entryData.scoreboard && entryData.scoreboard[0]) {
|
||||||
|
console.log("Setting FOrm");
|
||||||
|
form.setFieldsValue(entryData.scoreboard[0]);
|
||||||
|
}
|
||||||
|
}, [entryData, form]);
|
||||||
|
|
||||||
const handleFinish = async (values) => {
|
const handleFinish = async (values) => {
|
||||||
logImEXEvent("job_close_add_to_scoreboard");
|
logImEXEvent("job_close_add_to_scoreboard");
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const result = await insertScoreboardEntry({
|
let result;
|
||||||
variables: { sbInput: [{ jobid: job.id, ...values }] },
|
|
||||||
});
|
if (entryData && entryData.scoreboard && entryData.scoreboard[0]) {
|
||||||
|
result = await updateScoreboardEntry({
|
||||||
|
variables: {
|
||||||
|
sbId: entryData.scoreboard[0].id,
|
||||||
|
sbInput: values,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result = await insertScoreboardEntry({
|
||||||
|
variables: { sbInput: [{ jobid: job.id, ...values }] },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!!result.errors) {
|
if (!!result.errors) {
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
@@ -45,53 +86,62 @@ export default function ScoreboardAddButton({
|
|||||||
const overlay = (
|
const overlay = (
|
||||||
<Card>
|
<Card>
|
||||||
<div>
|
<div>
|
||||||
<Form
|
{entryLoading ? (
|
||||||
form={form}
|
<LoadingSpinner />
|
||||||
layout="vertical"
|
) : (
|
||||||
onFinish={handleFinish}
|
<Form
|
||||||
initialValues={{}}
|
form={form}
|
||||||
>
|
layout="vertical"
|
||||||
<Form.Item
|
onFinish={handleFinish}
|
||||||
label={t("scoreboard.fields.date")}
|
initialValues={{}}
|
||||||
name="date"
|
|
||||||
rules={[
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
//message: t("general.validation.required"),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<FormDatePicker />
|
<Form.Item
|
||||||
</Form.Item>
|
label={t("scoreboard.fields.date")}
|
||||||
<Form.Item
|
name="date"
|
||||||
label={t("scoreboard.fields.bodyhrs")}
|
rules={[
|
||||||
name="bodyhrs"
|
{
|
||||||
rules={[
|
required: true,
|
||||||
{
|
//message: t("general.validation.required"),
|
||||||
required: true,
|
},
|
||||||
//message: t("general.validation.required"),
|
]}
|
||||||
},
|
>
|
||||||
]}
|
<FormDatePicker />
|
||||||
>
|
</Form.Item>
|
||||||
<InputNumberCalculator precision={1} />
|
<Form.Item
|
||||||
</Form.Item>
|
label={t("scoreboard.fields.bodyhrs")}
|
||||||
<Form.Item
|
name="bodyhrs"
|
||||||
label={t("scoreboard.fields.painthrs")}
|
rules={[
|
||||||
name="painthrs"
|
{
|
||||||
rules={[
|
required: true,
|
||||||
{
|
//message: t("general.validation.required"),
|
||||||
required: true,
|
},
|
||||||
//message: t("general.validation.required"),
|
]}
|
||||||
},
|
>
|
||||||
]}
|
<InputNumber precision={1} />
|
||||||
>
|
</Form.Item>
|
||||||
<InputNumberCalculator precision={1} />
|
<Form.Item
|
||||||
</Form.Item>
|
label={t("scoreboard.fields.painthrs")}
|
||||||
|
name="painthrs"
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
//message: t("general.validation.required"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<InputNumber precision={1} />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
<Button type="primary" htmlType="submit">
|
<Space wrap>
|
||||||
{t("general.actions.save")}
|
<Button type="primary" htmlType="submit">
|
||||||
</Button>
|
{t("general.actions.save")}
|
||||||
</Form>
|
</Button>
|
||||||
|
<Button onClick={() => setVisibility(false)}>
|
||||||
|
{t("general.actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
@@ -100,7 +150,7 @@ export default function ScoreboardAddButton({
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
const v = job.joblines.reduce(
|
const v = job.joblines.reduce(
|
||||||
(acc, val) => {
|
(acc, val) => {
|
||||||
if (val.mod_lbr_ty === "LAB")
|
if (val.mod_lbr_ty !== "LAR")
|
||||||
acc = { ...acc, bodyhrs: acc.bodyhrs + val.mod_lb_hrs };
|
acc = { ...acc, bodyhrs: acc.bodyhrs + val.mod_lb_hrs };
|
||||||
if (val.mod_lbr_ty === "LAR")
|
if (val.mod_lbr_ty === "LAR")
|
||||||
acc = { ...acc, painthrs: acc.painthrs + val.mod_lb_hrs };
|
acc = { ...acc, painthrs: acc.painthrs + val.mod_lb_hrs };
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { LoadingOutlined } from "@ant-design/icons";
|
import { LoadingOutlined } from "@ant-design/icons";
|
||||||
import { useLazyQuery } from "@apollo/client";
|
import { useLazyQuery } from "@apollo/client";
|
||||||
import { Empty, Select } from "antd";
|
import { Empty, Select, Space, Tag } from "antd";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React, { forwardRef, useEffect } from "react";
|
import React, { forwardRef, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -15,6 +15,7 @@ const JobSearchSelect = (
|
|||||||
{
|
{
|
||||||
disabled,
|
disabled,
|
||||||
convertedOnly = false,
|
convertedOnly = false,
|
||||||
|
notInvoiced = false,
|
||||||
notExported = true,
|
notExported = true,
|
||||||
clm_no = false,
|
clm_no = false,
|
||||||
...restProps
|
...restProps
|
||||||
@@ -30,6 +31,7 @@ const JobSearchSelect = (
|
|||||||
variables: {
|
variables: {
|
||||||
...(convertedOnly ? { isConverted: true } : {}),
|
...(convertedOnly ? { isConverted: true } : {}),
|
||||||
...(notExported ? { notExported: true } : {}),
|
...(notExported ? { notExported: true } : {}),
|
||||||
|
...(notInvoiced ? { notInvoiced: true } : {}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
@@ -80,13 +82,20 @@ const JobSearchSelect = (
|
|||||||
{theOptions
|
{theOptions
|
||||||
? theOptions.map((o) => (
|
? theOptions.map((o) => (
|
||||||
<Option key={o.id} value={o.id} status={o.status}>
|
<Option key={o.id} value={o.id} status={o.status}>
|
||||||
{`${clm_no && o.clm_no ? `${o.clm_no} | ` : ""}${
|
<Space align="center">
|
||||||
o.ro_number || t("general.labels.na")
|
<span>
|
||||||
} | ${o.ownr_ln || ""} ${o.ownr_fn || ""} ${
|
{`${clm_no && o.clm_no ? `${o.clm_no} | ` : ""}${
|
||||||
o.ownr_co_nm ? ` ${o.ownr_co_num}` : ""
|
o.ro_number || t("general.labels.na")
|
||||||
}| ${o.v_model_yr || ""} ${o.v_make_desc || ""} ${
|
} | ${o.ownr_ln || ""} ${o.ownr_fn || ""} ${
|
||||||
o.v_model_desc || ""
|
o.ownr_co_nm ? ` ${o.ownr_co_num}` : ""
|
||||||
}`}
|
}| ${o.v_model_yr || ""} ${o.v_make_desc || ""} ${
|
||||||
|
o.v_model_desc || ""
|
||||||
|
}`}
|
||||||
|
</span>
|
||||||
|
<Tag>
|
||||||
|
<strong>{o.status}</strong>
|
||||||
|
</Tag>
|
||||||
|
</Space>
|
||||||
</Option>
|
</Option>
|
||||||
))
|
))
|
||||||
: null}
|
: null}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { DownCircleFilled } from "@ant-design/icons";
|
||||||
|
import { useMutation } from "@apollo/client";
|
||||||
|
import { Button, Dropdown, Menu, notification } from "antd";
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { UPDATE_JOB_STATUS } from "../../graphql/jobs.queries";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||||
|
});
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminStatus);
|
||||||
|
|
||||||
|
export function JobsAdminStatus({ bodyshop, job }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [mutationUpdateJobstatus] = useMutation(UPDATE_JOB_STATUS);
|
||||||
|
const updateJobStatus = (status) => {
|
||||||
|
mutationUpdateJobstatus({
|
||||||
|
variables: { jobId: job.id, status: status },
|
||||||
|
})
|
||||||
|
.then((r) => {
|
||||||
|
notification["success"]({ message: t("jobs.successes.save") });
|
||||||
|
// refetch();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
notification["error"]({ message: t("jobs.errors.saving") });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusmenu = (
|
||||||
|
<Menu
|
||||||
|
onClick={(e) => {
|
||||||
|
updateJobStatus(e.key);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{bodyshop.md_ro_statuses.statuses.map((item) => (
|
||||||
|
<Menu.Item key={item}>{item}</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown overlay={statusmenu} trigger={["click"]} key="changestatus">
|
||||||
|
<Button shape="round">
|
||||||
|
<span>{job.status}</span>
|
||||||
|
|
||||||
|
<DownCircleFilled />
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -74,7 +74,12 @@ export default function JobsAdminDatesChange({ job }) {
|
|||||||
<Form.Item label={t("jobs.fields.actual_in")} name="actual_in">
|
<Form.Item label={t("jobs.fields.actual_in")} name="actual_in">
|
||||||
<DateTimePicker />
|
<DateTimePicker />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.date_last_contacted")}
|
||||||
|
name="date_last_contacted"
|
||||||
|
>
|
||||||
|
<DateTimePicker />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.scheduled_completion")}
|
label={t("jobs.fields.scheduled_completion")}
|
||||||
name="scheduled_completion"
|
name="scheduled_completion"
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { useTranslation } from "react-i18next";
|
|||||||
export default function JobAdminDeleteIntake({ job }) {
|
export default function JobAdminDeleteIntake({ job }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [updateJob] = useMutation(gql`
|
const [deleteIntake] = useMutation(gql`
|
||||||
mutation UPDATE_JOB($jobId: uuid!) {
|
mutation DELETE_INTAKE($jobId: uuid!) {
|
||||||
update_jobs_by_pk(
|
update_jobs_by_pk(
|
||||||
pk_columns: { id: $jobId }
|
pk_columns: { id: $jobId }
|
||||||
_set: { intakechecklist: null }
|
_set: { intakechecklist: null }
|
||||||
@@ -18,9 +18,39 @@ export default function JobAdminDeleteIntake({ job }) {
|
|||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
const [DELETE_DELIVERY] = useMutation(gql`
|
||||||
|
mutation DELETE_DELIVERY($jobId: uuid!) {
|
||||||
|
update_jobs_by_pk(
|
||||||
|
pk_columns: { id: $jobId }
|
||||||
|
_set: { deliverchecklist: null }
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
deliverchecklist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
const handleDelete = async (values) => {
|
const handleDelete = async (values) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const result = await updateJob({
|
const result = await deleteIntake({
|
||||||
|
variables: { jobId: job.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!!!result.errors) {
|
||||||
|
notification["success"]({ message: t("jobs.successes.save") });
|
||||||
|
} else {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("jobs.errors.saving", {
|
||||||
|
error: JSON.stringify(result.errors),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteDelivery = async (values) => {
|
||||||
|
setLoading(true);
|
||||||
|
const result = await DELETE_DELIVERY({
|
||||||
variables: { jobId: job.id },
|
variables: { jobId: job.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -34,12 +64,16 @@ export default function JobAdminDeleteIntake({ job }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
//Get the owner details, populate it all back into the job.
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button loading={loading} onClick={handleDelete}>
|
<>
|
||||||
{t("jobs.labels.deleteintake")}
|
<Button loading={loading} onClick={handleDelete}>
|
||||||
</Button>
|
{t("jobs.labels.deleteintake")}
|
||||||
|
</Button>
|
||||||
|
<Button loading={loading} onClick={handleDeleteDelivery}>
|
||||||
|
{t("jobs.labels.deletedelivery")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import moment from "moment";
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
});
|
});
|
||||||
@@ -21,8 +22,8 @@ export default connect(
|
|||||||
export function JobAdminMarkReexport({ bodyshop, job }) {
|
export function JobAdminMarkReexport({ bodyshop, job }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [updateJob] = useMutation(gql`
|
const [markJobForReexport] = useMutation(gql`
|
||||||
mutation UPDATE_JOB($jobId: uuid!) {
|
mutation MARK_JOB_FOR_REEXPORT($jobId: uuid!) {
|
||||||
update_jobs_by_pk(
|
update_jobs_by_pk(
|
||||||
pk_columns: { id: $jobId }
|
pk_columns: { id: $jobId }
|
||||||
_set: { date_exported: null
|
_set: { date_exported: null
|
||||||
@@ -30,14 +31,84 @@ export function JobAdminMarkReexport({ bodyshop, job }) {
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
id
|
id
|
||||||
intakechecklist
|
date_exported
|
||||||
|
status
|
||||||
|
date_invoiced
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const handleUpdate = async (values) => {
|
const [markJobExported] = useMutation(gql`
|
||||||
|
mutation MARK_JOB_AS_EXPORTED($jobId: uuid!, $date_exported: timestamptz!) {
|
||||||
|
update_jobs_by_pk(
|
||||||
|
pk_columns: { id: $jobId }
|
||||||
|
_set: { date_exported: $date_exported
|
||||||
|
status: "${bodyshop.md_ro_statuses.default_exported}"
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
date_exported
|
||||||
|
date_invoiced
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
const [markJobUninvoiced] = useMutation(gql`
|
||||||
|
mutation MARK_JOB_AS_UNINVOICED($jobId: uuid!, ) {
|
||||||
|
update_jobs_by_pk(
|
||||||
|
pk_columns: { id: $jobId }
|
||||||
|
_set: { date_exported: null
|
||||||
|
date_invoiced: null
|
||||||
|
status: "${bodyshop.md_ro_statuses.default_delivered}"
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
date_exported
|
||||||
|
date_invoiced
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const handleMarkForExport = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const result = await updateJob({
|
const result = await markJobForReexport({
|
||||||
|
variables: { jobId: job.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.errors) {
|
||||||
|
notification["success"]({ message: t("jobs.successes.save") });
|
||||||
|
} else {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("jobs.errors.saving", {
|
||||||
|
error: JSON.stringify(result.errors),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMarkExported = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const result = await markJobExported({
|
||||||
|
variables: { jobId: job.id, date_exported: moment() },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.errors) {
|
||||||
|
notification["success"]({ message: t("jobs.successes.save") });
|
||||||
|
} else {
|
||||||
|
notification["error"]({
|
||||||
|
message: t("jobs.errors.saving", {
|
||||||
|
error: JSON.stringify(result.errors),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUninvoice = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const result = await markJobUninvoiced({
|
||||||
variables: { jobId: job.id },
|
variables: { jobId: job.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -51,16 +122,31 @@ export function JobAdminMarkReexport({ bodyshop, job }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
//Get the owner details, populate it all back into the job.
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<>
|
||||||
loading={loading}
|
<Button
|
||||||
disabled={!job.date_exported}
|
loading={loading}
|
||||||
onClick={handleUpdate}
|
disabled={!job.date_exported}
|
||||||
>
|
onClick={handleMarkForExport}
|
||||||
{t("jobs.labels.markforreexport")}
|
>
|
||||||
</Button>
|
{t("jobs.labels.markforreexport")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
loading={loading}
|
||||||
|
disabled={job.date_exported}
|
||||||
|
onClick={handleMarkExported}
|
||||||
|
>
|
||||||
|
{t("jobs.actions.markasexported")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
loading={loading}
|
||||||
|
disabled={!job.date_invoiced || job.date_exported}
|
||||||
|
onClick={handleUninvoice}
|
||||||
|
>
|
||||||
|
{t("jobs.actions.uninvoice")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const GetSupplementDelta = async (client, jobId, newLines) => {
|
|||||||
query: GET_ALL_JOBLINES_BY_PK,
|
query: GET_ALL_JOBLINES_BY_PK,
|
||||||
variables: { id: jobId },
|
variables: { id: jobId },
|
||||||
});
|
});
|
||||||
|
|
||||||
const existingLines = _.cloneDeep(existingLinesFromDb);
|
const existingLines = _.cloneDeep(existingLinesFromDb);
|
||||||
const linesToInsert = [];
|
const linesToInsert = [];
|
||||||
const linesToUpdate = [];
|
const linesToUpdate = [];
|
||||||
@@ -19,11 +20,14 @@ export const GetSupplementDelta = async (client, jobId, newLines) => {
|
|||||||
const matchingIndex = existingLines.findIndex(
|
const matchingIndex = existingLines.findIndex(
|
||||||
(eL) => eL.unq_seq === newLine.unq_seq
|
(eL) => eL.unq_seq === newLine.unq_seq
|
||||||
);
|
);
|
||||||
|
|
||||||
|
//Should do a check to make sure there is only 1 matching unq sequence number.
|
||||||
|
|
||||||
if (matchingIndex >= 0) {
|
if (matchingIndex >= 0) {
|
||||||
//Found a relevant matching line. Add it to lines to update.
|
//Found a relevant matching line. Add it to lines to update.
|
||||||
linesToUpdate.push({
|
linesToUpdate.push({
|
||||||
id: existingLines[matchingIndex].id,
|
id: existingLines[matchingIndex].id,
|
||||||
newData: newLine,
|
newData: { ...newLine, removed: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
//Splice out item we found for performance.
|
//Splice out item we found for performance.
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export function JobsAvailableComponent({
|
|||||||
title: t("jobs.fields.cieca_id"),
|
title: t("jobs.fields.cieca_id"),
|
||||||
dataIndex: "cieca_id",
|
dataIndex: "cieca_id",
|
||||||
key: "cieca_id",
|
key: "cieca_id",
|
||||||
sorter: (a, b) => alphaSort(a, b),
|
sorter: (a, b) => alphaSort(a.cieca_id, b.cieca_id),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "cieca_id" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "cieca_id" && state.sortedInfo.order,
|
||||||
},
|
},
|
||||||
@@ -68,9 +68,10 @@ export function JobsAvailableComponent({
|
|||||||
//width: "8%",
|
//width: "8%",
|
||||||
// onFilter: (value, record) => record.ro_number.includes(value),
|
// onFilter: (value, record) => record.ro_number.includes(value),
|
||||||
// filteredValue: state.filteredInfo.text || null,
|
// filteredValue: state.filteredInfo.text || null,
|
||||||
sorter: (a, b) => alphaSort(a, b),
|
sorter: (a, b) =>
|
||||||
|
alphaSort(a.job && a.job.ro_number, b.job && b.job.ro_number),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "cieca_id" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "job_id" && state.sortedInfo.order,
|
||||||
render: (text, record) =>
|
render: (text, record) =>
|
||||||
record.job ? (
|
record.job ? (
|
||||||
<Link to={`/manage/jobs/${record.job.id}`}>
|
<Link to={`/manage/jobs/${record.job.id}`}>
|
||||||
@@ -87,7 +88,7 @@ export function JobsAvailableComponent({
|
|||||||
dataIndex: "ownr_name",
|
dataIndex: "ownr_name",
|
||||||
key: "ownr_name",
|
key: "ownr_name",
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
sorter: (a, b) => alphaSort(a.ownr_ln, b.ownr_ln),
|
sorter: (a, b) => alphaSort(a.ownr_name, b.ownr_name),
|
||||||
sortOrder:
|
sortOrder:
|
||||||
state.sortedInfo.columnKey === "ownr_name" && state.sortedInfo.order,
|
state.sortedInfo.columnKey === "ownr_name" && state.sortedInfo.order,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
import { Col, notification, Row } from "antd";
|
import { Col, notification, Row } from "antd";
|
||||||
import Axios from "axios";
|
import Axios from "axios";
|
||||||
import Dinero from "dinero.js";
|
import Dinero from "dinero.js";
|
||||||
import _ from "lodash";
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
@@ -25,10 +24,13 @@ import {
|
|||||||
import { INSERT_NEW_JOB, UPDATE_JOB } from "../../graphql/jobs.queries";
|
import { INSERT_NEW_JOB, UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||||
import { INSERT_NEW_NOTE } from "../../graphql/notes.queries";
|
import { INSERT_NEW_NOTE } from "../../graphql/notes.queries";
|
||||||
import { SEARCH_VEHICLE_BY_VIN } from "../../graphql/vehicles.queries";
|
import { SEARCH_VEHICLE_BY_VIN } from "../../graphql/vehicles.queries";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import {
|
import {
|
||||||
selectBodyshop,
|
selectBodyshop,
|
||||||
selectCurrentUser,
|
selectCurrentUser,
|
||||||
} from "../../redux/user/user.selectors";
|
} from "../../redux/user/user.selectors";
|
||||||
|
import confirmDialog from "../../utils/asyncConfirm";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import JobsAvailableScan from "../jobs-available-scan/jobs-available-scan.component";
|
import JobsAvailableScan from "../jobs-available-scan/jobs-available-scan.component";
|
||||||
import JobsFindModalContainer from "../jobs-find-modal/jobs-find-modal.container";
|
import JobsFindModalContainer from "../jobs-find-modal/jobs-find-modal.container";
|
||||||
@@ -42,8 +44,15 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
});
|
});
|
||||||
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
export function JobsAvailableContainer({ bodyshop, currentUser }) {
|
insertAuditTrail: ({ jobid, operation }) =>
|
||||||
|
dispatch(insertAuditTrail({ jobid, operation })),
|
||||||
|
});
|
||||||
|
export function JobsAvailableContainer({
|
||||||
|
bodyshop,
|
||||||
|
currentUser,
|
||||||
|
insertAuditTrail,
|
||||||
|
}) {
|
||||||
const { loading, error, data, refetch } = useQuery(QUERY_AVAILABLE_JOBS, {
|
const { loading, error, data, refetch } = useQuery(QUERY_AVAILABLE_JOBS, {
|
||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
});
|
});
|
||||||
@@ -66,7 +75,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
|
|||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
|
|
||||||
const estDataLazyLoad = useLazyQuery(QUERY_AVAILABLE_NEW_JOBS_EST_DATA_BY_PK);
|
const estDataLazyLoad = useLazyQuery(QUERY_AVAILABLE_NEW_JOBS_EST_DATA_BY_PK);
|
||||||
const [loadEstData, estData] = estDataLazyLoad;
|
const [loadEstData, estDataRaw] = estDataLazyLoad;
|
||||||
|
|
||||||
const importOptionsState = useState({ overrideHeaders: false });
|
const importOptionsState = useState({ overrideHeaders: false });
|
||||||
const importOptions = importOptionsState[0];
|
const importOptions = importOptionsState[0];
|
||||||
@@ -79,13 +88,9 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
|
|||||||
setOwnerModalVisible(false);
|
setOwnerModalVisible(false);
|
||||||
setInsertLoading(true);
|
setInsertLoading(true);
|
||||||
|
|
||||||
if (
|
const estData = replaceEmpty(estDataRaw.data.available_jobs_by_pk);
|
||||||
!(
|
|
||||||
estData.data &&
|
if (!(estData && estData.est_data)) {
|
||||||
estData.data.available_jobs_by_pk &&
|
|
||||||
estData.data.available_jobs_by_pk.est_data
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
//We don't have the right data. Error!
|
//We don't have the right data. Error!
|
||||||
setInsertLoading(false);
|
setInsertLoading(false);
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
@@ -93,32 +98,31 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
//IO-539 Check for Parts Rate on PAL for SGI use case.
|
||||||
|
await CheckTaxRates(estData.est_data, bodyshop);
|
||||||
|
console.log(estData);
|
||||||
const newTotals = (
|
const newTotals = (
|
||||||
await Axios.post("/job/totals", {
|
await Axios.post("/job/totals", {
|
||||||
job: {
|
job: {
|
||||||
...estData.data.available_jobs_by_pk.est_data,
|
...estData.est_data,
|
||||||
joblines: estData.data.available_jobs_by_pk.est_data.joblines.data,
|
joblines: estData.est_data.joblines.data,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
).data;
|
).data;
|
||||||
|
|
||||||
let existingVehicles;
|
let existingVehicles;
|
||||||
if (
|
if (estData.est_data.vehicle && estData.est_data.vin) {
|
||||||
estData.data.available_jobs_by_pk.est_data.vehicle &&
|
|
||||||
estData.data.available_jobs_by_pk.est_data.vin
|
|
||||||
) {
|
|
||||||
//There's vehicle data, need to double check the VIN.
|
//There's vehicle data, need to double check the VIN.
|
||||||
existingVehicles = await client.query({
|
existingVehicles = await client.query({
|
||||||
query: SEARCH_VEHICLE_BY_VIN,
|
query: SEARCH_VEHICLE_BY_VIN,
|
||||||
variables: {
|
variables: {
|
||||||
vin: estData.data.available_jobs_by_pk.est_data.vehicle.data.v_vin,
|
vin: estData.est_data.vehicle.data.v_vin,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newJob = {
|
const newJob = {
|
||||||
...estData.data.available_jobs_by_pk.est_data,
|
...estData.est_data,
|
||||||
clm_total: Dinero(newTotals.totals.total_repairs).toFormat("0.00"),
|
clm_total: Dinero(newTotals.totals.total_repairs).toFormat("0.00"),
|
||||||
owner_owing: Dinero(newTotals.totals.custPayable.total).toFormat("0.00"),
|
owner_owing: Dinero(newTotals.totals.custPayable.total).toFormat("0.00"),
|
||||||
job_totals: newTotals,
|
job_totals: newTotals,
|
||||||
@@ -136,16 +140,17 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
|
|||||||
: {}),
|
: {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (selectedOwner) {
|
||||||
|
newJob.ownerid = selectedOwner;
|
||||||
|
delete newJob.owner;
|
||||||
|
}
|
||||||
|
if (newJob.vehicleid) {
|
||||||
|
delete newJob.vehicle;
|
||||||
|
}
|
||||||
|
|
||||||
insertNewJob({
|
insertNewJob({
|
||||||
variables: {
|
variables: {
|
||||||
job: selectedOwner
|
job: newJob,
|
||||||
? Object.assign(
|
|
||||||
{},
|
|
||||||
newJob,
|
|
||||||
{ owner: null },
|
|
||||||
{ ownerid: selectedOwner }
|
|
||||||
)
|
|
||||||
: newJob,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
@@ -157,8 +162,13 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
|
|||||||
});
|
});
|
||||||
//Job has been inserted. Clean up the available jobs record.
|
//Job has been inserted. Clean up the available jobs record.
|
||||||
|
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: r.data.insert_jobs.returning[0].id,
|
||||||
|
operation: AuditTrailMapping.jobimported(),
|
||||||
|
});
|
||||||
|
|
||||||
deleteJob({
|
deleteJob({
|
||||||
variables: { id: estData.data.available_jobs_by_pk.id },
|
variables: { id: estData.id },
|
||||||
}).then((r) => {
|
}).then((r) => {
|
||||||
refetch();
|
refetch();
|
||||||
setInsertLoading(false);
|
setInsertLoading(false);
|
||||||
@@ -181,13 +191,9 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
|
|||||||
setJobModalVisible(false);
|
setJobModalVisible(false);
|
||||||
setInsertLoading(true);
|
setInsertLoading(true);
|
||||||
|
|
||||||
if (
|
const estData = estDataRaw.data.available_jobs_by_pk;
|
||||||
!(
|
|
||||||
estData.data &&
|
if (!(estData && estData.est_data)) {
|
||||||
estData.data.available_jobs_by_pk &&
|
|
||||||
estData.data.available_jobs_by_pk.est_data
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
//We don't have the right data. Error!
|
//We don't have the right data. Error!
|
||||||
setInsertLoading(false);
|
setInsertLoading(false);
|
||||||
notification["error"]({
|
notification["error"]({
|
||||||
@@ -195,18 +201,21 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
//create upsert job
|
//create upsert job
|
||||||
let supp = _.cloneDeep(estData.data.available_jobs_by_pk.est_data);
|
let supp = replaceEmpty({ ...estData.est_data });
|
||||||
|
//IO-539 Check for Parts Rate on PAL for SGI use case.
|
||||||
|
await CheckTaxRates(supp, bodyshop);
|
||||||
|
|
||||||
delete supp.owner;
|
delete supp.owner;
|
||||||
delete supp.vehicle;
|
delete supp.vehicle;
|
||||||
if (importOptions.overrideHeaders) {
|
delete supp.ins_co_nm;
|
||||||
|
if (!importOptions.overrideHeaders) {
|
||||||
HeaderFields.forEach((item) => delete supp[item]);
|
HeaderFields.forEach((item) => delete supp[item]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let suppDelta = await GetSupplementDelta(
|
let suppDelta = await GetSupplementDelta(
|
||||||
client,
|
client,
|
||||||
selectedJob,
|
selectedJob,
|
||||||
estData.data.available_jobs_by_pk.est_data.joblines.data
|
supp.joblines.data
|
||||||
);
|
);
|
||||||
|
|
||||||
delete supp.joblines;
|
delete supp.joblines;
|
||||||
@@ -265,7 +274,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
|
|||||||
//Job has been inserted. Clean up the available jobs record.
|
//Job has been inserted. Clean up the available jobs record.
|
||||||
|
|
||||||
deleteJob({
|
deleteJob({
|
||||||
variables: { id: estData.data.available_jobs_by_pk.id },
|
variables: { id: estData.id },
|
||||||
}).then((r) => {
|
}).then((r) => {
|
||||||
refetch();
|
refetch();
|
||||||
setInsertLoading(false);
|
setInsertLoading(false);
|
||||||
@@ -283,17 +292,21 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: selectedJob,
|
||||||
|
operation: AuditTrailMapping.jobsupplement(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const owner =
|
const owner =
|
||||||
estData.data &&
|
estDataRaw.data &&
|
||||||
estData.data.available_jobs_by_pk &&
|
estDataRaw.data.available_jobs_by_pk &&
|
||||||
estData.data.available_jobs_by_pk.est_data &&
|
estDataRaw.data.available_jobs_by_pk.est_data &&
|
||||||
estData.data.available_jobs_by_pk.est_data.owner &&
|
estDataRaw.data.available_jobs_by_pk.est_data.owner &&
|
||||||
estData.data.available_jobs_by_pk.est_data.owner.data &&
|
estDataRaw.data.available_jobs_by_pk.est_data.owner.data &&
|
||||||
!estData.data.available_jobs_by_pk.issupplement
|
!estDataRaw.data.available_jobs_by_pk.issupplement
|
||||||
? estData.data.available_jobs_by_pk.est_data.owner.data
|
? estDataRaw.data.available_jobs_by_pk.est_data.owner.data
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const onOwnerModalCancel = () => {
|
const onOwnerModalCancel = () => {
|
||||||
@@ -331,8 +344,8 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
|
|||||||
message={t("jobs.labels.creating_new_job")}
|
message={t("jobs.labels.creating_new_job")}
|
||||||
>
|
>
|
||||||
<OwnerFindModalContainer
|
<OwnerFindModalContainer
|
||||||
loading={estData.loading}
|
loading={estDataRaw.loading}
|
||||||
error={estData.error}
|
error={estDataRaw.error}
|
||||||
owner={owner}
|
owner={owner}
|
||||||
selectedOwner={selectedOwner}
|
selectedOwner={selectedOwner}
|
||||||
setSelectedOwner={setSelectedOwner}
|
setSelectedOwner={setSelectedOwner}
|
||||||
@@ -341,8 +354,8 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
|
|||||||
onCancel={onOwnerModalCancel}
|
onCancel={onOwnerModalCancel}
|
||||||
/>
|
/>
|
||||||
<JobsFindModalContainer
|
<JobsFindModalContainer
|
||||||
loading={estData.loading}
|
loading={estDataRaw.loading}
|
||||||
error={estData.error}
|
error={estDataRaw.error}
|
||||||
selectedJob={selectedJob}
|
selectedJob={selectedJob}
|
||||||
setSelectedJob={setSelectedJob}
|
setSelectedJob={setSelectedJob}
|
||||||
importOptionsState={importOptionsState}
|
importOptionsState={importOptionsState}
|
||||||
@@ -368,4 +381,129 @@ export function JobsAvailableContainer({ bodyshop, currentUser }) {
|
|||||||
</LoadingSpinner>
|
</LoadingSpinner>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
export default connect(mapStateToProps, null)(JobsAvailableContainer);
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps
|
||||||
|
)(JobsAvailableContainer);
|
||||||
|
|
||||||
|
function replaceEmpty(someObj, replaceValue = null) {
|
||||||
|
const replacer = (key, value) =>
|
||||||
|
value === "" ? replaceValue || null : value;
|
||||||
|
//^ because you seem to want to replace (strings) "null" or "undefined" too
|
||||||
|
const temp = JSON.stringify(someObj, replacer);
|
||||||
|
return JSON.parse(temp);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function CheckTaxRates(estData, bodyshop) {
|
||||||
|
//LKQ Check
|
||||||
|
if (
|
||||||
|
!estData.parts_tax_rates?.PAL ||
|
||||||
|
estData.parts_tax_rates?.PAL?.prt_tax_rt === null ||
|
||||||
|
estData.parts_tax_rates?.PAL?.prt_tax_rt === 0
|
||||||
|
) {
|
||||||
|
const res = await confirmDialog(
|
||||||
|
`ImEX Online has detected that there is a missing tax rate for LKQ parts. Pressing OK will set the tax rate to ${bodyshop.bill_tax_rates.state_tax_rate}% and enable the rate. Pressing cancel will keep the tax rate as is.`
|
||||||
|
);
|
||||||
|
if (res) {
|
||||||
|
if (!estData.parts_tax_rates.PAL) {
|
||||||
|
estData.parts_tax_rates.PAL = {
|
||||||
|
prt_discp: 0,
|
||||||
|
prt_mktyp: true,
|
||||||
|
prt_mkupp: 0,
|
||||||
|
prt_type: "PAL",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
estData.parts_tax_rates.PAL.prt_tax_rt =
|
||||||
|
bodyshop.bill_tax_rates.state_tax_rate / 100;
|
||||||
|
estData.parts_tax_rates.PAL.prt_tax_in = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//PAC Check
|
||||||
|
if (
|
||||||
|
!estData.parts_tax_rates?.PAC ||
|
||||||
|
estData.parts_tax_rates?.PAC?.prt_tax_rt === null ||
|
||||||
|
estData.parts_tax_rates?.PAC?.prt_tax_rt === 0
|
||||||
|
) {
|
||||||
|
const res = await confirmDialog(
|
||||||
|
`ImEX Online has detected that there is a missing tax rate for rechromed parts. Pressing OK will set the tax rate to ${bodyshop.bill_tax_rates.state_tax_rate}% and enable the rate. Pressing cancel will keep the tax rate as is.`
|
||||||
|
);
|
||||||
|
if (res) {
|
||||||
|
if (!estData.parts_tax_rates.PAC) {
|
||||||
|
estData.parts_tax_rates.PAC = {
|
||||||
|
prt_discp: 0,
|
||||||
|
prt_mktyp: true,
|
||||||
|
prt_mkupp: 0,
|
||||||
|
prt_type: "PAC",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
estData.parts_tax_rates.PAC.prt_tax_rt =
|
||||||
|
bodyshop.bill_tax_rates.state_tax_rate / 100;
|
||||||
|
estData.parts_tax_rates.PAC.prt_tax_in = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//PAM Check
|
||||||
|
if (
|
||||||
|
!estData.parts_tax_rates?.PAM ||
|
||||||
|
estData.parts_tax_rates?.PAM?.prt_tax_rt === null ||
|
||||||
|
estData.parts_tax_rates?.PAM?.prt_tax_rt === 0
|
||||||
|
) {
|
||||||
|
const res = await confirmDialog(
|
||||||
|
`ImEX Online has detected that there is a missing tax rate for remanufactured parts. Pressing OK will set the tax rate to ${bodyshop.bill_tax_rates.state_tax_rate}% and enable the rate. Pressing cancel will keep the tax rate as is.`
|
||||||
|
);
|
||||||
|
if (res) {
|
||||||
|
if (!estData.parts_tax_rates.PAM) {
|
||||||
|
estData.parts_tax_rates.PAM = {
|
||||||
|
prt_discp: 0,
|
||||||
|
prt_mktyp: true,
|
||||||
|
prt_mkupp: 0,
|
||||||
|
prt_type: "PAM",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
estData.parts_tax_rates.PAM.prt_tax_rt =
|
||||||
|
bodyshop.bill_tax_rates.state_tax_rate / 100;
|
||||||
|
estData.parts_tax_rates.PAM.prt_tax_in = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!estData.parts_tax_rates?.PAR ||
|
||||||
|
estData.parts_tax_rates?.PAR?.prt_tax_rt === null ||
|
||||||
|
estData.parts_tax_rates?.PAR?.prt_tax_rt === 0
|
||||||
|
) {
|
||||||
|
const res = await confirmDialog(
|
||||||
|
`ImEX Online has detected that there is a missing tax rate for recored parts. Pressing OK will set the tax rate to ${bodyshop.bill_tax_rates.state_tax_rate}% and enable the rate. Pressing cancel will keep the tax rate as is.`
|
||||||
|
);
|
||||||
|
if (res) {
|
||||||
|
if (!estData.parts_tax_rates.PAR) {
|
||||||
|
estData.parts_tax_rates.PAR = {
|
||||||
|
prt_discp: 0,
|
||||||
|
prt_mktyp: true,
|
||||||
|
prt_mkupp: 0,
|
||||||
|
prt_type: "PAR",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
estData.parts_tax_rates.PAR.prt_tax_rt =
|
||||||
|
bodyshop.bill_tax_rates.state_tax_rate / 100;
|
||||||
|
estData.parts_tax_rates.PAR.prt_tax_in = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//IO-1387 If a sublet line is NOT R&R, use the labor tax. If it is, use the sublet tax rate.
|
||||||
|
//Currently limited to SK shops only.
|
||||||
|
//if (bodyshop.region_config === "CA_SK") {
|
||||||
|
estData.joblines.data.forEach((jl, index) => {
|
||||||
|
if (
|
||||||
|
(jl.part_type === "PASL" || jl.part_type === "PAS") &&
|
||||||
|
jl.lbr_op !== "OP11"
|
||||||
|
) {
|
||||||
|
estData.joblines.data[index].tax_part = jl.lbr_tax;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Set markup lines and tax lines as taxable.
|
||||||
|
//900510 is a mark up. 900510 is a discount.
|
||||||
|
if (jl.db_ref === "900510") {
|
||||||
|
estData.joblines.data[index].tax_part = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,18 +6,21 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { UPDATE_JOB_STATUS } from "../../graphql/jobs.queries";
|
import { UPDATE_JOB_STATUS } from "../../graphql/jobs.queries";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
insertAuditTrail: ({ jobid, operation }) =>
|
||||||
|
dispatch(insertAuditTrail({ jobid, operation })),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function JobsChangeStatus({ job, bodyshop, jobRO }) {
|
export function JobsChangeStatus({ job, bodyshop, jobRO, insertAuditTrail }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [availableStatuses, setAvailableStatuses] = useState([]);
|
const [availableStatuses, setAvailableStatuses] = useState([]);
|
||||||
@@ -29,6 +32,10 @@ export function JobsChangeStatus({ job, bodyshop, jobRO }) {
|
|||||||
})
|
})
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
notification["success"]({ message: t("jobs.successes.save") });
|
notification["success"]({ message: t("jobs.successes.save") });
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: job.id,
|
||||||
|
operation: AuditTrailMapping.jobstatuschange(status),
|
||||||
|
});
|
||||||
// refetch();
|
// refetch();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Button } from "antd";
|
import { Button, Dropdown, Menu } from "antd";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -12,11 +12,8 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
|
|
||||||
export function JobsCloseAutoAllocate({ bodyshop, joblines, form, disabled }) {
|
export function JobsCloseAutoAllocate({ bodyshop, joblines, form, disabled }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const handleAllocate = () => {
|
|
||||||
logImEXEvent("jobs_close_allocate_auto");
|
|
||||||
|
|
||||||
const { defaults } = bodyshop.md_responsibility_centers;
|
|
||||||
|
|
||||||
|
const handleAllocate = (defaults) => {
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
joblines: joblines.map((jl) => {
|
joblines: joblines.map((jl) => {
|
||||||
const ret = _.cloneDeep(jl);
|
const ret = _.cloneDeep(jl);
|
||||||
@@ -32,7 +29,7 @@ export function JobsCloseAutoAllocate({ bodyshop, joblines, form, disabled }) {
|
|||||||
}
|
}
|
||||||
//Verify that this is also manually updated in server/job-costing
|
//Verify that this is also manually updated in server/job-costing
|
||||||
if (!jl.part_type && !jl.mod_lbr_ty) {
|
if (!jl.part_type && !jl.mod_lbr_ty) {
|
||||||
const lineDesc = jl.line_desc.toLowerCase();
|
const lineDesc = jl.line_desc ? jl.line_desc.toLowerCase() : "";
|
||||||
if (lineDesc.includes("shop materials")) {
|
if (lineDesc.includes("shop materials")) {
|
||||||
ret.profitcenter_part = defaults.profits["MASH"];
|
ret.profitcenter_part = defaults.profits["MASH"];
|
||||||
} else if (lineDesc.includes("paint/materials")) {
|
} else if (lineDesc.includes("paint/materials")) {
|
||||||
@@ -48,8 +45,36 @@ export function JobsCloseAutoAllocate({ bodyshop, joblines, form, disabled }) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const handleAutoAllocateClick = () => {
|
||||||
<Button onClick={handleAllocate} disabled={disabled}>
|
logImEXEvent("jobs_close_allocate_auto");
|
||||||
|
|
||||||
|
const { defaults } = bodyshop.md_responsibility_centers;
|
||||||
|
handleAllocate(defaults);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMenuClick = ({ item, key, keyPath, domEvent }) => {
|
||||||
|
logImEXEvent("jobs_close_allocate_auto_dms");
|
||||||
|
handleAllocate(
|
||||||
|
bodyshop.md_responsibility_centers.dms_defaults.find(
|
||||||
|
(x) => x.name === key
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const overlay = bodyshop.cdk_dealerid && (
|
||||||
|
<Menu onClick={handleMenuClick}>
|
||||||
|
{bodyshop.md_responsibility_centers.dms_defaults.map((mapping) => (
|
||||||
|
<Menu.Item key={mapping.name}>{mapping.name}</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
|
||||||
|
return bodyshop.cdk_dealerid ? (
|
||||||
|
<Dropdown overlay={overlay}>
|
||||||
|
<Button disabled={disabled}>{t("jobs.actions.dmsautoallocate")}</Button>
|
||||||
|
</Dropdown>
|
||||||
|
) : (
|
||||||
|
<Button onClick={handleAutoAllocateClick} disabled={disabled}>
|
||||||
{t("jobs.actions.autoallocate")}
|
{t("jobs.actions.autoallocate")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
} from "../../redux/user/user.selectors";
|
} from "../../redux/user/user.selectors";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||||
|
import { useHistory } from "react-router-dom";
|
||||||
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -24,57 +26,78 @@ export function JobsCloseExportButton({
|
|||||||
currentUser,
|
currentUser,
|
||||||
jobId,
|
jobId,
|
||||||
disabled,
|
disabled,
|
||||||
|
setSelectedJobs,
|
||||||
}) {
|
}) {
|
||||||
|
const history = useHistory();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [updateJob] = useMutation(UPDATE_JOB);
|
const [updateJob] = useMutation(UPDATE_JOB);
|
||||||
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
|
const [insertExportLog] = useMutation(INSERT_EXPORT_LOG);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const handleQbxml = async () => {
|
const handleQbxml = async () => {
|
||||||
|
//Check if it's a CDK setup.
|
||||||
|
if (bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) {
|
||||||
|
history.push(`/manage/dms?jobId=${jobId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
logImEXEvent("jobs_close_export");
|
logImEXEvent("jobs_close_export");
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
let QbXmlResponse;
|
//Check if it's a QBO Setup.
|
||||||
try {
|
|
||||||
QbXmlResponse = await axios.post(
|
|
||||||
"/accounting/qbxml/receivables",
|
|
||||||
{ jobIds: [jobId] },
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
console.log("handle -> XML", QbXmlResponse);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Error getting QBXML from Server.", error);
|
|
||||||
notification["error"]({
|
|
||||||
message: t("jobs.errors.exporting", {
|
|
||||||
error: "Unable to retrieve QBXML. " + JSON.stringify(error.message),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let PartnerResponse;
|
let PartnerResponse;
|
||||||
try {
|
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
|
||||||
PartnerResponse = await axios.post(
|
PartnerResponse = await axios.post(
|
||||||
"http://localhost:1337/qb/",
|
`/qbo/receivables`,
|
||||||
// "http://609feaeae986.ngrok.io/qb/",
|
|
||||||
QbXmlResponse.data,
|
|
||||||
{
|
{
|
||||||
headers: {
|
jobIds: [jobId],
|
||||||
Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
|
},
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} else {
|
||||||
console.log("Error connecting to quickbooks or partner.", error);
|
//Default is QBD
|
||||||
notification["error"]({
|
|
||||||
message: t("jobs.errors.exporting-partner"),
|
let QbXmlResponse;
|
||||||
});
|
try {
|
||||||
setLoading(false);
|
QbXmlResponse = await axios.post(
|
||||||
return;
|
"/accounting/qbxml/receivables",
|
||||||
|
{ jobIds: [jobId] },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log("handle -> XML", QbXmlResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error getting QBXML from Server.", error);
|
||||||
|
notification["error"]({
|
||||||
|
message: t("jobs.errors.exporting", {
|
||||||
|
error: "Unable to retrieve QBXML. " + JSON.stringify(error.message),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
PartnerResponse = await axios.post(
|
||||||
|
"http://localhost:1337/qb/",
|
||||||
|
// "http://609feaeae986.ngrok.io/qb/",
|
||||||
|
QbXmlResponse.data,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error connecting to quickbooks or partner.", error);
|
||||||
|
notification["error"]({
|
||||||
|
message: t("jobs.errors.exporting-partner"),
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("PartnerResponse", PartnerResponse);
|
console.log("PartnerResponse", PartnerResponse);
|
||||||
@@ -147,18 +170,18 @@ export function JobsCloseExportButton({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (setSelectedJobs) {
|
||||||
|
setSelectedJobs((selectedJobs) => {
|
||||||
|
return selectedJobs.filter((i) => i !== jobId);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button onClick={handleQbxml} loading={loading} disabled={disabled}>
|
||||||
onClick={handleQbxml}
|
|
||||||
loading={loading}
|
|
||||||
disabled={disabled}
|
|
||||||
type="dashed"
|
|
||||||
>
|
|
||||||
{t("jobs.actions.export")}
|
{t("jobs.actions.export")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
|
|||||||
<th>{t("joblines.fields.line_desc")}</th>
|
<th>{t("joblines.fields.line_desc")}</th>
|
||||||
<th>{t("joblines.fields.part_type")}</th>
|
<th>{t("joblines.fields.part_type")}</th>
|
||||||
<th>{t("joblines.fields.act_price")}</th>
|
<th>{t("joblines.fields.act_price")}</th>
|
||||||
|
<th>{t("joblines.fields.prt_dsmk_m")}</th>
|
||||||
<th>{t("joblines.fields.op_code_desc")}</th>
|
<th>{t("joblines.fields.op_code_desc")}</th>
|
||||||
<th>{t("joblines.fields.mod_lbr_ty")}</th>
|
<th>{t("joblines.fields.mod_lbr_ty")}</th>
|
||||||
<th>{t("joblines.fields.mod_lb_hrs")}</th>
|
<th>{t("joblines.fields.mod_lb_hrs")}</th>
|
||||||
@@ -70,6 +71,16 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
|
|||||||
<ReadOnlyFormItem type="currency" />
|
<ReadOnlyFormItem type="currency" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<Form.Item
|
||||||
|
span={2}
|
||||||
|
// label={t("joblines.fields.prt_dsmk_m")}
|
||||||
|
key={`${index}prt_dsmk_m`}
|
||||||
|
name={[field.name, "prt_dsmk_m"]}
|
||||||
|
>
|
||||||
|
<ReadOnlyFormItem type="currency" />
|
||||||
|
</Form.Item>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
span={2}
|
span={2}
|
||||||
@@ -108,7 +119,9 @@ export function JobsCloseLines({ bodyshop, job, jobRO }) {
|
|||||||
labelCol={{ span: 0 }}
|
labelCol={{ span: 0 }}
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
required: !!job.joblines[index].act_price,
|
required:
|
||||||
|
!!job.joblines[index].act_price ||
|
||||||
|
!!job.joblines[index].prt_dsmk_m,
|
||||||
//message: t("general.validation.required"),
|
//message: t("general.validation.required"),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useMutation } from "@apollo/client";
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Form,
|
Form,
|
||||||
|
Input,
|
||||||
notification,
|
notification,
|
||||||
Popover,
|
Popover,
|
||||||
Select,
|
Select,
|
||||||
@@ -13,8 +14,10 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { CONVERT_JOB_TO_RO } from "../../graphql/jobs.queries";
|
import { CONVERT_JOB_TO_RO } from "../../graphql/jobs.queries";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
import { selectJobReadOnly } from "../../redux/application/application.selectors";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
@@ -22,10 +25,18 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
jobRO: selectJobReadOnly,
|
jobRO: selectJobReadOnly,
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
insertAuditTrail: ({ jobid, operation }) =>
|
||||||
|
dispatch(insertAuditTrail({ jobid, operation })),
|
||||||
});
|
});
|
||||||
|
|
||||||
export function JobsConvertButton({ bodyshop, job, refetch, jobRO }) {
|
export function JobsConvertButton({
|
||||||
|
bodyshop,
|
||||||
|
job,
|
||||||
|
refetch,
|
||||||
|
jobRO,
|
||||||
|
insertAuditTrail,
|
||||||
|
parentFormIsFieldsTouched,
|
||||||
|
}) {
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [mutationConvertJob] = useMutation(CONVERT_JOB_TO_RO);
|
const [mutationConvertJob] = useMutation(CONVERT_JOB_TO_RO);
|
||||||
@@ -33,6 +44,10 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO }) {
|
|||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
|
|
||||||
const handleConvert = async (values) => {
|
const handleConvert = async (values) => {
|
||||||
|
if (parentFormIsFieldsTouched()) {
|
||||||
|
alert(t("jobs.labels.savebeforeconversion"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await mutationConvertJob({
|
const res = await mutationConvertJob({
|
||||||
variables: { jobId: job.id, ...values },
|
variables: { jobId: job.id, ...values },
|
||||||
@@ -43,6 +58,14 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO }) {
|
|||||||
notification["success"]({
|
notification["success"]({
|
||||||
message: t("jobs.successes.converted"),
|
message: t("jobs.successes.converted"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: job.id,
|
||||||
|
operation: AuditTrailMapping.jobconverted(
|
||||||
|
res.data.update_jobs.returning[0].ro_number
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -95,24 +118,32 @@ export function JobsConvertButton({ bodyshop, job, refetch, jobRO }) {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
{bodyshop.enforce_referral && (
|
{bodyshop.enforce_referral && (
|
||||||
<Form.Item
|
<>
|
||||||
name={"referral_source"}
|
<Form.Item
|
||||||
label={t("jobs.fields.referralsource")}
|
name={"referral_source"}
|
||||||
rules={[
|
label={t("jobs.fields.referralsource")}
|
||||||
{
|
rules={[
|
||||||
required: bodyshop.enforce_referral,
|
{
|
||||||
//message: t("general.validation.required"),
|
required: bodyshop.enforce_referral,
|
||||||
},
|
//message: t("general.validation.required"),
|
||||||
]}
|
},
|
||||||
>
|
]}
|
||||||
<Select>
|
>
|
||||||
{bodyshop.md_referral_sources.map((s) => (
|
<Select>
|
||||||
<Select.Option key={s} value={s}>
|
{bodyshop.md_referral_sources.map((s) => (
|
||||||
{s}
|
<Select.Option key={s} value={s}>
|
||||||
</Select.Option>
|
{s}
|
||||||
))}
|
</Select.Option>
|
||||||
</Select>
|
))}
|
||||||
</Form.Item>
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.referral_source_extra")}
|
||||||
|
name="referral_source_extra"
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.ca_gst_registrant")}
|
label={t("jobs.fields.ca_gst_registrant")}
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import FormItemPhone, {
|
|||||||
PhoneItemFormatterValidation,
|
PhoneItemFormatterValidation,
|
||||||
} from "../form-items-formatted/phone-form-item.component";
|
} from "../form-items-formatted/phone-form-item.component";
|
||||||
import JobsDetailRatesChangeButton from "../jobs-detail-rates-change-button/jobs-detail-rates-change-button.component";
|
import JobsDetailRatesChangeButton from "../jobs-detail-rates-change-button/jobs-detail-rates-change-button.component";
|
||||||
import { JobsDetailRatesParts } from "../jobs-detail-rates/jobs-detail-rates.parts.component";
|
import JobsDetailRatesParts from "../jobs-detail-rates/jobs-detail-rates.parts.component";
|
||||||
|
import JobsMarkPstExempt from "../jobs-mark-pst-exempt/jobs-mark-pst-exempt.component";
|
||||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
//currentUser: selectCurrentUser
|
//currentUser: selectCurrentUser
|
||||||
@@ -149,6 +150,9 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
|
|||||||
<Form.Item label={t("jobs.fields.loss_desc")} name="loss_desc">
|
<Form.Item label={t("jobs.fields.loss_desc")} name="loss_desc">
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.loss_of_use")} name="loss_of_use">
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.ponumber")} name="po_number">
|
<Form.Item label={t("jobs.fields.ponumber")} name="po_number">
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -180,6 +184,12 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
|
|||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.referral_source_extra")}
|
||||||
|
name="referral_source_extra"
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
</Collapse.Panel>
|
</Collapse.Panel>
|
||||||
<Collapse.Panel
|
<Collapse.Panel
|
||||||
@@ -187,10 +197,10 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
|
|||||||
header={t("menus.jobsdetail.financials")}
|
header={t("menus.jobsdetail.financials")}
|
||||||
>
|
>
|
||||||
<JobsDetailRatesChangeButton form={form} />
|
<JobsDetailRatesChangeButton form={form} />
|
||||||
|
<JobsMarkPstExempt form={form} />
|
||||||
<LayoutFormRow>
|
<LayoutFormRow>
|
||||||
<Form.Item label={t("jobs.fields.ded_amt")} name="ded_amt">
|
<Form.Item label={t("jobs.fields.ded_amt")} name="ded_amt">
|
||||||
<CurrencyInput />
|
<CurrencyInput min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.ded_status")} name="ded_status">
|
<Form.Item label={t("jobs.fields.ded_status")} name="ded_status">
|
||||||
<Select>
|
<Select>
|
||||||
|
|||||||
@@ -129,6 +129,19 @@ export default function JobsCreateOwnerInfoNewComponent() {
|
|||||||
>
|
>
|
||||||
<FormItemPhone disabled={!state.owner.new} />
|
<FormItemPhone disabled={!state.owner.new} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("owners.fields.ownr_ph2")}
|
||||||
|
name={["owner", "data", "ownr_ph2"]}
|
||||||
|
rules={[
|
||||||
|
({ getFieldValue }) =>
|
||||||
|
PhoneItemFormatterValidation(
|
||||||
|
getFieldValue,
|
||||||
|
"owner.data.ownr_ph2"
|
||||||
|
),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<FormItemPhone disabled={!state.owner.new} />
|
||||||
|
</Form.Item>
|
||||||
</LayoutFormRow>
|
</LayoutFormRow>
|
||||||
<LayoutFormRow grow>
|
<LayoutFormRow grow>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
|||||||
@@ -84,6 +84,18 @@ export default function JobsCreateOwnerInfoSearchComponent({
|
|||||||
tableState.sortedInfo.columnKey === "ownr_ph1" &&
|
tableState.sortedInfo.columnKey === "ownr_ph1" &&
|
||||||
tableState.sortedInfo.order,
|
tableState.sortedInfo.order,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: t("owners.fields.ownr_ph2"),
|
||||||
|
dataIndex: "ownr_ph2",
|
||||||
|
key: "ownr_ph2",
|
||||||
|
render: (text, record) => (
|
||||||
|
<PhoneFormatter>{record.ownr_ph2}</PhoneFormatter>
|
||||||
|
),
|
||||||
|
sorter: (a, b) => alphaSort(a.ownr_ph2, b.ownr_ph2),
|
||||||
|
sortOrder:
|
||||||
|
tableState.sortedInfo.columnKey === "ownr_ph2" &&
|
||||||
|
tableState.sortedInfo.order,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleTableChange = (pagination, filters, sorter) => {
|
const handleTableChange = (pagination, filters, sorter) => {
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { DownOutlined } from "@ant-design/icons";
|
||||||
|
import { Dropdown, Menu } from "antd";
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function JobsDetailChangeEstimator({ disabled, form, bodyshop }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const handleClick = ({ item, key, keyPath }) => {
|
||||||
|
const est = item.props.value;
|
||||||
|
form.setFieldsValue(est);
|
||||||
|
};
|
||||||
|
|
||||||
|
const menu = (
|
||||||
|
<div>
|
||||||
|
<Menu onClick={handleClick}>
|
||||||
|
{bodyshop.md_estimators.map((est, idx) => (
|
||||||
|
<Menu.Item value={est} key={idx}>
|
||||||
|
{`${est.est_ct_fn} ${est.est_ct_ln}`}
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown overlay={menu} disabled={disabled}>
|
||||||
|
<a
|
||||||
|
className="ant-dropdown-link"
|
||||||
|
href=" #"
|
||||||
|
onClick={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
{t("jobs.actions.changestimator")} <DownOutlined />
|
||||||
|
</a>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, null)(JobsDetailChangeEstimator);
|
||||||
@@ -69,6 +69,12 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
<FormRow header={t("jobs.forms.repairdates")}>
|
<FormRow header={t("jobs.forms.repairdates")}>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.date_last_contacted")}
|
||||||
|
name="date_last_contacted"
|
||||||
|
>
|
||||||
|
<DateTimePicker />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.scheduled_completion")}
|
label={t("jobs.fields.scheduled_completion")}
|
||||||
name="scheduled_completion"
|
name="scheduled_completion"
|
||||||
@@ -93,7 +99,6 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label={t("jobs.fields.scheduled_delivery")}
|
label={t("jobs.fields.scheduled_delivery")}
|
||||||
name="scheduled_delivery"
|
name="scheduled_delivery"
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
import { Col, Form, Input, InputNumber, Row, Select, Switch } from "antd";
|
import {
|
||||||
|
Col,
|
||||||
|
Divider,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
Row,
|
||||||
|
Select,
|
||||||
|
Switch,
|
||||||
|
} from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -12,6 +21,7 @@ import FormItemPhone, {
|
|||||||
PhoneItemFormatterValidation,
|
PhoneItemFormatterValidation,
|
||||||
} from "../form-items-formatted/phone-form-item.component";
|
} from "../form-items-formatted/phone-form-item.component";
|
||||||
import Car from "../job-damage-visual/job-damage-visual.component";
|
import Car from "../job-damage-visual/job-damage-visual.component";
|
||||||
|
import JobsDetailChangeEstimator from "../jobs-detail-change-estimator/jobs-detail-change-estimator.component";
|
||||||
import FormRow from "../layout-form-row/layout-form-row.component";
|
import FormRow from "../layout-form-row/layout-form-row.component";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
@@ -44,7 +54,14 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
|
|||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.ded_amt")} name="ded_amt">
|
<Form.Item label={t("jobs.fields.ded_amt")} name="ded_amt">
|
||||||
<CurrencyInput disabled={jobRO} />
|
<CurrencyInput disabled={jobRO} min={0} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.ded_note")} name="ded_note">
|
||||||
|
<Select disabled={jobRO}>
|
||||||
|
{bodyshop.md_ded_notes.map((n, index) => (
|
||||||
|
<Select.Option key={index}>{n}</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.policy_no")} name="policy_no">
|
<Form.Item label={t("jobs.fields.policy_no")} name="policy_no">
|
||||||
<Input disabled={jobRO} />
|
<Input disabled={jobRO} />
|
||||||
@@ -114,6 +131,12 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
|
|||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
label={t("jobs.fields.referral_source_extra")}
|
||||||
|
name="referral_source_extra"
|
||||||
|
>
|
||||||
|
<Input />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.alt_transport")} name="alt_transport">
|
<Form.Item label={t("jobs.fields.alt_transport")} name="alt_transport">
|
||||||
<Select disabled={jobRO} allowClear>
|
<Select disabled={jobRO} allowClear>
|
||||||
{bodyshop.appt_alt_transport.map((s) => (
|
{bodyshop.appt_alt_transport.map((s) => (
|
||||||
@@ -133,6 +156,9 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
|
|||||||
<Form.Item label={t("jobs.fields.loss_date")} name="loss_date">
|
<Form.Item label={t("jobs.fields.loss_date")} name="loss_date">
|
||||||
<FormDatePicker disabled={jobRO} />
|
<FormDatePicker disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item label={t("jobs.fields.loss_of_use")} name="loss_of_use">
|
||||||
|
<Input disabled={jobRO} />
|
||||||
|
</Form.Item>
|
||||||
<Form.Item label={t("jobs.fields.kmin")} name="kmin">
|
<Form.Item label={t("jobs.fields.kmin")} name="kmin">
|
||||||
<InputNumber precision={0} min={0} disabled={jobRO} />
|
<InputNumber precision={0} min={0} disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -185,8 +211,16 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
|
|||||||
)}
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Divider
|
||||||
|
orientation="left"
|
||||||
|
type="horizontal"
|
||||||
|
style={{ marginTop: ".8rem", float: "right" }}
|
||||||
|
>
|
||||||
|
{t("jobs.forms.appraiserinfo")}
|
||||||
|
</Divider>
|
||||||
|
|
||||||
<FormRow header={t("jobs.forms.appraiserinfo")}>
|
<JobsDetailChangeEstimator form={form} disabled={jobRO} />
|
||||||
|
<FormRow noDivider>
|
||||||
<Form.Item label={t("jobs.fields.est_co_nm")} name="est_co_nm">
|
<Form.Item label={t("jobs.fields.est_co_nm")} name="est_co_nm">
|
||||||
<Input disabled={jobRO} />
|
<Input disabled={jobRO} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { notification } from "antd";
|
import { notification } from "antd";
|
||||||
import i18n from "i18next";
|
import i18n from "i18next";
|
||||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||||
|
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||||
|
import { store } from "../../redux/store";
|
||||||
|
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||||
|
|
||||||
export default function AddToProduction(
|
export default function AddToProduction(
|
||||||
apolloClient,
|
apolloClient,
|
||||||
@@ -21,6 +24,13 @@ export default function AddToProduction(
|
|||||||
notification["success"]({
|
notification["success"]({
|
||||||
message: i18n.t("jobs.successes.save"),
|
message: i18n.t("jobs.successes.save"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
store.dispatch(
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid: jobId,
|
||||||
|
operation: AuditTrailMapping.jobinproductionchange(!remove),
|
||||||
|
})
|
||||||
|
);
|
||||||
if (completionCallback) completionCallback();
|
if (completionCallback) completionCallback();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ export function JobsDetailHeaderActions({
|
|||||||
context: {
|
context: {
|
||||||
jobId: job.id,
|
jobId: job.id,
|
||||||
job: job,
|
job: job,
|
||||||
|
alt_transport: job.alt_transport,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -142,7 +143,10 @@ export function JobsDetailHeaderActions({
|
|||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
key="entertimetickets"
|
key="entertimetickets"
|
||||||
disabled={!job.converted}
|
disabled={
|
||||||
|
!job.converted ||
|
||||||
|
(!bodyshop.tt_allow_post_to_invoiced && job.date_invoiced)
|
||||||
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
logImEXEvent("job_header_enter_time_ticekts");
|
logImEXEvent("job_header_enter_time_ticekts");
|
||||||
|
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export function JobsDetailHeaderCsi({
|
|||||||
replyTo: bodyshop.email,
|
replyTo: bodyshop.email,
|
||||||
},
|
},
|
||||||
template: {
|
template: {
|
||||||
name: TemplateList("job").csi_invitation.key,
|
name: TemplateList("job_special").csi_invitation_action.key,
|
||||||
variables: {
|
variables: {
|
||||||
id: result.data.insert_csi.returning[0].id,
|
id: result.data.insert_csi.returning[0].id,
|
||||||
},
|
},
|
||||||
@@ -147,7 +147,7 @@ export function JobsDetailHeaderCsi({
|
|||||||
replyTo: bodyshop.email,
|
replyTo: bodyshop.email,
|
||||||
},
|
},
|
||||||
template: {
|
template: {
|
||||||
name: TemplateList("job").csi_invitation.key,
|
name: TemplateList("job_special").csi_invitation_action.key,
|
||||||
variables: {
|
variables: {
|
||||||
id: job.csiinvites[0].id,
|
id: job.csiinvites[0].id,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,47 +21,57 @@ export function JobsDetailHeaderActionexportCustomerData({
|
|||||||
|
|
||||||
const handleExportCustData = async (e) => {
|
const handleExportCustData = async (e) => {
|
||||||
logImEXEvent("job_export_cust_data");
|
logImEXEvent("job_export_cust_data");
|
||||||
let QbXmlResponse;
|
|
||||||
try {
|
|
||||||
QbXmlResponse = await axios.post(
|
|
||||||
"/accounting/qbxml/receivables",
|
|
||||||
{ jobIds: [job.id], custDataOnly: true },
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
console.log("handle -> XML", QbXmlResponse);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Error getting QBXML from Server.", error);
|
|
||||||
notification["error"]({
|
|
||||||
message: t("jobs.errors.exporting", {
|
|
||||||
error: "Unable to retrieve QBXML. " + JSON.stringify(error.message),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let PartnerResponse;
|
let PartnerResponse;
|
||||||
try {
|
if (bodyshop.accountingconfig && bodyshop.accountingconfig.qbo) {
|
||||||
PartnerResponse = await axios.post(
|
PartnerResponse = await axios.post(`/qbo/receivables`, {
|
||||||
"http://localhost:1337/qb/",
|
jobIds: [job.id],
|
||||||
QbXmlResponse.data,
|
custDataOnly: true,
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("Error connecting to quickbooks or partner.", error);
|
|
||||||
notification["error"]({
|
|
||||||
message: t("jobs.errors.exporting-partner"),
|
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
//Default is QBD
|
||||||
|
|
||||||
return;
|
let QbXmlResponse;
|
||||||
|
try {
|
||||||
|
QbXmlResponse = await axios.post(
|
||||||
|
"/accounting/qbxml/receivables",
|
||||||
|
{ jobIds: [job.id], custDataOnly: true },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log("handle -> XML", QbXmlResponse);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error getting QBXML from Server.", error);
|
||||||
|
notification["error"]({
|
||||||
|
message: t("jobs.errors.exporting", {
|
||||||
|
error: "Unable to retrieve QBXML. " + JSON.stringify(error.message),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//let PartnerResponse;
|
||||||
|
try {
|
||||||
|
PartnerResponse = await axios.post(
|
||||||
|
"http://localhost:1337/qb/",
|
||||||
|
QbXmlResponse.data,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${await auth.currentUser.getIdToken()}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.log("Error connecting to quickbooks or partner.", error);
|
||||||
|
notification["error"]({
|
||||||
|
message: t("jobs.errors.exporting-partner"),
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//Check to see if any of them failed. If they didn't don't execute the update.
|
//Check to see if any of them failed. If they didn't don't execute the update.
|
||||||
const failedTransactions = PartnerResponse.data.filter((r) => !r.success);
|
const failedTransactions = PartnerResponse.data.filter((r) => !r.success);
|
||||||
|
|||||||