Compare commits
166 Commits
release/20
...
rrScratch2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
910d388e05 | ||
|
|
9faad53b99 | ||
|
|
3b07055d5a | ||
|
|
ec29a22984 | ||
|
|
2b1836d450 | ||
|
|
ae7d150a6c | ||
|
|
b2184a2d11 | ||
|
|
9b1c8fa72b | ||
|
|
6d6b64ebc3 | ||
|
|
c3bc29fa9b | ||
|
|
c954695d3c | ||
|
|
338d8e2136 | ||
|
|
6674206b4f | ||
|
|
c46ad521d1 | ||
|
|
34f45379a6 | ||
|
|
66e5bec4d8 | ||
|
|
0d3161ef84 | ||
|
|
1cd11bdc18 | ||
|
|
9cce2696e2 | ||
|
|
e20ef4374c | ||
|
|
43dc760c95 | ||
|
|
cfd5aaff87 | ||
|
|
6162e7f18d | ||
|
|
cdee754042 | ||
|
|
a8fb16122a | ||
|
|
a88be98d45 | ||
|
|
e44bc07ffe | ||
|
|
60e8aadd8c | ||
|
|
daf9f197eb | ||
|
|
5848daef72 | ||
|
|
bc77bec610 | ||
|
|
c655badae2 | ||
|
|
7af70f7512 | ||
|
|
a8dcc542cc | ||
|
|
e4c87dd06d | ||
|
|
16899007d8 | ||
|
|
9cb1b25b1d | ||
|
|
4c250f6189 | ||
|
|
9c2c0b665d | ||
|
|
09ea6dff2b | ||
|
|
577c3bec04 | ||
|
|
90f653c0b7 | ||
|
|
556cd993b9 | ||
|
|
e3b4620d0c | ||
|
|
cbfda822c6 | ||
|
|
52811f5f45 | ||
|
|
508d32d2d9 | ||
|
|
cccc307862 | ||
|
|
29049cf1b0 | ||
|
|
b517f3966d | ||
|
|
0772139a60 | ||
|
|
70028c8be6 | ||
|
|
3dc22bfdab | ||
|
|
91f419f4b3 | ||
|
|
f3ee421030 | ||
|
|
a5f8fbacc1 | ||
|
|
8bb58df32e | ||
|
|
1e3b3b853e | ||
|
|
02eb212758 | ||
|
|
6b41d6f2a2 | ||
|
|
d45d557a81 | ||
|
|
c0157454e1 | ||
|
|
00e6d31a88 | ||
|
|
e5ed11287d | ||
|
|
fa250f10a2 | ||
|
|
5a8a5bf7ab | ||
|
|
9ce022b5e8 | ||
|
|
b0b73f1af8 | ||
|
|
a788beaa19 | ||
|
|
d6b295855d | ||
|
|
e36baaa682 | ||
|
|
35f00df77e | ||
|
|
286c49deb1 | ||
|
|
a16c680d04 | ||
|
|
9341806b0f | ||
|
|
da28fe8592 | ||
|
|
f2faa5b686 | ||
|
|
bedca60744 | ||
|
|
5344a2031d | ||
|
|
c60dfa4319 | ||
|
|
aa692d4d05 | ||
|
|
5bbda89fb9 | ||
|
|
ccf3d7df5b | ||
|
|
eeb685802e | ||
|
|
ea14606538 | ||
|
|
3d24d44274 | ||
|
|
65e26ed5c9 | ||
|
|
a73617fd3c | ||
|
|
32a0e89467 | ||
|
|
e06f0f9918 | ||
|
|
319f3220ed | ||
|
|
a4da874a1a | ||
|
|
2f4c0e329a | ||
|
|
c6d083ce02 | ||
|
|
e514cf8d6a | ||
|
|
6671db1724 | ||
|
|
5a9381ebdb | ||
|
|
6bab792b5e | ||
|
|
64207ef76c | ||
|
|
de02b34a63 | ||
|
|
2ffc4b81f4 | ||
|
|
ec30e73b3e | ||
|
|
c149d457e7 | ||
|
|
88c00cf34f | ||
|
|
d16c8d5bd5 | ||
|
|
92b05a290e | ||
|
|
6421fc8002 | ||
|
|
0d9a7dda53 | ||
|
|
601eed6db8 | ||
|
|
9b708d5e8e | ||
|
|
6b4bc27205 | ||
|
|
0eb0e335fe | ||
|
|
71b08855b8 | ||
|
|
67b1a7f9f4 | ||
|
|
24f017bfd2 | ||
|
|
42027f0858 | ||
|
|
3b663d7954 | ||
|
|
e737af2d41 | ||
|
|
10d3b4a485 | ||
|
|
99b79126c3 | ||
|
|
d0eeb7d55d | ||
|
|
d95c5ce8f9 | ||
|
|
b995e1f35d | ||
|
|
3d112ed2cd | ||
|
|
e978a5a561 | ||
|
|
cd1e8b0b15 | ||
|
|
3d910aa246 | ||
|
|
f7799ffd03 | ||
|
|
57baa3d9fd | ||
|
|
c6cd6d0f4e | ||
|
|
f796dd0f89 | ||
|
|
517c30787d | ||
|
|
c8771275ce | ||
|
|
7c8260685e | ||
|
|
2fec9fd16e | ||
|
|
0f1348496c | ||
|
|
de26979e44 | ||
|
|
661678eb1c | ||
|
|
1728982b2b | ||
|
|
8aa747edc5 | ||
|
|
544494ce0d | ||
|
|
2e25324ae9 | ||
|
|
f525ec6fb8 | ||
|
|
8296d914c5 | ||
|
|
3d691568ff | ||
|
|
7ab070e2bc | ||
|
|
47a974e3cb | ||
|
|
eac81cdffb | ||
|
|
77c3e6f7e7 | ||
|
|
7f07da4360 | ||
|
|
c47144df72 | ||
|
|
163eaac110 | ||
|
|
19ce1c66ad | ||
|
|
e2b4b408ed | ||
|
|
567171c722 | ||
|
|
03acb78ab2 | ||
|
|
e7c4797fef | ||
|
|
88c35e8c48 | ||
|
|
8623172aa1 | ||
|
|
6ecc67184d | ||
|
|
09973ecb3b | ||
|
|
db9e86e4c8 | ||
|
|
85c446bc57 | ||
|
|
5cf6f47bdc | ||
|
|
1db4cbeeb8 | ||
|
|
c88bf4065e |
346
Fortellis Notes.md
Normal file
346
Fortellis Notes.md
Normal file
@@ -0,0 +1,346 @@
|
||||
Fortellis Notes
|
||||
|
||||
Subscription ID
|
||||
|
||||
- Appears to give us a list of all dealerships we have access to, and `apiDmsInfo` contains the integrations that are enabled for that dealership.
|
||||
- Will likely need to filter based on the DMS ID or something?
|
||||
- Should store the whole subscription object. Contains department information needed in subsequent calls.
|
||||
|
||||
Department ID
|
||||
|
||||
- May have multiple departments. Appears that financial stuff goes to Accounting, History will go to Service.
|
||||
- TODO: How do we handle the multiple departments that may come up.
|
||||
|
||||
###Internal Questions
|
||||
|
||||
* Overview of the redis storing mechanism to cache this data.
|
||||
*
|
||||
|
||||
# GL Wip Posting
|
||||
|
||||
## Org Helper Return Data
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"acctgLgnID": "DEVWB-A",
|
||||
"applCode": "V",
|
||||
"coID": "77",
|
||||
"companyName": "TEST SYS C187092 DEVWB",
|
||||
"lgnDesc": "DEV WRITE BACK VMS",
|
||||
"logon": "DEVWB-V"
|
||||
},
|
||||
{
|
||||
"acctgLgnID": "DEVWB-A",
|
||||
"applCode": "F",
|
||||
"coID": "77",
|
||||
"companyName": "TEST SYS C187092 DEVWB",
|
||||
"lgnDesc": "DEV WRITE BACK F&I SALES",
|
||||
"logon": "DEVWB-FI"
|
||||
},
|
||||
{
|
||||
"acctgLgnID": "DEVWB-A",
|
||||
"applCode": "CS",
|
||||
"coID": "77",
|
||||
"companyName": "TEST SYS C187092 DEVWB",
|
||||
"lgnDesc": "DEV WRITE BACK SERVICE",
|
||||
"logon": "DEVWB-S"
|
||||
},
|
||||
{
|
||||
"acctgLgnID": "DEVWB-A",
|
||||
"applCode": "A",
|
||||
"coID": "77",
|
||||
"companyName": "TEST SYS C187092 DEVWB",
|
||||
"lgnDesc": "DEV WRITE BACK ACCTG",
|
||||
"logon": "DEVWB-A"
|
||||
},
|
||||
{
|
||||
"acctgLgnID": "DEVWB-A",
|
||||
"applCode": "SL",
|
||||
"coID": "77",
|
||||
"companyName": "TEST SYS C187092 DEVWB",
|
||||
"lgnDesc": "DEV WRTIE BACK SLS MGMT",
|
||||
"logon": "DEVWB-SL"
|
||||
},
|
||||
{
|
||||
"acctgLgnID": "DEVWB-A",
|
||||
"applCode": "O",
|
||||
"coID": "77",
|
||||
"companyName": "TEST SYS C187092 DEVWB",
|
||||
"lgnDesc": "DEV WRITE BACK PARTS",
|
||||
"logon": "DEVWB-I"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Journal Helper Return Data
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"companyNo": "77",
|
||||
"jrnlID": "32",
|
||||
"jrnlName": "PARTS SALES",
|
||||
"jrnlType": "S",
|
||||
"intercoFlag": "0",
|
||||
"defaultDocType": "4",
|
||||
"errCode": "",
|
||||
"errMsg": ""
|
||||
},
|
||||
{
|
||||
"companyNo": "77",
|
||||
"jrnlID": "92",
|
||||
"jrnlName": "YTD ADJUSTMENTS",
|
||||
"jrnlType": "Y",
|
||||
"intercoFlag": "0",
|
||||
"defaultDocType": "3",
|
||||
"errCode": "",
|
||||
"errMsg": ""
|
||||
},
|
||||
{
|
||||
"companyNo": "77",
|
||||
"jrnlID": "12",
|
||||
"jrnlName": "FLEET SALES",
|
||||
"jrnlType": "S",
|
||||
"intercoFlag": "0",
|
||||
"defaultDocType": "9",
|
||||
"errCode": "",
|
||||
"errMsg": ""
|
||||
},
|
||||
{
|
||||
"companyNo": "77",
|
||||
"jrnlID": "57",
|
||||
"jrnlName": "CASH RECEIPTS (OPEN-ITEM)",
|
||||
"jrnlType": "R",
|
||||
"intercoFlag": "0",
|
||||
"defaultDocType": "1",
|
||||
"errCode": "",
|
||||
"errMsg": ""
|
||||
},
|
||||
{
|
||||
"companyNo": "77",
|
||||
"jrnlID": "93",
|
||||
"jrnlName": "SET UP HISTORY",
|
||||
"jrnlType": "H",
|
||||
"intercoFlag": "0",
|
||||
"defaultDocType": "10",
|
||||
"errCode": "",
|
||||
"errMsg": ""
|
||||
},
|
||||
{
|
||||
"companyNo": "77",
|
||||
"jrnlID": "88",
|
||||
"jrnlName": "F/S STATISCAL DATA",
|
||||
"jrnlType": "F",
|
||||
"intercoFlag": "0",
|
||||
"defaultDocType": "10",
|
||||
"errCode": "",
|
||||
"errMsg": ""
|
||||
},
|
||||
{
|
||||
"companyNo": "77",
|
||||
"jrnlID": "58",
|
||||
"jrnlName": "WARRANTY CREDITS",
|
||||
"jrnlType": "G",
|
||||
"intercoFlag": "0",
|
||||
"defaultDocType": "3",
|
||||
"errCode": "",
|
||||
"errMsg": ""
|
||||
},
|
||||
{
|
||||
"companyNo": "77",
|
||||
"jrnlID": "FC",
|
||||
"jrnlName": "FINANCE CHARGE",
|
||||
"jrnlType": "A",
|
||||
"intercoFlag": "0",
|
||||
"defaultDocType": "12",
|
||||
"errCode": "",
|
||||
"errMsg": ""
|
||||
},
|
||||
{
|
||||
"companyNo": "77",
|
||||
"jrnlID": "94",
|
||||
"jrnlName": "SET UP SCHEDULES",
|
||||
"jrnlType": "C",
|
||||
"intercoFlag": "0",
|
||||
"defaultDocType": "3",
|
||||
"errCode": "",
|
||||
"errMsg": ""
|
||||
},
|
||||
{
|
||||
"companyNo": "77",
|
||||
"jrnlID": "95",
|
||||
"jrnlName": "SET UP GENERAL LEDGER",
|
||||
"jrnlType": "B",
|
||||
"intercoFlag": "0",
|
||||
"defaultDocType": "3",
|
||||
"errCode": "",
|
||||
"errMsg": ""
|
||||
},
|
||||
{
|
||||
"companyNo": "77",
|
||||
"jrnlID": "20",
|
||||
"jrnlName": "USED VEHICLE SALES",
|
||||
"jrnlType": "S",
|
||||
"intercoFlag": "0",
|
||||
"defaultDocType": "9",
|
||||
"errCode": "",
|
||||
"errMsg": ""
|
||||
},
|
||||
{
|
||||
"companyNo": "77",
|
||||
"jrnlID": "60",
|
||||
"jrnlName": "CASH DISBURSEMENTS",
|
||||
"jrnlType": "G",
|
||||
"intercoFlag": "0",
|
||||
"defaultDocType": "2",
|
||||
"errCode": "",
|
||||
"errMsg": ""
|
||||
},
|
||||
{
|
||||
"companyNo": "77",
|
||||
"jrnlID": "30",
|
||||
"jrnlName": "SERVICE SALES",
|
||||
"jrnlType": "S",
|
||||
"intercoFlag": "0",
|
||||
"defaultDocType": "7",
|
||||
"errCode": "",
|
||||
"errMsg": ""
|
||||
},
|
||||
{
|
||||
"companyNo": "77",
|
||||
"jrnlID": "40",
|
||||
"jrnlName": "PAYROLL",
|
||||
"jrnlType": "G",
|
||||
"intercoFlag": "0",
|
||||
"defaultDocType": "11",
|
||||
"errCode": "",
|
||||
"errMsg": ""
|
||||
},
|
||||
{
|
||||
"companyNo": "77",
|
||||
"jrnlID": "15",
|
||||
"jrnlName": "DEALER TRADES",
|
||||
"jrnlType": "S",
|
||||
"intercoFlag": "0",
|
||||
"defaultDocType": "9",
|
||||
"errCode": "",
|
||||
"errMsg": ""
|
||||
},
|
||||
{
|
||||
"companyNo": "77",
|
||||
"jrnlID": "70",
|
||||
"jrnlName": "NEW VEHICLE PURCHASES",
|
||||
"jrnlType": "G",
|
||||
"intercoFlag": "0",
|
||||
"defaultDocType": "8",
|
||||
"errCode": "",
|
||||
"errMsg": ""
|
||||
},
|
||||
{
|
||||
"companyNo": "77",
|
||||
"jrnlID": "25",
|
||||
"jrnlName": "USED WHOLESALE",
|
||||
"jrnlType": "S",
|
||||
"intercoFlag": "0",
|
||||
"defaultDocType": "9",
|
||||
"errCode": "",
|
||||
"errMsg": ""
|
||||
},
|
||||
{
|
||||
"companyNo": "77",
|
||||
"jrnlID": "75",
|
||||
"jrnlName": "GENERAL PURCHASES",
|
||||
"jrnlType": "G",
|
||||
"intercoFlag": "0",
|
||||
"defaultDocType": "5",
|
||||
"errCode": "",
|
||||
"errMsg": ""
|
||||
},
|
||||
{
|
||||
"companyNo": "77",
|
||||
"jrnlID": "10",
|
||||
"jrnlName": "NEW VEHICLE SALES",
|
||||
"jrnlType": "S",
|
||||
"intercoFlag": "0",
|
||||
"defaultDocType": "9",
|
||||
"errCode": "",
|
||||
"errMsg": ""
|
||||
},
|
||||
{
|
||||
"companyNo": "77",
|
||||
"jrnlID": "80",
|
||||
"jrnlName": "GENERAL JOURNAL",
|
||||
"jrnlType": "G",
|
||||
"intercoFlag": "0",
|
||||
"defaultDocType": "3",
|
||||
"errCode": "",
|
||||
"errMsg": ""
|
||||
},
|
||||
{
|
||||
"companyNo": "77",
|
||||
"jrnlID": "11",
|
||||
"jrnlName": "WORK IN PROGRESS",
|
||||
"jrnlType": "G",
|
||||
"intercoFlag": "0",
|
||||
"defaultDocType": "10",
|
||||
"errCode": "",
|
||||
"errMsg": ""
|
||||
},
|
||||
{
|
||||
"companyNo": "77",
|
||||
"jrnlID": "56",
|
||||
"jrnlName": "CASH RECEIPTS (BALANCE FWD)",
|
||||
"jrnlType": "G",
|
||||
"intercoFlag": "0",
|
||||
"defaultDocType": "1",
|
||||
"errCode": "",
|
||||
"errMsg": ""
|
||||
},
|
||||
{
|
||||
"companyNo": "77",
|
||||
"jrnlID": "81",
|
||||
"jrnlName": "STANDARD ENTRIES",
|
||||
"jrnlType": "G",
|
||||
"intercoFlag": "0",
|
||||
"defaultDocType": "6",
|
||||
"errCode": "",
|
||||
"errMsg": ""
|
||||
},
|
||||
{
|
||||
"companyNo": "77",
|
||||
"jrnlID": "51",
|
||||
"jrnlName": "CASH RECEIPTS JOURNAL - EFT",
|
||||
"jrnlType": "G",
|
||||
"intercoFlag": "0",
|
||||
"defaultDocType": "10",
|
||||
"errCode": "",
|
||||
"errMsg": ""
|
||||
},
|
||||
{
|
||||
"companyNo": "77",
|
||||
"jrnlID": "61",
|
||||
"jrnlName": "CASH DISBURSMENTS -EFT",
|
||||
"jrnlType": "G",
|
||||
"intercoFlag": "0",
|
||||
"defaultDocType": "10",
|
||||
"errCode": "",
|
||||
"errMsg": ""
|
||||
},
|
||||
{
|
||||
"companyNo": "77",
|
||||
"jrnlID": "71",
|
||||
"jrnlName": "USED VEHICLE PURCHASES",
|
||||
"jrnlType": "G",
|
||||
"intercoFlag": "0",
|
||||
"defaultDocType": "8",
|
||||
"errCode": "",
|
||||
"errMsg": ""
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
# Feedback
|
||||
|
||||
- Receiving bad request errors, with no details. API errors page doesn't indicate what's wrong for certain types of error codes.
|
||||
- API Error page works on a several minute delay.
|
||||
708
client/package-lock.json
generated
708
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,41 +8,41 @@
|
||||
"private": true,
|
||||
"proxy": "http://localhost:4000",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-browser": "^2.25.2",
|
||||
"@amplitude/analytics-browser": "^2.30.1",
|
||||
"@ant-design/pro-layout": "^7.22.6",
|
||||
"@apollo/client": "^3.13.9",
|
||||
"@emotion/is-prop-valid": "^1.4.0",
|
||||
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
||||
"@firebase/analytics": "^0.10.17",
|
||||
"@firebase/app": "^0.14.3",
|
||||
"@firebase/auth": "^1.10.8",
|
||||
"@firebase/analytics": "^0.10.19",
|
||||
"@firebase/app": "^0.14.6",
|
||||
"@firebase/auth": "^1.11.1",
|
||||
"@firebase/firestore": "^4.9.2",
|
||||
"@firebase/messaging": "^0.12.22",
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@reduxjs/toolkit": "^2.9.0",
|
||||
"@sentry/cli": "^2.56.0",
|
||||
"@reduxjs/toolkit": "^2.10.1",
|
||||
"@sentry/cli": "^2.58.2",
|
||||
"@sentry/react": "^9.43.0",
|
||||
"@sentry/vite-plugin": "^4.3.0",
|
||||
"@splitsoftware/splitio-react": "^2.5.0",
|
||||
"@tanem/react-nprogress": "^5.0.53",
|
||||
"antd": "^5.27.4",
|
||||
"@sentry/vite-plugin": "^4.6.0",
|
||||
"@splitsoftware/splitio-react": "^2.6.0",
|
||||
"@tanem/react-nprogress": "^5.0.56",
|
||||
"antd": "^5.28.1",
|
||||
"apollo-link-logger": "^2.0.1",
|
||||
"apollo-link-sentry": "^4.4.0",
|
||||
"autosize": "^6.0.1",
|
||||
"axios": "^1.12.2",
|
||||
"axios": "^1.13.2",
|
||||
"classnames": "^2.5.1",
|
||||
"css-box-model": "^1.2.1",
|
||||
"dayjs": "^1.11.18",
|
||||
"dayjs-business-days2": "^1.3.0",
|
||||
"dayjs": "^1.11.19",
|
||||
"dayjs-business-days2": "^1.3.1",
|
||||
"dinero.js": "^1.9.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"env-cmd": "^10.1.0",
|
||||
"exifr": "^7.1.3",
|
||||
"graphql": "^16.11.0",
|
||||
"i18next": "^25.5.3",
|
||||
"graphql": "^16.12.0",
|
||||
"i18next": "^25.6.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"libphonenumber-js": "^1.12.23",
|
||||
"libphonenumber-js": "^1.12.26",
|
||||
"lightningcss": "^1.30.2",
|
||||
"logrocket": "^9.0.2",
|
||||
"markerjs2": "^2.32.7",
|
||||
@@ -50,7 +50,7 @@
|
||||
"normalize-url": "^8.1.0",
|
||||
"object-hash": "^3.0.0",
|
||||
"phone": "^3.1.67",
|
||||
"posthog-js": "^1.271.0",
|
||||
"posthog-js": "^1.294.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^9.3.1",
|
||||
"raf-schd": "^4.0.3",
|
||||
@@ -68,7 +68,7 @@
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-number-format": "^5.4.3",
|
||||
"react-popopo": "^2.1.9",
|
||||
"react-product-fruits": "^2.2.61",
|
||||
"react-product-fruits": "^2.2.62",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-resizable": "^3.0.5",
|
||||
"react-router-dom": "^6.30.0",
|
||||
@@ -78,10 +78,10 @@
|
||||
"redux": "^5.0.1",
|
||||
"redux-actions": "^3.0.3",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-saga": "^1.3.0",
|
||||
"redux-saga": "^1.4.2",
|
||||
"redux-state-sync": "^3.1.4",
|
||||
"reselect": "^5.1.1",
|
||||
"sass": "^1.93.2",
|
||||
"sass": "^1.94.0",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"styled-components": "^6.1.19",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
@@ -135,35 +135,35 @@
|
||||
"devDependencies": {
|
||||
"@ant-design/icons": "^6.1.0",
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/preset-react": "^7.27.1",
|
||||
"@dotenvx/dotenvx": "^1.51.0",
|
||||
"@babel/preset-react": "^7.28.5",
|
||||
"@dotenvx/dotenvx": "^1.51.1",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@eslint/js": "^9.37.0",
|
||||
"@playwright/test": "^1.56.0",
|
||||
"@sentry/webpack-plugin": "^4.3.0",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@playwright/test": "^1.56.1",
|
||||
"@sentry/webpack-plugin": "^4.6.0",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"browserslist": "^4.26.3",
|
||||
"browserslist": "^4.28.0",
|
||||
"browserslist-to-esbuild": "^2.1.1",
|
||||
"chalk": "^5.6.2",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^15.15.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"memfs": "^4.48.1",
|
||||
"memfs": "^4.51.0",
|
||||
"os-browserify": "^0.3.0",
|
||||
"playwright": "^1.56.0",
|
||||
"playwright": "^1.56.1",
|
||||
"react-error-overlay": "^6.1.0",
|
||||
"redux-logger": "^3.0.6",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"vite": "^7.1.9",
|
||||
"vite": "^7.2.2",
|
||||
"vite-plugin-babel": "^1.3.2",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-node-polyfills": "^0.24.0",
|
||||
"vite-plugin-pwa": "^1.0.3",
|
||||
"vite-plugin-pwa": "^1.1.0",
|
||||
"vite-plugin-style-import": "^2.0.0",
|
||||
"vitest": "^3.2.4",
|
||||
"workbox-window": "^7.3.0"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@use "react-big-calendar/lib/sass/styles" as rbc;
|
||||
|
||||
:root {
|
||||
--table-stripe-bg: #f4f4f4; /* Light mode table stripe */
|
||||
--menu-divider-color: #74695c; /* Light mode menu divider */
|
||||
@@ -211,9 +213,6 @@
|
||||
--svg-background: #FFF; /* Dark mode SVG background */
|
||||
}
|
||||
|
||||
// Global Styles
|
||||
@import "react-big-calendar/lib/sass/styles";
|
||||
|
||||
.ant-menu-item-divider {
|
||||
border-bottom: 1px solid var(--menu-divider-color) !important;
|
||||
}
|
||||
@@ -426,6 +425,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
.dms-equal-height-col {
|
||||
display: flex; // make the Col a flex container
|
||||
}
|
||||
|
||||
/* If the direct child is an AntD Card, make it fill the column */
|
||||
.dms-equal-height-col > .ant-card {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Optional: if you want the card body to fill vertically too */
|
||||
.dms-equal-height-col > .ant-card .ant-card-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
//.rbc-time-header-gutter {
|
||||
// padding: 0;
|
||||
//}
|
||||
//}
|
||||
|
||||
@@ -180,7 +180,7 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
|
||||
<Card
|
||||
extra={
|
||||
<Space wrap>
|
||||
{!bodyshop.cdk_dealerid && !bodyshop.pbs_serialnumber && (
|
||||
{!bodyshop.cdk_dealerid && !bodyshop.pbs_serialnumber && !bodyshop.rr_dealerid && (
|
||||
<>
|
||||
<JobMarkSelectedExported
|
||||
jobIds={selectedJobs}
|
||||
@@ -198,7 +198,7 @@ export function AccountingReceivablesTableComponent({ bodyshop, loading, jobs, r
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{bodyshop.accountingconfig && bodyshop.accountingconfig.qbo && <QboAuthorizeComponent />}
|
||||
{bodyshop.accountingconfig?.qbo && <QboAuthorizeComponent />}
|
||||
<Input.Search
|
||||
value={state.search}
|
||||
onChange={handleSearch}
|
||||
|
||||
@@ -26,6 +26,7 @@ import { CalculateBillTotal } from "../bill-form/bill-form.totals.utility";
|
||||
import { handleUpload as handleLocalUpload } from "../documents-local-upload/documents-local-upload.utility";
|
||||
import { handleUpload } from "../documents-upload/documents-upload.utility";
|
||||
import { handleUpload as handleUploadToImageProxy } from "../documents-upload-imgproxy/documents-upload-imgproxy.utility";
|
||||
import RbacWrapper from "../../components/rbac-wrapper/rbac-wrapper.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
billEnterModal: selectBillEnterModal,
|
||||
@@ -450,7 +451,9 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
||||
setEnterAgain(false);
|
||||
}}
|
||||
>
|
||||
<BillFormContainer form={form} disableInvNumber={billEnterModal.context.disableInvNumber} />
|
||||
<RbacWrapper action="bills:enter">
|
||||
<BillFormContainer form={form} disableInvNumber={billEnterModal.context.disableInvNumber} />
|
||||
</RbacWrapper>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||
import CiecaSelect from "../../utils/Ciecaselect";
|
||||
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -44,7 +45,7 @@ export function BillFormItemsExtendedFormItem({
|
||||
quantity: record.part_qty || 1,
|
||||
actual_price: record.act_price,
|
||||
cost_center: record.part_type
|
||||
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
|
||||
? bodyshopHasDmsKey(bodyshop)
|
||||
? record.part_type
|
||||
: responsibilityCenters.defaults && (responsibilityCenters.defaults.costs[record.part_type] || null)
|
||||
: null
|
||||
@@ -100,7 +101,7 @@ export function BillFormItemsExtendedFormItem({
|
||||
</Form.Item>
|
||||
<Form.Item label={t("billlines.fields.cost_center")} name={["billlineskeys", record.id, "cost_center"]}>
|
||||
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}>
|
||||
{bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
|
||||
{bodyshopHasDmsKey(bodyshop)
|
||||
? CiecaSelect(true, false)
|
||||
: responsibilityCenters.costs.map((item) => <Select.Option key={item.name}>{item.name}</Select.Option>)}
|
||||
</Select>
|
||||
|
||||
@@ -22,6 +22,7 @@ import VendorSearchSelect from "../vendor-search-select/vendor-search-select.com
|
||||
import BillFormLines from "./bill-form.lines.component";
|
||||
import { CalculateBillTotal } from "./bill-form.totals.utility";
|
||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
||||
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -354,7 +355,7 @@ export function BillFormComponent({
|
||||
<Form.Item span={3} label={t("bills.fields.local_tax_rate")} name="local_tax_rate">
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
|
||||
{bodyshopHasDmsKey(bodyshop) ? (
|
||||
<Form.Item span={2} label={t("bills.labels.federal_tax_exempt")} name="federal_tax_exempt">
|
||||
<Switch onChange={handleFederalTaxExemptSwitchToggle} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -10,6 +10,7 @@ import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import BillLineSearchSelect from "../bill-line-search-select/bill-line-search-select.component";
|
||||
import BilllineAddInventory from "../billline-add-inventory/billline-add-inventory.component";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
@@ -90,7 +91,7 @@ export function BillEnterModalLinesComponent({
|
||||
actual_price: opt.cost,
|
||||
original_actual_price: opt.cost,
|
||||
cost_center: opt.part_type
|
||||
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
|
||||
? bodyshopHasDmsKey(bodyshop)
|
||||
? opt.part_type !== "PAE"
|
||||
? opt.part_type
|
||||
: null
|
||||
@@ -322,7 +323,7 @@ export function BillEnterModalLinesComponent({
|
||||
},
|
||||
formInput: () => (
|
||||
<Select showSearch style={{ minWidth: "3rem" }} disabled={disabled}>
|
||||
{bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
|
||||
{bodyshopHasDmsKey(bodyshop)
|
||||
? CiecaSelect(true, false)
|
||||
: responsibilityCenters.costs.map((item) => <Select.Option key={item.name}>{item.name}</Select.Option>)}
|
||||
</Select>
|
||||
|
||||
@@ -55,7 +55,7 @@ export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) {
|
||||
});
|
||||
}
|
||||
}, [socket, socket.connected, billids]);
|
||||
console.log(allocationsSummary);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("general.labels.status"),
|
||||
@@ -122,7 +122,7 @@ export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) {
|
||||
<Form.Item
|
||||
name="journal"
|
||||
label={t("jobs.fields.dms.journal")}
|
||||
initialValue={bodyshop.cdk_configuration && bodyshop.cdk_configuration.default_journal}
|
||||
initialValue={bodyshop.cdk_configuration?.default_journal}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
|
||||
@@ -1,72 +1,97 @@
|
||||
import { Alert, Button, Card, Table, Typography } from "antd";
|
||||
import { useEffect, useState } from "react";
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { useCallback, useEffect, useState } 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";
|
||||
import { DMS_MAP } from "../../utils/dmsUtils";
|
||||
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { pageLimit } from "../../utils/config";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
const mapDispatchToProps = () => ({});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DmsAllocationsSummary);
|
||||
|
||||
export function DmsAllocationsSummary({ socket, bodyshop, jobId, title }) {
|
||||
/**
|
||||
* DMS Allocations Summary component
|
||||
* @param mode
|
||||
* @param socket
|
||||
* @param bodyshop
|
||||
* @param jobId
|
||||
* @param title
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
export function DmsAllocationsSummary({ mode, 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;
|
||||
// Resolve event name by mode (PBS reuses the CDK event per existing behavior)
|
||||
const allocationsEvent =
|
||||
mode === DMS_MAP.reynolds
|
||||
? "rr-calculate-allocations"
|
||||
: mode === DMS_MAP.fortellis
|
||||
? "fortellis-calculate-allocations"
|
||||
: /* "cdk" | "pbs" (legacy) */ "cdk-calculate-allocations";
|
||||
|
||||
const fetchAllocations = useCallback(() => {
|
||||
if (!socket || !jobId || !mode) return;
|
||||
|
||||
try {
|
||||
socket.emit(allocationsEvent, jobId, (ack) => {
|
||||
const list = Array.isArray(ack) ? ack : [];
|
||||
setAllocationsSummary(list);
|
||||
// Preserve side-channel used by the post form for discrepancy checks
|
||||
socket.allocationsSummary = list;
|
||||
});
|
||||
} catch {
|
||||
// Best-effort; leave table empty on error
|
||||
setAllocationsSummary([]);
|
||||
socket && (socket.allocationsSummary = []);
|
||||
}
|
||||
}, [socket, socket.connected, jobId]);
|
||||
}, [socket, jobId, mode, allocationsEvent]);
|
||||
|
||||
// Initial + whenever mode/socket/jobId changes
|
||||
useEffect(() => {
|
||||
fetchAllocations();
|
||||
}, [fetchAllocations]);
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("jobs.fields.dms.center"),
|
||||
dataIndex: "center",
|
||||
key: "center"
|
||||
},
|
||||
{ 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()
|
||||
render: (_text, record) => Dinero(record.sale).toFormat()
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.dms.cost"),
|
||||
dataIndex: "cost",
|
||||
key: "cost",
|
||||
render: (text, record) => Dinero(record.cost).toFormat()
|
||||
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?.dms_acctnumber
|
||||
render: (_text, record) => 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?.dms_acctnumber
|
||||
render: (_text, record) => 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?.dms_wip_acctnumber
|
||||
render: (_text, record) => record.costCenter?.dms_wip_acctnumber
|
||||
}
|
||||
];
|
||||
|
||||
@@ -74,11 +99,7 @@ export function DmsAllocationsSummary({ socket, bodyshop, jobId, title }) {
|
||||
<Card
|
||||
title={title}
|
||||
extra={
|
||||
<Button
|
||||
onClick={() => {
|
||||
socket.emit("cdk-calculate-allocations", jobId, (ack) => setAllocationsSummary(ack));
|
||||
}}
|
||||
>
|
||||
<Button onClick={fetchAllocations} aria-label={t("general.actions.refresh")}>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
}
|
||||
@@ -86,6 +107,7 @@ export function DmsAllocationsSummary({ socket, bodyshop, jobId, title }) {
|
||||
{bodyshop.pbs_configuration?.disablebillwip && (
|
||||
<Alert type="warning" message={t("jobs.labels.dms.disablebillwip")} />
|
||||
)}
|
||||
|
||||
<Table
|
||||
pagination={{ position: "top", defaultPageSize: pageLimit }}
|
||||
columns={columns}
|
||||
@@ -94,34 +116,25 @@ export function DmsAllocationsSummary({ socket, bodyshop, jobId, title }) {
|
||||
locale={{ emptyText: t("dms.labels.refreshallocations") }}
|
||||
scroll={{ x: true }}
|
||||
summary={() => {
|
||||
const totals =
|
||||
allocationsSummary &&
|
||||
allocationsSummary.reduce(
|
||||
(acc, val) => {
|
||||
return {
|
||||
totalSale: acc.totalSale.add(Dinero(val.sale)),
|
||||
totalCost: acc.totalCost.add(Dinero(val.cost))
|
||||
};
|
||||
},
|
||||
{
|
||||
totalSale: Dinero(),
|
||||
totalCost: Dinero()
|
||||
}
|
||||
);
|
||||
const totals = allocationsSummary?.reduce(
|
||||
(acc, val) => ({
|
||||
totalSale: acc.totalSale.add(Dinero(val.sale)),
|
||||
totalCost: acc.totalCost.add(Dinero(val.cost))
|
||||
}),
|
||||
{ totalSale: Dinero(), totalCost: Dinero() }
|
||||
) || { totalSale: Dinero(), totalCost: Dinero() };
|
||||
|
||||
const hasNonZeroSaleTotal = totals.totalSale.getAmount() !== 0;
|
||||
|
||||
return (
|
||||
<Table.Summary.Row>
|
||||
<Table.Summary.Cell>
|
||||
<Typography.Title level={4}>{t("general.labels.totals")}</Typography.Title>
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell>{totals && 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.Cell>{hasNonZeroSaleTotal ? totals.totalSale.toFormat() : null}</Table.Summary.Cell>
|
||||
<Table.Summary.Cell />
|
||||
<Table.Summary.Cell />
|
||||
<Table.Summary.Cell />
|
||||
</Table.Summary.Row>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
import { Alert, Button, Card, Table, Tabs, Typography } from "antd";
|
||||
import { SyncOutlined } from "@ant-design/icons";
|
||||
import { useCallback, useEffect, useMemo, useState } 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
|
||||
});
|
||||
const mapDispatchToProps = () => ({});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(RrAllocationsSummary);
|
||||
|
||||
/**
|
||||
* Normalize job allocations into a flat list for display / preview building.
|
||||
* @param ack
|
||||
* @returns {{center: *, sale, partsSale, laborTaxableSale, laborNonTaxableSale, extrasSale, cost, profitCenter, costCenter}[]|*[]}
|
||||
*/
|
||||
function normalizeJobAllocations(ack) {
|
||||
if (!ack || !Array.isArray(ack.jobAllocations)) return [];
|
||||
|
||||
return ack.jobAllocations.map((row) => ({
|
||||
center: row.center,
|
||||
|
||||
// legacy "sale" (total) if we ever want to show it again
|
||||
sale: row.sale || row.totalSale || null,
|
||||
|
||||
// bucketed sales used to build split ROGOG/ROLABOR
|
||||
partsSale: row.partsSale || null,
|
||||
laborTaxableSale: row.laborTaxableSale || null,
|
||||
laborNonTaxableSale: row.laborNonTaxableSale || null,
|
||||
extrasSale: row.extrasSale || null,
|
||||
|
||||
cost: row.cost || null,
|
||||
profitCenter: row.profitCenter || null,
|
||||
costCenter: row.costCenter || null
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* RR-specific DMS Allocations Summary
|
||||
* Focused on what we actually send to RR:
|
||||
* - ROGOG (split by taxable / non-taxable segments)
|
||||
* - ROLABOR shell
|
||||
*
|
||||
* The heavy lifting (ROGOG/ROLABOR split, cost allocation, tax flags)
|
||||
* is now done on the backend via buildRogogFromAllocations/buildRolaborFromRogog.
|
||||
* This component just renders the preview from `ack.rogg` / `ack.rolabor`.
|
||||
*/
|
||||
export function RrAllocationsSummary({ socket, bodyshop, jobId, title }) {
|
||||
const { t } = useTranslation();
|
||||
const [roggPreview, setRoggPreview] = useState(null);
|
||||
const [rolaborPreview, setRolaborPreview] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const fetchAllocations = useCallback(() => {
|
||||
if (!socket || !jobId) return;
|
||||
|
||||
try {
|
||||
socket.emit("rr-calculate-allocations", jobId, (ack) => {
|
||||
if (ack && ack.ok === false) {
|
||||
setRoggPreview(null);
|
||||
setRolaborPreview(null);
|
||||
setError(ack.error || t("dms.labels.allocations_error"));
|
||||
if (socket) {
|
||||
socket.allocationsSummary = [];
|
||||
socket.rrAllocationsRaw = ack;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const jobAllocRows = normalizeJobAllocations(ack);
|
||||
|
||||
setRoggPreview(ack?.rogg || null);
|
||||
setRolaborPreview(ack?.rolabor || null);
|
||||
setError(null);
|
||||
|
||||
if (socket) {
|
||||
socket.allocationsSummary = jobAllocRows;
|
||||
socket.rrAllocationsRaw = ack;
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
setRoggPreview(null);
|
||||
setRolaborPreview(null);
|
||||
setError(t("dms.labels.allocations_error"));
|
||||
if (socket) {
|
||||
socket.allocationsSummary = [];
|
||||
}
|
||||
}
|
||||
}, [socket, jobId, t]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAllocations();
|
||||
}, [fetchAllocations]);
|
||||
|
||||
const opCode = bodyshop?.rr_configuration?.baseOpCode || "28TOZ";
|
||||
|
||||
const segmentLabelMap = {
|
||||
partsExtras: "Parts/Extras",
|
||||
laborTaxable: "Taxable Labor",
|
||||
laborNonTaxable: "Non-Taxable Labor"
|
||||
};
|
||||
|
||||
const roggRows = useMemo(() => {
|
||||
if (!roggPreview || !Array.isArray(roggPreview.ops)) return [];
|
||||
const rows = [];
|
||||
roggPreview.ops.forEach((op) => {
|
||||
(op.lines || []).forEach((line, idx) => {
|
||||
const baseDesc = line.itemDesc;
|
||||
const segmentKind = op.segmentKind;
|
||||
const segmentCount = op.segmentCount || 0;
|
||||
const segmentLabel = segmentLabelMap[segmentKind] || segmentKind;
|
||||
const displayDesc = segmentCount > 1 && segmentLabel ? `${baseDesc} (${segmentLabel})` : baseDesc;
|
||||
|
||||
rows.push({
|
||||
key: `${op.jobNo}-${idx}`,
|
||||
opCode: op.opCode,
|
||||
jobNo: op.jobNo,
|
||||
breakOut: line.breakOut,
|
||||
itemType: line.itemType,
|
||||
itemDesc: displayDesc,
|
||||
custQty: line.custQty,
|
||||
custPayTypeFlag: line.custPayTypeFlag,
|
||||
custTxblNtxblFlag: line.custTxblNtxblFlag,
|
||||
custPrice: line.amount?.custPrice,
|
||||
dlrCost: line.amount?.dlrCost,
|
||||
// segment metadata for visual styling
|
||||
segmentKind,
|
||||
segmentCount
|
||||
});
|
||||
});
|
||||
});
|
||||
return rows;
|
||||
}, [roggPreview]);
|
||||
|
||||
const rolaborRows = useMemo(() => {
|
||||
if (!rolaborPreview || !Array.isArray(rolaborPreview.ops)) return [];
|
||||
return rolaborPreview.ops.map((op, idx) => ({
|
||||
key: `${op.jobNo}-${idx}`,
|
||||
opCode: op.opCode,
|
||||
jobNo: op.jobNo,
|
||||
custPayTypeFlag: op.custPayTypeFlag,
|
||||
custTxblNtxblFlag: op.custTxblNtxblFlag,
|
||||
payType: op.bill?.payType,
|
||||
amtType: op.amount?.amtType,
|
||||
custPrice: op.amount?.custPrice,
|
||||
totalAmt: op.amount?.totalAmt
|
||||
}));
|
||||
}, [rolaborPreview]);
|
||||
|
||||
// Totals for ROGOG (sum custPrice + dlrCost over all lines)
|
||||
const roggTotals = useMemo(() => {
|
||||
if (!roggPreview || !Array.isArray(roggPreview.ops)) {
|
||||
return { totalCustPrice: "0.00", totalDlrCost: "0.00" };
|
||||
}
|
||||
|
||||
let totalCustCents = 0;
|
||||
let totalCostCents = 0;
|
||||
|
||||
roggPreview.ops.forEach((op) => {
|
||||
(op.lines || []).forEach((line) => {
|
||||
const cp = parseFloat(line.amount?.custPrice || "0");
|
||||
if (!Number.isNaN(cp)) {
|
||||
totalCustCents += Math.round(cp * 100);
|
||||
}
|
||||
|
||||
const dc = parseFloat(line.amount?.dlrCost || "0");
|
||||
if (!Number.isNaN(dc)) {
|
||||
totalCostCents += Math.round(dc * 100);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
totalCustPrice: (totalCustCents / 100).toFixed(2),
|
||||
totalDlrCost: (totalCostCents / 100).toFixed(2)
|
||||
};
|
||||
}, [roggPreview]);
|
||||
|
||||
const roggColumns = [
|
||||
{ title: "JobNo", dataIndex: "jobNo", key: "jobNo" },
|
||||
{ title: "OpCode", dataIndex: "opCode", key: "opCode" },
|
||||
{ title: "BreakOut", dataIndex: "breakOut", key: "breakOut" },
|
||||
{ title: "ItemType", dataIndex: "itemType", key: "itemType" },
|
||||
{ title: "ItemDesc", dataIndex: "itemDesc", key: "itemDesc" },
|
||||
{ title: "CustQty", dataIndex: "custQty", key: "custQty" },
|
||||
{ title: "CustTxblFlag", dataIndex: "custTxblNtxblFlag", key: "custTxblNtxblFlag" },
|
||||
{ title: "CustPrice", dataIndex: "custPrice", key: "custPrice" },
|
||||
{ title: "DlrCost", dataIndex: "dlrCost", key: "dlrCost" }
|
||||
];
|
||||
|
||||
const rolaborColumns = [
|
||||
{ title: "JobNo", dataIndex: "jobNo", key: "jobNo" },
|
||||
{ title: "OpCode", dataIndex: "opCode", key: "opCode" },
|
||||
{ title: "CustPayType", dataIndex: "custPayTypeFlag", key: "custPayTypeFlag" },
|
||||
{ title: "CustTxblFlag", dataIndex: "custTxblNtxblFlag", key: "custTxblNtxblFlag" },
|
||||
{ title: "PayType", dataIndex: "payType", key: "payType" },
|
||||
{ title: "AmtType", dataIndex: "amtType", key: "amtType" },
|
||||
{ title: "CustPrice", dataIndex: "custPrice", key: "custPrice" },
|
||||
{ title: "TotalAmt", dataIndex: "totalAmt", key: "totalAmt" }
|
||||
];
|
||||
|
||||
const tabItems = [
|
||||
{
|
||||
key: "rogog",
|
||||
label: "ROGOG Preview",
|
||||
children: (
|
||||
<>
|
||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 8 }}>
|
||||
OpCode: <strong>{opCode}</strong>. Only centers with RR GOG mapping (rr_gogcode & rr_item_type) are
|
||||
included. Totals below reflect exactly what will be sent in ROGOG.
|
||||
</Typography.Paragraph>
|
||||
<Table
|
||||
pagination={false}
|
||||
columns={roggColumns}
|
||||
rowKey="key"
|
||||
dataSource={roggRows}
|
||||
locale={{ emptyText: "No ROGOG lines would be generated." }}
|
||||
scroll={{ x: true }}
|
||||
// 👇 visually highlight splits; especially taxable/non-taxable labor segments
|
||||
rowClassName={(record) => {
|
||||
if (
|
||||
record.segmentCount > 1 &&
|
||||
(record.segmentKind === "laborTaxable" || record.segmentKind === "laborNonTaxable")
|
||||
) {
|
||||
return "rr-allocations-tax-split-row";
|
||||
}
|
||||
if (record.segmentCount > 1) {
|
||||
return "rr-allocations-split-row";
|
||||
}
|
||||
return "";
|
||||
}}
|
||||
summary={() => {
|
||||
const hasCustTotal = Number(roggTotals.totalCustPrice) !== 0;
|
||||
const hasCostTotal = Number(roggTotals.totalDlrCost) !== 0;
|
||||
|
||||
return (
|
||||
<Table.Summary.Row>
|
||||
<Table.Summary.Cell index={0}>
|
||||
<Typography.Title level={5}>{t("general.labels.totals")}</Typography.Title>
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell index={1} />
|
||||
<Table.Summary.Cell index={2} />
|
||||
<Table.Summary.Cell index={3} />
|
||||
<Table.Summary.Cell index={4} />
|
||||
<Table.Summary.Cell index={5} />
|
||||
<Table.Summary.Cell index={6} />
|
||||
<Table.Summary.Cell index={7}>{hasCustTotal ? roggTotals.totalCustPrice : null}</Table.Summary.Cell>
|
||||
<Table.Summary.Cell index={8}>{hasCostTotal ? roggTotals.totalDlrCost : null}</Table.Summary.Cell>
|
||||
</Table.Summary.Row>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: "rolabor",
|
||||
label: "ROLABOR Preview",
|
||||
children: (
|
||||
<>
|
||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 8 }}>
|
||||
This mirrors the shell that would be sent for ROLABOR when all financials are carried in GOG.
|
||||
</Typography.Paragraph>
|
||||
<Table
|
||||
pagination={false}
|
||||
columns={rolaborColumns}
|
||||
rowKey="key"
|
||||
dataSource={rolaborRows}
|
||||
locale={{ emptyText: "No ROLABOR lines would be generated." }}
|
||||
scroll={{ x: true }}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={title}
|
||||
extra={
|
||||
<Button onClick={fetchAllocations} aria-label={t("general.actions.refresh")}>
|
||||
<SyncOutlined />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{bodyshop.pbs_configuration?.disablebillwip && (
|
||||
<Alert type="warning" message={t("jobs.labels.dms.disablebillwip")} />
|
||||
)}
|
||||
|
||||
{error && <Alert type="error" style={{ marginTop: 8, marginBottom: 8 }} message={error} />}
|
||||
|
||||
<Tabs defaultActiveKey="rogog" items={tabItems} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
currentUser: selectCurrentUser,
|
||||
@@ -18,15 +19,26 @@ export default connect(mapStateToProps, mapDispatchToProps)(DmsCdkMakesRefetch);
|
||||
export function DmsCdkMakesRefetch({ currentUser, bodyshop }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
treatments: { Fortellis }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["Fortellis"],
|
||||
splitKey: bodyshop.imexshopid
|
||||
});
|
||||
|
||||
if (!currentUser.email.includes("@imex.")) return null;
|
||||
|
||||
const handleRefetch = async () => {
|
||||
setLoading(true);
|
||||
await axios.post("/cdk/getvehicles", {
|
||||
cdk_dealerid: bodyshop.cdk_dealerid,
|
||||
bodyshopid: bodyshop.id
|
||||
});
|
||||
try {
|
||||
setLoading(true);
|
||||
await axios.post(`cdk${Fortellis.treatment === "on" ? "/fortellis" : ""}/getvehicles`, {
|
||||
cdk_dealerid: bodyshop.cdk_dealerid,
|
||||
bodyshopid: bodyshop.id
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { Button, Checkbox, Col, Table } from "antd";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
|
||||
export default function CDKCustomerSelector({ bodyshop, socket }) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [customerList, setCustomerList] = useState([]);
|
||||
const [selectedCustomer, setSelectedCustomer] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket) return;
|
||||
const handleCdkSelectCustomer = (list) => {
|
||||
setOpen(true);
|
||||
setCustomerList(Array.isArray(list) ? list : []);
|
||||
setSelectedCustomer(null);
|
||||
};
|
||||
socket.on("cdk-select-customer", handleCdkSelectCustomer);
|
||||
return () => {
|
||||
socket.off("cdk-select-customer", handleCdkSelectCustomer);
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
const onUseSelected = () => {
|
||||
if (!selectedCustomer) return;
|
||||
setOpen(false);
|
||||
socket.emit("cdk-selected-customer", selectedCustomer);
|
||||
setSelectedCustomer(null);
|
||||
};
|
||||
|
||||
const onUseGeneric = () => {
|
||||
const generic = bodyshop.cdk_configuration?.generic_customer_number || null;
|
||||
setOpen(false);
|
||||
socket.emit("cdk-selected-customer", generic);
|
||||
setSelectedCustomer(null);
|
||||
};
|
||||
|
||||
const onCreateNew = () => {
|
||||
setOpen(false);
|
||||
socket.emit("cdk-selected-customer", null);
|
||||
setSelectedCustomer(null);
|
||||
};
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const columns = [
|
||||
{ title: t("jobs.fields.dms.id"), dataIndex: ["id", "value"], key: "id" },
|
||||
{
|
||||
title: t("jobs.fields.dms.vinowner"),
|
||||
dataIndex: "vinOwner",
|
||||
key: "vinOwner",
|
||||
render: (_t, r) => <Checkbox disabled checked={r.vinOwner} />
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.dms.name1"),
|
||||
dataIndex: ["name1", "fullName"],
|
||||
key: "name1",
|
||||
sorter: (a, b) => alphaSort(a.name1?.fullName, b.name1?.fullName)
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.dms.address"),
|
||||
key: "address",
|
||||
render: (record) =>
|
||||
`${record.address?.addressLine && record.address.addressLine[0]}, ${record.address?.city} ${
|
||||
record.address?.stateOrProvince
|
||||
} ${record.address?.postalCode}`
|
||||
}
|
||||
];
|
||||
|
||||
const rowKey = (r) => r.id?.value || r.customerId;
|
||||
|
||||
return (
|
||||
<Col span={24}>
|
||||
<Table
|
||||
title={() => (
|
||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
||||
<Button onClick={onUseSelected} disabled={!selectedCustomer}>
|
||||
{t("jobs.actions.dms.useselected")}
|
||||
</Button>
|
||||
<Button onClick={onUseGeneric} disabled={!bodyshop.cdk_configuration?.generic_customer_number}>
|
||||
{t("jobs.actions.dms.usegeneric")}
|
||||
</Button>
|
||||
<Button onClick={onCreateNew}>{t("jobs.actions.dms.createnewcustomer")}</Button>
|
||||
</div>
|
||||
)}
|
||||
pagination={{ position: "top" }}
|
||||
columns={columns}
|
||||
rowKey={rowKey}
|
||||
dataSource={customerList}
|
||||
rowSelection={{
|
||||
onSelect: (r) => {
|
||||
const key = r.id?.value || r.customerId;
|
||||
setSelectedCustomer(key ? String(key) : null);
|
||||
},
|
||||
type: "radio",
|
||||
selectedRowKeys: selectedCustomer ? [selectedCustomer] : []
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
@@ -1,134 +1,54 @@
|
||||
import { Button, Checkbox, Col, Table } from "antd";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMemo } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { socket } from "../../pages/dms/dms.container";
|
||||
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
|
||||
import RRCustomerSelector from "./rr-customer-selector";
|
||||
import FortellisCustomerSelector from "./fortellis-customer-selector";
|
||||
import CDKCustomerSelector from "./cdk-customer-selector";
|
||||
import PBSCustomerSelector from "./pbs-customer-selector";
|
||||
import { DMS_MAP } from "../../utils/dmsUtils";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
const mapDispatchToProps = () => ({});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DmsCustomerSelector);
|
||||
|
||||
export function DmsCustomerSelector({ bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
const [customerList, setcustomerList] = useState([]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedCustomer, setSelectedCustomer] = useState(null);
|
||||
const [dmsType, setDmsType] = useState("cdk");
|
||||
/**
|
||||
* DMS Customer Selector component that renders the appropriate customer selector
|
||||
* @param props
|
||||
* @returns {JSX.Element|null}
|
||||
* @constructor
|
||||
*/
|
||||
export function DmsCustomerSelector(props) {
|
||||
const { bodyshop, jobid, socket, rrOptions = {} } = props;
|
||||
|
||||
socket.on("cdk-select-customer", (customerList) => {
|
||||
setOpen(true);
|
||||
setDmsType("cdk");
|
||||
setcustomerList(customerList);
|
||||
});
|
||||
socket.on("pbs-select-customer", (customerList) => {
|
||||
setOpen(true);
|
||||
setDmsType("pbs");
|
||||
setcustomerList(customerList);
|
||||
});
|
||||
// Centralized "mode" (provider + transport)
|
||||
const mode = props.mode;
|
||||
|
||||
const onUseSelected = () => {
|
||||
setOpen(false);
|
||||
socket.emit(`${dmsType}-selected-customer`, selectedCustomer);
|
||||
setSelectedCustomer(null);
|
||||
};
|
||||
// Stable base props for children
|
||||
const base = useMemo(() => ({ bodyshop, jobid, socket }), [bodyshop, jobid, socket]);
|
||||
|
||||
const onUseGeneric = () => {
|
||||
setOpen(false);
|
||||
socket.emit(`${dmsType}-selected-customer`, bodyshop.cdk_configuration.generic_customer_number);
|
||||
setSelectedCustomer(null);
|
||||
};
|
||||
|
||||
const onCreateNew = () => {
|
||||
setOpen(false);
|
||||
socket.emit(`${dmsType}-selected-customer`, null);
|
||||
setSelectedCustomer(null);
|
||||
};
|
||||
|
||||
const cdkColumns = [
|
||||
{
|
||||
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"],
|
||||
key: "name1",
|
||||
sorter: (a, b) => alphaSort(a.name1?.fullName, b.name1?.fullName)
|
||||
},
|
||||
|
||||
{
|
||||
title: t("jobs.fields.dms.address"),
|
||||
//dataIndex: ["name2", "fullName"],
|
||||
key: "address",
|
||||
render: (record) =>
|
||||
`${record.address?.addressLine && record.address.addressLine[0]}, ${record.address?.city} ${
|
||||
record.address?.stateOrProvince
|
||||
} ${record.address?.postalCode}`
|
||||
switch (mode) {
|
||||
case DMS_MAP.reynolds: {
|
||||
// Map rrOptions to current RR prop shape (you can also just pass rrOptions through and unpack in RR)
|
||||
const rrProps = {
|
||||
rrOpenRoLimit: rrOptions.openRoLimit,
|
||||
onRrOpenRoFinished: rrOptions.onOpenRoFinished,
|
||||
rrValidationPending: rrOptions.validationPending,
|
||||
onValidationFinished: rrOptions.onValidationFinished
|
||||
};
|
||||
return <RRCustomerSelector {...base} {...rrProps} />;
|
||||
}
|
||||
];
|
||||
|
||||
const pbsColumns = [
|
||||
{
|
||||
title: t("jobs.fields.dms.id"),
|
||||
dataIndex: "ContactId",
|
||||
key: "ContactId"
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.dms.name1"),
|
||||
key: "name1",
|
||||
sorter: (a, b) => alphaSort(a.LastName, b.LastName),
|
||||
render: (text, record) => `${record.FirstName || ""} ${record.LastName || ""}`
|
||||
},
|
||||
|
||||
{
|
||||
title: t("jobs.fields.dms.address"),
|
||||
key: "address",
|
||||
render: (record) => `${record.Address}, ${record.City} ${record.State} ${record.ZipCode}`
|
||||
}
|
||||
];
|
||||
|
||||
if (!open) return null;
|
||||
return (
|
||||
<Col span={24}>
|
||||
<Table
|
||||
title={() => (
|
||||
<div>
|
||||
<Button onClick={onUseSelected} disabled={!selectedCustomer}>
|
||||
{t("jobs.actions.dms.useselected")}
|
||||
</Button>
|
||||
<Button onClick={onUseGeneric} disabled={!bodyshop.cdk_configuration?.generic_customer_number}>
|
||||
{t("jobs.actions.dms.usegeneric")}
|
||||
</Button>
|
||||
<Button onClick={onCreateNew}>{t("jobs.actions.dms.createnewcustomer")}</Button>
|
||||
</div>
|
||||
)}
|
||||
pagination={{ position: "top" }}
|
||||
columns={dmsType === "cdk" ? cdkColumns : pbsColumns}
|
||||
rowKey={(record) => (dmsType === "cdk" ? record.id.value : record.ContactId)}
|
||||
dataSource={customerList}
|
||||
//onChange={handleTableChange}
|
||||
rowSelection={{
|
||||
onSelect: (record) => {
|
||||
setSelectedCustomer(dmsType === "cdk" ? record.id.value : record.ContactId);
|
||||
},
|
||||
type: "radio",
|
||||
selectedRowKeys: [selectedCustomer]
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
);
|
||||
case DMS_MAP.fortellis:
|
||||
return <FortellisCustomerSelector {...base} />;
|
||||
case DMS_MAP.cdk:
|
||||
return <CDKCustomerSelector {...base} />;
|
||||
case DMS_MAP.pbs:
|
||||
return <PBSCustomerSelector {...base} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { Button, Checkbox, Col, Table } from "antd";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
|
||||
export default function FortellisCustomerSelector({ bodyshop, jobid, socket }) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [customerList, setCustomerList] = useState([]);
|
||||
const [selectedCustomer, setSelectedCustomer] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket) return;
|
||||
const handleFortellisSelectCustomer = (list) => {
|
||||
setOpen(true);
|
||||
setCustomerList(Array.isArray(list) ? list : []);
|
||||
setSelectedCustomer(null);
|
||||
};
|
||||
socket.on("fortellis-select-customer", handleFortellisSelectCustomer);
|
||||
return () => {
|
||||
socket.off("fortellis-select-customer", handleFortellisSelectCustomer);
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
const onUseSelected = () => {
|
||||
if (!selectedCustomer) return;
|
||||
setOpen(false);
|
||||
socket.emit("fortellis-selected-customer", { selectedCustomerId: selectedCustomer, jobid });
|
||||
setSelectedCustomer(null);
|
||||
};
|
||||
|
||||
const onUseGeneric = () => {
|
||||
const generic = bodyshop.cdk_configuration?.generic_customer_number || null;
|
||||
setOpen(false);
|
||||
socket.emit("fortellis-selected-customer", { selectedCustomerId: generic, jobid });
|
||||
setSelectedCustomer(null);
|
||||
};
|
||||
|
||||
const onCreateNew = () => {
|
||||
setOpen(false);
|
||||
socket.emit("fortellis-selected-customer", { selectedCustomerId: null, jobid });
|
||||
setSelectedCustomer(null);
|
||||
};
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const columns = [
|
||||
{ title: t("jobs.fields.dms.id"), dataIndex: "customerId", key: "id" },
|
||||
{
|
||||
title: t("jobs.fields.dms.vinowner"),
|
||||
dataIndex: "vinOwner",
|
||||
key: "vinOwner",
|
||||
render: (_t, r) => <Checkbox disabled checked={r.vinOwner} />
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.dms.name1"),
|
||||
dataIndex: ["customerName", "firstName"],
|
||||
key: "firstName",
|
||||
sorter: (a, b) => alphaSort(a.customerName?.firstName, b.customerName?.firstName)
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.dms.name1"),
|
||||
dataIndex: ["customerName", "lastName"],
|
||||
key: "lastName",
|
||||
sorter: (a, b) => alphaSort(a.customerName?.lastName, b.customerName?.lastName)
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.dms.address"),
|
||||
key: "address",
|
||||
render: (record) =>
|
||||
`${record.postalAddress?.addressLine1 || ""}${
|
||||
record.postalAddress?.addressLine2 ? `, ${record.postalAddress.addressLine2}` : ""
|
||||
}, ${record.postalAddress?.city || ""} ${record.postalAddress?.state || ""} ${
|
||||
record.postalAddress?.postalCode || ""
|
||||
} ${record.postalAddress?.country || ""}`
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Col span={24}>
|
||||
<Table
|
||||
title={() => (
|
||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
||||
<Button onClick={onUseSelected} disabled={!selectedCustomer}>
|
||||
{t("jobs.actions.dms.useselected")}
|
||||
</Button>
|
||||
<Button onClick={onUseGeneric} disabled={!bodyshop.cdk_configuration?.generic_customer_number}>
|
||||
{t("jobs.actions.dms.usegeneric")}
|
||||
</Button>
|
||||
<Button onClick={onCreateNew}>{t("jobs.actions.dms.createnewcustomer")}</Button>
|
||||
</div>
|
||||
)}
|
||||
pagination={{ position: "top" }}
|
||||
columns={columns}
|
||||
rowKey={(r) => r.customerId}
|
||||
dataSource={customerList}
|
||||
rowSelection={{
|
||||
onSelect: (r) => setSelectedCustomer(r?.customerId ? String(r.customerId) : null),
|
||||
type: "radio",
|
||||
selectedRowKeys: selectedCustomer ? [selectedCustomer] : []
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Button, Col, Table } from "antd";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
|
||||
export default function PBSCustomerSelector({ bodyshop, socket }) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [customerList, setCustomerList] = useState([]);
|
||||
const [selectedCustomer, setSelectedCustomer] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket) return;
|
||||
const handlePbsSelectCustomer = (list) => {
|
||||
setOpen(true);
|
||||
setCustomerList(Array.isArray(list) ? list : []);
|
||||
setSelectedCustomer(null);
|
||||
};
|
||||
socket.on("pbs-select-customer", handlePbsSelectCustomer);
|
||||
return () => {
|
||||
socket.off("pbs-select-customer", handlePbsSelectCustomer);
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
const onUseSelected = () => {
|
||||
if (!selectedCustomer) return;
|
||||
setOpen(false);
|
||||
socket.emit("pbs-selected-customer", selectedCustomer);
|
||||
setSelectedCustomer(null);
|
||||
};
|
||||
|
||||
// Restores old behavior: reuse the CDK-named generic number for PBS too,
|
||||
// matching the previous single-component implementation.
|
||||
const onUseGeneric = () => {
|
||||
const generic = bodyshop?.cdk_configuration?.generic_customer_number || null;
|
||||
if (!generic) return;
|
||||
setOpen(false);
|
||||
socket.emit("pbs-selected-customer", generic);
|
||||
setSelectedCustomer(null);
|
||||
};
|
||||
|
||||
const onCreateNew = () => {
|
||||
setOpen(false);
|
||||
socket.emit("pbs-selected-customer", null);
|
||||
setSelectedCustomer(null);
|
||||
};
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const columns = [
|
||||
{ title: t("jobs.fields.dms.id"), dataIndex: "ContactId", key: "ContactId" },
|
||||
{
|
||||
title: t("jobs.fields.dms.name1"),
|
||||
key: "name1",
|
||||
sorter: (a, b) => alphaSort(a.LastName, b.LastName),
|
||||
render: (_t, r) => `${r.FirstName || ""} ${r.LastName || ""}`
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.dms.address"),
|
||||
key: "address",
|
||||
render: (r) => `${r.Address}, ${r.City} ${r.State} ${r.ZipCode}`
|
||||
}
|
||||
];
|
||||
|
||||
const hasGeneric = !!bodyshop?.cdk_configuration?.generic_customer_number;
|
||||
|
||||
return (
|
||||
<Col span={24}>
|
||||
<Table
|
||||
title={() => (
|
||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
||||
<Button onClick={onUseSelected} disabled={!selectedCustomer}>
|
||||
{t("jobs.actions.dms.useselected")}
|
||||
</Button>
|
||||
<Button onClick={onUseGeneric} disabled={!hasGeneric}>
|
||||
{t("jobs.actions.dms.usegeneric")}
|
||||
</Button>
|
||||
<Button onClick={onCreateNew}>{t("jobs.actions.dms.createnewcustomer")}</Button>
|
||||
</div>
|
||||
)}
|
||||
pagination={{ position: "top" }}
|
||||
columns={columns}
|
||||
rowKey={(r) => r.ContactId}
|
||||
dataSource={customerList}
|
||||
rowSelection={{
|
||||
onSelect: (r) => setSelectedCustomer(r?.ContactId ? String(r.ContactId) : null),
|
||||
type: "radio",
|
||||
selectedRowKeys: selectedCustomer ? [selectedCustomer] : []
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
import { Alert, Button, Checkbox, Col, message, Space, Table } from "antd";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { alphaSort } from "../../utils/sorters";
|
||||
|
||||
const normalizeRrList = (list) => {
|
||||
if (!Array.isArray(list)) return [];
|
||||
return list
|
||||
.map((row) => {
|
||||
const custNo = row.custNo || row.CustomerId || row.customerId || null;
|
||||
const name =
|
||||
row.name ||
|
||||
[row.CustomerName?.FirstName, row.CustomerName?.LastName].filter(Boolean).join(" ").trim() ||
|
||||
(custNo ? String(custNo) : "");
|
||||
if (!custNo) return null;
|
||||
const vinOwner = !!(row.vinOwner ?? row.isVehicleOwner);
|
||||
|
||||
const address =
|
||||
row.address && typeof row.address === "object"
|
||||
? {
|
||||
line1: row.address.line1 ?? row.address.addr1 ?? row.address.Address1 ?? undefined,
|
||||
line2: row.address.line2 ?? row.address.addr2 ?? row.address.Address2 ?? undefined,
|
||||
city: row.address.city ?? undefined,
|
||||
state: row.address.state ?? row.address.stateOrProvince ?? undefined,
|
||||
postalCode: row.address.postalCode ?? row.address.zip ?? undefined,
|
||||
country: row.address.country ?? row.address.countryCode ?? undefined
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return { custNo: String(custNo), name, vinOwner, address };
|
||||
})
|
||||
.filter(Boolean);
|
||||
};
|
||||
|
||||
const rrAddressToString = (addr) => {
|
||||
if (!addr) return "";
|
||||
const parts = [
|
||||
addr.line1,
|
||||
addr.line2,
|
||||
[addr.city, addr.state].filter(Boolean).join(" "),
|
||||
addr.postalCode,
|
||||
addr.country
|
||||
].filter(Boolean);
|
||||
return parts.join(", ");
|
||||
};
|
||||
|
||||
export default function RRCustomerSelector({
|
||||
jobid,
|
||||
socket,
|
||||
rrOpenRoLimit = false,
|
||||
onRrOpenRoFinished,
|
||||
rrValidationPending = false,
|
||||
onValidationFinished
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [customerList, setCustomerList] = useState([]);
|
||||
const [selectedCustomer, setSelectedCustomer] = useState(null);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
// Show dialog automatically when validation is pending
|
||||
useEffect(() => {
|
||||
if (rrValidationPending) setOpen(true);
|
||||
}, [rrValidationPending]);
|
||||
|
||||
// Listen for RR customer selection list
|
||||
useEffect(() => {
|
||||
if (!socket) return;
|
||||
const handleRrSelectCustomer = (list) => {
|
||||
const normalized = normalizeRrList(list);
|
||||
setOpen(true);
|
||||
setCustomerList(normalized);
|
||||
const firstOwner = normalized.find((r) => r.vinOwner)?.custNo;
|
||||
setSelectedCustomer(firstOwner ? String(firstOwner) : null);
|
||||
setRefreshing(false);
|
||||
};
|
||||
socket.on("rr-select-customer", handleRrSelectCustomer);
|
||||
return () => {
|
||||
socket.off("rr-select-customer", handleRrSelectCustomer);
|
||||
};
|
||||
}, [socket]);
|
||||
|
||||
// VIN owner set
|
||||
const rrOwnerSet = useMemo(() => {
|
||||
return new Set(customerList.filter((c) => c?.vinOwner || c?.isVehicleOwner).map((c) => String(c.custNo)));
|
||||
}, [customerList]);
|
||||
const rrHasVinOwner = rrOwnerSet.size > 0;
|
||||
|
||||
// Enforce VIN owner stays selected if present
|
||||
useEffect(() => {
|
||||
if (!rrHasVinOwner) return;
|
||||
const firstOwner = (customerList.find((c) => c.vinOwner) || {}).custNo;
|
||||
if (firstOwner && String(selectedCustomer) !== String(firstOwner)) {
|
||||
setSelectedCustomer(String(firstOwner));
|
||||
}
|
||||
}, [rrHasVinOwner, customerList, selectedCustomer]);
|
||||
|
||||
const onUseSelected = () => {
|
||||
if (!selectedCustomer) {
|
||||
message.warning(t("general.actions.select"));
|
||||
return;
|
||||
}
|
||||
if (rrHasVinOwner && !rrOwnerSet.has(String(selectedCustomer))) {
|
||||
message.warning(
|
||||
"This VIN is already assigned in Reynolds. Only the VIN owner can be selected. To choose a different customer, change ownership in Reynolds first."
|
||||
);
|
||||
return;
|
||||
}
|
||||
socket.emit("rr-selected-customer", { jobId: jobid, custNo: String(selectedCustomer) }, (ack) => {
|
||||
if (ack?.ok) {
|
||||
message.success(t("dms.messages.customerSelected"));
|
||||
} else if (ack?.error) {
|
||||
message.error(ack.error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onCreateNew = () => {
|
||||
if (rrHasVinOwner) return;
|
||||
socket.emit("rr-selected-customer", { jobId: jobid, create: true }, (ack) => {
|
||||
if (ack?.ok) {
|
||||
if (ack.custNo) setSelectedCustomer(String(ack.custNo));
|
||||
message.success(t("dms.messages.customerCreated"));
|
||||
} else if (ack?.error) {
|
||||
message.error(ack.error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const refreshRrSearch = () => {
|
||||
setRefreshing(true);
|
||||
const to = setTimeout(() => setRefreshing(false), 12000);
|
||||
const stop = () => {
|
||||
clearTimeout(to);
|
||||
setRefreshing(false);
|
||||
socket.off("export-failed", stop);
|
||||
socket.off("rr-select-customer", stop);
|
||||
};
|
||||
socket.once("rr-select-customer", stop);
|
||||
socket.once("export-failed", stop);
|
||||
socket.emit("rr-export-job", { jobId: jobid });
|
||||
};
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const columns = [
|
||||
{ title: t("jobs.fields.dms.id"), dataIndex: "custNo", key: "custNo" },
|
||||
{
|
||||
title: t("jobs.fields.dms.vinowner"),
|
||||
dataIndex: "vinOwner",
|
||||
key: "vinOwner",
|
||||
render: (_t, r) => <Checkbox disabled checked={!!(r.vinOwner ?? r.isVehicleOwner)} />
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.dms.name1"),
|
||||
dataIndex: "name",
|
||||
key: "name",
|
||||
sorter: (a, b) => alphaSort(a?.name, b?.name)
|
||||
},
|
||||
{
|
||||
title: t("jobs.fields.dms.address"),
|
||||
key: "address",
|
||||
render: (record) => rrAddressToString(record.address)
|
||||
}
|
||||
];
|
||||
|
||||
const rrDisableRow = (record) => {
|
||||
if (!rrHasVinOwner) return false;
|
||||
return !rrOwnerSet.has(String(record.custNo));
|
||||
};
|
||||
|
||||
return (
|
||||
<Col span={24}>
|
||||
<Table
|
||||
title={() => (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{/* Open RO limit banner */}
|
||||
{rrOpenRoLimit && (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
message="Open RO limit reached in Reynolds"
|
||||
description={
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<div>
|
||||
Reynolds has reached the maximum number of open Repair Orders for this Customer. Close or finalize
|
||||
an RO in Reynolds, then click <strong>Finished</strong> to continue.
|
||||
</div>
|
||||
<div>
|
||||
<Button type="primary" danger onClick={onRrOpenRoFinished}>
|
||||
Finished
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Validation step banner */}
|
||||
{rrValidationPending && (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="Complete Validation in Reynolds"
|
||||
description={
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
<div>
|
||||
We created the Repair Order. Please validate the totals and taxes in the DMS system. When done,
|
||||
click <strong>Finished</strong> to finalize and mark this export as complete.
|
||||
</div>
|
||||
<div>
|
||||
<Space>
|
||||
<Button type="primary" onClick={onValidationFinished}>
|
||||
Finished
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
||||
<Button onClick={onUseSelected} disabled={!selectedCustomer || rrOpenRoLimit}>
|
||||
{t("jobs.actions.dms.useselected")}
|
||||
</Button>
|
||||
{/* No generic in RR */}
|
||||
<Button onClick={onCreateNew} disabled={rrHasVinOwner}>
|
||||
{t("jobs.actions.dms.createnewcustomer")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{rrHasVinOwner && (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="VIN ownership enforced"
|
||||
description={
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", gap: 12 }}>
|
||||
<div>
|
||||
This VIN is already assigned in Reynolds. Only the VIN owner is selectable here. To use a
|
||||
different customer, please change the vehicle ownership in Reynolds first, then return to complete
|
||||
the export.
|
||||
</div>
|
||||
<Button onClick={refreshRrSearch} loading={refreshing}>
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
pagination={{ position: "top" }}
|
||||
columns={columns}
|
||||
rowKey={(r) => r.custNo}
|
||||
dataSource={customerList}
|
||||
rowSelection={{
|
||||
onSelect: (record) => setSelectedCustomer(record?.custNo ? String(record.custNo) : null),
|
||||
type: "radio",
|
||||
selectedRowKeys: selectedCustomer ? [selectedCustomer] : [],
|
||||
getCheckboxProps: (record) => ({ disabled: rrDisableRow(record) })
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
@@ -1,50 +1,234 @@
|
||||
import { Divider, Space, Tag, Timeline } from "antd";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import dayjs from "../../utils/day";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
||||
import { selectDarkMode } from "../../redux/application/application.selectors.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
|
||||
setSelectedHeader: (key) => dispatch(setSelectedHeader(key))
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
isDarkMode: selectDarkMode
|
||||
});
|
||||
|
||||
const mapDispatchToProps = () => ({});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DmsLogEvents);
|
||||
|
||||
export function DmsLogEvents({ logs }) {
|
||||
return (
|
||||
<Timeline
|
||||
pending
|
||||
reverse={true}
|
||||
items={logs.map((log, idx) => ({
|
||||
key: idx,
|
||||
color: LogLevelHierarchy(log.level),
|
||||
children: (
|
||||
<Space wrap align="start" style={{}}>
|
||||
<Tag color={LogLevelHierarchy(log.level)}>{log.level}</Tag>
|
||||
<span>{dayjs(log.timestamp).format("MM/DD/YYYY HH:mm:ss")}</span>
|
||||
<Divider type="vertical" />
|
||||
<span>{log.message}</span>
|
||||
</Space>
|
||||
)
|
||||
}))}
|
||||
/>
|
||||
export function DmsLogEvents({
|
||||
logs,
|
||||
detailsOpen,
|
||||
detailsNonce,
|
||||
isDarkMode,
|
||||
colorizeJson = false,
|
||||
showDetails = true
|
||||
}) {
|
||||
const [openSet, setOpenSet] = useState(() => new Set());
|
||||
|
||||
// Inject JSON highlight styles once (only when colorize is enabled)
|
||||
useEffect(() => {
|
||||
if (!colorizeJson) return;
|
||||
if (typeof document === "undefined") return;
|
||||
if (document.getElementById("json-highlight-styles")) return;
|
||||
const style = document.createElement("style");
|
||||
style.id = "json-highlight-styles";
|
||||
style.textContent = `
|
||||
.json-key { color: #fa8c16; }
|
||||
.json-string { color: #52c41a; }
|
||||
.json-number { color: #722ed1; }
|
||||
.json-boolean { color: #1890ff; }
|
||||
.json-null { color: #faad14; }
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}, [colorizeJson]);
|
||||
|
||||
// Trim openSet if logs shrink
|
||||
useEffect(() => {
|
||||
const len = (logs || []).length;
|
||||
setOpenSet((prev) => {
|
||||
const next = new Set();
|
||||
for (let i = 0; i < len; i++) if (prev.has(i)) next.add(i);
|
||||
return next;
|
||||
});
|
||||
}, [logs?.length]);
|
||||
|
||||
// Respond to global toggle button
|
||||
useEffect(() => {
|
||||
if (detailsNonce == null) return;
|
||||
const len = (logs || []).length;
|
||||
setOpenSet(detailsOpen ? new Set(Array.from({ length: len }, (_, i) => i)) : new Set());
|
||||
}, [detailsNonce, detailsOpen, logs?.length]);
|
||||
|
||||
const items = useMemo(
|
||||
() =>
|
||||
(logs || []).map((raw, idx) => {
|
||||
const { level, message, timestamp, meta } = normalizeLog(raw);
|
||||
|
||||
// Only treat meta as "present" when we are allowed to show details
|
||||
const hasMeta = !isEmpty(meta) && showDetails;
|
||||
const isOpen = hasMeta && openSet.has(idx);
|
||||
|
||||
return {
|
||||
key: idx,
|
||||
color: logLevelColor(level),
|
||||
children: (
|
||||
<Space direction="vertical" size={4} style={{ display: "flex" }}>
|
||||
{/* Row 1: summary + inline "Details" toggle */}
|
||||
<Space wrap align="start">
|
||||
<Tag color={logLevelColor(level)}>{level}</Tag>
|
||||
<Divider type="vertical" />
|
||||
<span>{dayjs(timestamp).format("MM/DD/YYYY HH:mm:ss")}</span>
|
||||
<Divider type="vertical" />
|
||||
<span>{message}</span>
|
||||
{hasMeta && (
|
||||
<>
|
||||
<Divider type="vertical" />
|
||||
<a
|
||||
role="button"
|
||||
aria-expanded={isOpen}
|
||||
onClick={() =>
|
||||
setOpenSet((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (isOpen) next.delete(idx);
|
||||
else next.add(idx);
|
||||
return next;
|
||||
})
|
||||
}
|
||||
style={{ cursor: "pointer", userSelect: "none" }}
|
||||
>
|
||||
{isOpen ? "Hide details" : "Details"}
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
</Space>
|
||||
|
||||
{/* Row 2: details body (only when open) */}
|
||||
{hasMeta && isOpen && (
|
||||
<div style={{ marginLeft: 6 }}>
|
||||
<JsonBlock isDarkMode={isDarkMode} data={meta} colorize={colorizeJson} />
|
||||
</div>
|
||||
)}
|
||||
</Space>
|
||||
)
|
||||
};
|
||||
}),
|
||||
[logs, openSet, colorizeJson, isDarkMode, showDetails]
|
||||
);
|
||||
|
||||
return <Timeline pending reverse items={items} />;
|
||||
}
|
||||
|
||||
function LogLevelHierarchy(level) {
|
||||
switch (level) {
|
||||
/**
|
||||
* Normalize various log input formats into a standard structure.
|
||||
* @param input
|
||||
* @returns {{level: string, message: *|string, timestamp: Date, meta: *}}
|
||||
*/
|
||||
const normalizeLog = (input) => {
|
||||
const n = input?.normalized || input || {};
|
||||
const level = (n.level || input?.level || "INFO").toString().toUpperCase();
|
||||
const message = n.message ?? input?.message ?? "";
|
||||
const meta = input?.meta != null ? input.meta : n.meta != null ? n.meta : undefined;
|
||||
const tsRaw = input?.timestamp ?? n.timestamp ?? input?.ts ?? Date.now();
|
||||
const timestamp = typeof tsRaw === "number" ? new Date(tsRaw) : new Date(tsRaw);
|
||||
return { level, message, timestamp, meta };
|
||||
};
|
||||
|
||||
/**
|
||||
* Map log level to tag color.
|
||||
* @param level
|
||||
* @returns {string}
|
||||
*/
|
||||
const logLevelColor = (level) => {
|
||||
switch ((level || "").toUpperCase()) {
|
||||
case "SILLY":
|
||||
return "purple";
|
||||
case "DEBUG":
|
||||
return "orange";
|
||||
case "INFO":
|
||||
return "blue";
|
||||
case "WARN":
|
||||
case "WARNING":
|
||||
return "yellow";
|
||||
case "ERROR":
|
||||
return "red";
|
||||
default:
|
||||
return 0;
|
||||
return "default";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a value is "empty" (null/undefined, empty array, or empty object).
|
||||
* @param v
|
||||
*/
|
||||
const isEmpty = (v) => {
|
||||
if (v == null) return true;
|
||||
if (Array.isArray(v)) return v.length === 0;
|
||||
if (typeof v === "object") return Object.keys(v).length === 0;
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Safely stringify an object to JSON, falling back to String() on failure.
|
||||
* @param obj
|
||||
* @param spaces
|
||||
* @returns {string}
|
||||
*/
|
||||
const safeStringify = (obj, spaces = 2) => {
|
||||
try {
|
||||
return JSON.stringify(obj, null, spaces);
|
||||
} catch {
|
||||
return String(obj);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* JSON display block with optional syntax highlighting.
|
||||
* @param data
|
||||
* @param colorize
|
||||
* @param isDarkMode
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const JsonBlock = ({ data, colorize, isDarkMode }) => {
|
||||
const jsonText = safeStringify(data, 2);
|
||||
const preStyle = {
|
||||
margin: "6px 0 0",
|
||||
maxWidth: 720,
|
||||
overflowX: "auto",
|
||||
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
|
||||
fontSize: 12,
|
||||
lineHeight: 1.45,
|
||||
padding: 8,
|
||||
borderRadius: 6,
|
||||
background: isDarkMode ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.04)",
|
||||
border: isDarkMode ? "1px solid rgba(255,255,255,0.12)" : "1px solid rgba(0,0,0,0.08)",
|
||||
color: isDarkMode ? "var(--card-text-fallback)" : "#141414"
|
||||
};
|
||||
|
||||
if (colorize) {
|
||||
const html = syntaxHighlight(jsonText);
|
||||
return <pre style={preStyle} dangerouslySetInnerHTML={{ __html: html }} />;
|
||||
}
|
||||
return <pre style={preStyle}>{jsonText}</pre>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Syntax highlight JSON text for HTML display.
|
||||
* @param jsonText
|
||||
* @returns {*}
|
||||
*/
|
||||
const syntaxHighlight = (jsonText) => {
|
||||
const esc = jsonText.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
return esc.replace(
|
||||
/("(?:\\u[\da-fA-F]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(?:true|false|null)\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g,
|
||||
(match) => {
|
||||
let cls = "json-number";
|
||||
if (match.startsWith('"')) {
|
||||
cls = match.endsWith(":") ? "json-key" : "json-string";
|
||||
} else if (match === "true" || match === "false") {
|
||||
cls = "json-boolean";
|
||||
} else if (match === "null") {
|
||||
cls = "json-null";
|
||||
}
|
||||
return `<span class="${cls}">${match}</span>`;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
403
client/src/components/dms-post-form/cdklike-dms-post-form.jsx
Normal file
403
client/src/components/dms-post-form/cdklike-dms-post-form.jsx
Normal file
@@ -0,0 +1,403 @@
|
||||
import { DeleteFilled, DownOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Divider,
|
||||
Dropdown,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Statistic,
|
||||
Switch,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMemo, useState } from "react";
|
||||
import i18n from "../../translations/i18n";
|
||||
import dayjs from "../../utils/day";
|
||||
import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component";
|
||||
import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component";
|
||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||
import { DMS_MAP } from "../../utils/dmsUtils";
|
||||
|
||||
/**
|
||||
* CDK-like DMS post form:
|
||||
* - CDK / Fortellis / PBS
|
||||
* - CDK vehicle details + make/model selection
|
||||
* - Payer list with discrepancy gating
|
||||
* - Submit: "{mode}-export-job"
|
||||
* @param bodyshop
|
||||
* @param socket
|
||||
* @param job
|
||||
* @param logsRef
|
||||
* @param mode
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
export default function CdkLikePostForm({ bodyshop, socket, job, logsRef, mode }) {
|
||||
const [form] = Form.useForm();
|
||||
const { t } = useTranslation();
|
||||
const [, /*unused*/ setTick] = useState(0); // handy if you need a forceUpdate later
|
||||
|
||||
const initialValues = useMemo(
|
||||
() => ({
|
||||
story: `${t("jobs.labels.dms.defaultstory", {
|
||||
ro_number: job.ro_number,
|
||||
ownr_nm: `${job.ownr_fn || ""} ${job.ownr_ln || ""} ${job.ownr_co_nm || ""}`.trim(),
|
||||
ins_co_nm: job.ins_co_nm || "N/A",
|
||||
clm_po: `${job.clm_no ? `${job.clm_no} ` : ""}${job.po_number || ""}`
|
||||
}).trim()}.${
|
||||
job.area_of_damage?.impact1
|
||||
? " " +
|
||||
t("jobs.labels.dms.damageto", {
|
||||
area_of_damage: (job.area_of_damage && job.area_of_damage.impact1.padStart(2, "0")) || "UNKNOWN"
|
||||
})
|
||||
: ""
|
||||
}`.slice(0, 239),
|
||||
inservicedate: dayjs(
|
||||
`${
|
||||
(job.v_model_yr &&
|
||||
(job.v_model_yr < 100
|
||||
? job.v_model_yr >= (dayjs().year() + 1) % 100
|
||||
? 1900 + parseInt(job.v_model_yr, 10)
|
||||
: 2000 + parseInt(job.v_model_yr, 10)
|
||||
: job.v_model_yr)) ||
|
||||
2019
|
||||
}-01-01`
|
||||
),
|
||||
journal: bodyshop.cdk_configuration?.default_journal
|
||||
}),
|
||||
[job, bodyshop, t]
|
||||
);
|
||||
|
||||
// Payers helpers
|
||||
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?.[cdkPayer.control_type]
|
||||
};
|
||||
})
|
||||
});
|
||||
setTick((n) => n + 1);
|
||||
};
|
||||
|
||||
const handleFinish = (values) => {
|
||||
if (!socket) return;
|
||||
|
||||
if (mode === DMS_MAP.fortellis) {
|
||||
socket.emit("fortellis-export-job", {
|
||||
jobid: job.id,
|
||||
txEnvelope: { ...values, SubscriptionID: bodyshop.cdk_dealerid }
|
||||
});
|
||||
} else {
|
||||
socket.emit(`${mode}-export-job`, { jobid: job.id, txEnvelope: values });
|
||||
}
|
||||
|
||||
logsRef?.current?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
// Totals & discrepancy
|
||||
const totals = socket?.allocationsSummary
|
||||
? socket.allocationsSummary.reduce(
|
||||
(acc, val) => ({
|
||||
totalSale: acc.totalSale.add(Dinero(val.sale)),
|
||||
totalCost: acc.totalCost.add(Dinero(val.cost))
|
||||
}),
|
||||
{ totalSale: Dinero(), totalCost: Dinero() }
|
||||
)
|
||||
: { totalSale: Dinero(), totalCost: Dinero() };
|
||||
|
||||
return (
|
||||
<Card title={t("jobs.labels.dms.postingform")}>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleFinish}
|
||||
style={{ width: "100%" }}
|
||||
initialValues={initialValues}
|
||||
>
|
||||
{/* TOP ROW */}
|
||||
<Row gutter={[16, 12]} align="bottom">
|
||||
<Col xs={24} sm={12} md={8} lg={6}>
|
||||
<Form.Item name="journal" label={t("jobs.fields.dms.journal")} rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={12} sm={8} md={6} lg={4}>
|
||||
<Form.Item name="kmin" label={t("jobs.fields.kmin")} initialValue={job?.kmin} rules={[{ required: true }]}>
|
||||
<InputNumber style={{ width: "100%" }} disabled />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={12} sm={8} md={6} lg={4}>
|
||||
<Form.Item
|
||||
name="kmout"
|
||||
label={t("jobs.fields.kmout")}
|
||||
initialValue={job?.kmout}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<InputNumber style={{ width: "100%" }} disabled />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* CDK vehicle details (kept for CDK/Fortellis paths when dealer id exists) */}
|
||||
{bodyshop.cdk_dealerid && (
|
||||
<>
|
||||
<Row gutter={[16, 12]}>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Form.Item name="dms_make" label={t("jobs.fields.dms.dms_make")} rules={[{ required: true }]}>
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Form.Item name="dms_model" label={t("jobs.fields.dms.dms_model")} rules={[{ required: true }]}>
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Form.Item name="inservicedate" label={t("jobs.fields.dms.inservicedate")}>
|
||||
<DateTimePicker isDateOnly />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 12]} align="middle">
|
||||
<Col>
|
||||
<DmsCdkMakes form={form} job={job} />
|
||||
</Col>
|
||||
<Col>
|
||||
<DmsCdkMakesRefetch />
|
||||
</Col>
|
||||
<Col>
|
||||
<Form.Item name="dms_unsold" label={t("jobs.fields.dms.dms_unsold")} initialValue={false}>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col>
|
||||
<Form.Item
|
||||
name="dms_model_override"
|
||||
label={t("jobs.fields.dms.dms_model_override")}
|
||||
initialValue={false}
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Row gutter={[16, 12]}>
|
||||
<Col span={24}>
|
||||
<Form.Item name="story" label={t("jobs.fields.dms.story")} rules={[{ required: true }]}>
|
||||
<Input.TextArea maxLength={240} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Totals */}
|
||||
<Space size="large" wrap align="center" style={{ marginBottom: 16 }}>
|
||||
<Statistic
|
||||
title={t("jobs.fields.ded_amt")}
|
||||
value={Dinero(job.job_totals.totals.custPayable.deductible).toFormat()}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("jobs.labels.total_cust_payable")}
|
||||
value={Dinero(job.job_totals.totals.custPayable.total).toFormat()}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("jobs.labels.net_repairs")}
|
||||
value={Dinero(job.job_totals.totals.net_repairs).toFormat()}
|
||||
/>
|
||||
</Space>
|
||||
|
||||
{/* Payers list */}
|
||||
<Divider />
|
||||
<Form.List name={["payers"]}>
|
||||
{(fields, { add, remove }) => (
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
<Card
|
||||
key={field.key}
|
||||
size="small"
|
||||
style={{ marginBottom: 12 }}
|
||||
title={`${t("jobs.fields.dms.payer.payer_type")} #${index + 1}`}
|
||||
extra={
|
||||
<Tooltip title={t("general.actions.remove", "Remove")}>
|
||||
<Button
|
||||
type="text"
|
||||
danger
|
||||
icon={<DeleteFilled />}
|
||||
aria-label={t("general.actions.remove", "Remove")}
|
||||
onClick={() => remove(field.name)}
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<Row gutter={[16, 8]} align="middle">
|
||||
<Col xs={24} sm={12} md={8} lg={6}>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.dms.payer.name")}
|
||||
name={[field.name, "name"]}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select style={{ minWidth: "15rem" }} onSelect={(value) => handlePayerSelect(value, index)}>
|
||||
{bodyshop.cdk_configuration?.payers?.map((payer) => (
|
||||
<Select.Option key={payer.name}>{payer.name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={8} lg={6}>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.dms.payer.dms_acctnumber")}
|
||||
name={[field.name, "dms_acctnumber"]}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={8} lg={5}>
|
||||
<Form.Item
|
||||
label={t("jobs.fields.dms.payer.amount")}
|
||||
name={[field.name, "amount"]}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<CurrencyInput min={0} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={10} lg={7}>
|
||||
<Form.Item
|
||||
label={
|
||||
<div>
|
||||
{t("jobs.fields.dms.payer.controlnumber")}{" "}
|
||||
<Dropdown
|
||||
trigger={["click"]}
|
||||
menu={{
|
||||
items:
|
||||
bodyshop.cdk_configuration.controllist?.map((key, idx) => ({
|
||||
key: idx,
|
||||
label: key.name,
|
||||
onClick: () => {
|
||||
form.setFieldsValue({
|
||||
payers: (form.getFieldValue("payers") || []).map((row, mapIndex) => {
|
||||
if (index !== mapIndex) return row;
|
||||
return { ...row, controlnumber: key.controlnumber };
|
||||
})
|
||||
});
|
||||
}
|
||||
})) ?? []
|
||||
}}
|
||||
>
|
||||
<a href="#" onClick={(e) => e.preventDefault()}>
|
||||
<DownOutlined />
|
||||
</a>
|
||||
</Dropdown>
|
||||
</div>
|
||||
}
|
||||
name={[field.name, "controlnumber"]}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={24}>
|
||||
<Form.Item shouldUpdate noStyle>
|
||||
{() => {
|
||||
const payers = form.getFieldValue("payers");
|
||||
const row = payers?.[index];
|
||||
const cdkPayer =
|
||||
bodyshop.cdk_configuration.payers &&
|
||||
bodyshop.cdk_configuration.payers.find((i) => i && row && i.name === row.name);
|
||||
if (i18n.exists(`jobs.fields.${cdkPayer?.control_type}`))
|
||||
return <div>{cdkPayer && t(`jobs.fields.${cdkPayer?.control_type}`)}</div>;
|
||||
else if (i18n.exists(`jobs.fields.dms.control_type.${cdkPayer?.control_type}`)) {
|
||||
return <div>{cdkPayer && t(`jobs.fields.dms.control_type.${cdkPayer?.control_type}`)}</div>;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
))}
|
||||
<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>
|
||||
|
||||
{/* Validation gates & summary */}
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
let totalAllocated = Dinero();
|
||||
const payers = form.getFieldValue("payers") || [];
|
||||
payers.forEach((payer) => {
|
||||
totalAllocated = totalAllocated.add(Dinero({ amount: Math.round((payer?.amount || 0) * 100) }));
|
||||
});
|
||||
|
||||
const discrep = totals ? totals.totalSale.subtract(totalAllocated) : Dinero();
|
||||
|
||||
// gate: must have payers filled + zero discrepancy when we have a summary
|
||||
const payersOk =
|
||||
payers.length > 0 &&
|
||||
payers.every((p) => p?.name && p.dms_acctnumber && (p.amount ?? "") !== "" && p.controlnumber);
|
||||
const nonRrDiscrepancyGate = socket?.allocationsSummary ? discrep.getAmount() !== 0 : true;
|
||||
const disablePost = !payersOk || nonRrDiscrepancyGate;
|
||||
|
||||
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={disablePost} htmlType="submit">
|
||||
{t("jobs.actions.dms.post")}
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,396 +1,40 @@
|
||||
import { DeleteFilled, DownOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Dropdown,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Select,
|
||||
Space,
|
||||
Statistic,
|
||||
Switch,
|
||||
Typography
|
||||
} from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
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 i18n from "../../translations/i18n";
|
||||
import dayjs from "../../utils/day";
|
||||
import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component";
|
||||
import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component";
|
||||
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
|
||||
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import { DMS_MAP } from "../../utils/dmsUtils";
|
||||
import RRPostForm from "./rr-dms-post-form";
|
||||
import CdkLikePostForm from "./cdklike-dms-post-form";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
const mapDispatchToProps = () => ({});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DmsPostForm);
|
||||
|
||||
export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
|
||||
const [form] = Form.useForm();
|
||||
const { t } = useTranslation();
|
||||
/**
|
||||
* DMS Post Form component that renders the appropriate post form
|
||||
* @param mode
|
||||
* @param bodyshop
|
||||
* @param socket
|
||||
* @param job
|
||||
* @param logsRef
|
||||
* @returns {JSX.Element|null}
|
||||
* @constructor
|
||||
*/
|
||||
export function DmsPostForm({ mode, bodyshop, socket, job, logsRef }) {
|
||||
switch (mode) {
|
||||
case DMS_MAP.reynolds:
|
||||
return <RRPostForm bodyshop={bodyshop} socket={socket} job={job} logsRef={logsRef} />;
|
||||
|
||||
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);
|
||||
// CDK (legacy /ws), Fortellis (CDK-over-WSS), and PBS share the same UI;
|
||||
// we pass mode down so the child can choose the correct event name.
|
||||
case DMS_MAP.fortellis:
|
||||
case DMS_MAP.cdk:
|
||||
case DMS_MAP.pbs:
|
||||
return <CdkLikePostForm mode={mode} bodyshop={bodyshop} socket={socket} job={job} logsRef={logsRef} />;
|
||||
|
||||
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
|
||||
});
|
||||
console.log(logsRef);
|
||||
if (logsRef) {
|
||||
console.log("executing", logsRef);
|
||||
logsRef.curent &&
|
||||
logsRef.current.scrollIntoView({
|
||||
behavior: "smooth"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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,
|
||||
ownr_nm: `${job.ownr_fn || ""} ${job.ownr_ln || ""} ${job.ownr_co_nm || ""}`.trim(),
|
||||
ins_co_nm: job.ins_co_nm || "N/A",
|
||||
clm_po: `${job.clm_no ? `${job.clm_no} ` : ""}${job.po_number || ""}`
|
||||
}).trim()}.${
|
||||
job.area_of_damage && job.area_of_damage.impact1
|
||||
? " " +
|
||||
t("jobs.labels.dms.damageto", {
|
||||
area_of_damage: (job.area_of_damage && job.area_of_damage.impact1.padStart(2, "0")) || "UNKNOWN"
|
||||
})
|
||||
: ""
|
||||
}`.slice(0, 239),
|
||||
inservicedate: dayjs(
|
||||
`${(job.v_model_yr && (job.v_model_yr < 100 ? (job.v_model_yr >= (dayjs().year() + 1) % 100 ? 1900 + parseInt(job.v_model_yr) : 2000 + parseInt(job.v_model_yr)) : job.v_model_yr)) || 2019}-01-01`
|
||||
)
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
|
||||
{bodyshop.cdk_dealerid && (
|
||||
<div>
|
||||
<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>
|
||||
<Form.Item name="inservicedate" label={t("jobs.fields.dms.inservicedate")}>
|
||||
<DateTimePicker isDateOnly />
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<Space>
|
||||
<DmsCdkMakes form={form} socket={socket} job={job} />
|
||||
<DmsCdkMakesRefetch />
|
||||
<Form.Item name="dms_unsold" label={t("jobs.fields.dms.dms_unsold")} initialValue={false}>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name="dms_model_override" label={t("jobs.fields.dms.dms_model_override")} initialValue={false}>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
<Form.Item
|
||||
name="story"
|
||||
label={t("jobs.fields.dms.story")}
|
||||
rules={[
|
||||
{
|
||||
required: true
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input.TextArea maxLength={240} />
|
||||
</Form.Item>
|
||||
|
||||
<Divider />
|
||||
<Space size="large" wrap align="center">
|
||||
<Statistic
|
||||
title={t("jobs.fields.ded_amt")}
|
||||
value={Dinero(job.job_totals.totals.custPayable.deductible).toFormat()}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("jobs.labels.total_cust_payable")}
|
||||
value={Dinero(job.job_totals.totals.custPayable.total).toFormat()}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("jobs.labels.net_repairs")}
|
||||
value={Dinero(job.job_totals.totals.net_repairs).toFormat()}
|
||||
/>
|
||||
</Space>
|
||||
<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={
|
||||
<div>
|
||||
{t("jobs.fields.dms.payer.controlnumber")}{" "}
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: bodyshop.cdk_configuration.controllist?.map((key, idx) => ({
|
||||
key: idx,
|
||||
label: key.name,
|
||||
onClick: () => {
|
||||
form.setFieldsValue({
|
||||
payers: form.getFieldValue("payers").map((row, mapIndex) => {
|
||||
if (index !== mapIndex) return row;
|
||||
return {
|
||||
...row,
|
||||
controlnumber: key.controlnumber
|
||||
};
|
||||
})
|
||||
});
|
||||
}
|
||||
}))
|
||||
}}
|
||||
>
|
||||
<a href=" #" onClick={(e) => e.preventDefault()}>
|
||||
<DownOutlined />
|
||||
</a>
|
||||
</Dropdown>
|
||||
</div>
|
||||
}
|
||||
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);
|
||||
if (i18n.exists(`jobs.fields.${cdkPayer?.control_type}`))
|
||||
return <div>{cdkPayer && t(`jobs.fields.${cdkPayer?.control_type}`)}</div>;
|
||||
else if (i18n.exists(`jobs.fields.dms.control_type.${cdkPayer?.control_type}`)) {
|
||||
return <div>{cdkPayer && t(`jobs.fields.dms.control_type.${cdkPayer?.control_type}`)}</div>;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}}
|
||||
</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>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
242
client/src/components/dms-post-form/rr-dms-post-form.jsx
Normal file
242
client/src/components/dms-post-form/rr-dms-post-form.jsx
Normal file
@@ -0,0 +1,242 @@
|
||||
import { ReloadOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Statistic,
|
||||
Tooltip,
|
||||
Typography
|
||||
} from "antd";
|
||||
import Dinero from "dinero.js";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import dayjs from "../../utils/day";
|
||||
|
||||
/**
|
||||
* RR DMS Post Form component
|
||||
* Submit: "rr-export-job"
|
||||
* @param bodyshop
|
||||
* @param socket
|
||||
* @param job
|
||||
* @param logsRef
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
export default function RRPostForm({ bodyshop, socket, job, logsRef }) {
|
||||
const [form] = Form.useForm();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Advisors
|
||||
const [advisors, setAdvisors] = useState([]);
|
||||
const [advLoading, setAdvLoading] = useState(false);
|
||||
|
||||
const getAdvisorNumber = (a) => a?.advisorId;
|
||||
|
||||
const getAdvisorLabel = (a) => `${a?.firstName || ""} ${a?.lastName || ""}`.trim();
|
||||
|
||||
const fetchRrAdvisors = (refresh = false) => {
|
||||
if (!socket) return;
|
||||
setAdvLoading(true);
|
||||
|
||||
const onResult = (payload) => {
|
||||
try {
|
||||
const list = payload?.result ?? payload ?? [];
|
||||
setAdvisors(Array.isArray(list) ? list : []);
|
||||
} finally {
|
||||
setAdvLoading(false);
|
||||
socket.off("rr-get-advisors:result", onResult);
|
||||
}
|
||||
};
|
||||
|
||||
socket.once("rr-get-advisors:result", onResult);
|
||||
socket.emit("rr-get-advisors", { departmentType: "B", refresh }, (ack) => {
|
||||
if (ack?.ok) {
|
||||
const list = ack.result ?? [];
|
||||
setAdvisors(Array.isArray(list) ? list : []);
|
||||
} else if (ack) {
|
||||
console.error("Something went wrong fetching DMS Advisors");
|
||||
}
|
||||
setAdvLoading(false);
|
||||
socket.off("rr-get-advisors:result", onResult);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchRrAdvisors(false);
|
||||
}, [bodyshop?.id, socket]);
|
||||
|
||||
const initialValues = useMemo(
|
||||
() => ({
|
||||
story: `${t("jobs.labels.dms.defaultstory", {
|
||||
ro_number: job.ro_number,
|
||||
ownr_nm: `${job.ownr_fn || ""} ${job.ownr_ln || ""} ${job.ownr_co_nm || ""}`.trim(),
|
||||
ins_co_nm: job.ins_co_nm || "N/A",
|
||||
clm_po: `${job.clm_no ? `${job.clm_no} ` : ""}${job.po_number || ""}`
|
||||
}).trim()}.${
|
||||
job.area_of_damage?.impact1
|
||||
? " " +
|
||||
t("jobs.labels.dms.damageto", {
|
||||
area_of_damage: (job.area_of_damage && job.area_of_damage.impact1.padStart(2, "0")) || "UNKNOWN"
|
||||
})
|
||||
: ""
|
||||
}`.slice(0, 239),
|
||||
inservicedate: dayjs(
|
||||
`${
|
||||
(job.v_model_yr &&
|
||||
(job.v_model_yr < 100
|
||||
? job.v_model_yr >= (dayjs().year() + 1) % 100
|
||||
? 1900 + parseInt(job.v_model_yr, 10)
|
||||
: 2000 + parseInt(job.v_model_yr, 10)
|
||||
: job.v_model_yr)) ||
|
||||
2019
|
||||
}-01-01`
|
||||
)
|
||||
}),
|
||||
[job, t]
|
||||
);
|
||||
|
||||
const handleFinish = (values) => {
|
||||
if (!socket) return;
|
||||
socket.emit("rr-export-job", {
|
||||
bodyshopId: bodyshop?.id,
|
||||
jobId: job.id,
|
||||
job,
|
||||
txEnvelope: values
|
||||
});
|
||||
logsRef?.current?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
// Discrepancy is ignored for RR; we still show totals for operator context
|
||||
const totals = socket?.allocationsSummary
|
||||
? socket.allocationsSummary.reduce(
|
||||
(acc, val) => ({
|
||||
totalSale: acc.totalSale.add(Dinero(val.sale)),
|
||||
totalCost: acc.totalCost.add(Dinero(val.cost))
|
||||
}),
|
||||
{ totalSale: Dinero(), totalCost: Dinero() }
|
||||
)
|
||||
: { totalSale: Dinero(), totalCost: Dinero() };
|
||||
|
||||
return (
|
||||
<Card title={t("jobs.labels.dms.postingform")}>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleFinish}
|
||||
style={{ width: "100%" }}
|
||||
initialValues={initialValues}
|
||||
>
|
||||
<Row gutter={[16, 12]} align="bottom">
|
||||
{/* Advisor + inline Refresh */}
|
||||
<Col xs={24} sm={24} md={12} lg={8}>
|
||||
<Form.Item label={t("jobs.fields.dms.advisor")} required>
|
||||
<Space.Compact block>
|
||||
<Form.Item
|
||||
name="advisorNo"
|
||||
noStyle
|
||||
rules={[{ required: true, message: t("general.validation.required") }]}
|
||||
>
|
||||
<Select
|
||||
style={{ flex: 1 }}
|
||||
loading={advLoading}
|
||||
allowClear
|
||||
placeholder={t("general.actions.select", "Select...")}
|
||||
popupMatchSelectWidth
|
||||
options={advisors
|
||||
.map((a) => {
|
||||
const value = getAdvisorNumber(a);
|
||||
if (value == null) return null;
|
||||
return { value: String(value), label: getAdvisorLabel(a) || String(value) };
|
||||
})
|
||||
.filter(Boolean)}
|
||||
notFoundContent={advLoading ? t("general.labels.loading") : t("general.labels.none")}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Tooltip title={t("general.actions.refresh")}>
|
||||
<Button
|
||||
aria-label={t("general.actions.refresh")}
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => fetchRrAdvisors(true)}
|
||||
loading={advLoading}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space.Compact>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
{/* Make Override */}
|
||||
<Col xs={24} sm={12} md={12} lg={8}>
|
||||
<Form.Item name="makeOverride" label={t("jobs.fields.dms.make_override")}>
|
||||
<Input allowClear placeholder={t("general.actions.optional")} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={12} sm={8} md={6} lg={4}>
|
||||
<Form.Item name="kmin" label={t("jobs.fields.kmin")} initialValue={job?.kmin} rules={[{ required: true }]}>
|
||||
<InputNumber style={{ width: "100%" }} disabled />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
<Col xs={12} sm={8} md={6} lg={4}>
|
||||
<Form.Item
|
||||
name="kmout"
|
||||
label={t("jobs.fields.kmout")}
|
||||
initialValue={job?.kmout}
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<InputNumber style={{ width: "100%" }} disabled />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 12]}>
|
||||
<Col span={24}>
|
||||
<Form.Item name="story" label={t("jobs.fields.dms.story")} rules={[{ required: true }]}>
|
||||
<Input.TextArea maxLength={240} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Space size="large" wrap align="center" style={{ marginBottom: 16 }}>
|
||||
<Statistic
|
||||
title={t("jobs.fields.ded_amt")}
|
||||
value={Dinero(job.job_totals.totals.custPayable.deductible).toFormat()}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("jobs.labels.total_cust_payable")}
|
||||
value={Dinero(job.job_totals.totals.custPayable.total).toFormat()}
|
||||
/>
|
||||
<Statistic
|
||||
title={t("jobs.labels.net_repairs")}
|
||||
value={Dinero(job.job_totals.totals.net_repairs).toFormat()}
|
||||
/>
|
||||
</Space>
|
||||
|
||||
{/* Validation */}
|
||||
<Form.Item shouldUpdate>
|
||||
{() => {
|
||||
const advisorOk = !!form.getFieldValue("advisorNo");
|
||||
return (
|
||||
<Space size="large" wrap align="center">
|
||||
<Statistic title={t("jobs.labels.subtotal")} value={totals.totalSale.toFormat()} />
|
||||
<Typography.Title>=</Typography.Title>
|
||||
<Button disabled={!advisorOk} htmlType="submit">
|
||||
{t("jobs.actions.dms.post")}
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
}}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -141,7 +141,7 @@ const buildAccountingChildren = ({
|
||||
</Link>
|
||||
)
|
||||
},
|
||||
...(!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber)) || DmsAp.treatment === "on"
|
||||
...(!(bodyshop?.cdk_dealerid || bodyshop?.pbs_serialnumber || bodyshop?.rr_dealerid) || DmsAp.treatment === "on"
|
||||
? [
|
||||
{
|
||||
key: "payables",
|
||||
@@ -156,7 +156,7 @@ const buildAccountingChildren = ({
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(!((bodyshop && bodyshop.cdk_dealerid) || (bodyshop && bodyshop.pbs_serialnumber))
|
||||
...(!(bodyshop?.cdk_dealerid || bodyshop?.pbs_serialnumber || bodyshop?.rr_dealerid) || DmsAp.treatment === "on"
|
||||
? [
|
||||
{
|
||||
key: "payments",
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createStructuredSelector } from "reselect";
|
||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -14,6 +15,8 @@ const mapStateToProps = createStructuredSelector({
|
||||
export function JobsCloseAutoAllocate({ bodyshop, joblines, form, disabled }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const hasDmsKey = bodyshopHasDmsKey(bodyshop);
|
||||
|
||||
const handleAllocate = (defaults) => {
|
||||
form.setFieldsValue({
|
||||
joblines: joblines.map((jl) => {
|
||||
@@ -64,21 +67,20 @@ export function JobsCloseAutoAllocate({ bodyshop, joblines, form, disabled }) {
|
||||
handleAllocate(bodyshop.md_responsibility_centers.dms_defaults.find((x) => x.name === key));
|
||||
};
|
||||
|
||||
const menu =
|
||||
bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
|
||||
? {
|
||||
items: bodyshop.md_responsibility_centers.dms_defaults.map((mapping) => ({
|
||||
key: mapping.name,
|
||||
label: mapping.name,
|
||||
disabled: disabled
|
||||
})),
|
||||
onClick: handleMenuClick
|
||||
}
|
||||
: {
|
||||
items: []
|
||||
};
|
||||
const menu = hasDmsKey
|
||||
? {
|
||||
items: bodyshop.md_responsibility_centers.dms_defaults.map((mapping) => ({
|
||||
key: mapping.name,
|
||||
label: mapping.name,
|
||||
disabled: disabled
|
||||
})),
|
||||
onClick: handleMenuClick
|
||||
}
|
||||
: {
|
||||
items: []
|
||||
};
|
||||
|
||||
return bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber ? (
|
||||
return hasDmsKey ? (
|
||||
<Dropdown menu={menu}>
|
||||
<Button disabled={disabled}>{t("jobs.actions.dmsautoallocate")}</Button>
|
||||
</Dropdown>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||
import { INSERT_EXPORT_LOG } from "../../graphql/accounting.queries";
|
||||
import { UPDATE_JOB } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail } from "../../redux/application/application.actions";
|
||||
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import client from "../../utils/GraphQLClient";
|
||||
@@ -45,7 +46,7 @@ export function JobsCloseExportButton({ bodyshop, currentUser, jobId, disabled,
|
||||
|
||||
const handleQbxml = async () => {
|
||||
//Check if it's a CDK setup.
|
||||
if (bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) {
|
||||
if (bodyshopHasDmsKey(bodyshop)) {
|
||||
history(`/manage/dms?jobId=${jobId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import ProductionListColumnProductionNote from "../production-list-columns/produ
|
||||
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
|
||||
import "./jobs-detail-header.styles.scss";
|
||||
import getPartsBasePath from "../../utils/getPartsBasePath.js";
|
||||
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
@@ -309,7 +310,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
||||
</DataLabel>
|
||||
<DataLabel key="4" label={t("vehicles.fields.v_vin")}>
|
||||
<VehicleVinDisplay>{`${job.v_vin || t("general.labels.na")}`}</VehicleVinDisplay>
|
||||
{bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid ? (
|
||||
{bodyshopHasDmsKey(bodyshop) ? (
|
||||
job.v_vin?.length !== 17 ? (
|
||||
<WarningFilled style={{ color: "tomato", marginLeft: ".3rem" }} />
|
||||
) : null
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import i18next from "i18next";
|
||||
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
|
||||
|
||||
export const CalculateAllocationsTotals = (bodyshop, joblines, timetickets, adjustments = []) => {
|
||||
const responsibilitycenters = bodyshop.md_responsibility_centers;
|
||||
@@ -14,10 +15,9 @@ export const CalculateAllocationsTotals = (bodyshop, joblines, timetickets, adju
|
||||
const r = allCodes.reduce((acc, value) => {
|
||||
const r = {
|
||||
opcode: value,
|
||||
cost_center:
|
||||
bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
|
||||
? i18next.t(`joblines.fields.lbr_types.${value && value.toUpperCase()}`)
|
||||
: responsibilitycenters.defaults.costs[value],
|
||||
cost_center: bodyshopHasDmsKey(bodyshop)
|
||||
? i18next.t(`joblines.fields.lbr_types.${value && value.toUpperCase()}`)
|
||||
: responsibilitycenters.defaults.costs[value],
|
||||
mod_lbr_ty: value,
|
||||
total: joblines.reduce((acc2, val2) => {
|
||||
return val2.mod_lbr_ty === value ? acc2 + val2.mod_lb_hrs : acc2;
|
||||
|
||||
@@ -44,6 +44,7 @@ export async function checkPartnerStatus(bodyshop) {
|
||||
// bodyshop &&
|
||||
// (bodyshop.cdk_dealerid ||
|
||||
// bodyshop.pbs_serialnumber ||
|
||||
// bodyshop.rr_dealerid ||
|
||||
// bodyshop.accountingconfig.qbo)
|
||||
// )
|
||||
// ) {
|
||||
|
||||
@@ -28,6 +28,7 @@ import PartsOrderDeleteLine from "../parts-order-delete-line/parts-order-delete-
|
||||
import PartsOrderLineBackorderButton from "../parts-order-line-backorder-button/parts-order-line-backorder-button.component";
|
||||
import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-modal.container";
|
||||
import PrintWrapper from "../print-wrapper/print-wrapper.component";
|
||||
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
@@ -196,7 +197,7 @@ export function PartsOrderListTableDrawerComponent({
|
||||
quantity: pol.quantity,
|
||||
actual_price: pol.act_price,
|
||||
cost_center: pol.jobline?.part_type
|
||||
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
|
||||
? bodyshopHasDmsKey(bodyshop)
|
||||
? pol.jobline.part_type !== "PAE"
|
||||
? pol.jobline.part_type
|
||||
: null
|
||||
|
||||
@@ -20,6 +20,7 @@ import PartsReceiveModalContainer from "../parts-receive-modal/parts-receive-mod
|
||||
import PrintWrapper from "../print-wrapper/print-wrapper.component";
|
||||
import PartsOrderDrawer from "./parts-order-list-table-drawer.component";
|
||||
import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
|
||||
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
jobRO: selectJobReadOnly,
|
||||
@@ -69,6 +70,7 @@ export function PartsOrderListTableComponent({
|
||||
|
||||
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
|
||||
const { refetch } = billsQuery;
|
||||
|
||||
const recordActions = (record, showView = false) => (
|
||||
<Space direction="horizontal" wrap>
|
||||
<ShareToTeamsButton
|
||||
@@ -172,7 +174,7 @@ export function PartsOrderListTableComponent({
|
||||
actual_price: pol.act_price,
|
||||
|
||||
cost_center: pol.jobline?.part_type
|
||||
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
|
||||
? bodyshopHasDmsKey(bodyshop)
|
||||
? pol.jobline.part_type !== "PAE"
|
||||
? pol.jobline.part_type
|
||||
: null
|
||||
|
||||
@@ -331,7 +331,10 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
|
||||
{t("timetickets.labels.shift")}
|
||||
</Select.Option>
|
||||
|
||||
{bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber || Enhanced_Payroll.treatment === "on"
|
||||
{bodyshop.cdk_dealerid ||
|
||||
bodyshop.pbs_serialnumber ||
|
||||
bodyshop.rr_dealerid ||
|
||||
Enhanced_Payroll.treatment === "on"
|
||||
? CiecaSelect(false, true)
|
||||
: bodyshop.md_responsibility_centers.costs.map((c) => (
|
||||
<Select.Option key={c.name} value={c.name}>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
//currentUser: selectCurrentUser
|
||||
@@ -2126,7 +2127,7 @@ function TaxFormItems({ typeNum, typeNumIterator, rootElements, bodyshop }) {
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
{(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) && (
|
||||
{bodyshopHasDmsKey(bodyshop) && (
|
||||
<Form.Item
|
||||
label={t("bodyshop.fields.dms.dms_acctnumber")}
|
||||
rules={[
|
||||
|
||||
@@ -4,10 +4,18 @@ import { useTranslation } from "react-i18next";
|
||||
import { TemplateList } from "../../utils/TemplateConstants";
|
||||
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
|
||||
export default function ShopInfoSpeedPrint() {
|
||||
const { t } = useTranslation();
|
||||
const TemplateListGenerated = TemplateList("job");
|
||||
const allTemplates = TemplateList("job");
|
||||
const TemplateListGenerated = InstanceRenderManager({
|
||||
imex: Object.fromEntries(
|
||||
Object.entries(allTemplates).filter(([, { enhanced_payroll }]) => !enhanced_payroll)
|
||||
),
|
||||
rome: allTemplates
|
||||
});
|
||||
|
||||
return (
|
||||
<Form.List name={["speedprint"]}>
|
||||
{(fields, { add, remove, move }) => {
|
||||
|
||||
@@ -63,7 +63,10 @@ export function TechClockInComponent({ form, bodyshop, technician }) {
|
||||
<Select.Option key={item.cost_center} value={item.cost_center}>
|
||||
{item.cost_center === "timetickets.labels.shift"
|
||||
? t(item.cost_center)
|
||||
: bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber || Enhanced_Payroll.treatment === "on"
|
||||
: bodyshop.cdk_dealerid ||
|
||||
bodyshop.pbs_serialnumber ||
|
||||
bodyshop.rr_dealerid ||
|
||||
Enhanced_Payroll.treatment === "on"
|
||||
? t(`joblines.fields.lbr_types.${item.cost_center.toUpperCase()}`)
|
||||
: item.cost_center}
|
||||
</Select.Option>
|
||||
|
||||
@@ -75,7 +75,10 @@ export function TechClockInContainer({ setTimeTicketContext, technician, bodysho
|
||||
jobid: values.jobid,
|
||||
cost_center: values.cost_center,
|
||||
ciecacode:
|
||||
bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber || Enhanced_Payroll.treatment === "on"
|
||||
bodyshop.cdk_dealerid ||
|
||||
bodyshop.pbs_serialnumber ||
|
||||
bodyshop.rr_dealerid ||
|
||||
Enhanced_Payroll.treatment === "on"
|
||||
? values.cost_center
|
||||
: Object.keys(bodyshop.md_responsibility_centers.defaults.costs).find((key) => {
|
||||
return bodyshop.md_responsibility_centers.defaults.costs[key] === values.cost_center;
|
||||
|
||||
@@ -16,6 +16,7 @@ import TechJobClockoutDelete from "../tech-job-clock-out-delete/tech-job-clock-o
|
||||
import { LaborAllocationContainer } from "../time-ticket-modal/time-ticket-modal.component";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -53,7 +54,9 @@ export function TechClockOffButton({
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
const { t } = useTranslation();
|
||||
const emps = bodyshop.employees.filter((e) => e.id === (technician && technician.id))[0];
|
||||
|
||||
const emps = bodyshop.employees.filter((e) => e.id === technician?.id)[0];
|
||||
const hasDmsKey = bodyshopHasDmsKey(bodyshop);
|
||||
|
||||
const handleFinish = async (values) => {
|
||||
logImEXEvent("tech_clock_out_job");
|
||||
@@ -69,7 +72,10 @@ export function TechClockOffButton({
|
||||
rate: emps && emps.rates.filter((r) => r.cost_center === values.cost_center)[0]?.rate,
|
||||
flat_rate: emps && emps.flat_rate,
|
||||
ciecacode:
|
||||
bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber || Enhanced_Payroll.treatment === "on"
|
||||
bodyshop.cdk_dealerid ||
|
||||
bodyshop.pbs_serialnumber ||
|
||||
bodyshop.rr_dealerid ||
|
||||
Enhanced_Payroll.treatment === "on"
|
||||
? values.cost_center
|
||||
: Object.keys(bodyshop.md_responsibility_centers.defaults.costs).find((key) => {
|
||||
return bodyshop.md_responsibility_centers.defaults.costs[key] === values.cost_center;
|
||||
@@ -164,8 +170,7 @@ export function TechClockOffButton({
|
||||
lineTicketData.jobs_by_pk.lbr_adjustments
|
||||
);
|
||||
|
||||
const fieldTypeToCheck =
|
||||
bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber ? "mod_lbr_ty" : "cost_center";
|
||||
const fieldTypeToCheck = hasDmsKey ? "mod_lbr_ty" : "cost_center";
|
||||
|
||||
const costCenterDiff =
|
||||
Math.round(
|
||||
@@ -205,7 +210,7 @@ export function TechClockOffButton({
|
||||
<Select.Option key={item.cost_center}>
|
||||
{item.cost_center === "timetickets.labels.shift"
|
||||
? t(item.cost_center)
|
||||
: bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber
|
||||
: hasDmsKey
|
||||
? t(`joblines.fields.lbr_types.${item.cost_center.toUpperCase()}`)
|
||||
: item.cost_center}
|
||||
</Select.Option>
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import JobSearchSelect from "../job-search-select/job-search-select.component";
|
||||
import LaborAllocationsTable from "../labor-allocations-table/labor-allocations-table.component";
|
||||
import { CalculateAllocationsTotals } from "../labor-allocations-table/labor-allocations-table.utility";
|
||||
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
|
||||
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
|
||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
|
||||
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
|
||||
@@ -60,7 +61,10 @@ export function TimeTicketModalComponent({
|
||||
<Select.Option key={item.cost_center} value={item.cost_center}>
|
||||
{item.cost_center === "timetickets.labels.shift"
|
||||
? t(item.cost_center)
|
||||
: bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber || Enhanced_Payroll.treatment === "on"
|
||||
: bodyshop.cdk_dealerid ||
|
||||
bodyshop.pbs_serialnumber ||
|
||||
bodyshop.rr_dealerid ||
|
||||
Enhanced_Payroll.treatment === "on"
|
||||
? t(`joblines.fields.lbr_types.${item.cost_center.toUpperCase()}`)
|
||||
: item.cost_center}
|
||||
</Select.Option>
|
||||
@@ -124,7 +128,7 @@ export function TimeTicketModalComponent({
|
||||
onSelect={(value) => {
|
||||
const emps = employeeAutoCompleteOptions && employeeAutoCompleteOptions.filter((e) => e.id === value)[0];
|
||||
|
||||
form.setFieldsValue({ flat_rate: emps && emps.flat_rate });
|
||||
form.setFieldsValue({ flat_rate: emps?.flat_rate });
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -186,8 +190,7 @@ export function TimeTicketModalComponent({
|
||||
lineTicketData.jobs_by_pk.lbr_adjustments
|
||||
);
|
||||
|
||||
const fieldTypeToCheck =
|
||||
bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber ? "mod_lbr_ty" : "cost_center";
|
||||
const fieldTypeToCheck = bodyshopHasDmsKey(bodyshop) ? "mod_lbr_ty" : "cost_center";
|
||||
|
||||
const costCenterDiff =
|
||||
Math.round(
|
||||
@@ -261,7 +264,7 @@ export function TimeTicketModalComponent({
|
||||
if (!value) return Promise.resolve();
|
||||
if (!clockon && value) return Promise.reject(t("timetickets.validation.clockoffwithoutclockon"));
|
||||
// TODO - Verify this exists
|
||||
if (value && value.isSameOrAfter && !value.isSameOrAfter(clockon))
|
||||
if (value?.isSameOrAfter && !value.isSameOrAfter(clockon))
|
||||
return Promise.reject(t("timetickets.validation.clockoffmustbeafterclockon"));
|
||||
|
||||
return Promise.resolve();
|
||||
|
||||
@@ -137,7 +137,10 @@ export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible,
|
||||
if (!!changedFields.cost_center && !!EmployeeAutoCompleteData) {
|
||||
form.setFieldsValue({
|
||||
ciecacode:
|
||||
bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber || Enhanced_Payroll.treatment === "on"
|
||||
bodyshop.cdk_dealerid ||
|
||||
bodyshop.pbs_serialnumber ||
|
||||
bodyshop.rr_dealerid ||
|
||||
Enhanced_Payroll.treatment === "on"
|
||||
? changedFields.cost_center
|
||||
: Object.keys(bodyshop.md_responsibility_centers.defaults.costs).find(
|
||||
(key) => bodyshop.md_responsibility_centers.defaults.costs[key] === changedFields.cost_center
|
||||
|
||||
@@ -149,7 +149,7 @@ const SocketProvider = ({ children, bodyshop, navigate, currentUser }) => {
|
||||
|
||||
useEffect(() => {
|
||||
const initializeSocket = async (token) => {
|
||||
if (!bodyshop || !bodyshop.id || socketRef.current) return;
|
||||
if (!bodyshop?.id || socketRef.current) return;
|
||||
|
||||
const endpoint = import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "";
|
||||
const socketInstance = SocketIO(endpoint, {
|
||||
|
||||
@@ -111,6 +111,7 @@ export const QUERY_BODYSHOP = gql`
|
||||
jc_hourly_rates
|
||||
md_jobline_presets
|
||||
cdk_dealerid
|
||||
rr_dealerid
|
||||
features
|
||||
attach_pdf_to_email
|
||||
tt_allow_post_to_invoiced
|
||||
@@ -123,6 +124,7 @@ export const QUERY_BODYSHOP = gql`
|
||||
md_email_cc
|
||||
timezone
|
||||
ss_configuration
|
||||
rr_configuration
|
||||
md_from_emails
|
||||
last_name_first
|
||||
md_parts_order_comment
|
||||
@@ -245,6 +247,7 @@ export const UPDATE_SHOP = gql`
|
||||
jc_hourly_rates
|
||||
md_jobline_presets
|
||||
cdk_dealerid
|
||||
rr_dealerid
|
||||
attach_pdf_to_email
|
||||
tt_allow_post_to_invoiced
|
||||
cdk_configuration
|
||||
@@ -256,6 +259,7 @@ export const UPDATE_SHOP = gql`
|
||||
md_email_cc
|
||||
timezone
|
||||
ss_configuration
|
||||
rr_configuration
|
||||
md_from_emails
|
||||
last_name_first
|
||||
md_parts_order_comment
|
||||
|
||||
@@ -714,7 +714,7 @@ export const GET_JOB_BY_PK = gql`
|
||||
v_model_yr
|
||||
v_model_desc
|
||||
v_vin
|
||||
notes(where:{pinned: {_eq: true}}, order_by: {updated_at: desc}) {
|
||||
notes(where: { pinned: { _eq: true } }, order_by: { updated_at: desc }) {
|
||||
created_at
|
||||
created_by
|
||||
critical
|
||||
@@ -1000,7 +1000,6 @@ export const QUERY_JOB_CARD_DETAILS = gql`
|
||||
key
|
||||
type
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -2172,6 +2171,8 @@ export const QUERY_JOB_EXPORT_DMS = gql`
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
ownr_co_nm
|
||||
ownr_ph1
|
||||
ownr_ph2
|
||||
ins_co_nm
|
||||
kmin
|
||||
kmout
|
||||
@@ -2180,6 +2181,10 @@ export const QUERY_JOB_EXPORT_DMS = gql`
|
||||
v_model_desc
|
||||
area_of_damage
|
||||
date_exported
|
||||
v_vin
|
||||
plate_no
|
||||
plate_st
|
||||
ownr_co_nm
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -2415,7 +2420,7 @@ export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql`
|
||||
start
|
||||
status
|
||||
}
|
||||
notes(where:{pinned: {_eq: true}}, order_by: {updated_at: desc}) {
|
||||
notes(where: { pinned: { _eq: true } }, order_by: { updated_at: desc }) {
|
||||
created_at
|
||||
created_by
|
||||
critical
|
||||
|
||||
@@ -15,6 +15,7 @@ import FeatureWrapperComponent from "../../components/feature-wrapper/feature-wr
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import UpsellComponent, { upsellEnum } from "../../components/upsell/upsell.component";
|
||||
import { Card } from "antd";
|
||||
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -54,8 +55,7 @@ export function AccountingPayablesContainer({ bodyshop, setBreadcrumbs, setSelec
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
const noPath =
|
||||
!partnerVersion?.qbpath &&
|
||||
!(bodyshop && (bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber || bodyshop.accountingconfig.qbo));
|
||||
!partnerVersion?.qbpath && !(bodyshop && (bodyshopHasDmsKey(bodyshop) || bodyshop?.accountingconfig?.qbo));
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -15,6 +15,7 @@ import FeatureWrapperComponent from "../../components/feature-wrapper/feature-wr
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import UpsellComponent, { upsellEnum } from "../../components/upsell/upsell.component";
|
||||
import { Card } from "antd";
|
||||
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -52,9 +53,10 @@ export function AccountingPaymentsContainer({ bodyshop, setBreadcrumbs, setSelec
|
||||
});
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
const noPath =
|
||||
!partnerVersion?.qbpath &&
|
||||
!(bodyshop && (bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber || bodyshop.accountingconfig.qbo));
|
||||
!partnerVersion?.qbpath && !(bodyshop && (bodyshopHasDmsKey(bodyshop) || bodyshop?.accountingconfig?.qbo));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FeatureWrapperComponent
|
||||
|
||||
@@ -15,6 +15,7 @@ import FeatureWrapperComponent from "../../components/feature-wrapper/feature-wr
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { Card } from "antd";
|
||||
import UpsellComponent, { upsellEnum } from "../../components/upsell/upsell.component";
|
||||
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -57,8 +58,7 @@ export function AccountingReceivablesContainer({ bodyshop, setBreadcrumbs, setSe
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
const noPath =
|
||||
!partnerVersion?.qbpath &&
|
||||
!(bodyshop && (bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber || bodyshop.accountingconfig.qbo));
|
||||
!partnerVersion?.qbpath && !(bodyshop && (bodyshopHasDmsKey(bodyshop) || bodyshop?.accountingconfig?.qbo));
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -90,6 +90,7 @@ export function DmsContainer({ setBreadcrumbs, setSelectedHeader }) {
|
||||
});
|
||||
|
||||
if (socket.disconnected) socket.connect();
|
||||
|
||||
return () => {
|
||||
socket.removeAllListeners();
|
||||
socket.disconnect();
|
||||
@@ -139,17 +140,10 @@ export function DmsContainer({ setBreadcrumbs, setSelectedHeader }) {
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<DmsLogEvents socket={socket} logs={logs} />
|
||||
<DmsLogEvents logs={logs} />
|
||||
</Card>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
export const determineDmsType = (bodyshop) => {
|
||||
if (bodyshop.cdk_dealerid) return "cdk";
|
||||
else {
|
||||
return "pbs";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Button, Card, Col, Result, Row, Select, Space } from "antd";
|
||||
import queryString from "query-string";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { connect } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import SocketIO from "socket.io-client";
|
||||
import AlertComponent from "../../components/alert/alert.component";
|
||||
import DmsAllocationsSummary from "../../components/dms-allocations-summary/dms-allocations-summary.component";
|
||||
import DmsCustomerSelector from "../../components/dms-customer-selector/dms-customer-selector.component";
|
||||
import DmsLogEvents from "../../components/dms-log-events/dms-log-events.component";
|
||||
import DmsPostForm from "../../components/dms-post-form/dms-post-form.component";
|
||||
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
|
||||
import { OwnerNameDisplayFunction } from "../../components/owner-name-display/owner-name-display.component";
|
||||
import { auth } from "../../firebase/firebase.utils";
|
||||
import queryString from "query-string";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { Button, Card, Col, Result, Row, Select, Space, Switch } from "antd";
|
||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
import { QUERY_JOB_EXPORT_DMS } from "../../graphql/jobs.queries";
|
||||
import { insertAuditTrail, setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { insertAuditTrail, setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
||||
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
import { bodyshopHasDmsKey, DMS_MAP, getDmsMode, isWssMode } from "../../utils/dmsUtils.js";
|
||||
import legacySocket from "../../utils/legacySocket";
|
||||
|
||||
import { OwnerNameDisplayFunction } from "../../components/owner-name-display/owner-name-display.component";
|
||||
import AlertComponent from "../../components/alert/alert.component";
|
||||
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
|
||||
import DmsPostForm from "../../components/dms-post-form/dms-post-form.component";
|
||||
import DmsLogEvents from "../../components/dms-log-events/dms-log-events.component";
|
||||
import DmsCustomerSelector from "../../components/dms-customer-selector/dms-customer-selector.component";
|
||||
import DmsAllocationsSummary from "../../components/dms-allocations-summary/dms-allocations-summary.component";
|
||||
import RrAllocationsSummary from "../../components/dms-allocations-summary/rr-dms-allocations-summary.component";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
@@ -34,130 +40,413 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
|
||||
|
||||
export const socket = SocketIO(
|
||||
import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "", // for dev testing,
|
||||
{
|
||||
path: "/ws",
|
||||
withCredentials: true,
|
||||
auth: async (callback) => {
|
||||
const token = auth.currentUser && (await auth.currentUser.getIdToken());
|
||||
callback({ token });
|
||||
}
|
||||
const DMS_SOCKET_EVENTS = {
|
||||
[DMS_MAP.reynolds]: {
|
||||
log: "rr-log-event",
|
||||
partialResult: "rr-export-job:result",
|
||||
validationNeeded: "rr-validation-required",
|
||||
exportSuccess: "export-success",
|
||||
exportFailed: "export-failed"
|
||||
},
|
||||
[DMS_MAP.fortellis]: {
|
||||
log: "fortellis-log-event",
|
||||
exportSuccess: "export-success",
|
||||
exportFailed: "export-failed"
|
||||
},
|
||||
[DMS_MAP.cdk]: {
|
||||
log: "log-event",
|
||||
exportSuccess: "export-success",
|
||||
exportFailed: "export-failed"
|
||||
},
|
||||
[DMS_MAP.pbs]: {
|
||||
log: "log-event",
|
||||
exportSuccess: "export-success",
|
||||
exportFailed: "export-failed"
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, insertAuditTrail }) {
|
||||
const { t } = useTranslation();
|
||||
const [logLevel, setLogLevel] = useState(determineDmsType(bodyshop) === "pbs" ? "INFO" : "DEBUG");
|
||||
const [resetAfterReconnect, setResetAfterReconnect] = useState(false);
|
||||
|
||||
const history = useNavigate();
|
||||
const [logs, setLogs] = useState([]);
|
||||
const search = queryString.parse(useLocation().search);
|
||||
const { jobId } = search;
|
||||
|
||||
const notification = useNotification();
|
||||
|
||||
const {
|
||||
treatments: { Fortellis }
|
||||
} = useSplitTreatments({
|
||||
attributes: {},
|
||||
names: ["Fortellis"],
|
||||
splitKey: bodyshop.imexshopid
|
||||
});
|
||||
|
||||
// Compute a single normalized mode and pick the proper socket
|
||||
const mode = getDmsMode(bodyshop, Fortellis.treatment); // "rr" | "fortellis" | "cdk" | "pbs" | "none"
|
||||
const isRrMode = mode === DMS_MAP.reynolds;
|
||||
|
||||
const { socket: wsssocket } = useSocket();
|
||||
const activeSocket = useMemo(() => (isWssMode(mode) ? wsssocket : legacySocket), [mode, wsssocket]);
|
||||
|
||||
const [isConnected, setIsConnected] = useState(!!activeSocket?.connected);
|
||||
|
||||
// One place to set log level
|
||||
const [logLevel, setLogLevel] = useState(mode === DMS_MAP.pbs ? "INFO" : "DEBUG");
|
||||
|
||||
const setActiveLogLevel = (level) => {
|
||||
if (!activeSocket) return;
|
||||
activeSocket.emit("set-log-level", level);
|
||||
};
|
||||
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [detailsOpen, setDetailsOpen] = useState(false);
|
||||
const [detailsNonce, setDetailsNonce] = useState(0);
|
||||
const [colorizeJson, setColorizeJson] = useState(false);
|
||||
|
||||
const [rrOpenRoLimit, setRrOpenRoLimit] = useState(false);
|
||||
const clearRrOpenRoLimit = () => setRrOpenRoLimit(false);
|
||||
|
||||
const [rrValidationPending, setrrValidationPending] = useState(false);
|
||||
|
||||
const { loading, error, data } = useQuery(QUERY_JOB_EXPORT_DMS, {
|
||||
variables: { id: jobId },
|
||||
skip: !jobId,
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only"
|
||||
});
|
||||
|
||||
const logsRef = useRef(null);
|
||||
|
||||
const toggleDetailsAll = () => {
|
||||
setDetailsOpen((v) => !v);
|
||||
setDetailsNonce((n) => n + 1);
|
||||
};
|
||||
|
||||
// Channel names per mode to avoid branching everywhere
|
||||
const channels = useMemo(() => DMS_SOCKET_EVENTS[mode] || {}, [mode]);
|
||||
|
||||
const providerLabel = useMemo(
|
||||
() =>
|
||||
({
|
||||
[DMS_MAP.reynolds]: "Reynolds",
|
||||
[DMS_MAP.fortellis]: "Fortellis",
|
||||
[DMS_MAP.cdk]: "CDK",
|
||||
[DMS_MAP.pbs]: "PBS"
|
||||
})[mode] || "DMS",
|
||||
[mode]
|
||||
);
|
||||
|
||||
const transportLabel = isWssMode(mode) ? "(WSS)" : "(WS)";
|
||||
|
||||
const bannerMessage = `Posting to ${providerLabel} | ${transportLabel} | ${
|
||||
isConnected ? "Connected" : "Disconnected"
|
||||
}`;
|
||||
|
||||
const resetKey = useMemo(() => `${mode || "none"}-${jobId || "none"}`, [mode, jobId]);
|
||||
|
||||
// 🔄 Hard reset of local + server-side DMS context when the page/job loads
|
||||
useEffect(() => {
|
||||
// Clear any local ephemeral state that might be stale
|
||||
setLogs([]);
|
||||
setRrOpenRoLimit(false);
|
||||
setrrValidationPending(false);
|
||||
|
||||
if (!activeSocket) return;
|
||||
|
||||
const emitReset = () => {
|
||||
// Generic reset; server can branch on `mode` if needed
|
||||
activeSocket.emit("dms-reset-context", { jobId, mode });
|
||||
};
|
||||
|
||||
if (activeSocket.connected) {
|
||||
// WSS usually lands here
|
||||
emitReset();
|
||||
return;
|
||||
}
|
||||
|
||||
// Legacy WS: wait for the connect before emitting reset
|
||||
const handleConnectOnce = () => {
|
||||
emitReset();
|
||||
activeSocket.off("connect", handleConnectOnce);
|
||||
};
|
||||
|
||||
activeSocket.on("connect", handleConnectOnce);
|
||||
|
||||
return () => {
|
||||
activeSocket.off("connect", handleConnectOnce);
|
||||
};
|
||||
}, [jobId, mode, activeSocket]);
|
||||
|
||||
const handleExportFailed = (payload = {}) => {
|
||||
const { title, friendlyMessage, error: errText, severity, errorCode, vendorStatusCode } = payload;
|
||||
|
||||
const msg =
|
||||
friendlyMessage ||
|
||||
errText ||
|
||||
t("dms.errors.exportfailedgeneric", "We couldn't complete the export. Please try again.");
|
||||
|
||||
const vendorTitle = title || (isRrMode ? "Reynolds" : "DMS");
|
||||
|
||||
const isRrOpenRoLimit =
|
||||
isRrMode &&
|
||||
(vendorStatusCode === 507 ||
|
||||
/MAX_OPEN_ROS/i.test(String(errorCode || "")) ||
|
||||
/maximum number of open repair orders/i.test(String(msg || "").toLowerCase()));
|
||||
|
||||
const sev = severity || (isRrOpenRoLimit ? "warning" : "error");
|
||||
|
||||
if (!isRrOpenRoLimit) {
|
||||
const notifyKind = sev === "warning" && typeof notification.warning === "function" ? "warning" : "error";
|
||||
notification[notifyKind]({ message: vendorTitle, description: msg, duration: 10 });
|
||||
} else {
|
||||
setRrOpenRoLimit(true);
|
||||
}
|
||||
|
||||
setLogs((prev) => [
|
||||
...prev,
|
||||
{
|
||||
timestamp: new Date(),
|
||||
level: (sev || "error").toUpperCase(),
|
||||
message: `${vendorTitle}: ${msg}`,
|
||||
meta: { errorCode, vendorStatusCode, raw: payload, blockedByOpenRoLimit: !!isRrOpenRoLimit }
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
// keep this in sync if mode/socket flips
|
||||
useEffect(() => {
|
||||
setIsConnected(!!activeSocket?.connected);
|
||||
}, [activeSocket]);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = t("titles.dms", {
|
||||
app: InstanceRenderManager({
|
||||
imex: "$t(titles.imexonline)",
|
||||
rome: "$t(titles.romeonline)"
|
||||
})
|
||||
app: InstanceRenderManager({ imex: "$t(titles.imexonline)", rome: "$t(titles.romeonline)" })
|
||||
});
|
||||
setSelectedHeader("dms");
|
||||
setBreadcrumbs([
|
||||
{
|
||||
link: "/manage/accounting/receivables",
|
||||
label: t("titles.bc.accounting-receivables")
|
||||
},
|
||||
{
|
||||
link: "/manage/dms",
|
||||
label: t("titles.bc.dms")
|
||||
}
|
||||
{ link: "/manage/accounting/receivables", label: t("titles.bc.accounting-receivables") }
|
||||
// { link: "/manage/dms", label: t("titles.bc.dms") }
|
||||
]);
|
||||
}, [t, setBreadcrumbs, setSelectedHeader]);
|
||||
|
||||
// Socket wiring (mode-aware)
|
||||
useEffect(() => {
|
||||
socket.on("connect", () => socket.emit("set-log-level", logLevel));
|
||||
socket.on("reconnect", () => {
|
||||
setLogs((logs) => {
|
||||
return [
|
||||
...logs,
|
||||
{
|
||||
timestamp: new Date(),
|
||||
level: "warn",
|
||||
message: "Reconnected to CDK Export Service"
|
||||
}
|
||||
];
|
||||
});
|
||||
});
|
||||
socket.on("connect_error", (err) => {
|
||||
if (!activeSocket) return;
|
||||
|
||||
// Connect legacy socket if needed
|
||||
if (!isWssMode(mode)) {
|
||||
if (activeSocket.disconnected) activeSocket.connect();
|
||||
}
|
||||
|
||||
// Set log level now and on connect/reconnect
|
||||
setActiveLogLevel(logLevel);
|
||||
|
||||
const onConnect = () => {
|
||||
setIsConnected(true);
|
||||
setActiveLogLevel(logLevel);
|
||||
|
||||
if (resetAfterReconnect) {
|
||||
activeSocket.emit("dms-reset-context", { jobId, mode });
|
||||
setResetAfterReconnect(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onDisconnect = () => setIsConnected(false);
|
||||
|
||||
const onReconnect = () => {
|
||||
setIsConnected(true);
|
||||
setLogs((prev) => [
|
||||
...prev,
|
||||
{
|
||||
timestamp: new Date(),
|
||||
level: "warn",
|
||||
message: `Reconnected to ${isRrMode ? "RR" : mode === DMS_MAP.fortellis ? "Fortellis" : "DMS"} Export Service`
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
const onConnectError = (err) => {
|
||||
// Legacy and WSS both emit this
|
||||
console.log(`connect_error due to ${err}`, err);
|
||||
notification.error({ message: err.message });
|
||||
});
|
||||
socket.on("log-event", (payload) => {
|
||||
setLogs((logs) => {
|
||||
return [...logs, payload];
|
||||
});
|
||||
});
|
||||
socket.on("export-success", (payload) => {
|
||||
notification.success({
|
||||
message: t("jobs.successes.exported")
|
||||
});
|
||||
};
|
||||
|
||||
activeSocket.on("disconnect", onDisconnect);
|
||||
activeSocket.on("connect", onConnect);
|
||||
activeSocket.on("reconnect", onReconnect);
|
||||
activeSocket.on("connect_error", onConnectError);
|
||||
|
||||
// Logs
|
||||
const onLog = isRrMode
|
||||
? (payload = {}) => {
|
||||
const normalized = {
|
||||
timestamp: payload.timestamp ? new Date(payload.timestamp) : payload.ts ? new Date(payload.ts) : new Date(),
|
||||
level: (payload.level || "INFO").toUpperCase(),
|
||||
message: payload.message || payload.msg || "",
|
||||
meta: payload.meta ?? payload.ctx ?? payload.details ?? null
|
||||
};
|
||||
setLogs((prev) => [...prev, normalized]);
|
||||
}
|
||||
: (payload) => setLogs((prev) => [...prev, payload]);
|
||||
|
||||
if (channels.log) activeSocket.on(channels.log, onLog);
|
||||
|
||||
// Success / Failed
|
||||
const onExportSuccess = (payload) => {
|
||||
const jobIdResolved = payload?.jobId ?? payload;
|
||||
notification.success({ message: t("jobs.successes.exported") });
|
||||
|
||||
// Clear RR Validation flag if any
|
||||
setrrValidationPending(false);
|
||||
|
||||
insertAuditTrail({
|
||||
jobid: payload,
|
||||
jobid: jobIdResolved,
|
||||
operation: AuditTrailMapping.jobexported(),
|
||||
type: "jobexported"
|
||||
});
|
||||
history("/manage/accounting/receivables");
|
||||
});
|
||||
|
||||
if (socket.disconnected) socket.connect();
|
||||
return () => {
|
||||
socket.removeAllListeners();
|
||||
socket.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (channels.exportSuccess) activeSocket.on(channels.exportSuccess, onExportSuccess);
|
||||
if (channels.exportFailed) activeSocket.on(channels.exportFailed, handleExportFailed);
|
||||
|
||||
// RR-only extras
|
||||
|
||||
const onPartialResult = () => {
|
||||
setrrValidationPending(true);
|
||||
setLogs((prev) => [
|
||||
...prev,
|
||||
{
|
||||
timestamp: new Date(),
|
||||
level: "INFO",
|
||||
message:
|
||||
"Repair Order created in Reynolds. Complete validation in Reynolds, then click Finished/Close to finalize."
|
||||
}
|
||||
]);
|
||||
notification.info({
|
||||
message: "Reynolds RO created",
|
||||
description:
|
||||
"Complete validation in Reynolds, then click Finished/Close to finalize and mark this export complete.",
|
||||
duration: 8
|
||||
});
|
||||
};
|
||||
|
||||
const onValidationRequired = (payload) => {
|
||||
setrrValidationPending(true);
|
||||
setLogs((prev) => [
|
||||
...prev,
|
||||
{
|
||||
timestamp: new Date(),
|
||||
level: "INFO",
|
||||
message:
|
||||
"Repair Order created in Reynolds. Complete validation in Reynolds, then click Finished/Close to finalize.",
|
||||
meta: { payload }
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
if (isRrMode && channels.partialResult) activeSocket.on(channels.partialResult, onPartialResult);
|
||||
if (isRrMode && channels.validationNeeded) activeSocket.on(channels.validationNeeded, onValidationRequired);
|
||||
|
||||
return () => {
|
||||
activeSocket.off("connect", onConnect);
|
||||
activeSocket.off("reconnect", onReconnect);
|
||||
activeSocket.off("connect_error", onConnectError);
|
||||
activeSocket.off("disconnect", onDisconnect);
|
||||
|
||||
if (channels.log) activeSocket.off(channels.log, onLog);
|
||||
if (channels.exportSuccess) activeSocket.off(channels.exportSuccess, onExportSuccess);
|
||||
if (channels.exportFailed) activeSocket.off(channels.exportFailed, handleExportFailed);
|
||||
|
||||
if (isRrMode && channels.partialResult) activeSocket.off(channels.partialResult, onPartialResult);
|
||||
if (isRrMode && channels.validationNeeded) activeSocket.off(channels.validationNeeded, onValidationRequired);
|
||||
|
||||
// Only tear down legacy socket listeners; don't disconnect WSS from here
|
||||
if (!isWssMode(mode)) {
|
||||
activeSocket.removeAllListeners();
|
||||
activeSocket.disconnect();
|
||||
}
|
||||
};
|
||||
}, [mode, activeSocket, channels, logLevel, notification, t, insertAuditTrail, history]);
|
||||
|
||||
// RR finalize callback (unchanged public behavior)
|
||||
const handleRrValidationFinished = () => {
|
||||
if (!jobId) return;
|
||||
if (!isWssMode(mode)) return; // RR is WSS-only
|
||||
activeSocket.emit("rr-finalize-repair-order", { jobId }, (ack) => {
|
||||
if (ack?.ok) return;
|
||||
if (ack?.error) notification.error({ message: ack.error });
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) return <LoadingSpinner />;
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
|
||||
if (!jobId || !(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) || !(data && data.jobs_by_pk))
|
||||
if (!jobId || !bodyshopHasDmsKey(bodyshop) || !data?.jobs_by_pk)
|
||||
return <Result status="404" title={t("general.errors.notfound")} />;
|
||||
|
||||
if (data.jobs_by_pk && data.jobs_by_pk.date_exported)
|
||||
return <Result status="warning" title={t("dms.errors.alreadyexported")} />;
|
||||
if (data.jobs_by_pk?.date_exported) return <Result status="warning" title={t("dms.errors.alreadyexported")} />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AlertComponent style={{ marginBottom: 10 }} message={bannerMessage} type="warning" showIcon closable />
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col md={24} lg={10}>
|
||||
<DmsAllocationsSummary
|
||||
title={
|
||||
<span>
|
||||
<Link to={`/manage/jobs/${data && data.jobs_by_pk.id}`}>{`${
|
||||
data && data.jobs_by_pk && data.jobs_by_pk.ro_number
|
||||
}`}</Link>
|
||||
{` | ${OwnerNameDisplayFunction(data.jobs_by_pk)} | ${
|
||||
data.jobs_by_pk.v_model_yr || ""
|
||||
} ${data.jobs_by_pk.v_make_desc || ""} ${data.jobs_by_pk.v_model_desc || ""}`}
|
||||
</span>
|
||||
}
|
||||
socket={socket}
|
||||
jobId={jobId}
|
||||
/>
|
||||
</Col>
|
||||
<Col md={24} lg={14}>
|
||||
<DmsPostForm socket={socket} jobId={jobId} job={data && data.jobs_by_pk} logsRef={logsRef} />
|
||||
<Col md={24} lg={10} className="dms-equal-height-col">
|
||||
{!isRrMode ? (
|
||||
<DmsAllocationsSummary
|
||||
key={resetKey}
|
||||
title={
|
||||
<span>
|
||||
<Link
|
||||
to={`/manage/jobs/${data && data.jobs_by_pk.id}`}
|
||||
>{`${data?.jobs_by_pk && data.jobs_by_pk.ro_number}`}</Link>
|
||||
{` | ${OwnerNameDisplayFunction(data.jobs_by_pk)} | ${data.jobs_by_pk.v_model_yr || ""} ${
|
||||
data.jobs_by_pk.v_make_desc || ""
|
||||
} ${data.jobs_by_pk.v_model_desc || ""}`}
|
||||
</span>
|
||||
}
|
||||
socket={activeSocket}
|
||||
jobId={jobId}
|
||||
mode={mode}
|
||||
/>
|
||||
) : (
|
||||
<RrAllocationsSummary
|
||||
key={resetKey}
|
||||
title={
|
||||
<span>
|
||||
<Link to={`/manage/jobs/${data && data.jobs_by_pk.id}`}>
|
||||
{data?.jobs_by_pk && data.jobs_by_pk.ro_number}
|
||||
</Link>
|
||||
{` | ${OwnerNameDisplayFunction(data.jobs_by_pk)} | ${
|
||||
data.jobs_by_pk.v_model_yr || ""
|
||||
} ${data.jobs_by_pk.v_make_desc || ""} ${data.jobs_by_pk.v_model_desc || ""}`}
|
||||
</span>
|
||||
}
|
||||
socket={activeSocket}
|
||||
jobId={jobId}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
<DmsCustomerSelector />
|
||||
<Col md={24} lg={14} className="dms-equal-height-col">
|
||||
<DmsPostForm key={resetKey} socket={activeSocket} job={data?.jobs_by_pk} logsRef={logsRef} mode={mode} />
|
||||
</Col>
|
||||
|
||||
<DmsCustomerSelector
|
||||
jobid={jobId}
|
||||
bodyshop={bodyshop}
|
||||
socket={activeSocket}
|
||||
mode={mode}
|
||||
rrOptions={{
|
||||
openRoLimit: rrOpenRoLimit,
|
||||
onOpenRoFinished: clearRrOpenRoLimit,
|
||||
validationPending: rrValidationPending,
|
||||
onValidationFinished: handleRrValidationFinished
|
||||
}}
|
||||
/>
|
||||
|
||||
<Col span={24}>
|
||||
<div ref={logsRef}>
|
||||
@@ -165,14 +454,27 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
||||
title={t("jobs.labels.dms.logs")}
|
||||
extra={
|
||||
<Space wrap>
|
||||
{isRrMode && (
|
||||
<>
|
||||
<Switch
|
||||
checked={colorizeJson}
|
||||
onChange={setColorizeJson}
|
||||
checkedChildren="Color JSON"
|
||||
unCheckedChildren="Plain JSON"
|
||||
/>
|
||||
<Button onClick={toggleDetailsAll}>{detailsOpen ? "Collapse All" : "Expand All"}</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Select
|
||||
placeholder="Log Level"
|
||||
value={logLevel}
|
||||
onChange={(value) => {
|
||||
setLogLevel(value);
|
||||
socket.emit("set-log-level", value);
|
||||
setActiveLogLevel(value);
|
||||
}}
|
||||
>
|
||||
<Select.Option key="SILLY">SILLY</Select.Option>
|
||||
<Select.Option key="DEBUG">DEBUG</Select.Option>
|
||||
<Select.Option key="INFO">INFO</Select.Option>
|
||||
<Select.Option key="WARN">WARN</Select.Option>
|
||||
@@ -182,8 +484,14 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
||||
<Button
|
||||
onClick={() => {
|
||||
setLogs([]);
|
||||
socket.disconnect();
|
||||
socket.connect();
|
||||
setResetAfterReconnect(true);
|
||||
if (isWssMode(mode)) {
|
||||
setActiveLogLevel(logLevel);
|
||||
}
|
||||
if (activeSocket) {
|
||||
activeSocket.disconnect();
|
||||
setTimeout(() => activeSocket.connect(), 100);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Reconnect
|
||||
@@ -191,7 +499,15 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<DmsLogEvents socket={socket} logs={logs} />
|
||||
<DmsLogEvents
|
||||
logs={logs}
|
||||
// Only honour details/colorized JSON in RR mode;
|
||||
// in other modes DmsLogEvents can render a simple, flat list.
|
||||
detailsOpen={isRrMode ? detailsOpen : false}
|
||||
detailsNonce={detailsNonce}
|
||||
colorizeJson={isRrMode ? colorizeJson : false}
|
||||
showDetails={isRrMode}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</Col>
|
||||
@@ -199,10 +515,3 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const determineDmsType = (bodyshop) => {
|
||||
if (bodyshop.cdk_dealerid) return "cdk";
|
||||
else {
|
||||
return "pbs";
|
||||
}
|
||||
};
|
||||
|
||||
@@ -42,6 +42,7 @@ import { setModalContext } from "../../redux/modals/modals.actions.js";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import AuditTrailMapping from "../../utils/AuditTrailMappings";
|
||||
import dayjs from "../../utils/day";
|
||||
import { bodyshopHasDmsKey } from "../../utils/dmsUtils.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -69,6 +70,8 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
|
||||
const [loading, setLoading] = useState(false);
|
||||
const notification = useNotification();
|
||||
|
||||
const hasDMSKey = bodyshopHasDmsKey(bodyshop);
|
||||
|
||||
const {
|
||||
treatments: { Qb_Multi_Ar, ClosingPeriod }
|
||||
} = useSplitTreatments({
|
||||
@@ -191,7 +194,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
|
||||
{t("general.actions.close")}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
{(bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid) && (
|
||||
{bodyshopHasDmsKey(bodyshop) && (
|
||||
<Link to={`/manage/dms?jobId=${job.id}`}>
|
||||
<Button disabled={job.date_exported || !jobRO}>{t("jobs.actions.sendtodms")}</Button>
|
||||
</Link>
|
||||
@@ -311,7 +314,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
)}
|
||||
{(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) && (
|
||||
{hasDMSKey && (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.kmin")}
|
||||
name="kmin"
|
||||
@@ -324,7 +327,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
|
||||
<InputNumber precision={0} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
)}
|
||||
{(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) && (
|
||||
{hasDMSKey && (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.kmout")}
|
||||
name="kmout"
|
||||
@@ -348,7 +351,7 @@ export function JobsCloseComponent({ job, bodyshop, jobRO, insertAuditTrail, set
|
||||
<InputNumber precision={0} disabled={jobRO} />
|
||||
</Form.Item>
|
||||
)}
|
||||
{(bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber) && (
|
||||
{hasDMSKey && (
|
||||
<Form.Item
|
||||
label={t("jobs.fields.dms_allocation")}
|
||||
name="dms_allocation"
|
||||
|
||||
@@ -558,6 +558,14 @@
|
||||
"responsibilitycenter_tax_tier": "Tax {{typeNum}} Tier {{typeNumIterator}}",
|
||||
"responsibilitycenter_tax_type": "Tax {{typeNum}} Type",
|
||||
"responsibilitycenters": {
|
||||
"gogcode": "GOG Code (BreakOut)",
|
||||
"item_type": "Item Type",
|
||||
"item_type_gog": "GOG",
|
||||
"item_type_paint": "Paint Materials",
|
||||
"item_type_freight": "Freight",
|
||||
"taxable_flag": "Taxable?",
|
||||
"taxable": "Taxable",
|
||||
"nontaxable": "Non-taxable",
|
||||
"ap": "Accounts Payable",
|
||||
"ar": "Accounts Receivable",
|
||||
"ats": "ATS",
|
||||
@@ -688,6 +696,7 @@
|
||||
"payers": "Payers"
|
||||
},
|
||||
"cdk_dealerid": "CDK Dealer ID",
|
||||
"rr_dealerid": "Reynolds Store Number",
|
||||
"costsmapping": "Costs Mapping",
|
||||
"dms_allocations": "DMS Allocations",
|
||||
"pbs_serialnumber": "PBS Serial Number",
|
||||
@@ -1035,7 +1044,7 @@
|
||||
"alreadyexported": "This job has already been sent to the DMS. If you need to resend it, please use admin permissions to mark the job for re-export."
|
||||
},
|
||||
"labels": {
|
||||
"refreshallocations": "Refresh to see DMS Allocataions."
|
||||
"refreshallocations": "Refresh to see DMS Allocations."
|
||||
}
|
||||
},
|
||||
"documents": {
|
||||
@@ -1212,6 +1221,8 @@
|
||||
},
|
||||
"general": {
|
||||
"actions": {
|
||||
"select": "Select",
|
||||
"optional": "Optional",
|
||||
"add": "Add",
|
||||
"autoupdate": "{{app}} will automatically update in {{time}} seconds. Please save all changes.",
|
||||
"calculate": "Calculate",
|
||||
@@ -1777,6 +1788,8 @@
|
||||
"id": "DMS ID",
|
||||
"inservicedate": "In Service Date",
|
||||
"journal": "Journal #",
|
||||
"make_override": "Make Override",
|
||||
"advisor": "Advisor #",
|
||||
"lines": "Posting Lines",
|
||||
"name1": "Customer Name",
|
||||
"payer": {
|
||||
@@ -1784,7 +1797,8 @@
|
||||
"control_type": "Control Type",
|
||||
"controlnumber": "Control Number",
|
||||
"dms_acctnumber": "DMS Account #",
|
||||
"name": "Payer Name"
|
||||
"name": "Payer Name",
|
||||
"payer_type": "Payer"
|
||||
},
|
||||
"sale": "Sale",
|
||||
"sale_dms_acctnumber": "Sale DMS Acct #",
|
||||
@@ -1851,7 +1865,7 @@
|
||||
"loss_of_use": "Loss of Use",
|
||||
"lost_sale_reason": "Lost Sale Reason",
|
||||
"ma2s": "2 Stage Paint",
|
||||
"ma3s": "3 Stage Pain",
|
||||
"ma3s": "3 Stage Paint",
|
||||
"mabl": "MABL?",
|
||||
"macs": "MACS?",
|
||||
"mahw": "Hazardous Waste",
|
||||
@@ -3209,6 +3223,7 @@
|
||||
"parts_not_recieved_vendor": "Parts Not Received by Vendor",
|
||||
"parts_received_not_scheduled": "Parts Received for Jobs Not Scheduled",
|
||||
"payments_by_date": "Payments by Date",
|
||||
"payments_by_date_excel": "Payments by Date - Excel",
|
||||
"payments_by_date_payment": "Payments by Date and Payment Type",
|
||||
"payments_by_date_type": "Payments by Date and Customer Type",
|
||||
"production_by_category": "Production by Category",
|
||||
|
||||
@@ -558,6 +558,14 @@
|
||||
"responsibilitycenter_tax_tier": "",
|
||||
"responsibilitycenter_tax_type": "",
|
||||
"responsibilitycenters": {
|
||||
"gogcode": "",
|
||||
"item_type": "Item Type",
|
||||
"item_type_gog": "",
|
||||
"item_type_paint": "",
|
||||
"item_type_freight": "",
|
||||
"taxable_flag": "",
|
||||
"taxable": "",
|
||||
"nontaxable": "",
|
||||
"ap": "",
|
||||
"ar": "",
|
||||
"ats": "",
|
||||
@@ -688,6 +696,7 @@
|
||||
"payers": ""
|
||||
},
|
||||
"cdk_dealerid": "",
|
||||
"rr_dealerid": "",
|
||||
"costsmapping": "",
|
||||
"dms_allocations": "",
|
||||
"pbs_serialnumber": "",
|
||||
@@ -1772,6 +1781,8 @@
|
||||
"dms_make": "",
|
||||
"dms_model": "",
|
||||
"dms_model_override": "",
|
||||
"make_override": "",
|
||||
"advisor": "",
|
||||
"dms_unsold": "",
|
||||
"dms_wip_acctnumber": "",
|
||||
"id": "",
|
||||
@@ -3209,6 +3220,7 @@
|
||||
"parts_not_recieved_vendor": "",
|
||||
"parts_received_not_scheduled": "",
|
||||
"payments_by_date": "",
|
||||
"payments_by_date_excel": "",
|
||||
"payments_by_date_payment": "",
|
||||
"payments_by_date_type": "",
|
||||
"production_by_category": "",
|
||||
|
||||
@@ -558,6 +558,14 @@
|
||||
"responsibilitycenter_tax_tier": "",
|
||||
"responsibilitycenter_tax_type": "",
|
||||
"responsibilitycenters": {
|
||||
"gogcode": "",
|
||||
"item_type": "Item Type",
|
||||
"item_type_gog": "",
|
||||
"item_type_paint": "",
|
||||
"item_type_freight": "",
|
||||
"taxable_flag": "",
|
||||
"taxable": "",
|
||||
"nontaxable": "",
|
||||
"ap": "",
|
||||
"ar": "",
|
||||
"ats": "",
|
||||
@@ -688,6 +696,7 @@
|
||||
"payers": ""
|
||||
},
|
||||
"cdk_dealerid": "",
|
||||
"rr_dealerid": "",
|
||||
"costsmapping": "",
|
||||
"dms_allocations": "",
|
||||
"pbs_serialnumber": "",
|
||||
@@ -1772,6 +1781,8 @@
|
||||
"dms_make": "",
|
||||
"dms_model": "",
|
||||
"dms_model_override": "",
|
||||
"make_override": "",
|
||||
"advisor": "",
|
||||
"dms_unsold": "",
|
||||
"dms_wip_acctnumber": "",
|
||||
"id": "",
|
||||
@@ -3209,6 +3220,7 @@
|
||||
"parts_not_recieved_vendor": "",
|
||||
"parts_received_not_scheduled": "",
|
||||
"payments_by_date": "",
|
||||
"payments_by_date_excel": "",
|
||||
"payments_by_date_payment": "",
|
||||
"payments_by_date_type": "",
|
||||
"production_by_category": "",
|
||||
|
||||
@@ -1218,6 +1218,18 @@ export const TemplateList = (type, context) => {
|
||||
},
|
||||
group: "customers"
|
||||
},
|
||||
payments_by_date_excel: {
|
||||
title: i18n.t("reportcenter.templates.payments_by_date_excel"),
|
||||
subject: i18n.t("reportcenter.templates.payments_by_date_excel"),
|
||||
key: "payments_by_date",
|
||||
reporttype: "excel",
|
||||
disabled: false,
|
||||
rangeFilter: {
|
||||
object: i18n.t("reportcenter.labels.objects.payments"),
|
||||
field: i18n.t("payments.fields.date")
|
||||
},
|
||||
group: "customers"
|
||||
},
|
||||
schedule: {
|
||||
title: i18n.t("reportcenter.templates.schedule"),
|
||||
subject: i18n.t("reportcenter.templates.schedule"),
|
||||
|
||||
72
client/src/utils/dmsUtils.js
Normal file
72
client/src/utils/dmsUtils.js
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* DMS type mapping constants.
|
||||
* CAREFUL: the values here are used as canonical "mode" strings elsewhere in the app.
|
||||
* @type {{reynolds: string, cdk: string, pbs: string, fortellis: string}}
|
||||
*/
|
||||
export const DMS_MAP = {
|
||||
reynolds: "rr",
|
||||
cdk: "cdk",
|
||||
pbs: "pbs",
|
||||
fortellis: "fortellis"
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines the DMS type for a given bodyshop object.
|
||||
* @param bodyshop
|
||||
* @returns {*|string}
|
||||
*/
|
||||
export const determineDMSTypeByBodyshop = (bodyshop) => {
|
||||
const dmsMapping = {
|
||||
cdk_dealerid: DMS_MAP.cdk,
|
||||
pbs_serialnumber: DMS_MAP.pbs,
|
||||
rr_dealerid: DMS_MAP.reynolds
|
||||
};
|
||||
|
||||
return Object.keys(dmsMapping).find((key) => bodyshop[key])
|
||||
? dmsMapping[Object.keys(dmsMapping).find((key) => bodyshop[key])]
|
||||
: DMS_MAP.pbs;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines the translation key for a given DMS type.
|
||||
* @param dmsType
|
||||
* @returns {*|string}
|
||||
*/
|
||||
export const determineDmsTypeTranslationKey = (dmsType) => {
|
||||
const dmsTypeMapping = {
|
||||
[DMS_MAP.cdk]: "bodyshop.labels.dms.cdk",
|
||||
[DMS_MAP.pbs]: "bodyshop.labels.dms.pbs",
|
||||
[DMS_MAP.reynolds]: "bodyshop.labels.dms.rr"
|
||||
};
|
||||
|
||||
return dmsTypeMapping[dmsType] || dmsTypeMapping[DMS_MAP.pbs];
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a normalized "mode" we can switch on:
|
||||
* @param bodyshop
|
||||
* @param fortellisTreatment
|
||||
* @returns {*|string|string}
|
||||
*/
|
||||
export const getDmsMode = (bodyshop, fortellisTreatment) => {
|
||||
const base = determineDMSTypeByBodyshop(bodyshop); // "rr" | "cdk" | "pbs" | undefined
|
||||
if (base === DMS_MAP.cdk && fortellisTreatment === "on") return DMS_MAP.fortellis;
|
||||
return base ?? "none";
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the DMS mode uses WSS.
|
||||
* @param mode
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const isWssMode = (mode) => {
|
||||
return mode === DMS_MAP.reynolds || mode === DMS_MAP.fortellis;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the bodyshop has any DMS key configured.
|
||||
* @param bodyshop
|
||||
* @returns {*|string}
|
||||
*/
|
||||
export const bodyshopHasDmsKey = (bodyshop) =>
|
||||
bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber || bodyshop.rr_dealerid;
|
||||
16
client/src/utils/legacySocket.js
Normal file
16
client/src/utils/legacySocket.js
Normal file
@@ -0,0 +1,16 @@
|
||||
// client/src/utils/legacySocket.js
|
||||
import SocketIO from "socket.io-client";
|
||||
import { auth } from "../firebase/firebase.utils";
|
||||
|
||||
// Create once, reuse everywhere.
|
||||
const legacySocket = SocketIO(import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "", {
|
||||
path: "/ws",
|
||||
withCredentials: true,
|
||||
autoConnect: false,
|
||||
auth: async (callback) => {
|
||||
const token = auth.currentUser && (await auth.currentUser.getIdToken());
|
||||
callback({ token });
|
||||
}
|
||||
});
|
||||
|
||||
export default legacySocket;
|
||||
@@ -0,0 +1,5 @@
|
||||
alter table "public"."media_analytics_detail" drop constraint "media_analytics_detail_jobid_fkey",
|
||||
add constraint "media_analytics_detail_jobid_fkey"
|
||||
foreign key ("jobid")
|
||||
references "public"."jobs"
|
||||
("id") on update restrict on delete restrict;
|
||||
@@ -0,0 +1,5 @@
|
||||
alter table "public"."media_analytics_detail" drop constraint "media_analytics_detail_jobid_fkey",
|
||||
add constraint "media_analytics_detail_jobid_fkey"
|
||||
foreign key ("jobid")
|
||||
references "public"."jobs"
|
||||
("id") on update set null on delete set null;
|
||||
@@ -0,0 +1,23 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- CREATE OR REPLACE FUNCTION set_fk_to_null_if_invalid_media_analytics()
|
||||
-- RETURNS TRIGGER AS $$
|
||||
-- BEGIN
|
||||
-- -- Check if the foreign key value is not NULL
|
||||
-- IF NEW.jobid IS NOT NULL THEN
|
||||
-- -- Check if the corresponding record exists in the parent table
|
||||
-- IF NOT EXISTS (SELECT 1 FROM jobs WHERE id = NEW.jobid) THEN
|
||||
-- -- If it doesn't exist, set the foreign key to NULL
|
||||
-- NEW.jobid = NULL;
|
||||
-- END IF;
|
||||
-- END IF;
|
||||
--
|
||||
-- -- Return the (potentially modified) record to be inserted/updated
|
||||
-- RETURN NEW;
|
||||
-- END;
|
||||
-- $$ LANGUAGE plpgsql;
|
||||
--
|
||||
-- CREATE TRIGGER media_analytics_fk_null
|
||||
-- BEFORE INSERT OR UPDATE ON media_analytics_detail
|
||||
-- FOR EACH ROW
|
||||
-- EXECUTE FUNCTION set_fk_to_null_if_invalid_media_analytics();
|
||||
@@ -0,0 +1,21 @@
|
||||
CREATE OR REPLACE FUNCTION set_fk_to_null_if_invalid_media_analytics()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- Check if the foreign key value is not NULL
|
||||
IF NEW.jobid IS NOT NULL THEN
|
||||
-- Check if the corresponding record exists in the parent table
|
||||
IF NOT EXISTS (SELECT 1 FROM jobs WHERE id = NEW.jobid) THEN
|
||||
-- If it doesn't exist, set the foreign key to NULL
|
||||
NEW.jobid = NULL;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- Return the (potentially modified) record to be inserted/updated
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER media_analytics_fk_null
|
||||
BEFORE INSERT OR UPDATE ON media_analytics_detail
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_fk_to_null_if_invalid_media_analytics();
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS "public"."media_analytics_detail_bodyshopid";
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE INDEX "media_analytics_detail_bodyshopid" on
|
||||
"public"."media_analytics_detail" using btree ("bodyshopid");
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS "public"."media_analytics_detail_jobid";
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE INDEX "media_analytics_detail_jobid" on
|
||||
"public"."media_analytics_detail" using btree ("jobid");
|
||||
@@ -0,0 +1 @@
|
||||
DROP INDEX IF EXISTS "public"."media_analytics_detail_media_analytics";
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE INDEX "media_analytics_detail_media_analytics" on
|
||||
"public"."media_analytics_detail" using btree ("media_analytics_id");
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."media_analytics" add column "unique_documents" numeric
|
||||
-- null;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."media_analytics" add column "unique_documents" numeric
|
||||
null;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."media_analytics" add column "duplicate_documents" numeric
|
||||
-- null;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."media_analytics" add column "duplicate_documents" numeric
|
||||
null;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."media_analytics_detail" add column "unique_documents" numeric
|
||||
-- null;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."media_analytics_detail" add column "unique_documents" numeric
|
||||
null;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."media_analytics_detail" add column "duplicate_documents" numeric
|
||||
-- null;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."media_analytics_detail" add column "duplicate_documents" numeric
|
||||
null;
|
||||
@@ -0,0 +1 @@
|
||||
alter table "public"."media_analytics_detail" rename column "unique_document_count" to "unique_documents";
|
||||
@@ -0,0 +1 @@
|
||||
alter table "public"."media_analytics_detail" rename column "unique_documents" to "unique_document_count";
|
||||
@@ -0,0 +1 @@
|
||||
alter table "public"."media_analytics_detail" rename column "duplicate_count" to "duplicate_documents";
|
||||
@@ -0,0 +1 @@
|
||||
alter table "public"."media_analytics_detail" rename column "duplicate_documents" to "duplicate_count";
|
||||
2813
package-lock.json
generated
2813
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
52
package.json
52
package.json
@@ -18,24 +18,25 @@
|
||||
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-cloudwatch-logs": "^3.901.0",
|
||||
"@aws-sdk/client-elasticache": "^3.901.0",
|
||||
"@aws-sdk/client-s3": "^3.901.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.901.0",
|
||||
"@aws-sdk/client-ses": "^3.901.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.901.0",
|
||||
"@aws-sdk/lib-storage": "^3.903.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.901.0",
|
||||
"@aws-sdk/client-cloudwatch-logs": "^3.932.0",
|
||||
"@aws-sdk/client-elasticache": "^3.932.0",
|
||||
"@aws-sdk/client-s3": "^3.932.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.932.0",
|
||||
"@aws-sdk/client-ses": "^3.932.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.932.0",
|
||||
"@aws-sdk/lib-storage": "^3.932.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.932.0",
|
||||
"@opensearch-project/opensearch": "^2.13.0",
|
||||
"@socket.io/admin-ui": "^0.5.1",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
"archiver": "^7.0.1",
|
||||
"aws4": "^1.13.2",
|
||||
"axios": "^1.12.2",
|
||||
"axios": "^1.13.2",
|
||||
"axios-curlirize": "^2.0.0",
|
||||
"better-queue": "^3.8.12",
|
||||
"bullmq": "^5.61.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"cloudinary": "^2.7.0",
|
||||
"bullmq": "^5.63.2",
|
||||
"chart.js": "^4.5.1",
|
||||
"cloudinary": "^2.8.0",
|
||||
"compression": "^1.8.1",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
@@ -43,40 +44,43 @@
|
||||
"dinero.js": "^1.9.1",
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^4.21.1",
|
||||
"firebase-admin": "^13.5.0",
|
||||
"graphql": "^16.11.0",
|
||||
"fast-xml-parser": "^5.3.2",
|
||||
"firebase-admin": "^13.6.0",
|
||||
"graphql": "^16.12.0",
|
||||
"graphql-request": "^6.1.0",
|
||||
"intuit-oauth": "^4.2.0",
|
||||
"ioredis": "^5.8.1",
|
||||
"json-2-csv": "^5.5.9",
|
||||
"intuit-oauth": "^4.2.2",
|
||||
"ioredis": "^5.8.2",
|
||||
"json-2-csv": "^5.5.10",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"juice": "^11.0.3",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.6.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"multer": "^2.0.2",
|
||||
"mustache": "^4.2.0",
|
||||
"node-persist": "^4.0.4",
|
||||
"nodemailer": "^6.10.0",
|
||||
"phone": "^3.1.67",
|
||||
"query-string": "7.1.3",
|
||||
"recursive-diff": "^1.0.9",
|
||||
"rimraf": "^6.0.1",
|
||||
"rimraf": "^6.1.0",
|
||||
"skia-canvas": "^3.0.8",
|
||||
"soap": "^1.5.0",
|
||||
"soap": "^1.6.0",
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-adapter": "^2.5.5",
|
||||
"ssh2-sftp-client": "^11.0.0",
|
||||
"twilio": "^5.10.2",
|
||||
"twilio": "^5.10.5",
|
||||
"uuid": "^11.1.0",
|
||||
"winston": "^3.18.3",
|
||||
"winston-cloudwatch": "^6.3.0",
|
||||
"xml-formatter": "^3.6.7",
|
||||
"xml2js": "^0.6.2",
|
||||
"xmlbuilder2": "^3.1.1",
|
||||
"xmlbuilder2": "^4.0.0",
|
||||
"yazl": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.37.0",
|
||||
"eslint": "^9.37.0",
|
||||
"@eslint/js": "^9.39.1",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^15.15.0",
|
||||
"mock-require": "^3.0.3",
|
||||
|
||||
31
server.js
31
server.js
@@ -38,7 +38,7 @@ const { registerCleanupTask, initializeCleanupManager } = require("./server/util
|
||||
|
||||
const { loadEmailQueue } = require("./server/notifications/queues/emailQueue");
|
||||
const { loadAppQueue } = require("./server/notifications/queues/appQueue");
|
||||
|
||||
const { SetLegacyWebsocketHandlers } = require("./server/web-sockets/web-socket");
|
||||
const CLUSTER_RETRY_BASE_DELAY = 100;
|
||||
const CLUSTER_RETRY_MAX_DELAY = 5000;
|
||||
const CLUSTER_RETRY_JITTER = 100;
|
||||
@@ -247,6 +247,29 @@ const connectToRedisCluster = async () => {
|
||||
});
|
||||
reject(err);
|
||||
});
|
||||
|
||||
redisCluster.on("+node", (node) => {
|
||||
logger.log(`Redis cluster node added`, "INFO", "redis", "api", {
|
||||
host: node.options.host,
|
||||
port: node.options.port
|
||||
});
|
||||
});
|
||||
|
||||
redisCluster.on("-node", (node) => {
|
||||
logger.log(`Redis cluster node removed`, "WARN", "redis", "api", {
|
||||
host: node.options.host,
|
||||
port: node.options.port
|
||||
});
|
||||
});
|
||||
|
||||
redisCluster.on("node error", (error, node) => {
|
||||
console.dir(error);
|
||||
logger.log(`Redis node error`, "ERROR", "redis", "api", {
|
||||
host: node.options.host,
|
||||
port: node.options.port,
|
||||
message: error.message
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -324,6 +347,9 @@ const applySocketIO = async ({ server, app }) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Legacy Socket Events
|
||||
SetLegacyWebsocketHandlers(io);
|
||||
|
||||
const api = {
|
||||
pubClient,
|
||||
io,
|
||||
@@ -387,9 +413,6 @@ const main = async () => {
|
||||
const redisHelpers = applyRedisHelpers({ pubClient, app, logger });
|
||||
const ioHelpers = applyIOHelpers({ app, redisHelpers, ioRedis, logger });
|
||||
|
||||
// Legacy Socket Events
|
||||
require("./server/web-sockets/web-socket");
|
||||
|
||||
// Initialize Queues
|
||||
await loadQueues({ pubClient: pubClient, logger, redisHelpers, ioRedis });
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const GraphQLClient = require("graphql-request").GraphQLClient;
|
||||
|
||||
const queries = require("../../graphql-client/queries");
|
||||
const CdkBase = require("../../web-sockets/web-socket");
|
||||
const WsLogger = require("../../web-sockets/createLogEvent");
|
||||
const moment = require("moment");
|
||||
const Dinero = require("dinero.js");
|
||||
const AxiosLib = require("axios").default;
|
||||
@@ -23,7 +23,7 @@ axios.interceptors.request.use((x) => {
|
||||
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
|
||||
//console.log(printable);
|
||||
|
||||
CdkBase.createJsonEvent(socket, "SILLY", `Raw Request: ${printable}`, x.data);
|
||||
WsLogger.createJsonEvent(socket, "SILLY", `Raw Request: ${printable}`, x.data);
|
||||
|
||||
return x;
|
||||
});
|
||||
@@ -33,14 +33,14 @@ axios.interceptors.response.use((x) => {
|
||||
|
||||
const printable = `${new Date()} | Response: ${x.status} | ${JSON.stringify(x.data)}`;
|
||||
//console.log(printable);
|
||||
CdkBase.createJsonEvent(socket, "SILLY", `Raw Response: ${printable}`, x.data);
|
||||
WsLogger.createJsonEvent(socket, "SILLY", `Raw Response: ${printable}`, x.data);
|
||||
|
||||
return x;
|
||||
});
|
||||
|
||||
async function PbsCalculateAllocationsAp(socket, billids) {
|
||||
try {
|
||||
CdkBase.createLogEvent(socket, "DEBUG", `Received request to calculate allocations for ${billids}`);
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `Received request to calculate allocations for ${billids}`);
|
||||
const { bills, bodyshops } = await QueryBillData(socket, billids);
|
||||
const bodyshop = bodyshops[0];
|
||||
socket.bodyshop = bodyshop;
|
||||
@@ -50,7 +50,7 @@ async function PbsCalculateAllocationsAp(socket, billids) {
|
||||
|
||||
const transactionlist = [];
|
||||
if (bills.length === 0) {
|
||||
CdkBase.createLogEvent(
|
||||
WsLogger.createLogEvent(
|
||||
socket,
|
||||
"ERROR",
|
||||
`No bills found for export. Ensure they have not already been exported and try again.`
|
||||
@@ -147,7 +147,9 @@ async function PbsCalculateAllocationsAp(socket, billids) {
|
||||
...billHash[key],
|
||||
Amount: billHash[key].Amount.toFormat("0.00")
|
||||
});
|
||||
APAmount = APAmount.add(billHash[key].Amount); //Calculate the total expense for the bill iteratively to create the corresponding credit to AP.
|
||||
//Calculate the total expense for the bill iteratively to
|
||||
// create the corresponding credit to AP.
|
||||
APAmount = APAmount.add(billHash[key].Amount);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -166,19 +168,21 @@ async function PbsCalculateAllocationsAp(socket, billids) {
|
||||
|
||||
return transactionlist;
|
||||
} catch (error) {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error encountered in PbsCalculateAllocationsAp. ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error encountered in PbsCalculateAllocationsAp. ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
exports.PbsCalculateAllocationsAp = PbsCalculateAllocationsAp;
|
||||
|
||||
async function QueryBillData(socket, billids) {
|
||||
CdkBase.createLogEvent(socket, "DEBUG", `Querying bill data for id(s) ${billids}`);
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `Querying bill data for id(s) ${billids}`);
|
||||
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
|
||||
|
||||
const result = await client
|
||||
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
|
||||
.request(queries.GET_PBS_AP_ALLOCATIONS, { billids: billids });
|
||||
CdkBase.createLogEvent(socket, "SILLY", `Bill data query result ${JSON.stringify(result, null, 2)}`);
|
||||
|
||||
WsLogger.createLogEvent(socket, "SILLY", `Bill data query result ${JSON.stringify(result, null, 2)}`);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -192,40 +196,10 @@ function getCostAccount(billline, respcenters) {
|
||||
return respcenters.costs.find((c) => c.name === acctName);
|
||||
}
|
||||
|
||||
exports.PbsExportAp = async function (socket, { billids, txEnvelope }) {
|
||||
CdkBase.createLogEvent(socket, "DEBUG", `Exporting selected AP.`);
|
||||
|
||||
//apAllocations has the same shap as the lines key for the accounting posting to PBS.
|
||||
socket.apAllocations = await PbsCalculateAllocationsAp(socket, billids);
|
||||
socket.txEnvelope = txEnvelope;
|
||||
for (const allocation of socket.apAllocations) {
|
||||
const { billid, ...restAllocation } = allocation;
|
||||
const { data: AccountPostingChange } = await axios.post(PBS_ENDPOINTS.AccountingPostingChange, restAllocation, {
|
||||
auth: PBS_CREDENTIALS,
|
||||
socket
|
||||
});
|
||||
|
||||
CheckForErrors(socket, AccountPostingChange);
|
||||
|
||||
if (AccountPostingChange.WasSuccessful) {
|
||||
CdkBase.createLogEvent(socket, "DEBUG", `Marking bill as exported.`);
|
||||
await MarkApExported(socket, [billid]);
|
||||
|
||||
socket.emit("ap-export-success", billid);
|
||||
} else {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Export was not successful.`);
|
||||
socket.emit("ap-export-failure", {
|
||||
billid,
|
||||
error: AccountPostingChange.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
socket.emit("ap-export-complete");
|
||||
};
|
||||
|
||||
async function MarkApExported(socket, billids) {
|
||||
CdkBase.createLogEvent(socket, "DEBUG", `Marking bills as exported for id ${billids}`);
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `Marking bills as exported for id ${billids}`);
|
||||
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
|
||||
|
||||
const result = await client
|
||||
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
|
||||
.request(queries.MARK_BILLS_EXPORTED, {
|
||||
@@ -244,3 +218,36 @@ async function MarkApExported(socket, billids) {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const defaultHandler = async (socket, { billids, txEnvelope }) => {
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `Exporting selected AP.`);
|
||||
|
||||
//apAllocations has the same shap as the lines key for the accounting posting to PBS.
|
||||
socket.apAllocations = await PbsCalculateAllocationsAp(socket, billids);
|
||||
socket.txEnvelope = txEnvelope;
|
||||
for (const allocation of socket.apAllocations) {
|
||||
const { billid, ...restAllocation } = allocation;
|
||||
const { data: AccountPostingChange } = await axios.post(PBS_ENDPOINTS.AccountingPostingChange, restAllocation, {
|
||||
auth: PBS_CREDENTIALS,
|
||||
socket
|
||||
});
|
||||
|
||||
CheckForErrors(socket, AccountPostingChange);
|
||||
|
||||
if (AccountPostingChange.WasSuccessful) {
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `Marking bill as exported.`);
|
||||
await MarkApExported(socket, [billid]);
|
||||
|
||||
socket.emit("ap-export-success", billid);
|
||||
} else {
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Export was not successful.`);
|
||||
socket.emit("ap-export-failure", {
|
||||
billid,
|
||||
error: AccountPostingChange.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
socket.emit("ap-export-complete");
|
||||
};
|
||||
|
||||
exports.PbsExportAp = defaultHandler;
|
||||
|
||||
@@ -2,10 +2,12 @@ const GraphQLClient = require("graphql-request").GraphQLClient;
|
||||
const AxiosLib = require("axios").default;
|
||||
const queries = require("../../graphql-client/queries");
|
||||
const { PBS_ENDPOINTS, PBS_CREDENTIALS } = require("./pbs-constants");
|
||||
const WsLogger = require("../../web-sockets/createLogEvent");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
//const { CDK_CREDENTIALS, CheckCdkResponseForError } = require("./cdk-wsdl");
|
||||
const CalculateAllocations = require("../../cdk/cdk-calculate-allocations").default;
|
||||
const CdkBase = require("../../web-sockets/web-socket");
|
||||
const moment = require("moment-timezone");
|
||||
const Dinero = require("dinero.js");
|
||||
const InstanceManager = require("../../utils/instanceMgr").default;
|
||||
@@ -19,11 +21,12 @@ axios.interceptors.request.use((x) => {
|
||||
...x.headers[x.method],
|
||||
...x.headers
|
||||
};
|
||||
const printable = `${new Date()} | Request: ${x.method.toUpperCase()} | ${x.url
|
||||
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
|
||||
const printable = `${new Date()} | Request: ${x.method.toUpperCase()} | ${
|
||||
x.url
|
||||
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
|
||||
//logRequestToFile(printable);
|
||||
|
||||
CdkBase.createJsonEvent(socket, "DEBUG", `Raw Request: ${printable}`, x.data);
|
||||
WsLogger.createJsonEvent(socket, "DEBUG", `Raw Request: ${printable}`, x.data);
|
||||
|
||||
return x;
|
||||
});
|
||||
@@ -33,13 +36,11 @@ axios.interceptors.response.use((x) => {
|
||||
|
||||
const printable = `${new Date()} | Response: ${x.status} ${x.statusText} |${JSON.stringify(x.data)}`;
|
||||
//logRequestToFile(printable);
|
||||
CdkBase.createJsonEvent(socket, "DEBUG", `Raw Response: ${printable}`, x.data);
|
||||
WsLogger.createJsonEvent(socket, "DEBUG", `Raw Response: ${printable}`, x.data);
|
||||
|
||||
return x;
|
||||
});
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require("path");
|
||||
function logRequestToFile(printable) {
|
||||
try {
|
||||
const logDir = path.join(process.cwd(), "logs");
|
||||
@@ -53,21 +54,20 @@ function logRequestToFile(printable) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
exports.default = async function (socket, { txEnvelope, jobid }) {
|
||||
const defaultHandler = async (socket, { txEnvelope, jobid }) => {
|
||||
socket.logEvents = [];
|
||||
socket.recordid = jobid;
|
||||
socket.txEnvelope = txEnvelope;
|
||||
try {
|
||||
CdkBase.createLogEvent(socket, "INFO", `Received Job export request for id ${jobid}`);
|
||||
WsLogger.createLogEvent(socket, "INFO", `Received Job export request for id ${jobid}`);
|
||||
|
||||
const JobData = await QueryJobData(socket, jobid);
|
||||
socket.JobData = JobData;
|
||||
CdkBase.createLogEvent(socket, "INFO", `Querying the DMS for the Vehicle Record.`);
|
||||
WsLogger.createLogEvent(socket, "INFO", `Querying the DMS for the Vehicle Record.`);
|
||||
//Query for the Vehicle record to get the associated customer.
|
||||
socket.DmsVeh = await QueryVehicleFromDms(socket);
|
||||
//Todo: Need to validate the lines and methods below.
|
||||
if (socket.DmsVeh && socket.DmsVeh.CustomerRef) {
|
||||
if (socket.DmsVeh?.CustomerRef) {
|
||||
//Get the associated customer from the Vehicle Record.
|
||||
socket.DMSVehCustomer = await QueryCustomerBycodeFromDms(socket, socket.DmsVeh.CustomerRef);
|
||||
}
|
||||
@@ -78,35 +78,39 @@ exports.default = async function (socket, { txEnvelope, jobid }) {
|
||||
...socket.DMSCustList
|
||||
]);
|
||||
} catch (error) {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error encountered in PbsJobExport. ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error encountered in PbsJobExport. ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
exports.default = defaultHandler;
|
||||
|
||||
exports.PbsSelectedCustomer = async function PbsSelectedCustomer(socket, selectedCustomerId) {
|
||||
try {
|
||||
socket.selectedCustomerId = selectedCustomerId;
|
||||
if (socket.JobData.bodyshop.pbs_configuration.disablecontactvehicle !== true) {
|
||||
CdkBase.createLogEvent(socket, "INFO", `User selected customer ${selectedCustomerId || "NEW"}`);
|
||||
WsLogger.createLogEvent(socket, "INFO", `User selected customer ${selectedCustomerId || "NEW"}`);
|
||||
|
||||
//Upsert the contact information as per Wafaa's Email.
|
||||
CdkBase.createLogEvent(
|
||||
WsLogger.createLogEvent(
|
||||
socket,
|
||||
"INFO",
|
||||
`Upserting contact information to DMS for ${socket.JobData.ownr_fn || ""
|
||||
`Upserting contact information to DMS for ${
|
||||
socket.JobData.ownr_fn || ""
|
||||
} ${socket.JobData.ownr_ln || ""} ${socket.JobData.ownr_co_nm || ""}`
|
||||
);
|
||||
const ownerRef = await UpsertContactData(socket, selectedCustomerId);
|
||||
socket.ownerRef = ownerRef;
|
||||
CdkBase.createLogEvent(socket, "INFO", `Upserting vehicle information to DMS for ${socket.JobData.v_vin}`);
|
||||
|
||||
WsLogger.createLogEvent(socket, "INFO", `Upserting vehicle information to DMS for ${socket.JobData.v_vin}`);
|
||||
const vehicleRef = await UpsertVehicleData(socket, ownerRef.ReferenceId);
|
||||
socket.vehicleRef = vehicleRef;
|
||||
} else {
|
||||
CdkBase.createLogEvent(
|
||||
WsLogger.createLogEvent(
|
||||
socket,
|
||||
"INFO",
|
||||
`Contact and Vehicle updates disabled. Querying data and skipping to accounting data insert.`
|
||||
);
|
||||
//Must query for records to insert $0 RO.
|
||||
//Must query for records to insert $0 RO.
|
||||
if (!socket.ownerRef) {
|
||||
const ownerRef = (await QueryCustomerBycodeFromDms(socket, selectedCustomerId))?.[0];
|
||||
socket.ownerRef = ownerRef;
|
||||
@@ -114,22 +118,21 @@ exports.PbsSelectedCustomer = async function PbsSelectedCustomer(socket, selecte
|
||||
const vehicleRef = await GetVehicleData(socket, socket.ownerRef?.ReferenceId || socket.selectedCustomerId);
|
||||
socket.vehicleRef = vehicleRef;
|
||||
}
|
||||
CdkBase.createLogEvent(socket, "INFO", `Inserting accounting posting data..`);
|
||||
WsLogger.createLogEvent(socket, "INFO", `Inserting account posting data...`);
|
||||
const insertResponse = await InsertAccountPostingData(socket);
|
||||
|
||||
if (insertResponse.WasSuccessful) {
|
||||
if (socket.JobData.bodyshop.pbs_configuration.ro_posting) {
|
||||
|
||||
await CreateRepairOrderInPBS(socket, socket.ownerRef, socket.vehicleRef)
|
||||
await CreateRepairOrderInPBS(socket, socket.ownerRef, socket.vehicleRef);
|
||||
}
|
||||
CdkBase.createLogEvent(socket, "INFO", `Marking job as exported.`);
|
||||
WsLogger.createLogEvent(socket, "INFO", `Marking job as exported.`);
|
||||
await MarkJobExported(socket, socket.JobData.id);
|
||||
socket.emit("export-success", socket.JobData.id);
|
||||
} else {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Export was not successful.`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Export was not successful.`);
|
||||
}
|
||||
} catch (error) {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error encountered in PbsSelectedCustomer. ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error encountered in PbsSelectedCustomer. ${error}`);
|
||||
await InsertFailedExportLog(socket, error);
|
||||
}
|
||||
};
|
||||
@@ -137,22 +140,24 @@ exports.PbsSelectedCustomer = async function PbsSelectedCustomer(socket, selecte
|
||||
// Was Successful
|
||||
async function CheckForErrors(socket, response) {
|
||||
if (response.WasSuccessful === undefined || response.WasSuccessful === true) {
|
||||
CdkBase.createLogEvent(socket, "INFO", `Successful response from DMS. ${response.Message || ""}`);
|
||||
WsLogger.createLogEvent(socket, "INFO", `Successful response from DMS. ${response.Message || ""}`);
|
||||
} else {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error received from DMS: ${response.Message}`);
|
||||
CdkBase.createLogEvent(socket, "DEBUG", `Error received from DMS: ${JSON.stringify(response)}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error received from DMS: ${response.Message}`);
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `Error received from DMS: ${JSON.stringify(response)}`);
|
||||
}
|
||||
}
|
||||
|
||||
exports.CheckForErrors = CheckForErrors;
|
||||
|
||||
async function QueryJobData(socket, jobid) {
|
||||
CdkBase.createLogEvent(socket, "INFO", `Querying job data for id ${jobid}`);
|
||||
WsLogger.createLogEvent(socket, "INFO", `Querying job data for id ${jobid}`);
|
||||
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
|
||||
|
||||
const result = await client
|
||||
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
|
||||
.request(queries.QUERY_JOBS_FOR_PBS_EXPORT, { id: jobid });
|
||||
CdkBase.createLogEvent(socket, "DEBUG", `Job data query result ${JSON.stringify(result, null, 2)}`);
|
||||
|
||||
//WsLogger.createLogEvent(socket, "DEBUG", `Job data query result ${JSON.stringify(result, null, 2)}`);
|
||||
return result.jobs_by_pk;
|
||||
}
|
||||
|
||||
@@ -191,7 +196,7 @@ async function QueryVehicleFromDms(socket) {
|
||||
CheckForErrors(socket, VehicleGetResponse);
|
||||
return VehicleGetResponse;
|
||||
} catch (error) {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error in QueryVehicleFromDms - ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in QueryVehicleFromDms - ${error}`);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
@@ -220,9 +225,9 @@ async function QueryCustomersFromDms(socket) {
|
||||
{ auth: PBS_CREDENTIALS, socket }
|
||||
);
|
||||
CheckForErrors(socket, CustomerGetResponse);
|
||||
return CustomerGetResponse && CustomerGetResponse.Contacts;
|
||||
return CustomerGetResponse?.Contacts;
|
||||
} catch (error) {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error in QueryCustomersFromDms - ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in QueryCustomersFromDms - ${error}`);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
@@ -253,9 +258,9 @@ async function QueryCustomerBycodeFromDms(socket, CustomerRef) {
|
||||
{ auth: PBS_CREDENTIALS, socket }
|
||||
);
|
||||
CheckForErrors(socket, CustomerGetResponse);
|
||||
return CustomerGetResponse && CustomerGetResponse.Contacts;
|
||||
return CustomerGetResponse?.Contacts;
|
||||
} catch (error) {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error in QueryCustomersFromDms - ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in QueryCustomersFromDms - ${error}`);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
@@ -272,15 +277,15 @@ async function UpsertContactData(socket, selectedCustomerId) {
|
||||
Code: socket.JobData.owner.accountingid,
|
||||
...(socket.JobData.ownr_co_nm
|
||||
? {
|
||||
//LastName: socket.JobData.ownr_ln,
|
||||
FirstName: socket.JobData.ownr_co_nm,
|
||||
IsBusiness: true
|
||||
}
|
||||
//LastName: socket.JobData.ownr_ln,
|
||||
FirstName: socket.JobData.ownr_co_nm,
|
||||
IsBusiness: true
|
||||
}
|
||||
: {
|
||||
LastName: socket.JobData.ownr_ln,
|
||||
FirstName: socket.JobData.ownr_fn,
|
||||
IsBusiness: false
|
||||
}),
|
||||
LastName: socket.JobData.ownr_ln,
|
||||
FirstName: socket.JobData.ownr_fn,
|
||||
IsBusiness: false
|
||||
}),
|
||||
|
||||
//Salutation: "String",
|
||||
//MiddleName: "String",
|
||||
@@ -337,7 +342,7 @@ async function UpsertContactData(socket, selectedCustomerId) {
|
||||
CheckForErrors(socket, ContactChangeResponse);
|
||||
return ContactChangeResponse;
|
||||
} catch (error) {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error in UpsertContactData - ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in UpsertContactData - ${error}`);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
@@ -357,10 +362,10 @@ async function UpsertVehicleData(socket, ownerRef) {
|
||||
//FleetNumber: "String",
|
||||
//Status: "String",
|
||||
OwnerRef: ownerRef, // "00000000000000000000000000000000",
|
||||
// ModelNumber: socket.JobData.vehicle && socket.JobData.vehicle.v_makecode,
|
||||
// ModelNumber: socket.JobData.vehicle?.v_makecode,
|
||||
Make: socket.JobData.v_make_desc,
|
||||
Model: socket.JobData.v_model_desc,
|
||||
Trim: socket.JobData.vehicle && socket.JobData.vehicle.v_trimcode,
|
||||
Trim: socket.JobData.vehicle?.v_trimcode,
|
||||
//VehicleType: "String",
|
||||
Year: socket.JobData.v_model_yr,
|
||||
Odometer: socket.JobData.kmout,
|
||||
@@ -490,14 +495,16 @@ async function UpsertVehicleData(socket, ownerRef) {
|
||||
CheckForErrors(socket, VehicleChangeResponse);
|
||||
return VehicleChangeResponse;
|
||||
} catch (error) {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error in UpsertVehicleData - ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in UpsertVehicleData - ${error}`);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function GetVehicleData(socket, ownerRef) {
|
||||
try {
|
||||
const { data: { Vehicles } } = await axios.post(
|
||||
const {
|
||||
data: { Vehicles }
|
||||
} = await axios.post(
|
||||
PBS_ENDPOINTS.VehicleGet,
|
||||
{
|
||||
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
|
||||
@@ -510,7 +517,7 @@ async function GetVehicleData(socket, ownerRef) {
|
||||
// "Trim": "String",
|
||||
// "ModelNumber": "String",
|
||||
// "StockNumber": "String",
|
||||
VIN: socket.JobData.v_vin,
|
||||
VIN: socket.JobData.v_vin
|
||||
// "LicenseNumber": "String",
|
||||
// "Lot": "String",
|
||||
// "Status": "String",
|
||||
@@ -528,24 +535,21 @@ async function GetVehicleData(socket, ownerRef) {
|
||||
// "LotAccessDivisions": [0],
|
||||
// "OdometerTo": 0,
|
||||
// "OdometerFrom": 0
|
||||
}
|
||||
,
|
||||
},
|
||||
{ auth: PBS_CREDENTIALS, socket }
|
||||
);
|
||||
CheckForErrors(socket, Vehicles);
|
||||
if (Vehicles.length === 1) {
|
||||
return Vehicles[0];
|
||||
|
||||
} else {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error in Getting Vehicle Data - ${Vehicles.length} vehicle(s) found`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in Getting Vehicle Data - ${Vehicles.length} vehicle(s) found`);
|
||||
}
|
||||
} catch (error) {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error in UpsertVehicleData - ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in UpsertVehicleData - ${error}`);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function InsertAccountPostingData(socket) {
|
||||
try {
|
||||
const allocations = await CalculateAllocations(socket, socket.JobData.id);
|
||||
@@ -629,7 +633,8 @@ async function InsertAccountPostingData(socket) {
|
||||
Posting: {
|
||||
Reference: socket.JobData.ro_number,
|
||||
JournalCode: socket.txEnvelope.journal,
|
||||
TransactionDate: moment(socket.JobData.date_invoiced).tz(socket.JobData.bodyshop.timezone).toISOString(), //"0001-01-01T00:00:00.0000000Z",
|
||||
//Sample TransactionDate: "0001-01-01T00:00:00.0000000Z",
|
||||
TransactionDate: moment(socket.JobData.date_invoiced).tz(socket.JobData.bodyshop.timezone).toISOString(),
|
||||
Description: socket.txEnvelope.story,
|
||||
//AdditionalInfo: "String",
|
||||
Source: InstanceManager({ imex: "ImEX Online", rome: "Rome Online" }),
|
||||
@@ -642,14 +647,15 @@ async function InsertAccountPostingData(socket) {
|
||||
CheckForErrors(socket, AccountPostingChange);
|
||||
return AccountPostingChange;
|
||||
} catch (error) {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error in InsertAccountPostingData - ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in InsertAccountPostingData - ${error}`);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function MarkJobExported(socket, jobid) {
|
||||
CdkBase.createLogEvent(socket, "INFO", `Marking job as exported for id ${jobid}`);
|
||||
WsLogger.createLogEvent(socket, "INFO", `Marking job as exported for id ${jobid}`);
|
||||
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
|
||||
|
||||
const result = await client
|
||||
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
|
||||
.request(queries.MARK_JOB_EXPORTED, {
|
||||
@@ -677,47 +683,52 @@ async function MarkJobExported(socket, jobid) {
|
||||
async function InsertFailedExportLog(socket, error) {
|
||||
try {
|
||||
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
|
||||
|
||||
const result = await client
|
||||
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
|
||||
.request(queries.INSERT_EXPORT_LOG, {
|
||||
log: {
|
||||
logs: [{
|
||||
bodyshopid: socket.JobData.bodyshop.id,
|
||||
jobid: socket.JobData.id,
|
||||
successful: false,
|
||||
message: JSON.stringify(error),
|
||||
useremail: socket.user.email
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error2) {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error in InsertFailedExportLog - ${error} - ${JSON.stringify(error2)}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in InsertFailedExportLog - ${error} - ${JSON.stringify(error2)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function CreateRepairOrderInPBS(socket) {
|
||||
try {
|
||||
const { RepairOrders } = await RepairOrderGet(socket);
|
||||
if (RepairOrders.length === 0) {
|
||||
const InsertedRepairOrder = await RepairOrderChange(socket)
|
||||
const InsertedRepairOrder = await RepairOrderChange(socket);
|
||||
socket.InsertedRepairOrder = InsertedRepairOrder;
|
||||
CdkBase.createLogEvent(socket, "INFO", `No repair orders found for vehicle. Inserting record.`);
|
||||
|
||||
WsLogger.createLogEvent(socket, "INFO", `No repair orders found for vehicle. Inserting record.`);
|
||||
} else if (RepairOrders.length > 0) {
|
||||
//Find out if it's a matching RO.
|
||||
//This logic is used because the integration will simply add another line to an open RO if it exists.
|
||||
const matchingRo = RepairOrders.find(ro => ro.Memo?.toLowerCase()?.includes(socket.JobData.ro_number.toLowerCase()))
|
||||
//Find out if it's a matching RO.
|
||||
//This logic is used because the integration will simply add another line to an open RO if it exists.
|
||||
const matchingRo = RepairOrders.find((ro) =>
|
||||
ro.Memo?.toLowerCase()?.includes(socket.JobData.ro_number.toLowerCase())
|
||||
);
|
||||
if (!matchingRo) {
|
||||
CdkBase.createLogEvent(socket, "INFO", `ROs found for vehicle, but none match. Inserting record.`);
|
||||
const InsertedRepairOrder = await RepairOrderChange(socket)
|
||||
WsLogger.createLogEvent(socket, "INFO", `ROs found for vehicle, but none match. Inserting record.`);
|
||||
const InsertedRepairOrder = await RepairOrderChange(socket);
|
||||
socket.InsertedRepairOrder = InsertedRepairOrder;
|
||||
} else {
|
||||
CdkBase.createLogEvent(socket, "WARN", `Repair order appears to already exist in PBS. ${matchingRo.RepairOrderNumber}`);
|
||||
WsLogger.createLogEvent(
|
||||
socket,
|
||||
"WARN",
|
||||
`Repair order appears to already exist in PBS. ${matchingRo.RepairOrderNumber}`
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error in CreateRepairOrderInPBS - ${error} - ${JSON.stringify(error)}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in CreateRepairOrderInPBS - ${error} - ${JSON.stringify(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -733,7 +744,7 @@ async function RepairOrderGet(socket) {
|
||||
// "Tag": "String",
|
||||
//"ContactRef": socket.contactRef,
|
||||
// "ContactRefList": ["00000000000000000000000000000000"],
|
||||
"VehicleRef": socket.vehicleRef?.ReferenceId || socket.vehicleRef?.VehicleId,
|
||||
VehicleRef: socket.vehicleRef?.ReferenceId || socket.vehicleRef?.VehicleId
|
||||
// "VehicleRefList": ["00000000000000000000000000000000"],
|
||||
// "Status": "String",
|
||||
// "CashieredSince": "0001-01-01T00:00:00.0000000Z",
|
||||
@@ -749,7 +760,7 @@ async function RepairOrderGet(socket) {
|
||||
CheckForErrors(socket, RepairOrderGet);
|
||||
return RepairOrderGet;
|
||||
} catch (error) {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error in RepairOrderChange - ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in RepairOrderChange - ${error}`);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
@@ -758,31 +769,32 @@ async function RepairOrderChange(socket) {
|
||||
try {
|
||||
const { data: RepairOrderChangeResponse } = await axios.post(
|
||||
PBS_ENDPOINTS.RepairOrderChange,
|
||||
{ //Additional details at https://partnerhub.pbsdealers.com/json/metadata?op=RepairOrderChange
|
||||
"RepairOrderInfo": {
|
||||
{
|
||||
//Additional details at https://partnerhub.pbsdealers.com/json/metadata?op=RepairOrderChange
|
||||
RepairOrderInfo: {
|
||||
//"Id": "string/00000000-0000-0000-0000-000000000000",
|
||||
//"RepairOrderId": "00000000000000000000000000000000",
|
||||
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
|
||||
"RepairOrderNumber": "00000000000000000000000000000000", //This helps force a new RO.
|
||||
"RawRepairOrderNumber": "00000000000000000000000000000000",
|
||||
RepairOrderNumber: "00000000000000000000000000000000", //This helps force a new RO.
|
||||
RawRepairOrderNumber: "00000000000000000000000000000000",
|
||||
// "RepairOrderNumber": socket.JobData.ro_number, //These 2 values are ignored as confirmed by PBS.
|
||||
// "RawRepairOrderNumber": socket.JobData.ro_number,
|
||||
"DateOpened": moment(),
|
||||
DateOpened: moment(),
|
||||
// "DateOpenedUTC": "0001-01-01T00:00:00.0000000Z",
|
||||
// "DateCashiered": "0001-01-01T00:00:00.0000000Z",
|
||||
// "DateCashieredUTC": "0001-01-01T00:00:00.0000000Z",
|
||||
"DatePromised": socket.JobData.scheduled_completion,
|
||||
DatePromised: socket.JobData.scheduled_completion,
|
||||
// "DatePromisedUTC": "0001-01-01T00:00:00.0000000Z",
|
||||
"DateVehicleCompleted": socket.JobData.actual_completion,
|
||||
DateVehicleCompleted: socket.JobData.actual_completion,
|
||||
// "DateCustomerNotified": "0001-01-01T00:00:00.0000000Z",
|
||||
// "CSR": "String",
|
||||
// "CSRRef": "00000000000000000000000000000000",
|
||||
// "BookingUser": "String",
|
||||
// "BookingUserRef": "00000000000000000000000000000000",
|
||||
"ContactRef": socket.ownerRef?.ReferenceId || socket.ownerRef?.ContactId,
|
||||
"VehicleRef": socket.vehicleRef?.ReferenceId || socket.vehicleRef?.VehicleId,
|
||||
"MileageIn": socket.JobData.km_in,
|
||||
"Tag": "BODYSHOP",
|
||||
ContactRef: socket.ownerRef?.ReferenceId || socket.ownerRef?.ContactId,
|
||||
VehicleRef: socket.vehicleRef?.ReferenceId || socket.vehicleRef?.VehicleId,
|
||||
MileageIn: socket.JobData.km_in,
|
||||
Tag: "BODYSHOP",
|
||||
//"Status": "CLOSED", //Values here do not impact the status. Confirmed by PBS support.
|
||||
Requests: [
|
||||
{
|
||||
@@ -791,61 +803,59 @@ async function RepairOrderChange(socket) {
|
||||
// "CSR": "PBS",
|
||||
// "CSRRef": "1ce12ac692564e94bda955d529ee911a",
|
||||
// "Skill": "GEN",
|
||||
"RequestCode": "MISC",
|
||||
"RequestDescription": `VEHICLE REPAIRED AT BODYSHOP. PLEASE REFERENCE IMEX SHOP MANAGEMENT SYSTEM. ${socket.txEnvelope.story}`,
|
||||
"Status": "Completed",
|
||||
RequestCode: "MISC",
|
||||
RequestDescription: `VEHICLE REPAIRED AT BODYSHOP. PLEASE REFERENCE IMEX SHOP MANAGEMENT SYSTEM. ${socket.txEnvelope.story}`,
|
||||
Status: "Completed",
|
||||
// "TechRef": "00000000000000000000000000000000",
|
||||
"AllowedHours": 0,
|
||||
"EstimateLabour": 0,
|
||||
"EstimateParts": 0,
|
||||
"ComeBack": false,
|
||||
"AddedOperation": true,
|
||||
"PartLines": [],
|
||||
"PartRequestLines": [],
|
||||
"LabourLines": [],
|
||||
"SubletLines": [],
|
||||
"TimePunches": [],
|
||||
"Summary": {
|
||||
"Labour": 0,
|
||||
"Parts": 0,
|
||||
"OilGas": 0,
|
||||
"SubletTow": 0,
|
||||
"Misc": 0,
|
||||
"Environment": 0,
|
||||
"ShopSupplies": 0,
|
||||
"Freight": 0,
|
||||
"WarrantyDeductible": 0,
|
||||
"Discount": 0,
|
||||
"SubTotal": 0,
|
||||
"Tax1": 0,
|
||||
"Tax2": 0,
|
||||
"InvoiceTotal": 0,
|
||||
"CustomerDeductible": 0,
|
||||
"GrandTotal": 0,
|
||||
"LabourDiscount": 0,
|
||||
"PartDiscount": 0,
|
||||
"ServiceFeeTotal": 0,
|
||||
"OEMDiscount": 0
|
||||
AllowedHours: 0,
|
||||
EstimateLabour: 0,
|
||||
EstimateParts: 0,
|
||||
ComeBack: false,
|
||||
AddedOperation: true,
|
||||
PartLines: [],
|
||||
PartRequestLines: [],
|
||||
LabourLines: [],
|
||||
SubletLines: [],
|
||||
TimePunches: [],
|
||||
Summary: {
|
||||
Labour: 0,
|
||||
Parts: 0,
|
||||
OilGas: 0,
|
||||
SubletTow: 0,
|
||||
Misc: 0,
|
||||
Environment: 0,
|
||||
ShopSupplies: 0,
|
||||
Freight: 0,
|
||||
WarrantyDeductible: 0,
|
||||
Discount: 0,
|
||||
SubTotal: 0,
|
||||
Tax1: 0,
|
||||
Tax2: 0,
|
||||
InvoiceTotal: 0,
|
||||
CustomerDeductible: 0,
|
||||
GrandTotal: 0,
|
||||
LabourDiscount: 0,
|
||||
PartDiscount: 0,
|
||||
ServiceFeeTotal: 0,
|
||||
OEMDiscount: 0
|
||||
},
|
||||
"LineType": "RequestLine",
|
||||
},
|
||||
LineType: "RequestLine"
|
||||
}
|
||||
],
|
||||
|
||||
"Memo": socket.txEnvelope.story,
|
||||
|
||||
Memo: socket.txEnvelope.story
|
||||
},
|
||||
"IsAsynchronous": false,
|
||||
IsAsynchronous: false
|
||||
// "UserRequest": "String",
|
||||
// "UserRef": "00000000000000000000000000000000"
|
||||
}
|
||||
},
|
||||
|
||||
,
|
||||
{ auth: PBS_CREDENTIALS, socket }
|
||||
);
|
||||
CheckForErrors(socket, RepairOrderChangeResponse);
|
||||
return RepairOrderChangeResponse;
|
||||
} catch (error) {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error in RepairOrderChange - ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in RepairOrderChange - ${error}`);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
const GraphQLClient = require("graphql-request").GraphQLClient;
|
||||
|
||||
const queries = require("../graphql-client/queries");
|
||||
const CdkBase = require("../web-sockets/web-socket");
|
||||
|
||||
const CreateFortellisLogEvent = require("../fortellis/fortellis-logger");
|
||||
const Dinero = require("dinero.js");
|
||||
const _ = require("lodash");
|
||||
const WsLogger = require("../web-sockets/createLogEvent");
|
||||
|
||||
const InstanceManager = require("../utils/instanceMgr").default;
|
||||
const { DiscountNotAlreadyCounted } = InstanceManager({
|
||||
imex: require("../job/job-totals"),
|
||||
@@ -13,37 +14,40 @@ const { DiscountNotAlreadyCounted } = InstanceManager({
|
||||
|
||||
exports.defaultRoute = async function (req, res) {
|
||||
try {
|
||||
CdkBase.createLogEvent(req, "DEBUG", `Received request to calculate allocations for ${req.body.jobid}`);
|
||||
WsLogger.createLogEvent(req, "DEBUG", `Received request to calculate allocations for ${req.body.jobid}`);
|
||||
const jobData = await QueryJobData(req, req.BearerToken, req.body.jobid);
|
||||
return res.status(200).json({ data: calculateAllocations(req, jobData) });
|
||||
} catch (error) {
|
||||
////console.log(error);
|
||||
CdkBase.createLogEvent(req, "ERROR", `Error encountered in CdkCalculateAllocations. ${error}`);
|
||||
WsLogger.createLogEvent(req, "ERROR", `Error encountered in CdkCalculateAllocations. ${error}`);
|
||||
WsLogger.createLogEvent(req, "ERROR", `Error encountered in CdkCalculateAllocations. ${error.stack}`);
|
||||
res.status(500).json({ error: `Error encountered in CdkCalculateAllocations. ${error}` });
|
||||
}
|
||||
};
|
||||
|
||||
exports.default = async function (socket, jobid) {
|
||||
exports.default = async function (socket, jobid, isFortellis = false) {
|
||||
try {
|
||||
const jobData = await QueryJobData(socket, "Bearer " + socket.handshake.auth.token, jobid);
|
||||
return calculateAllocations(socket, jobData);
|
||||
const jobData = await QueryJobData(socket, "Bearer " + socket.handshake.auth.token, jobid, isFortellis);
|
||||
return calculateAllocations(socket, jobData, isFortellis);
|
||||
} catch (error) {
|
||||
////console.log(error);
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error encountered in CdkCalculateAllocations. ${error}`);
|
||||
const loggingFunction = isFortellis ? CreateFortellisLogEvent : WsLogger.createLogEvent;
|
||||
loggingFunction(socket, "ERROR", `Error encountered in CdkCalculateAllocations. ${error}`);
|
||||
loggingFunction(socket, "ERROR", `Error encountered in CdkCalculateAllocations. ${error.stack}`);
|
||||
}
|
||||
};
|
||||
|
||||
async function QueryJobData(connectionData, token, jobid) {
|
||||
CdkBase.createLogEvent(connectionData, "DEBUG", `Querying job data for id ${jobid}`);
|
||||
async function QueryJobData(connectionData, token, jobid, isFortellis) {
|
||||
const loggingFunction = isFortellis ? CreateFortellisLogEvent : WsLogger.createLogEvent;
|
||||
|
||||
loggingFunction(connectionData, "DEBUG", `Querying job data for id ${jobid}`);
|
||||
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
|
||||
const result = await client.setHeaders({ Authorization: token }).request(queries.GET_CDK_ALLOCATIONS, { id: jobid });
|
||||
CdkBase.createLogEvent(connectionData, "SILLY", `Job data query result ${JSON.stringify(result, null, 2)}`);
|
||||
//loggingFunction(connectionData, "DEBUG", `Job data query result ${JSON.stringify(result, null, 2)}`);
|
||||
return result.jobs_by_pk;
|
||||
}
|
||||
|
||||
function calculateAllocations(connectionData, job) {
|
||||
function calculateAllocations(connectionData, job, isFortellis) {
|
||||
const { bodyshop } = job;
|
||||
|
||||
const loggingFunction = isFortellis ? CreateFortellisLogEvent : WsLogger.createLogEvent;
|
||||
const taxAllocations = InstanceManager({
|
||||
executeFunction: true,
|
||||
deubg: true,
|
||||
@@ -159,7 +163,7 @@ function calculateAllocations(connectionData, job) {
|
||||
const selectedDmsAllocationConfig = bodyshop.md_responsibility_centers.dms_defaults.find(
|
||||
(d) => d.name === job.dms_allocation
|
||||
);
|
||||
CdkBase.createLogEvent(
|
||||
loggingFunction(
|
||||
connectionData,
|
||||
"DEBUG",
|
||||
`Using DMS Allocation ${selectedDmsAllocationConfig && selectedDmsAllocationConfig.name} for cost export.`
|
||||
@@ -360,10 +364,10 @@ function calculateAllocations(connectionData, job) {
|
||||
Dinero(job.job_totals.parts.adjustments[key])
|
||||
);
|
||||
} else {
|
||||
CdkBase.createLogEvent(
|
||||
loggingFunction(
|
||||
connectionData,
|
||||
"ERROR",
|
||||
`Error encountered in CdkCalculateAllocations. Unable to find adjustment account. ${error}`
|
||||
`Error encountered in CdkCalculateAllocations. Unable to find adjustment account: ${accountName}`
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -383,10 +387,10 @@ function calculateAllocations(connectionData, job) {
|
||||
Dinero(job.job_totals.rates[key].adjustments)
|
||||
);
|
||||
} else {
|
||||
CdkBase.createLogEvent(
|
||||
loggingFunction(
|
||||
connectionData,
|
||||
"ERROR",
|
||||
`Error encountered in CdkCalculateAllocations. Unable to find adjustment account. ${error}`
|
||||
`Error encountered in CdkCalculateAllocations. Unable to find adjustment account: ${accountName}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,12 @@ const CdkWsdl = require("./cdk-wsdl").default;
|
||||
const logger = require("../utils/logger");
|
||||
|
||||
const { CDK_CREDENTIALS, CheckCdkResponseForError } = require("./cdk-wsdl");
|
||||
const {
|
||||
MakeFortellisCall,
|
||||
FortellisActions,
|
||||
GetAuthToken,
|
||||
GetDepartmentId
|
||||
} = require("../fortellis/fortellis-helpers");
|
||||
|
||||
// exports.default = async function (socket, cdk_dealerid) {
|
||||
// try {
|
||||
@@ -105,3 +111,85 @@ async function GetCdkMakes(req, cdk_dealerid) {
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function GetFortellisMakes(req, cdk_dealerid) {
|
||||
logger.log("fortellis-replace-makes-models", "DEBUG", req.user.email, null, {
|
||||
cdk_dealerid
|
||||
});
|
||||
try {
|
||||
const result = await MakeFortellisCall({
|
||||
...FortellisActions.GetMakeModel,
|
||||
headers: {},
|
||||
redisHelpers: {
|
||||
setSessionTransactionData: () => {
|
||||
return null;
|
||||
},
|
||||
getSessionTransactionData: () => {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
socket: { emit: () => null },
|
||||
jobid: null,
|
||||
body: {},
|
||||
SubscriptionObject: {
|
||||
SubscriptionID: cdk_dealerid
|
||||
}
|
||||
});
|
||||
|
||||
logger.log("fortellis-replace-makes-models-response", "ERROR", req.user.email, null, {
|
||||
cdk_dealerid,
|
||||
xml: result
|
||||
});
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
logger.log("fortellis-replace-makes-models-error", "ERROR", req.user.email, null, {
|
||||
cdk_dealerid,
|
||||
error
|
||||
});
|
||||
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
exports.fortellis = async function ReloadFortellisMakes(req, res) {
|
||||
const { bodyshopid, cdk_dealerid } = req.body;
|
||||
try {
|
||||
//Query all CDK Models
|
||||
const newList = await GetFortellisMakes(req, cdk_dealerid);
|
||||
|
||||
const BearerToken = req.BearerToken;
|
||||
const client = req.userGraphQLClient;
|
||||
|
||||
const deleteResult = await client
|
||||
.setHeaders({ Authorization: BearerToken })
|
||||
.request(queries.DELETE_ALL_DMS_VEHICLES, {});
|
||||
|
||||
//Insert the new ones.
|
||||
|
||||
const insertResult = await client.setHeaders({ Authorization: BearerToken }).request(queries.INSERT_DMS_VEHICLES, {
|
||||
vehicles: newList.map((i) => {
|
||||
return {
|
||||
bodyshopid,
|
||||
makecode: i.makeCode,
|
||||
modelcode: i.modelCode,
|
||||
make: i.makeFullName,
|
||||
model: i.modelFullName
|
||||
};
|
||||
})
|
||||
});
|
||||
|
||||
logger.log("fortellis-replace-makes-models-success", "DEBUG", req.user.email, null, {
|
||||
cdk_dealerid,
|
||||
count: newList.length
|
||||
});
|
||||
|
||||
res.sendStatus(200);
|
||||
} catch (error) {
|
||||
logger.log("fortellis-replace-makes-models-error", "ERROR", req.user.email, null, {
|
||||
cdk_dealerid,
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
res.status(500).json(error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,33 +1,36 @@
|
||||
const GraphQLClient = require("graphql-request").GraphQLClient;
|
||||
const soap = require("soap");
|
||||
const queries = require("../graphql-client/queries");
|
||||
const CdkBase = require("../web-sockets/web-socket");
|
||||
const CdkWsdl = require("./cdk-wsdl").default;
|
||||
const { CDK_CREDENTIALS, CheckCdkResponseForError } = require("./cdk-wsdl");
|
||||
const CalcualteAllocations = require("./cdk-calculate-allocations").default;
|
||||
const InstanceMgr = require("../utils/instanceMgr").default;
|
||||
const WsLogger = require("../web-sockets/createLogEvent");
|
||||
|
||||
const moment = require("moment-timezone");
|
||||
|
||||
const replaceSpecialRegex = /[^a-zA-Z0-9 .,\n #]+/g;
|
||||
|
||||
exports.default = async function (socket, { txEnvelope, jobid }) {
|
||||
const defaultHandler = async (socket, { txEnvelope, jobid }) => {
|
||||
////Store the following information into the redis store for this transaction.
|
||||
socket.logEvents = [];
|
||||
socket.recordid = jobid;
|
||||
socket.txEnvelope = txEnvelope;
|
||||
////
|
||||
|
||||
try {
|
||||
CdkBase.createLogEvent(socket, "DEBUG", `Received Job export request for id ${jobid}`);
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `Received Job export request for id ${jobid}`);
|
||||
|
||||
const JobData = await QueryJobData(socket, jobid);
|
||||
socket.JobData = JobData;
|
||||
const DealerId = JobData.bodyshop.cdk_dealerid;
|
||||
CdkBase.createLogEvent(socket, "DEBUG", `Dealer ID detected: ${JSON.stringify(DealerId)}`);
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `Dealer ID detected: ${JSON.stringify(DealerId)}`);
|
||||
|
||||
CdkBase.createLogEvent(socket, "DEBUG", `{1} Begin Calculate DMS Vehicle ID using VIN: ${JobData.v_vin}`);
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `{1} Begin Calculate DMS Vehicle ID using VIN: ${JobData.v_vin}`);
|
||||
socket.DMSVid = await CalculateDmsVid(socket, JobData);
|
||||
|
||||
if (socket.DMSVid.newId === "N") {
|
||||
CdkBase.createLogEvent(
|
||||
WsLogger.createLogEvent(
|
||||
socket,
|
||||
"DEBUG",
|
||||
`{2.1} Querying the Vehicle using the DMSVid: ${socket.DMSVid.vehiclesVehId}`
|
||||
@@ -35,10 +38,10 @@ exports.default = async function (socket, { txEnvelope, jobid }) {
|
||||
socket.DMSVeh = await QueryDmsVehicleById(socket, JobData, socket.DMSVid);
|
||||
|
||||
const DMSVehCustomer =
|
||||
socket.DMSVeh && socket.DMSVeh.owners && socket.DMSVeh.owners.find((o) => o.id.assigningPartyId === "CURRENT");
|
||||
socket.DMSVeh?.owners && socket.DMSVeh.owners.find((o) => o.id.assigningPartyId === "CURRENT");
|
||||
|
||||
if (DMSVehCustomer && DMSVehCustomer.id && DMSVehCustomer.id.value) {
|
||||
CdkBase.createLogEvent(
|
||||
if (DMSVehCustomer?.id && DMSVehCustomer.id.value) {
|
||||
WsLogger.createLogEvent(
|
||||
socket,
|
||||
"DEBUG",
|
||||
`{2.2} Querying the Customer using the ID from DMSVeh: ${DMSVehCustomer.id.value}`
|
||||
@@ -47,7 +50,7 @@ exports.default = async function (socket, { txEnvelope, jobid }) {
|
||||
}
|
||||
}
|
||||
|
||||
CdkBase.createLogEvent(socket, "DEBUG", `{2.3} Querying the Customer using the name.`);
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `{2.3} Querying the Customer using the name.`);
|
||||
|
||||
socket.DMSCustList = await QueryDmsCustomerByName(socket, JobData);
|
||||
|
||||
@@ -56,43 +59,48 @@ exports.default = async function (socket, { txEnvelope, jobid }) {
|
||||
...socket.DMSCustList
|
||||
]);
|
||||
} catch (error) {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error encountered in CdkJobExport. ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error encountered in CdkJobExport. ${error}`);
|
||||
}
|
||||
};
|
||||
exports.default = defaultHandler;
|
||||
|
||||
async function CdkSelectedCustomer(socket, selectedCustomerId) {
|
||||
try {
|
||||
socket.selectedCustomerId = selectedCustomerId;
|
||||
if (selectedCustomerId) {
|
||||
CdkBase.createLogEvent(socket, "DEBUG", `{3.1} Querying the Customer using Customer ID: ${selectedCustomerId}`);
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `{3.1} Querying the Customer using Customer ID: ${selectedCustomerId}`);
|
||||
socket.DMSCust = await QueryDmsCustomerById(socket, socket.JobData, selectedCustomerId);
|
||||
} else {
|
||||
CdkBase.createLogEvent(socket, "DEBUG", `{3.2} Generating a new customer ID.`);
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `{3.2} Generating a new customer ID.`);
|
||||
const newCustomerId = await GenerateDmsCustomerNumber(socket);
|
||||
CdkBase.createLogEvent(socket, "DEBUG", `{3.3} Inserting new customer with ID: ${newCustomerId}`);
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `{3.3} Inserting new customer with ID: ${newCustomerId}`);
|
||||
socket.DMSCust = await InsertDmsCustomer(socket, newCustomerId);
|
||||
}
|
||||
|
||||
if (socket.DMSVid.newId === "Y") {
|
||||
CdkBase.createLogEvent(socket, "DEBUG", `{4.1} Inserting new vehicle with ID: ID ${socket.DMSVid.vehiclesVehId}`);
|
||||
WsLogger.createLogEvent(
|
||||
socket,
|
||||
"DEBUG",
|
||||
`{4.1} Inserting new vehicle with ID: ID ${socket.DMSVid.vehiclesVehId}`
|
||||
);
|
||||
socket.DMSVeh = await InsertDmsVehicle(socket);
|
||||
} else {
|
||||
CdkBase.createLogEvent(
|
||||
WsLogger.createLogEvent(
|
||||
socket,
|
||||
"DEBUG",
|
||||
`{4.2} Querying Existing Vehicle using ID ${socket.DMSVid.vehiclesVehId}`
|
||||
);
|
||||
socket.DMSVeh = await QueryDmsVehicleById(socket, socket.JobData, socket.DMSVid);
|
||||
CdkBase.createLogEvent(socket, "DEBUG", `{4.3} Updating Existing Vehicle to associate to owner.`);
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `{4.3} Updating Existing Vehicle to associate to owner.`);
|
||||
socket.DMSVeh = await UpdateDmsVehicle(socket);
|
||||
}
|
||||
|
||||
CdkBase.createLogEvent(socket, "DEBUG", `{5} Creating Transaction header with Dms Start WIP`);
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `{5} Creating Transaction header with Dms Start WIP`);
|
||||
socket.DMSTransHeader = await InsertDmsStartWip(socket);
|
||||
CdkBase.createLogEvent(socket, "DEBUG", `{5.1} Creating Transaction with ID ${socket.DMSTransHeader.transID}`);
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `{5.1} Creating Transaction with ID ${socket.DMSTransHeader.transID}`);
|
||||
|
||||
socket.DMSBatchTxn = await InsertDmsBatchWip(socket);
|
||||
CdkBase.createLogEvent(
|
||||
WsLogger.createLogEvent(
|
||||
socket,
|
||||
"DEBUG",
|
||||
`{6} Attempting to post Transaction with ID ${socket.DMSTransHeader.transID}`
|
||||
@@ -100,23 +108,23 @@ async function CdkSelectedCustomer(socket, selectedCustomerId) {
|
||||
socket.DmsBatchTxnPost = await PostDmsBatchWip(socket);
|
||||
if (socket.DmsBatchTxnPost.code === "success") {
|
||||
//something
|
||||
CdkBase.createLogEvent(socket, "DEBUG", `{6} Successfully posted sransaction to DMS.`);
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `{6} Successfully posted sransaction to DMS.`);
|
||||
|
||||
await MarkJobExported(socket, socket.JobData.id);
|
||||
|
||||
CdkBase.createLogEvent(socket, "DEBUG", `{5} Updating Service Vehicle History.`);
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `{5} Updating Service Vehicle History.`);
|
||||
socket.DMSVehHistory = await InsertServiceVehicleHistory(socket);
|
||||
socket.emit("export-success", socket.JobData.id);
|
||||
} else {
|
||||
//Get the error code
|
||||
CdkBase.createLogEvent(
|
||||
WsLogger.createLogEvent(
|
||||
socket,
|
||||
"DEBUG",
|
||||
`{6.1} Getting errors for Transaction ID ${socket.DMSTransHeader.transID}`
|
||||
);
|
||||
socket.DmsError = await QueryDmsErrWip(socket);
|
||||
//Delete the transaction
|
||||
CdkBase.createLogEvent(socket, "DEBUG", `{6.2} Deleting Transaction ID ${socket.DMSTransHeader.transID}`);
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `{6.2} Deleting Transaction ID ${socket.DMSTransHeader.transID}`);
|
||||
socket.DmsBatchTxnPost = await DeleteDmsWip(socket);
|
||||
|
||||
socket.DmsError.errMsg
|
||||
@@ -125,29 +133,33 @@ async function CdkSelectedCustomer(socket, selectedCustomerId) {
|
||||
(e) =>
|
||||
e !== null &&
|
||||
e !== "" &&
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error(s) encountered in posting transaction. ${e}`)
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error(s) encountered in posting transaction. ${e}`)
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error encountered in CdkSelectedCustomer. ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error encountered in CdkSelectedCustomer. ${error}`);
|
||||
await InsertFailedExportLog(socket, error);
|
||||
} finally {
|
||||
//Ensure we always insert logEvents
|
||||
//GQL to insert logevents.
|
||||
|
||||
CdkBase.createLogEvent(socket, "DEBUG", `Capturing log events to database.`);
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `Capturing log events to database.`);
|
||||
}
|
||||
}
|
||||
|
||||
exports.CdkSelectedCustomer = CdkSelectedCustomer;
|
||||
|
||||
async function QueryJobData(socket, jobid) {
|
||||
CdkBase.createLogEvent(socket, "DEBUG", `Querying job data for id ${jobid}`);
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `Querying job data for id ${jobid}`);
|
||||
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
|
||||
const currentToken =
|
||||
(socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token);
|
||||
|
||||
const result = await client
|
||||
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
|
||||
.setHeaders({ Authorization: `Bearer ${currentToken}` })
|
||||
.request(queries.QUERY_JOBS_FOR_CDK_EXPORT, { id: jobid });
|
||||
CdkBase.createLogEvent(socket, "SILLY", `Job data query result ${JSON.stringify(result, null, 2)}`);
|
||||
|
||||
//WsLogger.createLogEvent(socket, "SILLY", `Job data query result ${JSON.stringify(result, null, 2)}`);
|
||||
return result.jobs_by_pk;
|
||||
}
|
||||
|
||||
@@ -161,11 +173,11 @@ async function CalculateDmsVid(socket, JobData) {
|
||||
});
|
||||
|
||||
const [result, rawResponse, , rawRequest] = soapResponseVehicleInsertUpdate;
|
||||
CdkBase.createXmlEvent(socket, rawRequest, `soapClientVehicleInsertUpdate.getVehIdsAsync request.`);
|
||||
WsLogger.createXmlEvent(socket, rawRequest, `soapClientVehicleInsertUpdate.getVehIdsAsync request.`);
|
||||
|
||||
CdkBase.createXmlEvent(socket, rawResponse, `soapClientVehicleInsertUpdate.getVehIdsAsync response.`);
|
||||
WsLogger.createXmlEvent(socket, rawResponse, `soapClientVehicleInsertUpdate.getVehIdsAsync response.`);
|
||||
|
||||
CdkBase.createLogEvent(
|
||||
WsLogger.createLogEvent(
|
||||
socket,
|
||||
"SILLY",
|
||||
`soapClientVehicleInsertUpdate.getVehIdsAsync Result ${JSON.stringify(result, null, 2)}`
|
||||
@@ -178,15 +190,15 @@ async function CalculateDmsVid(socket, JobData) {
|
||||
|
||||
//return result && result.return && result.return[0];
|
||||
} catch (error) {
|
||||
CdkBase.createXmlEvent(socket, error.request, `soapClientVehicleInsertUpdate.getVehIdsAsync request.`, true);
|
||||
WsLogger.createXmlEvent(socket, error.request, `soapClientVehicleInsertUpdate.getVehIdsAsync request.`, true);
|
||||
|
||||
CdkBase.createXmlEvent(
|
||||
WsLogger.createXmlEvent(
|
||||
socket,
|
||||
error.response && error.response.data,
|
||||
error.response?.data,
|
||||
`soapClientVehicleInsertUpdate.getVehIdsAsync response.`,
|
||||
true
|
||||
);
|
||||
CdkBase.createLogEvent(socket, "ERROR", `{1} Error in CalculateDmsVid - ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `{1} Error in CalculateDmsVid - ${error}`);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
@@ -206,19 +218,19 @@ async function QueryDmsVehicleById(socket, JobData, DMSVid) {
|
||||
|
||||
const [result, rawResponse, , rawRequest] = soapResponseVehicleInsertUpdate;
|
||||
|
||||
CdkBase.createXmlEvent(socket, rawRequest, `soapClientVehicleInsertUpdate.readAsync request.`);
|
||||
WsLogger.createXmlEvent(socket, rawRequest, `soapClientVehicleInsertUpdate.readAsync request.`);
|
||||
|
||||
CdkBase.createLogEvent(
|
||||
WsLogger.createLogEvent(
|
||||
socket,
|
||||
"SILLY",
|
||||
`soapClientVehicleInsertUpdate.readAsync Result ${JSON.stringify(result, null, 2)}`
|
||||
);
|
||||
CdkBase.createXmlEvent(socket, rawResponse, `soapClientVehicleInsertUpdate.readAsync response.`);
|
||||
WsLogger.createXmlEvent(socket, rawResponse, `soapClientVehicleInsertUpdate.readAsync response.`);
|
||||
CheckCdkResponseForError(socket, soapResponseVehicleInsertUpdate);
|
||||
const VehicleFromDMS = result && result.return && result.return.vehicle;
|
||||
const VehicleFromDMS = result?.return && result.return.vehicle;
|
||||
return VehicleFromDMS;
|
||||
} catch (error) {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error in QueryDmsVehicleById - ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in QueryDmsVehicleById - ${error}`);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
@@ -237,28 +249,23 @@ async function QueryDmsCustomerById(socket, JobData, CustomerId) {
|
||||
|
||||
const [result, rawResponse, , rawRequest] = soapResponseCustomerInsertUpdate;
|
||||
|
||||
CdkBase.createXmlEvent(socket, rawRequest, `soapClientCustomerInsertUpdate.readAsync request.`);
|
||||
WsLogger.createXmlEvent(socket, rawRequest, `soapClientCustomerInsertUpdate.readAsync request.`);
|
||||
|
||||
CdkBase.createXmlEvent(socket, rawResponse, `soapClientCustomerInsertUpdate.readAsync response.`);
|
||||
CdkBase.createLogEvent(
|
||||
WsLogger.createXmlEvent(socket, rawResponse, `soapClientCustomerInsertUpdate.readAsync response.`);
|
||||
WsLogger.createLogEvent(
|
||||
socket,
|
||||
"SILLY",
|
||||
`soapClientCustomerInsertUpdate.readAsync Result ${JSON.stringify(result, null, 2)}`
|
||||
);
|
||||
CheckCdkResponseForError(socket, soapResponseCustomerInsertUpdate);
|
||||
const CustomersFromDms = result && result.return && result.return.customerParty;
|
||||
const CustomersFromDms = result?.return && result.return.customerParty;
|
||||
return CustomersFromDms;
|
||||
} catch (error) {
|
||||
CdkBase.createXmlEvent(socket, error.request, `soapClientCustomerInsertUpdate.readAsync request.`, true);
|
||||
WsLogger.createXmlEvent(socket, error.request, `soapClientCustomerInsertUpdate.readAsync request.`, true);
|
||||
|
||||
CdkBase.createXmlEvent(
|
||||
socket,
|
||||
error.response && error.response.data,
|
||||
`soapClientCustomerInsertUpdate.readAsync response.`,
|
||||
true
|
||||
);
|
||||
WsLogger.createXmlEvent(socket, error.response?.data, `soapClientCustomerInsertUpdate.readAsync response.`, true);
|
||||
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error in QueryDmsCustomerById - ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in QueryDmsCustomerById - ${error}`);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
@@ -270,7 +277,7 @@ async function QueryDmsCustomerByName(socket, JobData) {
|
||||
: `${JobData.ownr_ln},${JobData.ownr_fn}`
|
||||
).replace(replaceSpecialRegex, "");
|
||||
|
||||
CdkBase.createLogEvent(socket, "DEBUG", `Begin Query DMS Customer by Name using: ${ownerName}`);
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `Begin Query DMS Customer by Name using: ${ownerName}`);
|
||||
|
||||
try {
|
||||
const soapClientCustomerSearch = await soap.createClientAsync(CdkWsdl.CustomerSearch);
|
||||
@@ -285,29 +292,29 @@ async function QueryDmsCustomerByName(socket, JobData) {
|
||||
|
||||
const [result, rawResponse, , rawRequest] = soapResponseCustomerSearch;
|
||||
|
||||
CdkBase.createXmlEvent(socket, rawRequest, `soapClientCustomerSearch.executeSearchBulkAsync request.`);
|
||||
WsLogger.createXmlEvent(socket, rawRequest, `soapClientCustomerSearch.executeSearchBulkAsync request.`);
|
||||
|
||||
CdkBase.createXmlEvent(socket, rawResponse, `soapClientCustomerSearch.executeSearchBulkAsync response.`);
|
||||
WsLogger.createXmlEvent(socket, rawResponse, `soapClientCustomerSearch.executeSearchBulkAsync response.`);
|
||||
|
||||
CdkBase.createLogEvent(
|
||||
WsLogger.createLogEvent(
|
||||
socket,
|
||||
"SILLY",
|
||||
`soapClientCustomerSearch.executeSearchBulkAsync Result ${JSON.stringify(result, null, 2)}`
|
||||
);
|
||||
CheckCdkResponseForError(socket, soapResponseCustomerSearch);
|
||||
const CustomersFromDms = (result && result.return) || [];
|
||||
const CustomersFromDms = result?.return || [];
|
||||
return CustomersFromDms;
|
||||
} catch (error) {
|
||||
CdkBase.createXmlEvent(socket, error.request, `soapClientCustomerSearch.executeSearchBulkAsync request.`, true);
|
||||
WsLogger.createXmlEvent(socket, error.request, `soapClientCustomerSearch.executeSearchBulkAsync request.`, true);
|
||||
|
||||
CdkBase.createXmlEvent(
|
||||
WsLogger.createXmlEvent(
|
||||
socket,
|
||||
error.response && error.response.data,
|
||||
error.response?.data,
|
||||
`soapClientCustomerSearch.executeSearchBulkAsync response.`,
|
||||
true
|
||||
);
|
||||
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error in QueryDmsCustomerByName - ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in QueryDmsCustomerByName - ${error}`);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
@@ -318,7 +325,8 @@ async function GenerateDmsCustomerNumber(socket) {
|
||||
const soapResponseCustomerInsertUpdate = await soapClientCustomerInsertUpdate.getCustomerNumberAsync(
|
||||
{
|
||||
arg0: CDK_CREDENTIALS,
|
||||
arg1: { dealerId: socket.JobData.bodyshop.cdk_dealerid }, //TODO: Verify why this does not follow the other standards.
|
||||
//TODO: Verify why this does not follow the other standards.
|
||||
arg1: { dealerId: socket.JobData.bodyshop.cdk_dealerid },
|
||||
arg2: { userId: null }
|
||||
},
|
||||
|
||||
@@ -327,33 +335,33 @@ async function GenerateDmsCustomerNumber(socket) {
|
||||
|
||||
const [result, rawResponse, , rawRequest] = soapResponseCustomerInsertUpdate;
|
||||
|
||||
CdkBase.createXmlEvent(socket, rawRequest, `soapClientCustomerInsertUpdate.getCustomerNumberAsync request.`);
|
||||
WsLogger.createXmlEvent(socket, rawRequest, `soapClientCustomerInsertUpdate.getCustomerNumberAsync request.`);
|
||||
|
||||
CdkBase.createXmlEvent(socket, rawResponse, `soapClientCustomerInsertUpdate.getCustomerNumberAsync response.`);
|
||||
WsLogger.createXmlEvent(socket, rawResponse, `soapClientCustomerInsertUpdate.getCustomerNumberAsync response.`);
|
||||
|
||||
CdkBase.createLogEvent(
|
||||
WsLogger.createLogEvent(
|
||||
socket,
|
||||
"SILLY",
|
||||
`soapClientCustomerInsertUpdate.getCustomerNumberAsync Result ${JSON.stringify(result, null, 2)}`
|
||||
);
|
||||
CheckCdkResponseForError(socket, soapResponseCustomerInsertUpdate);
|
||||
const customerNumber = result && result.return && result.return.customerNumber;
|
||||
const customerNumber = result?.return && result.return.customerNumber;
|
||||
return customerNumber;
|
||||
} catch (error) {
|
||||
CdkBase.createXmlEvent(
|
||||
WsLogger.createXmlEvent(
|
||||
socket,
|
||||
error.request,
|
||||
`soapClientCustomerInsertUpdate.getCustomerNumberAsync request.`,
|
||||
true
|
||||
);
|
||||
|
||||
CdkBase.createXmlEvent(
|
||||
WsLogger.createXmlEvent(
|
||||
socket,
|
||||
error.response && error.response.data,
|
||||
error.response?.data,
|
||||
`soapClientCustomerInsertUpdate.getCustomerNumberAsync response.`,
|
||||
true
|
||||
);
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error in GenerateDmsCustomerNumber - ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in GenerateDmsCustomerNumber - ${error}`);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
@@ -416,27 +424,22 @@ async function InsertDmsCustomer(socket, newCustomerNumber) {
|
||||
);
|
||||
|
||||
const [result, rawResponse, , rawRequest] = soapResponseCustomerInsertUpdate;
|
||||
CdkBase.createXmlEvent(socket, rawRequest, `soapClientCustomerInsertUpdate.insertAsync request.`);
|
||||
WsLogger.createXmlEvent(socket, rawRequest, `soapClientCustomerInsertUpdate.insertAsync request.`);
|
||||
|
||||
CdkBase.createXmlEvent(socket, rawResponse, `soapClientCustomerInsertUpdate.insertAsync response.`);
|
||||
CdkBase.createLogEvent(
|
||||
WsLogger.createXmlEvent(socket, rawResponse, `soapClientCustomerInsertUpdate.insertAsync response.`);
|
||||
WsLogger.createLogEvent(
|
||||
socket,
|
||||
"SILLY",
|
||||
`soapClientCustomerInsertUpdate.insertAsync Result ${JSON.stringify(result, null, 2)}`
|
||||
);
|
||||
CheckCdkResponseForError(socket, soapResponseCustomerInsertUpdate);
|
||||
const customer = result && result.return && result.return.customerParty;
|
||||
const customer = result?.return && result.return.customerParty;
|
||||
return customer;
|
||||
} catch (error) {
|
||||
CdkBase.createXmlEvent(socket, error.request, `soapClientCustomerInsertUpdate.insertAsync request.`, true);
|
||||
WsLogger.createXmlEvent(socket, error.request, `soapClientCustomerInsertUpdate.insertAsync request.`, true);
|
||||
|
||||
CdkBase.createXmlEvent(
|
||||
socket,
|
||||
error.response && error.response.data,
|
||||
`soapClientCustomerInsertUpdate.insertAsync response.`,
|
||||
true
|
||||
);
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error in InsertDmsCustomer - ${error}`);
|
||||
WsLogger.createXmlEvent(socket, error.response?.data, `soapClientCustomerInsertUpdate.insertAsync response.`, true);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in InsertDmsCustomer - ${error}`);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
@@ -497,19 +500,19 @@ async function InsertDmsVehicle(socket) {
|
||||
|
||||
const [result, rawResponse, , rawRequest] = soapResponseVehicleInsertUpdate;
|
||||
|
||||
CdkBase.createXmlEvent(socket, rawRequest, `soapClientVehicleInsertUpdate.insertAsync request.`);
|
||||
WsLogger.createXmlEvent(socket, rawRequest, `soapClientVehicleInsertUpdate.insertAsync request.`);
|
||||
|
||||
CdkBase.createLogEvent(
|
||||
WsLogger.createLogEvent(
|
||||
socket,
|
||||
"SILLY",
|
||||
`soapClientVehicleInsertUpdate.insertAsync Result ${JSON.stringify(result, null, 2)}`
|
||||
);
|
||||
CdkBase.createXmlEvent(socket, rawResponse, `soapClientVehicleInsertUpdate.insertAsync response.`);
|
||||
WsLogger.createXmlEvent(socket, rawResponse, `soapClientVehicleInsertUpdate.insertAsync response.`);
|
||||
CheckCdkResponseForError(socket, soapResponseVehicleInsertUpdate);
|
||||
const VehicleFromDMS = result && result.return && result.return.vehicle;
|
||||
const VehicleFromDMS = result?.return && result.return.vehicle;
|
||||
return VehicleFromDMS;
|
||||
} catch (error) {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error in InsertDmsVehicle - ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in InsertDmsVehicle - ${error}`);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
@@ -523,12 +526,10 @@ async function UpdateDmsVehicle(socket) {
|
||||
//if it's a generic customer, don't update the vehicle owners.
|
||||
|
||||
if (socket.selectedCustomerId === socket.JobData.bodyshop.cdk_configuration.generic_customer_number) {
|
||||
ids = socket.DMSVeh && socket.DMSVeh.owners && socket.DMSVeh.owners;
|
||||
ids = socket.DMSVeh?.owners && socket.DMSVeh.owners;
|
||||
} else {
|
||||
const existingOwnerinVeh =
|
||||
socket.DMSVeh &&
|
||||
socket.DMSVeh.owners &&
|
||||
socket.DMSVeh.owners.find((o) => o.id.value === socket.DMSCust.id.value);
|
||||
socket.DMSVeh?.owners && socket.DMSVeh.owners.find((o) => o.id.value === socket.DMSCust.id.value);
|
||||
|
||||
if (existingOwnerinVeh) {
|
||||
ids = socket.DMSVeh.owners.map((o) => {
|
||||
@@ -540,10 +541,7 @@ async function UpdateDmsVehicle(socket) {
|
||||
};
|
||||
});
|
||||
} else {
|
||||
const oldOwner =
|
||||
socket.DMSVeh &&
|
||||
socket.DMSVeh.owners &&
|
||||
socket.DMSVeh.owners.find((o) => o.id.assigningPartyId === "CURRENT");
|
||||
const oldOwner = socket.DMSVeh?.owners && socket.DMSVeh.owners.find((o) => o.id.assigningPartyId === "CURRENT");
|
||||
|
||||
ids = [
|
||||
{
|
||||
@@ -603,19 +601,19 @@ async function UpdateDmsVehicle(socket) {
|
||||
});
|
||||
const [result, rawResponse, , rawRequest] = soapResponseVehicleInsertUpdate;
|
||||
|
||||
CdkBase.createXmlEvent(socket, rawRequest, `soapClientVehicleInsertUpdate.updateAsync request.`);
|
||||
WsLogger.createXmlEvent(socket, rawRequest, `soapClientVehicleInsertUpdate.updateAsync request.`);
|
||||
|
||||
CdkBase.createLogEvent(
|
||||
WsLogger.createLogEvent(
|
||||
socket,
|
||||
"DEBUG",
|
||||
`soapClientVehicleInsertUpdate.updateAsync Result ${JSON.stringify(result, null, 2)}`
|
||||
);
|
||||
CdkBase.createXmlEvent(socket, rawResponse, `soapClientVehicleInsertUpdate.updateAsync response.`);
|
||||
WsLogger.createXmlEvent(socket, rawResponse, `soapClientVehicleInsertUpdate.updateAsync response.`);
|
||||
CheckCdkResponseForError(socket, soapResponseVehicleInsertUpdate);
|
||||
const VehicleFromDMS = result && result.return && result.return.vehicle;
|
||||
const VehicleFromDMS = result?.return && result.return.vehicle;
|
||||
return VehicleFromDMS;
|
||||
} catch (error) {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error in UpdateDmsVehicle - ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in UpdateDmsVehicle - ${error}`);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
@@ -642,18 +640,18 @@ async function InsertServiceVehicleHistory(socket) {
|
||||
|
||||
const [result, rawResponse, , rawRequest] = soapResponseServiceHistoryInsert;
|
||||
|
||||
CdkBase.createXmlEvent(socket, rawRequest, `soapClientServiceHistoryInsert.serviceHistoryHeaderInsert request.`);
|
||||
WsLogger.createXmlEvent(socket, rawRequest, `soapClientServiceHistoryInsert.serviceHistoryHeaderInsert request.`);
|
||||
|
||||
CdkBase.createLogEvent(
|
||||
WsLogger.createLogEvent(
|
||||
socket,
|
||||
"SILLY",
|
||||
`soapClientServiceHistoryInsert.serviceHistoryHeaderInsert Result ${JSON.stringify(result, null, 2)}`
|
||||
);
|
||||
CdkBase.createXmlEvent(socket, rawResponse, `soapClientServiceHistoryInsert.serviceHistoryHeaderInsert response.`);
|
||||
WsLogger.createXmlEvent(socket, rawResponse, `soapClientServiceHistoryInsert.serviceHistoryHeaderInsert response.`);
|
||||
CheckCdkResponseForError(socket, soapResponseServiceHistoryInsert);
|
||||
return result && result.return;
|
||||
return result?.return;
|
||||
} catch (error) {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error in InsertServiceVehicleHistory - ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in InsertServiceVehicleHistory - ${error}`);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
@@ -669,8 +667,10 @@ async function InsertDmsStartWip(socket) {
|
||||
acctgDate: moment().tz(socket.JobData.bodyshop.timezone).format("YYYY-MM-DD"),
|
||||
//socket.JobData.invoice_date
|
||||
desc: socket.txEnvelope.story && socket.txEnvelope.story.replace(replaceSpecialRegex, ""),
|
||||
docType: 10 || 7, //Need to check what this usually would be? Apparently it is almost always 10 or 7.
|
||||
//1 Cash Receipt , 2 Check, 3 Journal Voucher, 4 Parts invoice, 5 Payable Invoice, 6 Recurring Entry, 7 Repair Order Invoice, 8 Vehicle Purchase Invoice, 9 Vehicle Sale Invoice, 10 Other, 11 Payroll, 12 Finance Charge, 13 FMLR Invoice, 14 Parts Credit Memo, 15 Manufacturer Document, 16 FMLR Credit Memo
|
||||
docType: 10, // Need to check what this usually would be? Apparently it is almost always 10 or 7.
|
||||
//1 Cash Receipt , 2 Check, 3 Journal Voucher, 4 Parts invoice, 5 Payable Invoice, 6 Recurring Entry, 7 Repair
|
||||
// Order Invoice, 8 Vehicle Purchase Invoice, 9 Vehicle Sale Invoice, 10 Other, 11 Payroll, 12 Finance Charge,
|
||||
// 13 FMLR Invoice, 14 Parts Credit Memo, 15 Manufacturer Document, 16 FMLR Credit Memo
|
||||
m13Flag: 0,
|
||||
refer: socket.JobData.ro_number,
|
||||
srcCo: socket.JobData.bodyshop.cdk_configuration.srcco,
|
||||
@@ -682,19 +682,19 @@ async function InsertDmsStartWip(socket) {
|
||||
|
||||
const [result, rawResponse, , rawRequest] = soapResponseAccountingGLInsertUpdate;
|
||||
|
||||
CdkBase.createXmlEvent(socket, rawRequest, `soapClientAccountingGLInsertUpdate.doStartWIPAsync request.`);
|
||||
WsLogger.createXmlEvent(socket, rawRequest, `soapClientAccountingGLInsertUpdate.doStartWIPAsync request.`);
|
||||
|
||||
CdkBase.createLogEvent(
|
||||
WsLogger.createLogEvent(
|
||||
socket,
|
||||
"SILLY",
|
||||
`soapClientAccountingGLInsertUpdate.doStartWIPAsync Result ${JSON.stringify(result, null, 2)}`
|
||||
);
|
||||
CdkBase.createXmlEvent(socket, rawResponse, `soapClientAccountingGLInsertUpdate.doStartWIPAsync response.`);
|
||||
WsLogger.createXmlEvent(socket, rawResponse, `soapClientAccountingGLInsertUpdate.doStartWIPAsync response.`);
|
||||
CheckCdkResponseForError(socket, soapResponseAccountingGLInsertUpdate);
|
||||
const TransactionHeader = result && result.return;
|
||||
const TransactionHeader = result?.return;
|
||||
return TransactionHeader;
|
||||
} catch (error) {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error in InsertDmsStartWip - ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in InsertDmsStartWip - ${error}`);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
@@ -713,19 +713,19 @@ async function InsertDmsBatchWip(socket) {
|
||||
|
||||
const [result, rawResponse, , rawRequest] = soapResponseAccountingGLInsertUpdate;
|
||||
|
||||
CdkBase.createXmlEvent(socket, rawRequest, `soapClientAccountingGLInsertUpdate.doTransBatchWIPAsync request.`);
|
||||
WsLogger.createXmlEvent(socket, rawRequest, `soapClientAccountingGLInsertUpdate.doTransBatchWIPAsync request.`);
|
||||
|
||||
CdkBase.createLogEvent(
|
||||
WsLogger.createLogEvent(
|
||||
socket,
|
||||
"SILLY",
|
||||
`soapClientAccountingGLInsertUpdate.doTransBatchWIPAsync Result ${JSON.stringify(result, null, 2)}`
|
||||
);
|
||||
CdkBase.createXmlEvent(socket, rawResponse, `soapClientAccountingGLInsertUpdate.doTransBatchWIPAsync response.`);
|
||||
WsLogger.createXmlEvent(socket, rawResponse, `soapClientAccountingGLInsertUpdate.doTransBatchWIPAsync response.`);
|
||||
CheckCdkResponseForError(socket, soapResponseAccountingGLInsertUpdate);
|
||||
const BatchWipResult = result && result.return;
|
||||
const BatchWipResult = result?.return;
|
||||
return BatchWipResult;
|
||||
} catch (error) {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error in InsertDmsBatchWip - ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in InsertDmsBatchWip - ${error}`);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
@@ -877,19 +877,19 @@ async function PostDmsBatchWip(socket) {
|
||||
|
||||
const [result, rawResponse, , rawRequest] = soapResponseAccountingGLInsertUpdate;
|
||||
|
||||
CdkBase.createXmlEvent(socket, rawRequest, `soapClientAccountingGLInsertUpdate.doPostBatchWIPAsync request.`);
|
||||
WsLogger.createXmlEvent(socket, rawRequest, `soapClientAccountingGLInsertUpdate.doPostBatchWIPAsync request.`);
|
||||
|
||||
CdkBase.createLogEvent(
|
||||
WsLogger.createLogEvent(
|
||||
socket,
|
||||
"SILLY",
|
||||
`soapClientAccountingGLInsertUpdate.doPostBatchWIPAsync Result ${JSON.stringify(result, null, 2)}`
|
||||
);
|
||||
CdkBase.createXmlEvent(socket, rawResponse, `soapClientAccountingGLInsertUpdate.doPostBatchWIPAsync response.`);
|
||||
WsLogger.createXmlEvent(socket, rawResponse, `soapClientAccountingGLInsertUpdate.doPostBatchWIPAsync response.`);
|
||||
// CheckCdkResponseForError(socket, soapResponseAccountingGLInsertUpdate);
|
||||
const PostResult = result && result.return;
|
||||
const PostResult = result?.return;
|
||||
return PostResult;
|
||||
} catch (error) {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error in PostDmsBatchWip - ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in PostDmsBatchWip - ${error}`);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
@@ -906,19 +906,19 @@ async function QueryDmsErrWip(socket) {
|
||||
|
||||
const [result, rawResponse, , rawRequest] = soapResponseAccountingGLInsertUpdate;
|
||||
|
||||
CdkBase.createXmlEvent(socket, rawRequest, `soapClientAccountingGLInsertUpdate.doErrWIPAsync request.`);
|
||||
WsLogger.createXmlEvent(socket, rawRequest, `soapClientAccountingGLInsertUpdate.doErrWIPAsync request.`);
|
||||
|
||||
CdkBase.createLogEvent(
|
||||
WsLogger.createLogEvent(
|
||||
socket,
|
||||
"DEBUG",
|
||||
`soapClientAccountingGLInsertUpdate.doErrWIPAsync Result ${JSON.stringify(result, null, 2)}`
|
||||
);
|
||||
CdkBase.createXmlEvent(socket, rawResponse, `soapClientAccountingGLInsertUpdate.doErrWIPAsync response.`);
|
||||
WsLogger.createXmlEvent(socket, rawResponse, `soapClientAccountingGLInsertUpdate.doErrWIPAsync response.`);
|
||||
CheckCdkResponseForError(socket, soapResponseAccountingGLInsertUpdate);
|
||||
const PostResult = result && result.return;
|
||||
const PostResult = result?.return;
|
||||
return PostResult;
|
||||
} catch (error) {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error in QueryDmsErrWip - ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in QueryDmsErrWip - ${error}`);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
@@ -937,28 +937,31 @@ async function DeleteDmsWip(socket) {
|
||||
|
||||
const [result, rawResponse, , rawRequest] = soapResponseAccountingGLInsertUpdate;
|
||||
|
||||
CdkBase.createXmlEvent(socket, rawRequest, `soapClientAccountingGLInsertUpdate.doPostBatchWIPAsync request.`);
|
||||
WsLogger.createXmlEvent(socket, rawRequest, `soapClientAccountingGLInsertUpdate.doPostBatchWIPAsync request.`);
|
||||
|
||||
CdkBase.createLogEvent(
|
||||
WsLogger.createLogEvent(
|
||||
socket,
|
||||
"SILLY",
|
||||
`soapClientAccountingGLInsertUpdate.doPostBatchWIPAsync Result ${JSON.stringify(result, null, 2)}`
|
||||
);
|
||||
CdkBase.createXmlEvent(socket, rawResponse, `soapClientAccountingGLInsertUpdate.doPostBatchWIPAsync response.`);
|
||||
WsLogger.createXmlEvent(socket, rawResponse, `soapClientAccountingGLInsertUpdate.doPostBatchWIPAsync response.`);
|
||||
CheckCdkResponseForError(socket, soapResponseAccountingGLInsertUpdate);
|
||||
const PostResult = result && result.return;
|
||||
const PostResult = result?.return;
|
||||
return PostResult;
|
||||
} catch (error) {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error in PostDmsBatchWip - ${error}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in PostDmsBatchWip - ${error}`);
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function MarkJobExported(socket, jobid) {
|
||||
CdkBase.createLogEvent(socket, "DEBUG", `Marking job as exported for id ${jobid}`);
|
||||
WsLogger.createLogEvent(socket, "DEBUG", `Marking job as exported for id ${jobid}`);
|
||||
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
|
||||
const currentToken =
|
||||
(socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token);
|
||||
|
||||
const result = await client
|
||||
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
|
||||
.setHeaders({ Authorization: `Bearer ${currentToken}` })
|
||||
.request(queries.MARK_JOB_EXPORTED, {
|
||||
jobId: jobid,
|
||||
job: {
|
||||
@@ -984,20 +987,23 @@ async function MarkJobExported(socket, jobid) {
|
||||
async function InsertFailedExportLog(socket, error) {
|
||||
try {
|
||||
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
|
||||
const currentToken =
|
||||
(socket?.data && socket.data.authToken) || (socket?.handshake?.auth && socket.handshake.auth.token);
|
||||
|
||||
const result = await client
|
||||
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
|
||||
.setHeaders({ Authorization: `Bearer ${currentToken}` })
|
||||
.request(queries.INSERT_EXPORT_LOG, {
|
||||
log: {
|
||||
logs: [{
|
||||
bodyshopid: socket.JobData.bodyshop.id,
|
||||
jobid: socket.JobData.id,
|
||||
successful: false,
|
||||
message: JSON.stringify(error),
|
||||
useremail: socket.user.email
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error2) {
|
||||
CdkBase.createLogEvent(socket, "ERROR", `Error in InsertFailedExportLog - ${error} - ${JSON.stringify(error2)}`);
|
||||
WsLogger.createLogEvent(socket, "ERROR", `Error in InsertFailedExportLog - ${error} - ${JSON.stringify(error2)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -736,7 +736,7 @@ const CreateCosts = (job) => {
|
||||
PASL: "PASL"
|
||||
};
|
||||
const defaultCosts =
|
||||
job.bodyshop.cdk_dealerid || job.bodyshop.pbs_serialnumber
|
||||
job.bodyshop.cdk_dealerid || job.bodyshop.pbs_serialnumber || job.bodyshop.rr_dealerid
|
||||
? ciecaObj
|
||||
: job.bodyshop.md_responsibility_centers.defaults.costs;
|
||||
|
||||
|
||||
@@ -591,7 +591,7 @@ const CreateCosts = (job) => {
|
||||
PASL: "PASL"
|
||||
};
|
||||
const defaultCosts =
|
||||
job.bodyshop.cdk_dealerid || job.bodyshop.pbs_serialnumber
|
||||
job.bodyshop.cdk_dealerid || job.bodyshop.pbs_serialnumber || job.bodyshop.rr_dealerid
|
||||
? ciecaObj
|
||||
: job.bodyshop.md_responsibility_centers.defaults.costs;
|
||||
|
||||
|
||||
@@ -219,8 +219,6 @@ const CreateRepairOrderTag = (job, errorCallback) => {
|
||||
}
|
||||
|
||||
const repairCosts = CreateCosts(job);
|
||||
const jobline = CreateJobLines(job.joblines);
|
||||
const timeticket = CreateTimeTickets(job.timetickets);
|
||||
|
||||
try {
|
||||
const ret = {
|
||||
@@ -290,8 +288,100 @@ const CreateRepairOrderTag = (job, errorCallback) => {
|
||||
(job.date_exported && moment(job.date_exported).tz(job.bodyshop.timezone).format(DateFormat)) || "",
|
||||
DateVoid: (job.date_void && moment(job.date_void).tz(job.bodyshop.timezone).format(DateFormat)) || ""
|
||||
},
|
||||
JobLineDetails: { jobline },
|
||||
TimeTicketDetails: { timeticket },
|
||||
JobLineDetails: (function () {
|
||||
const joblineSource = Array.isArray(job.joblines) ? job.joblines : job.joblines ? [job.joblines] : [];
|
||||
if (joblineSource.length === 0) return { jobline: [] };
|
||||
return {
|
||||
jobline: joblineSource.map((jl = {}) => ({
|
||||
line_description: jl.line_desc || jl.line_description || "",
|
||||
oem_part_no: jl.oem_partno || jl.oem_part_no || "",
|
||||
alt_part_no: jl.alt_partno || jl.alt_part_no || "",
|
||||
op_code_desc: jl.op_code_desc || "",
|
||||
part_type: jl.part_type || "",
|
||||
part_qty: jl.part_qty ?? jl.quantity ?? 0,
|
||||
part_price: jl.act_price ?? jl.part_price ?? 0,
|
||||
labor_type: jl.mod_lbr_ty || jl.labor_type || "",
|
||||
labor_hours: jl.mod_lb_hrs ?? jl.labor_hours ?? 0,
|
||||
labor_sale: jl.lbr_amt ?? jl.labor_sale ?? 0
|
||||
}))
|
||||
};
|
||||
})(),
|
||||
BillsDetails: (function () {
|
||||
const billsSource = Array.isArray(job.bills) ? job.bills : job.bills ? [job.bills] : [];
|
||||
if (billsSource.length === 0) return { BillDetails: [] };
|
||||
return {
|
||||
BillDetails: billsSource.map(
|
||||
({
|
||||
billlines = [],
|
||||
date = "",
|
||||
is_credit_memo = false,
|
||||
invoice_number = "",
|
||||
isinhouse = false,
|
||||
vendor = {}
|
||||
} = {}) => ({
|
||||
BillLines: {
|
||||
BillLine: billlines.map((bl = {}) => ({
|
||||
line_description: bl.line_desc || bl.line_description || "",
|
||||
part_price: bl.actual_price ?? bl.part_price ?? bl.act_price ?? 0,
|
||||
actual_cost: bl.actual_cost ?? 0,
|
||||
cost_center: bl.cost_center || "",
|
||||
deductedfromlbr: bl.deductedfromlbr || false,
|
||||
part_qty: bl.quantity ?? bl.part_qty ?? 0,
|
||||
oem_part_no: bl.oem_partno || bl.oem_part_no || "",
|
||||
alt_part_no: bl.alt_partno || bl.alt_part_no || ""
|
||||
}))
|
||||
},
|
||||
date,
|
||||
is_credit_memo,
|
||||
invoice_number,
|
||||
isinhouse,
|
||||
vendorName: vendor.name || ""
|
||||
})
|
||||
)
|
||||
};
|
||||
})(),
|
||||
JobNotes: (function () {
|
||||
const notesSource = Array.isArray(job.notes) ? job.notes : job.notes ? [job.notes] : [];
|
||||
if (notesSource.length === 0) return { JobNote: [] };
|
||||
return {
|
||||
JobNote: notesSource.map((note = {}) => ({
|
||||
created_at: note.created_at || "",
|
||||
created_by: note.created_by || "",
|
||||
critical: note.critical || false,
|
||||
private: note.private || false,
|
||||
text: note.text || "",
|
||||
type: note.type || ""
|
||||
}))
|
||||
};
|
||||
})(),
|
||||
TimeTicketDetails: (function () {
|
||||
const ticketSource = Array.isArray(job.timetickets)
|
||||
? job.timetickets
|
||||
: job.timetickets
|
||||
? [job.timetickets]
|
||||
: [];
|
||||
if (ticketSource.length === 0) return { timeticket: [] };
|
||||
return {
|
||||
timeticket: ticketSource.map((ticket = {}) => ({
|
||||
date: ticket.date || "",
|
||||
employee:
|
||||
ticket.employee && ticket.employee.employee_number
|
||||
? ticket.employee.employee_number
|
||||
.trim()
|
||||
.concat(" - ", ticket.employee.first_name.trim(), " ", ticket.employee.last_name.trim())
|
||||
.trim()
|
||||
: "",
|
||||
productive_hrs: ticket.productivehrs ?? 0,
|
||||
actual_hrs: ticket.actualhrs ?? 0,
|
||||
cost_center: ticket.cost_center || "",
|
||||
flat_rate: ticket.flat_rate || false,
|
||||
rate: ticket.rate ?? 0,
|
||||
ticket_cost: ticket.flat_rate
|
||||
? ticket.rate * (ticket.productivehrs || 0)
|
||||
: ticket.rate * (ticket.actualhrs || 0)
|
||||
}))
|
||||
};
|
||||
})(),
|
||||
Sales: {
|
||||
Labour: {
|
||||
Aluminum: Dinero(job.job_totals.rates.laa.total).toFormat(DineroFormat),
|
||||
@@ -557,7 +647,7 @@ const CreateCosts = (job) => {
|
||||
PASL: "PASL"
|
||||
};
|
||||
const defaultCosts =
|
||||
job.bodyshop.cdk_dealerid || job.bodyshop.pbs_serialnumber
|
||||
job.bodyshop.cdk_dealerid || job.bodyshop.pbs_serialnumber || job.bodyshop.rr_dealerid
|
||||
? ciecaObj
|
||||
: job.bodyshop.md_responsibility_centers.defaults.costs;
|
||||
|
||||
@@ -636,42 +726,3 @@ const CreateCosts = (job) => {
|
||||
}, 0)
|
||||
};
|
||||
};
|
||||
|
||||
const CreateJobLines = (joblines) => {
|
||||
const repairLines = [];
|
||||
joblines.forEach((jobline) => {
|
||||
repairLines.push({
|
||||
line_description: jobline.line_desc,
|
||||
oem_part_no: jobline.oem_partno,
|
||||
alt_part_no: jobline.alt_partno,
|
||||
op_code_desc: jobline.op_code_desc,
|
||||
part_type: jobline.part_type,
|
||||
part_qty: jobline.part_qty,
|
||||
part_price: jobline.act_price,
|
||||
labor_type: jobline.mod_lbr_ty,
|
||||
labor_hours: jobline.mod_lb_hrs,
|
||||
labor_sale: jobline.lbr_amt
|
||||
});
|
||||
});
|
||||
return repairLines;
|
||||
};
|
||||
|
||||
const CreateTimeTickets = (timetickets) => {
|
||||
const timeTickets = [];
|
||||
timetickets.forEach((ticket) => {
|
||||
timeTickets.push({
|
||||
date: ticket.date,
|
||||
employee: ticket.employee.employee_number
|
||||
.trim()
|
||||
.concat(" - ", ticket.employee.first_name.trim(), " ", ticket.employee.last_name.trim())
|
||||
.trim(),
|
||||
productive_hrs: ticket.productivehrs,
|
||||
actual_hrs: ticket.actualhrs,
|
||||
cost_center: ticket.cost_center,
|
||||
flat_rate: ticket.flat_rate,
|
||||
rate: ticket.rate,
|
||||
ticket_cost: ticket.flat_rate ? ticket.rate * ticket.productive_hrs : ticket.rate * ticket.actual_hrs
|
||||
});
|
||||
});
|
||||
return timeTickets;
|
||||
};
|
||||
|
||||
460
server/fortellis/fortellis-helpers.js
Normal file
460
server/fortellis/fortellis-helpers.js
Normal file
@@ -0,0 +1,460 @@
|
||||
const path = require("path");
|
||||
require("dotenv").config({
|
||||
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
||||
});
|
||||
|
||||
// const CalcualteAllocations = require("../cdk/cdk-calculate-allocations").default;
|
||||
const CreateFortellisLogEvent = require("./fortellis-logger");
|
||||
const logger = require("../utils/logger");
|
||||
const uuid = require("uuid").v4;
|
||||
const AxiosLib = require("axios").default;
|
||||
const axios = AxiosLib.create();
|
||||
const axiosCurlirize = require("axios-curlirize").default;
|
||||
|
||||
// Custom error class for Fortellis API errors
|
||||
class FortellisApiError extends Error {
|
||||
constructor(message, details) {
|
||||
super(message);
|
||||
this.name = "FortellisApiError";
|
||||
this.reqId = details.reqId;
|
||||
this.url = details.url;
|
||||
this.apiName = details.apiName;
|
||||
this.errorData = details.errorData;
|
||||
this.errorStatus = details.errorStatus;
|
||||
this.errorStatusText = details.errorStatusText;
|
||||
this.originalError = details.originalError;
|
||||
}
|
||||
}
|
||||
|
||||
axiosCurlirize(axios, (_result, _err) => {
|
||||
//Left intentionally blank. We don't want to console.log. We handle logging the cURL in MakeFortellisCall once completed.
|
||||
});
|
||||
|
||||
const getTransactionType = (jobid) => `fortellis:${jobid}`;
|
||||
const defaultFortellisTTL = 60 * 60;
|
||||
|
||||
async function GetAuthToken() {
|
||||
//Done with Authorization Code Flow
|
||||
//https://docs.fortellis.io/docs/tutorials/solution-integration/authorization-code-flow/
|
||||
|
||||
//TODO: This should get stored in the redis cache and only be refreshed when it expires.
|
||||
const {
|
||||
data: { access_token, expires_in, token_type }
|
||||
} = await axios.post(
|
||||
process.env.FORTELLIS_AUTH_URL,
|
||||
{},
|
||||
{
|
||||
auth: {
|
||||
username: process.env.FORTELLIS_KEY,
|
||||
password: process.env.FORTELLIS_SECRET
|
||||
},
|
||||
params: {
|
||||
grant_type: "client_credentials",
|
||||
scope: "anonymous"
|
||||
}
|
||||
}
|
||||
);
|
||||
return access_token;
|
||||
}
|
||||
|
||||
async function FetchSubscriptions({ redisHelpers, socket, jobid, SubscriptionObject }) {
|
||||
try {
|
||||
const { setSessionTransactionData, getSessionTransactionData } = redisHelpers;
|
||||
|
||||
//Get Subscription ID from Transaction Envelope
|
||||
const { SubscriptionID } = SubscriptionObject
|
||||
? SubscriptionObject
|
||||
: await getSessionTransactionData(socket.id, getTransactionType(jobid), `txEnvelope`);
|
||||
if (!SubscriptionID) {
|
||||
throw new Error("Subscription ID not found in transaction envelope.");
|
||||
}
|
||||
|
||||
//Check to See if the subscription meta is in the Redis Cache.
|
||||
const SubscriptionMetaFromCache = await getSessionTransactionData(
|
||||
socket.id,
|
||||
getTransactionType(jobid),
|
||||
FortellisCacheEnums.SubscriptionMeta
|
||||
);
|
||||
|
||||
// If it is, return it.
|
||||
if (SubscriptionMetaFromCache) {
|
||||
return SubscriptionMetaFromCache;
|
||||
} else {
|
||||
const access_token = await GetAuthToken();
|
||||
const subscriptions = await axios.get(FortellisActions.GetSubscription.url, {
|
||||
headers: { Authorization: `Bearer ${access_token}` },
|
||||
logRequest: false
|
||||
});
|
||||
const SubscriptionMeta = subscriptions.data.subscriptions.find((s) => s.subscriptionId === SubscriptionID);
|
||||
if (setSessionTransactionData) {
|
||||
await setSessionTransactionData(
|
||||
socket.id,
|
||||
getTransactionType(jobid),
|
||||
FortellisCacheEnums.SubscriptionMeta,
|
||||
SubscriptionMeta,
|
||||
defaultFortellisTTL
|
||||
);
|
||||
}
|
||||
return SubscriptionMeta;
|
||||
}
|
||||
} catch (error) {
|
||||
CreateFortellisLogEvent(socket, "ERROR", `Error fetching subscription metadata.`, {
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function GetDepartmentId({ apiName, debug = false, SubscriptionMeta, overrideDepartmentId }) {
|
||||
if (!apiName) throw new Error("apiName not provided. Unable to get department without apiName.");
|
||||
if (debug) {
|
||||
console.log("API Names & Departments ");
|
||||
console.log("===========");
|
||||
console.log(JSON.stringify(SubscriptionMeta.apiDmsInfo, null, 4));
|
||||
console.log("===========");
|
||||
}
|
||||
|
||||
const departmentIds = SubscriptionMeta.apiDmsInfo //Get the subscription object.
|
||||
.find((info) => info.name === apiName)?.departments; //Departments are categorized by API name and have an array of departments.
|
||||
|
||||
if (overrideDepartmentId) {
|
||||
return departmentIds && departmentIds.find(d => d.id === overrideDepartmentId)?.id
|
||||
} else {
|
||||
|
||||
return departmentIds && departmentIds[0] && departmentIds[0].id; //TODO: This makes the assumption that there is only 1 department.
|
||||
}
|
||||
}
|
||||
|
||||
//Highest level function call to make a call to fortellis. This should be the only call required, and it will handle all the logic for making the call.
|
||||
async function MakeFortellisCall({
|
||||
apiName,
|
||||
url,
|
||||
headers = {},
|
||||
body = {},
|
||||
type = "post",
|
||||
debug = false,
|
||||
requestPathParams,
|
||||
requestSearchParams = [], //Array of key/value strings like [["key", "value"]]
|
||||
jobid,
|
||||
redisHelpers,
|
||||
socket,
|
||||
SubscriptionObject, //This is used because of the get make models to bypass all of the redis calls.
|
||||
overrideDepartmentId
|
||||
}) {
|
||||
//const { setSessionTransactionData, getSessionTransactionData } = redisHelpers;
|
||||
|
||||
const fullUrl = constructFullUrl({ url, pathParams: requestPathParams, requestSearchParams });
|
||||
if (debug) console.log(`Executing ${type} to ${fullUrl}`);
|
||||
const ReqId = uuid();
|
||||
const access_token = await GetAuthToken();
|
||||
const SubscriptionMeta = await FetchSubscriptions({ redisHelpers, socket, jobid, SubscriptionObject });
|
||||
const DepartmentId = await GetDepartmentId({ apiName, debug, SubscriptionMeta, overrideDepartmentId });
|
||||
|
||||
if (debug) {
|
||||
console.log(
|
||||
`ReqID: ${ReqId} | SubscriptionID: ${SubscriptionMeta.subscriptionId} | DepartmentId: ${DepartmentId}`
|
||||
);
|
||||
console.log(`Body Contents: ${JSON.stringify(body, null, 4)}`);
|
||||
}
|
||||
|
||||
try {
|
||||
let result;
|
||||
switch (type) {
|
||||
case "post":
|
||||
default:
|
||||
result = await axios.post(fullUrl, body, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
"Subscription-Id": SubscriptionMeta.subscriptionId,
|
||||
"Request-Id": ReqId,
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
...(DepartmentId && { "Department-Id": DepartmentId }),
|
||||
...headers
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "get":
|
||||
result = await axios.get(fullUrl, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
"Subscription-Id": SubscriptionMeta.subscriptionId,
|
||||
"Request-Id": ReqId,
|
||||
Accept: "application/json",
|
||||
"Department-Id": DepartmentId,
|
||||
...headers
|
||||
}
|
||||
});
|
||||
break;
|
||||
case "put":
|
||||
result = await axios.put(fullUrl, body, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
"Subscription-Id": SubscriptionMeta.subscriptionId,
|
||||
"Request-Id": ReqId,
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"Department-Id": DepartmentId,
|
||||
...headers
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
console.log(`ReqID: ${ReqId} Data`);
|
||||
console.log(JSON.stringify(result.data, null, 4));
|
||||
}
|
||||
|
||||
if (result.data.checkStatusAfterSeconds) {
|
||||
return DelayedCallback({
|
||||
delayMeta: result.data,
|
||||
access_token,
|
||||
SubscriptionID: SubscriptionMeta.subscriptionId,
|
||||
ReqId,
|
||||
departmentIds: DepartmentId
|
||||
});
|
||||
}
|
||||
|
||||
logger.log(
|
||||
"fortellis-log-event-json",
|
||||
"DEBUG",
|
||||
socket?.user?.email,
|
||||
jobid,
|
||||
{
|
||||
requestcurl: result.config.curlCommand,
|
||||
reqid: result.config.headers["Request-Id"] || null,
|
||||
subscriptionId: result.config.headers["Subscription-Id"] || null,
|
||||
resultdata: result.data,
|
||||
resultStatus: result.status
|
||||
},
|
||||
);
|
||||
|
||||
return result.data;
|
||||
} catch (error) {
|
||||
const errorDetails = {
|
||||
reqId: ReqId,
|
||||
url: fullUrl,
|
||||
apiName,
|
||||
errorData: error.response?.data,
|
||||
errorStatus: error.response?.status,
|
||||
errorStatusText: error.response?.statusText,
|
||||
originalError: error
|
||||
};
|
||||
|
||||
logger.log(
|
||||
"fortellis-log-event-error",
|
||||
"ERROR",
|
||||
socket?.user?.email,
|
||||
socket?.recordid,
|
||||
{
|
||||
wsmessage: "",//message,
|
||||
curl: error.config.curl.curlCommand,
|
||||
reqid: error.request.headers["Request-Id"] || null,
|
||||
subscriptionId: error.request.headers["Subscription-Id"] || null,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
throw new FortellisApiError(`Fortellis API call failed for ${apiName}: ${error.message}`, errorDetails);
|
||||
}
|
||||
}
|
||||
|
||||
//Some Fortellis calls return a batch result that isn't ready immediately.
|
||||
//This function will check the status of the call and wait until it is ready.
|
||||
//It will try 5 times before giving up.
|
||||
async function DelayedCallback({ delayMeta, access_token, SubscriptionID, ReqId, departmentIds }) {
|
||||
for (let index = 0; index < 5; index++) {
|
||||
await sleep(delayMeta.checkStatusAfterSeconds * 1000);
|
||||
//Check to see if the call is ready.
|
||||
const statusResult = await axios.get(delayMeta._links.status.href, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
"Subscription-Id": SubscriptionID,
|
||||
"Request-Id": ReqId,
|
||||
"Department-Id": departmentIds[0].id
|
||||
}
|
||||
});
|
||||
|
||||
//TODO: Add a check if the status result is not ready, to try again.
|
||||
if (statusResult.data.status === "complete") {
|
||||
//This may have to check again if it isn't ready.
|
||||
const batchResult = await axios.get(statusResult.data._links.result.href, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
"Subscription-Id": SubscriptionID,
|
||||
"Request-Id": ReqId
|
||||
//"Department-Id": departmentIds[0].id
|
||||
}
|
||||
});
|
||||
return batchResult;
|
||||
} else {
|
||||
return "Error!!! Still need to implement batch waiting.";
|
||||
}
|
||||
}
|
||||
}
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
|
||||
//Get requests should have the trailing slash as they are used that way in the calls.
|
||||
const FortellisActions = {
|
||||
GetSubscription: {
|
||||
url: isProduction
|
||||
? "https://subscriptions.fortellis.io/v1/solution/subscriptions"
|
||||
: "https://subscriptions.fortellis.io/v1/solution/subscriptions",
|
||||
type: "get",
|
||||
apiName: "Fortellis Get Subscriptions"
|
||||
},
|
||||
QueryVehicles: {
|
||||
url: isProduction
|
||||
? "https://api.fortellis.io/cdkdrive/service/v1/vehicles/"
|
||||
: "https://api.fortellis.io/cdk-test/cdkdrive/service/v1/vehicles/",
|
||||
type: "get",
|
||||
apiName: "Service Vehicle - Query Vehicles"
|
||||
},
|
||||
GetMakeModel: {
|
||||
url: isProduction
|
||||
? "https://api.fortellis.io/cdk/drive/makemodel/v2/bulk"
|
||||
: "https://api.fortellis.io/cdk-test/drive/makemodel/v2",
|
||||
type: "get",
|
||||
apiName: "CDK Drive Get Make Model Lite"
|
||||
},
|
||||
GetVehicleId: {
|
||||
url: isProduction
|
||||
? "https://api.fortellis.io/cdk/drive/service-vehicle-mgmt/v2/vehicle-ids/" //Request path params of vins
|
||||
: "https://api.fortellis.io/cdk-test/drive/service-vehicle-mgmt/v2/vehicle-ids/",
|
||||
type: "get",
|
||||
apiName: "CDK Drive Post Service Vehicle"
|
||||
},
|
||||
GetVehicleById: {
|
||||
url: isProduction
|
||||
? "https://api.fortellis.io/cdk/drive/service-vehicle-mgmt/v2/" //Request path params of vehicleId
|
||||
: "https://api.fortellis.io/cdk-test/drive/service-vehicle-mgmt/v2/",
|
||||
type: "get",
|
||||
apiName: "CDK Drive Post Service Vehicle"
|
||||
},
|
||||
QueryCustomerByName: {
|
||||
url: isProduction
|
||||
? "https://api.fortellis.io/cdk/drive/customerpost/v1/search"
|
||||
: "https://api.fortellis.io/cdk-test/drive/customerpost/v1/search",
|
||||
type: "get",
|
||||
apiName: "CDK Drive Post Customer"
|
||||
},
|
||||
ReadCustomer: {
|
||||
url: isProduction
|
||||
? "https://api.fortellis.io/cdk/drive/customerpost/v1/" //Customer ID is request param.
|
||||
: "https://api.fortellis.io/cdk-test/drive/customerpost/v1/",
|
||||
type: "get",
|
||||
apiName: "CDK Drive Post Customer"
|
||||
},
|
||||
CreateCustomer: {
|
||||
url: isProduction
|
||||
? "https://api.fortellis.io/cdk/drive/customerpost/v1/"
|
||||
: "https://api.fortellis.io/cdk-test/drive/customerpost/v1/",
|
||||
type: "post",
|
||||
apiName: "CDK Drive Post Customer"
|
||||
},
|
||||
InsertVehicle: {
|
||||
url: isProduction
|
||||
? "https://api.fortellis.io/cdk/drive/service-vehicle-mgmt/v2/"
|
||||
: "https://api.fortellis.io/cdk-test/drive/service-vehicle-mgmt/v2/",
|
||||
type: "post",
|
||||
apiName: "CDK Drive Post Service Vehicle"
|
||||
},
|
||||
UpdateVehicle: {
|
||||
url: isProduction
|
||||
? "https://api.fortellis.io/cdk/drive/service-vehicle-mgmt/v2/"
|
||||
: "https://api.fortellis.io/cdk-test/drive/service-vehicle-mgmt/v2/",
|
||||
type: "put",
|
||||
apiName: "CDK Drive Post Service Vehicle"
|
||||
},
|
||||
GetCOA: {
|
||||
type: "get",
|
||||
apiName: "CDK Drive Post Accounts GL WIP",
|
||||
url: `https://api.fortellis.io/cdk-test/drive/chartofaccounts/v2/bulk/`,
|
||||
waitForResult: true
|
||||
},
|
||||
StartWip: {
|
||||
url: isProduction
|
||||
? "https://api.fortellis.io/cdk/drive/glpost/startWIP"
|
||||
: "https://api.fortellis.io/cdk-test/drive/glpost/startWIP",
|
||||
type: "post",
|
||||
apiName: "CDK Drive Post Accounts GL"
|
||||
},
|
||||
TranBatchWip: {
|
||||
url: isProduction
|
||||
? "https://api.fortellis.io/cdk/drive/glpost/transBatchWIP"
|
||||
: "https://api.fortellis.io/cdk-test/drive/glpost/transBatchWIP",
|
||||
type: "post",
|
||||
apiName: "CDK Drive Post Accounts GL"
|
||||
},
|
||||
PostBatchWip: {
|
||||
url: isProduction
|
||||
? "https://api.fortellis.io/cdk/drive/glpost/postBatchWIP"
|
||||
: "https://api.fortellis.io/cdk-test/drive/glpost/postBatchWIP",
|
||||
type: "post",
|
||||
apiName: "CDK Drive Post Accounts GL"
|
||||
},
|
||||
DeleteTranWip: {
|
||||
url: isProduction
|
||||
? "https://api.fortellis.io/cdk/drive/glpost/postWIP"
|
||||
: "https://api.fortellis.io/cdk-test/drive/glpost/postWIP",
|
||||
type: "post",
|
||||
apiName: "CDK Drive Post Accounts GL"
|
||||
},
|
||||
QueryErrorWip: {
|
||||
url: isProduction
|
||||
? "https://api.fortellis.io/cdk/drive/glpost/errWIP/"
|
||||
: "https://api.fortellis.io/cdk-test/drive/glpost/errWIP/",
|
||||
type: "get",
|
||||
apiName: "CDK Drive Post Accounts GL"
|
||||
},
|
||||
ServiceHistoryInsert: {
|
||||
url: isProduction
|
||||
? "https://api.fortellis.io/cdk/drive/post/service-vehicle-history-mgmt/v2/"
|
||||
: "https://api.fortellis.io/cdk-test/drive/post/service-vehicle-history-mgmt/v2/",
|
||||
type: "post",
|
||||
apiName: "CDK Drive Post Service Vehicle History"
|
||||
}
|
||||
};
|
||||
|
||||
const FortellisCacheEnums = {
|
||||
txEnvelope: "txEnvelope",
|
||||
DMSBatchTxn: "DMSBatchTxn",
|
||||
SubscriptionMeta: "SubscriptionMeta",
|
||||
DepartmentId: "DepartmentId",
|
||||
JobData: "JobData",
|
||||
DMSVid: "DMSVid",
|
||||
DMSVeh: "DMSVeh",
|
||||
DMSVehCustomer: "DMSVehCustomer",
|
||||
DMSCustList: "DMSCustList",
|
||||
DMSCust: "DMSCust",
|
||||
selectedCustomerId: "selectedCustomerId",
|
||||
DMSTransHeader: "DMSTransHeader",
|
||||
transWips: "transWips",
|
||||
DmsBatchTxnPost: "DmsBatchTxnPost",
|
||||
DMSVehHistory: "DMSVehHistory"
|
||||
};
|
||||
|
||||
function constructFullUrl({ url, pathParams = "", requestSearchParams = [] }) {
|
||||
// Ensure the base URL ends with a single "/"
|
||||
url = url.replace(/\/+$/, "/");
|
||||
const fullPath = pathParams ? `${url}${pathParams}` : url;
|
||||
const searchParams = new URLSearchParams(requestSearchParams).toString();
|
||||
const fullUrl = searchParams ? `${fullPath}?${searchParams}` : fullPath;
|
||||
return fullUrl;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
GetAuthToken,
|
||||
FortellisCacheEnums,
|
||||
MakeFortellisCall,
|
||||
FortellisActions,
|
||||
getTransactionType,
|
||||
defaultFortellisTTL,
|
||||
FortellisApiError,
|
||||
GetDepartmentId
|
||||
};
|
||||
8
server/fortellis/fortellis-logger.js
Normal file
8
server/fortellis/fortellis-logger.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const logger = require("../utils/logger");
|
||||
|
||||
const CreateFortellisLogEvent = (socket, level, message, txnDetails) => {
|
||||
logger.log("fortellis-log-event", level, socket?.user?.email, null, { wsmessage: message, txnDetails });
|
||||
socket.emit("fortellis-log-event", { level, message, txnDetails });
|
||||
};
|
||||
|
||||
module.exports = CreateFortellisLogEvent;
|
||||
1326
server/fortellis/fortellis.js
Normal file
1326
server/fortellis/fortellis.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -697,6 +697,7 @@ exports.AUTOHOUSE_QUERY = `query AUTOHOUSE_EXPORT($start: timestamptz, $bodyshop
|
||||
jc_hourly_rates
|
||||
cdk_dealerid
|
||||
pbs_serialnumber
|
||||
rr_dealerid
|
||||
use_paint_scale_data
|
||||
timezone
|
||||
}
|
||||
@@ -974,6 +975,7 @@ exports.CLAIMSCORP_QUERY = `query CLAIMSCORP_EXPORT($start: timestamptz, $bodysh
|
||||
jc_hourly_rates
|
||||
cdk_dealerid
|
||||
pbs_serialnumber
|
||||
rr_dealerid
|
||||
use_paint_scale_data
|
||||
timezone
|
||||
}
|
||||
@@ -1219,7 +1221,7 @@ query ENTEGRAL_EXPORT($bodyshopid: uuid!) {
|
||||
}`;
|
||||
|
||||
exports.KAIZEN_QUERY = `query KAIZEN_EXPORT($start: timestamptz, $bodyshopid: uuid!, $end: timestamptz) {
|
||||
bodyshops_by_pk(id: $bodyshopid){
|
||||
bodyshops_by_pk(id: $bodyshopid) {
|
||||
id
|
||||
shopname
|
||||
address1
|
||||
@@ -1235,6 +1237,7 @@ exports.KAIZEN_QUERY = `query KAIZEN_EXPORT($start: timestamptz, $bodyshopid: uu
|
||||
jc_hourly_rates
|
||||
cdk_dealerid
|
||||
pbs_serialnumber
|
||||
rr_dealerid
|
||||
use_paint_scale_data
|
||||
timezone
|
||||
}
|
||||
@@ -1246,15 +1249,24 @@ exports.KAIZEN_QUERY = `query KAIZEN_EXPORT($start: timestamptz, $bodyshopid: uu
|
||||
bills {
|
||||
billlines {
|
||||
actual_cost
|
||||
actual_price
|
||||
cost_center
|
||||
deductedfromlbr
|
||||
id
|
||||
line_desc
|
||||
quantity
|
||||
}
|
||||
date
|
||||
federal_tax_rate
|
||||
id
|
||||
is_credit_memo
|
||||
invoice_number
|
||||
isinhouse
|
||||
local_tax_rate
|
||||
state_tax_rate
|
||||
vendor {
|
||||
name
|
||||
}
|
||||
}
|
||||
created_at
|
||||
clm_no
|
||||
@@ -1296,7 +1308,7 @@ exports.KAIZEN_QUERY = `query KAIZEN_EXPORT($start: timestamptz, $bodyshopid: uu
|
||||
joblines(where: {removed: {_eq: false}}) {
|
||||
act_price
|
||||
alt_partno
|
||||
billlines(order_by: {bill: {date: desc_nulls_last}} limit: 1) {
|
||||
billlines(order_by: {bill: {date: desc_nulls_last}}, limit: 1) {
|
||||
actual_cost
|
||||
actual_price
|
||||
quantity
|
||||
@@ -1319,8 +1331,8 @@ exports.KAIZEN_QUERY = `query KAIZEN_EXPORT($start: timestamptz, $bodyshopid: uu
|
||||
mod_lbr_ty
|
||||
oem_partno
|
||||
op_code_desc
|
||||
parts_order_lines(order_by: {parts_order: {order_date: desc_nulls_last}} limit: 1){
|
||||
parts_order{
|
||||
parts_order_lines(order_by: {parts_order: {order_date: desc_nulls_last}}, limit: 1) {
|
||||
parts_order {
|
||||
id
|
||||
order_date
|
||||
}
|
||||
@@ -1339,6 +1351,14 @@ exports.KAIZEN_QUERY = `query KAIZEN_EXPORT($start: timestamptz, $bodyshopid: uu
|
||||
jobid
|
||||
totalliquidcost
|
||||
}
|
||||
notes {
|
||||
created_at
|
||||
created_by
|
||||
critical
|
||||
private
|
||||
text
|
||||
type
|
||||
}
|
||||
ownr_addr1
|
||||
ownr_addr2
|
||||
ownr_city
|
||||
@@ -1737,6 +1757,7 @@ query QUERY_JOB_COSTING_DETAILS($id: uuid!) {
|
||||
jc_hourly_rates
|
||||
cdk_dealerid
|
||||
pbs_serialnumber
|
||||
rr_dealerid
|
||||
use_paint_scale_data
|
||||
}
|
||||
}
|
||||
@@ -1853,6 +1874,7 @@ exports.QUERY_JOB_COSTING_DETAILS_MULTI = ` query QUERY_JOB_COSTING_DETAILS_MULT
|
||||
jc_hourly_rates
|
||||
cdk_dealerid
|
||||
pbs_serialnumber
|
||||
rr_dealerid
|
||||
use_paint_scale_data
|
||||
}
|
||||
}
|
||||
@@ -2173,19 +2195,7 @@ mutation UPDATE_BILLS($billids: [uuid!]!, $bill: bills_set_input!, $logs: [expor
|
||||
}
|
||||
}`;
|
||||
|
||||
exports.INSERT_EXPORT_LOG = `
|
||||
mutation INSERT_EXPORT_LOG($log: exportlog_insert_input!) {
|
||||
insert_exportlog_one(object: $log) {
|
||||
id
|
||||
}
|
||||
}`;
|
||||
|
||||
exports.QUERY_EXISTING_TRANSITION = `
|
||||
mutation INSERT_EXPORT_LOG($log: exportlog_insert_input!) {
|
||||
insert_exportlog_one(object: $log) {
|
||||
id
|
||||
}
|
||||
}`;
|
||||
|
||||
exports.UPDATE_OLD_TRANSITION = `mutation UPDATE_OLD_TRANSITION($jobid: uuid!, $existingTransition: transitions_set_input!){
|
||||
update_transitions(where:{jobid:{_eq:$jobid}, end:{_is_null:true
|
||||
@@ -2204,16 +2214,18 @@ exports.UPDATE_OLD_TRANSITION = `mutation UPDATE_OLD_TRANSITION($jobid: uuid!, $
|
||||
|
||||
exports.INSERT_NEW_TRANSITION = (
|
||||
includeOldTransition
|
||||
) => `mutation INSERT_NEW_TRANSITION($newTransition: transitions_insert_input!, ${includeOldTransition ? `$oldTransitionId: uuid!, $duration: numeric` : ""
|
||||
}) {
|
||||
) => `mutation INSERT_NEW_TRANSITION($newTransition: transitions_insert_input!, ${
|
||||
includeOldTransition ? `$oldTransitionId: uuid!, $duration: numeric` : ""
|
||||
}) {
|
||||
insert_transitions_one(object: $newTransition) {
|
||||
id
|
||||
}
|
||||
${includeOldTransition
|
||||
? `update_transitions(where: {id: {_eq: $oldTransitionId}}, _set: {duration: $duration}) {
|
||||
${
|
||||
includeOldTransition
|
||||
? `update_transitions(where: {id: {_eq: $oldTransitionId}}, _set: {duration: $duration}) {
|
||||
affected_rows
|
||||
}`
|
||||
: ""
|
||||
: ""
|
||||
}
|
||||
}`;
|
||||
|
||||
@@ -2903,6 +2915,8 @@ exports.GET_BODYSHOP_BY_ID = `
|
||||
state
|
||||
notification_followers
|
||||
timezone
|
||||
rr_dealerid
|
||||
rr_configuration
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -3152,11 +3166,10 @@ exports.DELETE_PHONE_NUMBER_OPT_OUT = `
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
exports.INSERT_MEDIA_ANALYTICS = `
|
||||
mutation INSERT_MEDIA_ANALYTICS($mediaObject: media_analytics_insert_input!) {
|
||||
insert_media_analytics_one(object: $mediaObject) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
`;
|
||||
|
||||
@@ -1224,6 +1224,7 @@
|
||||
}
|
||||
],
|
||||
"cdk_dealerid": null,
|
||||
"rr_dealerid": null,
|
||||
"features": {
|
||||
"allAccess": true,
|
||||
"singleDeviceOnly": false
|
||||
|
||||
@@ -601,7 +601,7 @@ function GenerateCostingData(job) {
|
||||
//At the bill level.
|
||||
bill_val.billlines.map((line_val) => {
|
||||
//At the bill line level.
|
||||
if (job.bodyshop.pbs_serialnumber || job.bodyshop.cdk_dealerid) {
|
||||
if (job.bodyshop.pbs_serialnumber || job.bodyshop.cdk_dealerid || job.bodyshop.rr_dealerid) {
|
||||
if (!bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]])
|
||||
bill_acc[selectedDmsAllocationConfig.costs[line_val.cost_center]] = Dinero();
|
||||
|
||||
@@ -716,7 +716,7 @@ function GenerateCostingData(job) {
|
||||
const ticketTotalsByCostCenter = job.timetickets.reduce((ticket_acc, ticket_val) => {
|
||||
//At the invoice level.
|
||||
|
||||
if (job.bodyshop.pbs_serialnumber || job.bodyshop.cdk_dealerid) {
|
||||
if (job.bodyshop.pbs_serialnumber || job.bodyshop.cdk_dealerid || job.bodyshop.rr_dealerid) {
|
||||
if (!ticket_acc[selectedDmsAllocationConfig.costs[ticket_val.ciecacode]])
|
||||
ticket_acc[selectedDmsAllocationConfig.costs[ticket_val.ciecacode]] = Dinero();
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLCl
|
||||
router.use(validateFirebaseIdTokenMiddleware);
|
||||
|
||||
router.post("/getvehicles", withUserGraphQLClientMiddleware, cdkGetMake.default);
|
||||
router.post("/fortellis/getvehicles", withUserGraphQLClientMiddleware, cdkGetMake.fortellis);
|
||||
router.post("/calculate-allocations", withUserGraphQLClientMiddleware, cdkCalculateAllocations.defaultRoute);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
658
server/rr/lib/README.md
Normal file
658
server/rr/lib/README.md
Normal file
@@ -0,0 +1,658 @@
|
||||
# rr-rome-client
|
||||
|
||||
A minimal Node.js wrapper for Reynolds & Reynolds (Rome/RIH) STAR `ProcessMessage` over SOAP, with WS-Security UsernameToken.
|
||||
|
||||
## Contents
|
||||
- Overview
|
||||
- Install
|
||||
- Supported Node Version
|
||||
- Peer / External Dependencies
|
||||
- Quick Start
|
||||
- Configuration & Environment Variables
|
||||
- Client API Methods
|
||||
- RRResult Structure
|
||||
- Types & IntelliSense
|
||||
- Errors & Validation
|
||||
- Retry Strategy
|
||||
- Debug / Dump Flags
|
||||
- Live Test Runner (`scripts/run-live.mjs`)
|
||||
- Bundling & Upload Helper (`scripts/bundle-for-upload.mjs`)
|
||||
- XML Templates & XSDs
|
||||
- Logging
|
||||
- Build & Development
|
||||
- Design Notes / Non-goals
|
||||
- License
|
||||
|
||||
## Overview
|
||||
`rr-rome-client` builds and sends STAR XML payloads (Customer, Service Vehicle, Combined Search, Advisors, Parts, BSM Repair Orders) inside a SOAP envelope using the STAR Transport `ProcessMessage` operation. It applies a WS-Security UsernameToken header and provides:
|
||||
- High-level `RRClient` methods for each supported Rome operation.
|
||||
- JSDoc typedefs for all payloads and response shapes (usable in JS and TS).
|
||||
- Structured parsing of response status blocks and operation-specific data.
|
||||
- Automatic BODId & CreationDateTime generation, with ability to override.
|
||||
- Basic input validation and error classification.
|
||||
- Exponential backoff + jitter for retryable transport/vendor lock scenarios.
|
||||
|
||||
## Install
|
||||
```bash
|
||||
npm i rr-rome-client
|
||||
```
|
||||
(You may also need peer dependencies; see below.)
|
||||
|
||||
## Supported Node Version
|
||||
`package.json` declares `"engines": { "node": ">=22.0.0" }`. The build targets modern Node 22 features (native ESM, improved performance). Earlier Node versions are not officially supported/tested.
|
||||
|
||||
## Peer / External Dependencies
|
||||
Rollup externalizes runtime libraries (they are listed in `dependencies` but not bundled). Ensure these are available in your environment:
|
||||
Required:
|
||||
- axios – HTTP transport
|
||||
- fast-xml-parser – STAR XML parsing
|
||||
- mustache – XML templating
|
||||
- uuid – BODId generation
|
||||
Optional (only if you use env loader helpers or live scripts):
|
||||
- dotenv
|
||||
- dotenv-expand
|
||||
|
||||
Install (versions per `package.json`):
|
||||
```bash
|
||||
npm install axios@^1.7.7 fast-xml-parser@^4.5.0 mustache@^4.2.0 uuid@^9.0.1
|
||||
# Optional env loader
|
||||
npm install dotenv@^17.2.3 dotenv-expand@^12.0.3
|
||||
```
|
||||
TypeScript users (optional):
|
||||
```bash
|
||||
npm install -D @types/node
|
||||
```
|
||||
Browser bundling is not officially supported; you would need polyfills for core modules if attempting.
|
||||
|
||||
## Quick Start
|
||||
```js
|
||||
import { RRClient } from 'rr-rome-client';
|
||||
import { loadEnv } from 'rr-rome-client/src/util/config.js'; // optional helper
|
||||
|
||||
// Load routing & credentials from environment variables (see below)
|
||||
const { baseUrl, username, password, routing } = loadEnv();
|
||||
|
||||
const client = new RRClient({ baseUrl, username, password });
|
||||
|
||||
// Minimal createRepairOrder example
|
||||
const result = await client.createRepairOrder({
|
||||
customerNo: '12345',
|
||||
departmentType: 'S', // DeptType
|
||||
vin: '1ABCDEF2GHIJ34567',
|
||||
outsdRoNo: 'EXT-RO-99'
|
||||
}, { routing });
|
||||
|
||||
if (result.success) {
|
||||
console.log('RO status:', result.data); // {status, date, time, outsdRoNo, dmsRoNo, errorMessage}
|
||||
} else {
|
||||
console.error('Failure:', result.statusBlocks?.transaction);
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration & Environment Variables
|
||||
You can manually provide configuration or use the helper `loadEnv(env)` in `src/util/config.js`.
|
||||
|
||||
Recognized environment variables:
|
||||
- RR_BASE_URL – SOAP endpoint URL (required)
|
||||
- RR_USERNAME – WS-Security UsernameToken username (required)
|
||||
- RR_PASSWORD – WS-Security UsernameToken password (required)
|
||||
- RR_DEALER_NUMBER – DealerNumber for Destination (required per call)
|
||||
- RR_STORE_NUMBER – StoreNumber (optional)
|
||||
- RR_AREANUMBER – AreaNumber (optional)
|
||||
|
||||
Example `.env`:
|
||||
```
|
||||
RR_BASE_URL=https://rome.example.com/soap
|
||||
RR_USERNAME=integratorUser
|
||||
RR_PASSWORD=superSecret
|
||||
RR_DEALER_NUMBER=1234
|
||||
RR_STORE_NUMBER=01
|
||||
RR_AREANUMBER=1
|
||||
```
|
||||
`loadEnv()` returns `{ baseUrl, username, password, routing: { dealerNumber, storeNumber, areaNumber } }`.
|
||||
|
||||
Per-call options object shape (`CallOptions`):
|
||||
```js
|
||||
{
|
||||
routing: { dealerNumber: '1234', storeNumber?: '01', areaNumber?: '1' },
|
||||
envelope?: { bodId?, creationDateTime?, sender?: { component?, task?, referenceId? } }
|
||||
}
|
||||
```
|
||||
If omitted, `RRClient` auto-generates `bodId` (UUID) and `creationDateTime` when sending.
|
||||
|
||||
## Client API Methods
|
||||
Each method returns a `Promise<RRResult<T>>` where `T` is operation-specific data (or array). `success` is true for vendor SUCCESS and NO_MATCH results; FAIL triggers an `RRVendorStatusError` exception before a `RRResult` is returned.
|
||||
|
||||
All methods require `opts.routing.dealerNumber`.
|
||||
|
||||
### combinedSearch(payload: CombinedSearchQuery, opts)
|
||||
Search customer + service vehicle combinations by exactly one criterion: `phone | license | vin | name | nameRecId | stkNo` plus optional `make`, `model`, `year`, `maxResults` (capped at 50).
|
||||
Minimal:
|
||||
```js
|
||||
const res = await client.combinedSearch({ kind: 'vin', vin: '12345' }, { routing });
|
||||
res.data; // Array<CombinedSearchBlock>
|
||||
```
|
||||
Errors: Throws `RRValidationError` if missing or multiple criteria.
|
||||
|
||||
### insertCustomer(payload: InsertCustomerPayload, opts)
|
||||
Insert a customer record. Required: `lastName` (or `customerName`). If individual (`ibFlag='I'` or inferred by presence of `firstName`), then `firstName` required.
|
||||
```js
|
||||
const res = await client.insertCustomer({ firstName: 'Jane', lastName: 'Doe', phones:[{number:'5551234567'}] }, { routing });
|
||||
res.data; // { dmsRecKey, status, statusCode }
|
||||
```
|
||||
|
||||
### updateCustomer(payload: UpdateCustomerPayload, opts)
|
||||
Update existing customer by `nameRecId` plus required `ibFlag`. Other fields optional.
|
||||
```js
|
||||
const res = await client.updateCustomer({ nameRecId:'998877', ibFlag:'I', lastName:'Doe' }, { routing });
|
||||
```
|
||||
|
||||
### insertServiceVehicle(payload: InsertServiceVehiclePayload, opts)
|
||||
Insert a service vehicle linked to a customer. Required: `vin`, `vehicleServInfo.customerNo`.
|
||||
```js
|
||||
const res = await client.insertServiceVehicle({
|
||||
vin:'1HGCM82633A004352',
|
||||
vehicleServInfo:{ customerNo:'12345' }
|
||||
}, { routing });
|
||||
res.data; // { status, statusCode }
|
||||
```
|
||||
|
||||
### getAdvisors(payload: GetAdvisorsParams, opts)
|
||||
Fetch advisors for a department. Department values normalized: S/P/B or long names.
|
||||
```js
|
||||
const res = await client.getAdvisors({ department:'SERVICE' }, { routing });
|
||||
res.data; // AdvisorRow[]
|
||||
```
|
||||
|
||||
### createRepairOrder(payload: CreateRepairOrderPayload, opts)
|
||||
Required: `customerNo`, `departmentType`, `vin`, `outsdRoNo`. Advisor optional. Complex nested labor/parts/misc blocks supported via payload.
|
||||
```js
|
||||
const res = await client.createRepairOrder({
|
||||
customerNo:'12345', departmentType:'S', vin:'1ABCDEF2GHIJ34567', outsdRoNo:'EXT-RO-99'
|
||||
}, { routing });
|
||||
res.data; // { status, date, time, outsdRoNo, dmsRoNo, errorMessage }
|
||||
```
|
||||
|
||||
### updateRepairOrder(payload: UpdateRepairOrderPayload, opts)
|
||||
Required: `finalUpdate ('Y'|'N')`, `outsdRoNo`. May include `roNo` and nested sections.
|
||||
```js
|
||||
const res = await client.updateRepairOrder({ finalUpdate:'N', outsdRoNo:'EXT-RO-99' }, { routing });
|
||||
```
|
||||
|
||||
### getParts(payload: GetPartsParams, opts)
|
||||
Required: `roNumber` (internal ERA RO number).
|
||||
```js
|
||||
const res = await client.getParts({ roNumber:'938275' }, { routing });
|
||||
res.data; // PartRow[]
|
||||
```
|
||||
|
||||
## Payload Schema Reference
|
||||
Comprehensive field-level summary sourced from `src/types.js`, operation builders, and validation logic. Types reflect accepted JS types (string|number where applicable). Constraints list enumerations, inference rules, and validation notes. Required = must be supplied by caller (or inferred automatically). Optional fields omitted become absent in generated XML.
|
||||
|
||||
### CombinedSearchQuery
|
||||
Only one criterion permitted; `maxResults` capped at 50.
|
||||
|
||||
| Field | Type | Required | Constraints / Notes |
|
||||
|-------|------|----------|----------------------|
|
||||
| kind | 'phone'|'license'|'vin'|'name'|'nameRecId'|'stkNo' | Yes | Determines which single criterion block is emitted |
|
||||
| phone | string\|number\|{phone:string} | Conditionally (if kind==='phone') | Value mapped to `<Phone Num="..."/>` |
|
||||
| license | string\|number\|{license:string} | Conditionally (kind==='license') | Value mapped to `<LicenseNum LicNo="..."/>` |
|
||||
| vin | string\|number\|{vin:string} | Conditionally (kind==='vin') | Value mapped to `<PartVIN Vin="..."/>` (partial VIN allowed) |
|
||||
| name | {fname,lname,mname} or {name} | Conditionally (kind==='name') | Either FullName triple or LName only; must supply all three for FullName |
|
||||
| nameRecId | string\|number\|{custId:string}|{nameRecId:string} | Conditionally (kind==='nameRecId') | Emits `<NameRecId CustIdStart="..."/>` |
|
||||
| stkNo | string\|number\|{stkNo:string} | Conditionally (kind==='stkNo') | Emits `<StkNo VehId="..."/>` |
|
||||
| maxResults | number | No | Capped at 50 (default 50) -> `MaxRecs` attribute |
|
||||
| make | string | No | Defaults 'ANY' -> `VehData MakePfx` |
|
||||
| model | string\|number | No | Defaults 'ANY' -> `VehData Model` |
|
||||
| year | string\|number | No | Defaults 'ANY' -> `VehData Year` |
|
||||
|
||||
### InsertCustomerPayload
|
||||
`ibFlag` inferred as 'I' if `firstName` present, else 'B'. Business requires `lastName` / `customerName`; individual requires `firstName` + last name.
|
||||
|
||||
| Field | Type | Required | Constraints / Notes |
|
||||
|-------|------|----------|----------------------|
|
||||
| ibFlag | 'I'|'B' | Auto / For update must supply | Inferred if omitted (firstName present => 'I') |
|
||||
| customerType | 'R'|'W'|'I'|'Retail'|'Wholesale'|'Internal' | No | Normalized to 'R','W','I'; must be one of listed |
|
||||
| createdBy | string | No | Optional CreatedBy attribute |
|
||||
| customerName | string | Conditional | Alias for `lastName` when business |
|
||||
| lastName | string | Yes (unless `customerName` provided) | Required base name; sanitized to A-Z0-9 space |
|
||||
| firstName | string | Required when ibFlag='I' | Sanitized; required for individuals |
|
||||
| midName | string | No | Sanitized |
|
||||
| salut | string | No | Sanitized |
|
||||
| suffix | string | No | Sanitized |
|
||||
| addresses | CustomerAddress[] | No | Each entry requires `line1`; Type defaults 'P' |
|
||||
| phones | CustomerPhone[] | No | Each entry requires `number`; Type defaults 'H' |
|
||||
| emails | CustomerEmail[] | No | First entry used -> `<Email MailTo="..."/>` |
|
||||
| personal.gender | 'M'|'F'|'U' | No | Optional |
|
||||
| personal.otherName | string | No | Sanitized alnum/space |
|
||||
| personal.anniversaryDate | string | No | Included if non-empty |
|
||||
| personal.employerName | string | No | Sanitized |
|
||||
| personal.employerPhone | string | No | Raw string |
|
||||
| personal.occupation | string | No | Sanitized |
|
||||
| personal.optOut | string | No | Pass-through |
|
||||
| personal.optOutUse | string | No | Pass-through |
|
||||
| personal.birthDates[].type | 'P'|'S' | No | Defaults 'P'; entry must have `date` |
|
||||
| personal.birthDates[].date | string | Conditional | Included only if non-empty |
|
||||
| personal.ssns[].type | 'P'|'S' | No | Defaults 'P'; entry must have `ssn` |
|
||||
| personal.ssns[].ssn | string | Conditional | Included only if non-empty |
|
||||
| personal.driver.type | 'P'|'S' | No | Defaults 'P' |
|
||||
| personal.driver.licenseNumber | string | Conditional | Required to emit DriverInfo |
|
||||
| personal.driver.licenseState | string | No | Optional |
|
||||
| personal.driver.licenseExpDate | string | No | Optional |
|
||||
| personal.children[].name | string | No | Sanitized; optional list |
|
||||
| dms.taxExemptNum | string | No | Optional |
|
||||
| dms.salesTerritory | string | No | Optional |
|
||||
| dms.deliveryRoute | string | No | Optional |
|
||||
| dms.salesmanNum | string | No | Optional |
|
||||
| dms.lastContactMethod | string | No | Optional |
|
||||
| dms.followups[].type | string | Conditional | Must have both type & value |
|
||||
| dms.followups[].value | string | Conditional | Must have both type & value |
|
||||
|
||||
### UpdateCustomerPayload
|
||||
Extends InsertCustomerPayload plus:
|
||||
|
||||
| Field | Type | Required | Constraints |
|
||||
|-------|------|----------|-------------|
|
||||
| nameRecId | string\|number | Yes | Required identifier |
|
||||
| ibFlag | 'I'|'B' | Yes | Must be explicitly provided on update |
|
||||
|
||||
### InsertServiceVehiclePayload
|
||||
|
||||
| Field | Type | Required | Constraints / Notes |
|
||||
|-------|------|----------|----------------------|
|
||||
| vin | string | Yes | Must be provided |
|
||||
| modelDesc | string | No | Optional attribute |
|
||||
| carline | string | No | Optional attribute |
|
||||
| extClrDesc | string | No | Optional attribute |
|
||||
| intClrDesc | string | No | Optional attribute |
|
||||
| trimDesc | string | No | Optional attribute |
|
||||
| bodyStyle | string | No | Optional attribute |
|
||||
| engineDesc | string | No | Optional attribute |
|
||||
| transDesc | string | No | Optional attribute |
|
||||
| year | string\|number | No | Emits `<Year>` element if present |
|
||||
| odometer | string\|number | No | Emits `<Odometer>` |
|
||||
| odometerUnits | string | No | Emits `<OdometerUnits>` |
|
||||
| vehicleDetail.licNo | string | No | Emits `<VehicleDetail LicNo="..."/>` |
|
||||
| vehicleServInfo.customerNo | string\|number | Yes | Required; becomes `CustomerNo` attribute |
|
||||
| vehicleServInfo.salesmanNo | string\|number | No | Optional element |
|
||||
| vehicleServInfo.inServiceDate | string\|number | No | Optional element |
|
||||
| vehicleServInfo.mileage | string\|number | No | Optional element |
|
||||
| vehicleServInfo.teamCode | string | No | Optional element |
|
||||
| vehicleServInfo.vehExtWarranty.contractNumber | string | Conditional | Included if any warranty field present |
|
||||
| vehicleServInfo.vehExtWarranty.expirationDate | string | Conditional | "" excluded |
|
||||
| vehicleServInfo.vehExtWarranty.expirationMileage | string\|number | Conditional | "" excluded |
|
||||
| vehicleServInfo.advisor.contactInfo.nameRecId | string\|number | No | Advisor block included only if provided |
|
||||
|
||||
### CreateRepairOrderPayload
|
||||
|
||||
| Field | Type | Required | Constraints / Notes |
|
||||
|-------|------|----------|----------------------|
|
||||
| customerNo | string\|number | Yes | CustNo |
|
||||
| departmentType | string\|number | Yes | DeptType |
|
||||
| vin | string | Yes | Vin |
|
||||
| outsdRoNo | string\|number | Yes | External RO identifier |
|
||||
| advisorNo | string\|number | No | AdvNo |
|
||||
| tagNo | string\|number | No | TagNo |
|
||||
| mileageIn | string\|number | No | MileageIn |
|
||||
| roComment | string | No | `<RoCommentInfo RoComment="..."/>` |
|
||||
| estimate.parts | string\|number | No | EstPartsAmt |
|
||||
| estimate.labor | string\|number | No | EstLaborAmt |
|
||||
| estimate.total | string\|number | No | EstTotalAmt |
|
||||
| tax.payType | 'All'|'Cust'|'Intr'|'Warr' | No | Enumeration validated |
|
||||
| tax.taxCode | string | No | TaxCode |
|
||||
| tax.txblGrossAmt | string\|number | No | TxblGrossAmt |
|
||||
| tax.grossTaxAmt | string\|number | No | GrossTaxAmt |
|
||||
| rolabor.ops[].opCode | string | No | Optional |
|
||||
| rolabor.ops[].jobNo | string\|number | No | Optional |
|
||||
| rolabor.ops[].custPayTypeFlag | string | No | Freeform (not validated) |
|
||||
| rolabor.ops[].warrPayTypeFlag | string | No | Freeform |
|
||||
| rolabor.ops[].intrPayTypeFlag | string | No | Freeform |
|
||||
| rolabor.ops[].custTxblNtxblFlag | 'T'|'N' | No | Enumerated & validated |
|
||||
| rolabor.ops[].warrTxblNtxblFlag | 'T'|'N' | No | Enumerated & validated |
|
||||
| rolabor.ops[].intrTxblNtxblFlag | 'T'|'N' | No | Enumerated & validated |
|
||||
| rolabor.ops[].vlrCode | string | No | Optional |
|
||||
| rolabor.ops[].bill.payType | 'All'|'Cust'|'Intr'|'Warr' | No | Enumeration |
|
||||
| rolabor.ops[].bill.jobTotalHrs | string\|number | No | Optional |
|
||||
| rolabor.ops[].bill.billTime | string\|number | No | Optional |
|
||||
| rolabor.ops[].bill.billRate | string\|number | No | Optional |
|
||||
| rolabor.ops[].ccc.cause | string | No | Optional |
|
||||
| rolabor.ops[].ccc.complaint | string | No | Optional |
|
||||
| rolabor.ops[].ccc.correction | string | No | Optional |
|
||||
| rolabor.ops[].amount.payType | 'All'|'Cust'|'Intr'|'Warr' | No | Enumeration |
|
||||
| rolabor.ops[].amount.amtType | string | No | Optional |
|
||||
| rolabor.ops[].amount.custPrice | string\|number | No | Optional |
|
||||
| rolabor.ops[].amount.totalAmt | string\|number | No | Optional |
|
||||
| ropart.jobs[].opCode | string | No | Optional |
|
||||
| ropart.jobs[].jobNo | string\|number | No | Optional |
|
||||
| ropart.jobs[].lines[].partNo | string | No | Optional |
|
||||
| ropart.jobs[].lines[].partNoDesc | string | No | Optional |
|
||||
| ropart.jobs[].lines[].partQty | string\|number | No | Emits QtyOrd |
|
||||
| ropart.jobs[].lines[].sale | string\|number | No | Sale |
|
||||
| ropart.jobs[].lines[].cost | string\|number | No | Cost |
|
||||
| ropart.jobs[].lines[].addDeleteFlag | string | No | AddDeleteFlag |
|
||||
| rogg.ops[].lines[].breakOut | string | No | Optional |
|
||||
| rogg.ops[].lines[].itemType | 'G'|'P'|'S'|'F' | No | Enumerated & validated |
|
||||
| rogg.ops[].lines[].itemDesc | string | No | Optional |
|
||||
| rogg.ops[].lines[].custQty | string\|number | No | Optional |
|
||||
| rogg.ops[].lines[].warrQty | string\|number | No | Optional |
|
||||
| rogg.ops[].lines[].intrQty | string\|number | No | Optional |
|
||||
| rogg.ops[].lines[].custPayTypeFlag | string | No | Optional |
|
||||
| rogg.ops[].lines[].warrPayTypeFlag | string | No | Optional |
|
||||
| rogg.ops[].lines[].intrPayTypeFlag | string | No | Optional |
|
||||
| rogg.ops[].lines[].custTxblNtxblFlag | 'T'|'N' | No | Enumerated |
|
||||
| rogg.ops[].lines[].warrTxblNtxblFlag | 'T'|'N' | No | Enumerated |
|
||||
| rogg.ops[].lines[].intrTxblNtxblFlag | 'T'|'N' | No | Enumerated |
|
||||
| rogg.ops[].lines[].amount.payType | 'All'|'Cust'|'Intr'|'Warr' | No | Enumeration |
|
||||
| rogg.ops[].lines[].amount.amtType | string | No | Optional |
|
||||
| rogg.ops[].lines[].amount.custPrice | string\|number | No | Optional |
|
||||
| rogg.ops[].lines[].amount.dlrCost | string\|number | No | Optional |
|
||||
| romisc.ops[].lines[].miscCode | string | No | Optional |
|
||||
| romisc.ops[].lines[].custPayTypeFlag | string | No | Optional |
|
||||
| romisc.ops[].lines[].warrPayTypeFlag | string | No | Optional |
|
||||
| romisc.ops[].lines[].intrPayTypeFlag | string | No | Optional |
|
||||
| romisc.ops[].lines[].custTxblNtxblFlag | 'T'|'N' | No | Enumerated |
|
||||
| romisc.ops[].lines[].warrTxblNtxblFlag | 'T'|'N' | No | Enumerated |
|
||||
| romisc.ops[].lines[].intrTxblNtxblFlag | 'T'|'N' | No | Enumerated |
|
||||
| romisc.ops[].lines[].codeAmt | string\|number | No | Optional |
|
||||
|
||||
### UpdateRepairOrderPayload
|
||||
Adds required `finalUpdate` and supports most Create fields + mileageOut.
|
||||
|
||||
| Field | Type | Required | Constraints / Notes |
|
||||
|-------|------|----------|----------------------|
|
||||
| finalUpdate | 'Y'|'N' | Yes | Must be 'Y' or 'N' (validated) |
|
||||
| outsdRoNo | string\|number | Yes | External RO identifier (validation requires present) |
|
||||
| roNo | string\|number | No | Optional internal RoNo |
|
||||
| mileageOut | string\|number | No | MileageOut |
|
||||
| (other fields) | see CreateRepairOrderPayload | Conditional | Same validations apply |
|
||||
|
||||
### GetAdvisorsParams
|
||||
|
||||
| Field | Type | Required | Constraints / Notes |
|
||||
|-------|------|----------|----------------------|
|
||||
| department | 'S'|'P'|'B'|'SERVICE'|'PARTS'|'BODY'|'BODYSHOP'|'BODY SHOP' | Yes | Normalized to S/P/B |
|
||||
| advisorNumber | string\|number | No | Optional filter |
|
||||
| maxResults | number | Ignored | Not in XSD (builder ignores) |
|
||||
|
||||
### GetPartsParams
|
||||
|
||||
| Field | Type | Required | Constraints / Notes |
|
||||
|-------|------|----------|----------------------|
|
||||
| roNumber | string\|number | Yes | Required; becomes `<RoInfo RoNumber="..."/>` |
|
||||
|
||||
### Common Routing & Envelope Fields
|
||||
|
||||
| Field | Type | Required | Constraints / Notes |
|
||||
|-------|------|----------|----------------------|
|
||||
| routing.dealerNumber | string | Yes | Required for all requests (Destination) |
|
||||
| routing.storeNumber | string | No | Optional Destination StoreNumber |
|
||||
| routing.areaNumber | string | No | Optional Destination AreaNumber |
|
||||
| envelope.bodId | string | Auto | UUID generated if omitted |
|
||||
| envelope.creationDateTime | Date\|string | Auto | Uses current time; formatted without milliseconds |
|
||||
| envelope.sender.component | string | No | Defaults 'Rome' if omitted |
|
||||
| envelope.sender.task | string | No | Op-specific defaults (e.g. 'CU','SV','BSMRO','CVC','RCT') |
|
||||
| envelope.sender.referenceId | string | No | Op-specific defaults ('Insert','Update','Query') |
|
||||
|
||||
### AdvisorRow (response convenience)
|
||||
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| advisorId | string\|number\|undefined | AdvisorNumber attribute |
|
||||
| firstName | string\|undefined | FirstName attribute |
|
||||
| lastName | string\|undefined | LastName attribute |
|
||||
| department | 'S'|'P'|'B'|undefined | Normalized department passed through |
|
||||
|
||||
### PartRow (response convenience)
|
||||
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| partNumber | string\|undefined | PartNumber |
|
||||
| partDescription | string\|undefined | PartDescription |
|
||||
| quantityOrdered | string\|number\|undefined | QuantityOrdered |
|
||||
| quantityShipped | string\|number\|undefined | QuantityShipped |
|
||||
| price | string\|number\|undefined | Price |
|
||||
| cost | string\|number\|undefined | Cost |
|
||||
| processedFlag | string\|undefined | ProcessedFlag |
|
||||
| addOrDelete | string\|undefined | AddOrDelete |
|
||||
|
||||
### CombinedSearch Response Shapes
|
||||
Hierarchical breakdown of the parsed array returned under `RRResult.data` for `combinedSearch` (each entry is a `CombinedSearchBlock`). Attribute-centric; absent fields are simply omitted.
|
||||
|
||||
#### CombinedSearchBlock
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| NameContactId | CombinedSearchNameContactId | Customer name/contact composite |
|
||||
| ServVehicle | CombinedSearchServVehicle[] | Zero or more service vehicle blocks |
|
||||
| Message | CombinedSearchMessage[] | Optional informational messages |
|
||||
|
||||
#### NameContactId
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| NameId | CombinedSearchNameId | Includes identifiers & individual/business name choice |
|
||||
| Address | Object[] | Each has address-related attributes (lines, city, state, zip, etc.) |
|
||||
| ContactOptions | Object[] | Arbitrary contact option attributes as present |
|
||||
| Phone | Object[] | Each phone entry carries attributes like Type, Num, Ext |
|
||||
| Email | Object[] | Each email entry carries attributes (MailTo, etc.) |
|
||||
|
||||
#### NameId
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| NameRecId | string\|number | Identifier used for subsequent operations |
|
||||
| IBFlag | 'I'|'B' | Individual or Business flag |
|
||||
| IndName | Object | Present for individual: attributes like FName, LName, MName |
|
||||
| BusName | Object | Present for business: attributes like Name (business name) |
|
||||
|
||||
#### ServVehicle (CombinedSearchServVehicle)
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| Vehicle | CombinedSearchVehicle | VIN + descriptive attributes |
|
||||
| VehicleServInfo | CombinedSearchVehicleServInfo | Service info attributes & nested warranty/advisor/comments |
|
||||
|
||||
#### Vehicle (CombinedSearchVehicle)
|
||||
| Attribute | Type | Notes |
|
||||
|-----------|------|-------|
|
||||
| Vin | string | Vehicle identification number (may be partial in search results) |
|
||||
| VehicleMake | string | Make code / description |
|
||||
| VehicleYr | string\|number | Year |
|
||||
| MdlNo | string | Model code |
|
||||
| ModelDesc | string | Model description |
|
||||
| Carline | string | Carline description |
|
||||
| ExtClrDesc | string | Exterior color |
|
||||
| IntClrDesc | string | Interior color |
|
||||
| MakeName | string | Full make name |
|
||||
| VehicleDetail.LicNo | string | License plate (if present) |
|
||||
|
||||
#### VehicleServInfo (CombinedSearchVehicleServInfo)
|
||||
| Attribute | Type | Notes |
|
||||
|-----------|------|-------|
|
||||
| CustomerNo | string\|number | Linked customer number |
|
||||
| SalesmanNo | string\|number | Optional salesman number |
|
||||
| InServiceDate | string\|number | Date vehicle placed in service |
|
||||
| Mileage | string\|number | Current mileage |
|
||||
| TeamCode | string | Team/department code |
|
||||
| VehExtWarranty.ContractNumber | string | Extended warranty contract number |
|
||||
| VehExtWarranty.ExpirationDate | string | Warranty expiration date |
|
||||
| VehExtWarranty.ExpirationMileage | string\|number | Warranty mileage limit |
|
||||
| Advisor.ContactInfo.NameRecId | string\|number | Advisor reference (if present) |
|
||||
| VehServComments[] | string[] | Freeform service comments (array of raw text) |
|
||||
|
||||
#### Message (CombinedSearchMessage)
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| MessageNo | string\|number | Optional message number (if provided) |
|
||||
| Text | string | Message text content |
|
||||
|
||||
### CustomerResponseData
|
||||
Returned as `result.data` for `insertCustomer` / `updateCustomer`.
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| dmsRecKey | string\|undefined | DMS record key identifier (if provided in TransStatus) |
|
||||
| status | string\|undefined | Vendor status string |
|
||||
| statusCode | string\|undefined | Vendor status code |
|
||||
|
||||
### ServiceVehicleResponseData
|
||||
Returned as `result.data` for `insertServiceVehicle`.
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| status | string\|undefined | GenTransStatus Status |
|
||||
| statusCode | string\|undefined | GenTransStatus StatusCode |
|
||||
|
||||
### RepairOrderData
|
||||
Returned as `result.data` for `createRepairOrder` / `updateRepairOrder` (parsed from RoRecordStatus).
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| status | string\|undefined | RoRecordStatus Status |
|
||||
| date | string\|undefined | Date attribute/text |
|
||||
| time | string\|undefined | Time attribute/text |
|
||||
| outsdRoNo | string\|undefined | External RO number (OutsdRoNo) |
|
||||
| dmsRoNo | string\|undefined | Internal DMS RO number (DMSRoNo) |
|
||||
| errorMessage | string\|undefined | ErrorMessage if provided |
|
||||
|
||||
## RRResult Structure
|
||||
Each successful call resolves to:
|
||||
```ts
|
||||
interface RRResult<T> {
|
||||
success: boolean; // SUCCESS or NO_MATCH
|
||||
data?: T; // op-specific parsed convenience data
|
||||
parsed: any; // entire parsed STAR payload root
|
||||
xml: { request: string; response: string }; // raw SOAP envelopes
|
||||
statusBlocks?: { transaction?: {status,statusCode,message}; roRecord?: {status,date,time,outsdRoNo,dmsRoNo,errorMessage} };
|
||||
applicationArea?: any; // raw ApplicationArea node
|
||||
}
|
||||
```
|
||||
Use `statusBlocks.transaction` for generic status; `data` for normalized op output.
|
||||
|
||||
`Convenience data` refers to the distilled, operation-specific subset placed on `RRResult.data` by a dedicated `postParse` function (e.g., extracting only RoRecordStatus identifiers or customer DMS keys). It is intentionally smaller and flatter than `RRResult.parsed`, which contains the entire parsed STAR payload tree. Use `data` for common identifiers/status checks; fall back to `parsed` when you need full raw XML-derived detail.
|
||||
|
||||
## Types & IntelliSense
|
||||
Rich JSDoc typedefs ship with the package.
|
||||
- ESM: `import { RRClient } from 'rr-rome-client';` optionally import `'rr-rome-client/types'` to prompt editor indexing.
|
||||
- CJS: `const { RRClient } = require('rr-rome-client'); require('rr-rome-client/types');`
|
||||
|
||||
Selected typedef categories (see `src/types.js`):
|
||||
- Routing / Envelope / CallOptions
|
||||
- Customer / Service Vehicle / Repair Order payload blocks
|
||||
- Combined Search structures
|
||||
- Advisor and Parts row shapes
|
||||
- `RRResult<T>` generic helper
|
||||
|
||||
The build also emits a TypeScript declaration bundle (`dist/types/index.d.ts`) generated via `tsconfig.types.json` (processing only `src/types.js`).
|
||||
|
||||
## Errors & Validation
|
||||
Three custom error classes (`src/errors.js`):
|
||||
- `RRTransportError` – Non-2xx HTTP status or network failure; `meta.status` / `meta.body` may be attached.
|
||||
- `RRVendorStatusError` – Vendor FAIL status (non-success & not NO_MATCH). Includes `meta.status` (raw status object) and full response XML. `retryable` may be set (currently determined by vendor message lock wording or explicit flag).
|
||||
- `RRValidationError` – Input validation failures (missing required fields, invalid enumeration values, etc.).
|
||||
|
||||
Enumerated validations (examples):
|
||||
- Customer: `ibFlag`, required `firstName` if individual, allowed `customerType` values.
|
||||
- Service Vehicle: mandatory `vin` and `vehicleServInfo.customerNo`.
|
||||
- Repair Orders: required header fields; enumerations for tax pay type (`All|Cust|Intr|Warr`), taxable flags (`T|N`), item types (`G|P|S|F`).
|
||||
|
||||
Error Handling Example:
|
||||
```js
|
||||
try {
|
||||
await client.createRepairOrder(/* ... */);
|
||||
} catch (e) {
|
||||
if (e instanceof RRVendorStatusError) {
|
||||
console.error('Vendor fail', e.meta.status);
|
||||
} else if (e instanceof RRValidationError) {
|
||||
console.error('Bad input', e.message);
|
||||
} else if (e instanceof RRTransportError) {
|
||||
console.error('HTTP/Network', e.message, e.meta.status);
|
||||
} else {
|
||||
console.error('Unexpected', e);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Retry Strategy
|
||||
`withBackoff(fn, {max, logger})` (used internally) retries on:
|
||||
- Transport/network errors (`RRTransportError`).
|
||||
- Vendor status errors indicating record lock/in use (message matching `/lock|in use|record.*busy/i`) or explicit `retryable` flag.
|
||||
Backoff: exponential starting at 400ms, capped at 10s, plus up to 250ms jitter.
|
||||
Configure via `new RRClient({ retries: { max: 5 }, ... })`.
|
||||
|
||||
## Debug / Dump Flags
|
||||
Set these environment variables to inspect request/response internals:
|
||||
- `RR_DEBUG` – Enables debug logging (`defaultLogger.debug`).
|
||||
- `RR_DUMP_ENVELOPE=1` – Prints outgoing SOAP envelope.
|
||||
- `RR_DUMP_XML=1` – Prints full response XML.
|
||||
- `RR_DUMP_STATUS=1` – Logs parsed status blocks.
|
||||
- `RR_DUMP_APPLICATION=1` – Logs raw ApplicationArea block.
|
||||
- `RR_DUMP_DATA=1` – Logs `result.data` (postParse convenience output).
|
||||
|
||||
## Live Test Runner (`scripts/run-live.mjs`)
|
||||
Provides curated integration tests against a live Rome system using Vitest. Loads `.env` then `.env.local` (override). Usage:
|
||||
```
|
||||
node scripts/run-live.mjs <test> [flags] [-- ...vitestArgs]
|
||||
```
|
||||
Available tests (see script for full list & flags):
|
||||
- `combinedSearch` – VIN or phone search.
|
||||
- `insertCustomer`, `updateCustomer` – Customer record operations (use `--write` to enable live writes).
|
||||
- `insertServiceVehicle` – Add vehicle (requires or discovers customer).
|
||||
- `getAdvisors` – Advisor listing by department.
|
||||
- `createRepairOrder`, `updateRepairOrder` – BSM RO operations.
|
||||
- `getParts` – Retrieve RO parts lines.
|
||||
- `all` – Run all sequentially.
|
||||
|
||||
Common flags:
|
||||
- `--dump` → `RR_DUMP_ENVELOPE`
|
||||
- `--write` → `RR_LIVE_WRITES` (enables writes)
|
||||
- Operation-specific flags map to `RR_TEST_*` env variables (see script comments for details).
|
||||
- Arbitrary env: `--set=KEY=VALUE`.
|
||||
|
||||
Example:
|
||||
```
|
||||
node scripts/run-live.mjs createRepairOrder --write --dump --customerNo=1134485 --vin=1ABCDEF2GHIJ34567 --ro=BSM123
|
||||
```
|
||||
|
||||
## Bundling & Upload Helper (`scripts/bundle-for-upload.mjs`)
|
||||
Creates text bundles of project files for transport/support purposes.
|
||||
- Includes key directories (`src/`, `test/`, `schemas/`) and specific root files.
|
||||
- Excludes large/irrelevant directories (`node_modules`, `dist`, etc.).
|
||||
- Options: `--max-bytes`, `--pattern=globish`, `--with-env` (include `.env` – caution), `--list-only`.
|
||||
Generates `bundles/bundle-<uuid>-N.txt` with file boundary markers.
|
||||
|
||||
## XML Templates & XSDs
|
||||
Templates reside in `src/templates/templateMap.js` using Mustache. Each operation builder renders a STAR root element plus `ApplicationArea`. XSD files (under `schemas/`) accompany operations:
|
||||
- Customer Insert/Update: `rey_RomeCustomerInsertReq.xsd`, `rey_RomeCustomerUpdateReq.xsd`
|
||||
- Service Vehicle Insert: `rey_RomeServVehicleInsertReq.xsd`
|
||||
- Combined Search: `rey_RomeCustServVehCombReq.xsd`
|
||||
- Advisors: `rey_RomeGetAdvisorsReq.xsd`
|
||||
- Parts: `rey_RomeGetPartsReq.xsd`
|
||||
- Repair Orders Create/Update: `rey_RomeCreateBSMRepairOrderReq.xsd`, `rey_RomeUpdateBSMRepairOrderReq.xsd`
|
||||
Response XSDs also present for repair orders and others.
|
||||
|
||||
Builders attach an `xsdFilename` hint and `elementName`; no runtime XSD validation is performed (the project does not contain a validation module beyond these references).
|
||||
|
||||
## Logging
|
||||
Default logger (`src/logger.js`): logs to console via `info`, `warn`, `error`; `debug` gated by `RR_DEBUG`.
|
||||
Provide a custom logger with matching method names in `RRClientConfig`:
|
||||
```js
|
||||
const logger = { info:()=>{}, warn:()=>{}, error:console.error, debug:()=>{} };
|
||||
const client = new RRClient({ baseUrl, username, password, logger });
|
||||
```
|
||||
|
||||
## Build & Development
|
||||
Scripts:
|
||||
- `npm run build` – Rollup builds `dist/index.cjs` & `dist/index.mjs`, copies JSDoc types (`scripts/postbuild-copy-types.mjs`), emits `.d.ts` (via `tsc -p tsconfig.types.json`).
|
||||
- `npm test` – Runs unit tests (`vitest` with `vitest.config.unit.mjs`).
|
||||
- `npm run bundle` – Invoke bundle creation (see above).
|
||||
- `npm run live:<op>` – Convenience commands mapping to `scripts/run-live.mjs` (e.g. `npm run live:getParts`).
|
||||
|
||||
Rollup configuration (`rollup.config.mjs`):
|
||||
- Externalizes Node core modules and listed dependencies.
|
||||
- Applies terser minification (2 passes, hoisting) for compact output.
|
||||
- Generates both CJS and ESM entrypoints.
|
||||
|
||||
Tree-shaking is enabled (`treeshake.moduleSideEffects = false`). `sideEffects: false` in `package.json` allows downstream bundlers to drop unused exports.
|
||||
|
||||
## Design Notes / Non-goals
|
||||
- WS-Security: Implements only UsernameToken with password type Text or Digest (configurable via `wssePasswordType`). No nonce or timestamp included.
|
||||
- Idempotency: `bodId` generated per request unless provided; `ensureBodAndDates` centralizes creation.
|
||||
- Parsing: Focused on extracting status and op-specific identifiers; raw parsed STAR tree still accessible via `result.parsed`.
|
||||
- Status Handling: Treats `NO_MATCH` (codes like 2 or 213) as non-error with `success=true` so callers can differentiate empty results from failures.
|
||||
- Validation: Enforces only practical minimum & enumerations; does not attempt full schema compliance.
|
||||
- No automatic `fetch` transport fallback yet (axios chosen for reliability). You can replace `postSoap` if providing a compatible function returning raw response text and error semantics.
|
||||
- No built-in XSD validator; XSDs present for reference only.
|
||||
1
server/rr/lib/index.cjs
Normal file
1
server/rr/lib/index.cjs
Normal file
File diff suppressed because one or more lines are too long
1
server/rr/lib/index.mjs
Normal file
1
server/rr/lib/index.mjs
Normal file
File diff suppressed because one or more lines are too long
3
server/rr/lib/types.cjs
Normal file
3
server/rr/lib/types.cjs
Normal file
@@ -0,0 +1,3 @@
|
||||
'use strict';
|
||||
// CJS access point for JSDoc typedefs
|
||||
module.exports = require('./types.js');
|
||||
144
server/rr/lib/types.js
Normal file
144
server/rr/lib/types.js
Normal file
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* @typedef {Object} RRClientConfig
|
||||
* @property {string} baseUrl Base URL of the Rome endpoint
|
||||
* @property {string} username WS-Security username
|
||||
* @property {string} password WS-Security password
|
||||
* @property {'Text'|'Digest'} [wssePasswordType] Password type (defaults to Text)
|
||||
* @property {number} [timeoutMs] Request timeout in milliseconds
|
||||
* @property {{max?: number}} [retries] Retry configuration
|
||||
* @property {{ info:Function, warn:Function, error:Function, debug:Function }} [logger] Custom logger implementation
|
||||
*/
|
||||
|
||||
/** Dealer/store routing (required each call). */
|
||||
export const _routingDoc = null;
|
||||
/**
|
||||
* @typedef {Object} Routing
|
||||
* @property {string} dealerNumber
|
||||
* @property {string} [storeNumber]
|
||||
* @property {string} [areaNumber]
|
||||
*/
|
||||
|
||||
/** Optional envelope overrides; BODId/CreationDateTime auto-generated if omitted. */
|
||||
export const _envelopeDoc = null;
|
||||
/**
|
||||
* @typedef {Object} EnvelopeOptions
|
||||
* @property {string} [bodId]
|
||||
* @property {string|Date} [creationDateTime]
|
||||
* @property {{ component?: string, task?: string, referenceId?: string }} [sender]
|
||||
*/
|
||||
|
||||
/** Per-call options */
|
||||
export const _callOptionsDoc = null;
|
||||
/**
|
||||
* @typedef {Object} CallOptions
|
||||
* @property {Routing} routing
|
||||
* @property {EnvelopeOptions} [envelope]
|
||||
*/
|
||||
|
||||
/** @template T @typedef {Object} RRResult { @property {boolean} success @property {T} [data] @property {any} parsed @property {{request:string,response:string}} xml @property {{transaction?:any,roRecord?:any}} [statusBlocks] @property {any} [applicationArea] } */
|
||||
|
||||
/** @typedef {Object} CombinedSearchName { @property {string} [fname] @property {string} [lname] @property {string} [mname] @property {string} [name] } */
|
||||
/** @typedef {Object} CombinedSearchQuery
|
||||
* @property {'phone'|'license'|'vin'|'name'|'nameRecId'|'stkNo'} kind Search kind (only one criterion allowed)
|
||||
* @property {string|number|{phone:string}} [phone] Phone number criterion
|
||||
* @property {string|number|{license:string}} [license] License plate criterion
|
||||
* @property {string|number|{vin:string}} [vin] VIN criterion
|
||||
* @property {CombinedSearchName} [name] Customer name criterion
|
||||
* @property {string|number|{custId:string}|{nameRecId:string}} [nameRecId] Name record ID criterion
|
||||
* @property {string|number|{stkNo:string}} [stkNo] Stock number criterion
|
||||
* @property {number} [maxResults] Max results (capped at 50)
|
||||
* @property {string} [make] Vehicle make filter
|
||||
* @property {string|number} [model] Vehicle model filter
|
||||
* @property {string|number} [year] Vehicle year filter
|
||||
} */
|
||||
/** @typedef {Object} CombinedSearchVehicleDetail { @property {string} [LicNo] } */
|
||||
/** @typedef {Object} CombinedSearchVehicle { @property {string} [Vin] @property {string} [VehicleMake] @property {string|number} [VehicleYr] @property {string} [MdlNo] @property {string} [ModelDesc] @property {string} [Carline] @property {string} [ExtClrDesc] @property {string} [IntClrDesc] @property {string} [MakeName] @property {CombinedSearchVehicleDetail} [VehicleDetail] } */
|
||||
/** @typedef {Object} CombinedSearchVehicleWarranty { @property {string} [ContractNumber] @property {string} [ExpirationDate] @property {string|number} [ExpirationMileage] } */
|
||||
/** @typedef {Object} CombinedSearchAdvisorContactInfo { @property {string|number} [NameRecId] } */
|
||||
/** @typedef {Object} CombinedSearchAdvisor { @property {CombinedSearchAdvisorContactInfo} [ContactInfo] } */
|
||||
/** @typedef {Object} CombinedSearchVehicleServInfo { @property {string|number} [CustomerNo] @property {string|number} [SalesmanNo] @property {string|number} [InServiceDate] @property {string|number} [Mileage] @property {string} [TeamCode] @property {CombinedSearchVehicleWarranty} [VehExtWarranty] @property {CombinedSearchAdvisor} [Advisor] @property {string[]} [VehServComments] } */
|
||||
/** @typedef {Object} CombinedSearchServVehicle { @property {CombinedSearchVehicle} [Vehicle] @property {CombinedSearchVehicleServInfo} [VehicleServInfo] } */
|
||||
/** @typedef {Object} CombinedSearchNameId { @property {string|number} [NameRecId] @property {'I'|'B'} [IBFlag] @property {Object} [IndName] @property {Object} [BusName] } */
|
||||
/** @typedef {Object} CombinedSearchNameContactId { @property {CombinedSearchNameId} [NameId] @property {Object[]} [Address] @property {Object[]} [ContactOptions] @property {Object[]} [Phone] @property {Object[]} [Email] } */
|
||||
/** @typedef {Object} CombinedSearchMessage { @property {string|number} [MessageNo] @property {string} [Text] } */
|
||||
/** @typedef {Object} CombinedSearchBlock { @property {CombinedSearchNameContactId} [NameContactId] @property {CombinedSearchServVehicle[]} [ServVehicle] @property {CombinedSearchMessage[]} [Message] } */
|
||||
|
||||
/** @typedef {Object} CustomerAddress { @property {'P'|'B'|'M'|'S'|'D'} [type] @property {string} line1 @property {string} [line2] @property {string} [city] @property {string} [state] @property {string} [postalCode] @property {string} [county] @property {string} [country] } */
|
||||
/** @typedef {Object} CustomerPhone { @property {'H'|'W'|'M'|'C'|'F'} [type] @property {string|number} number @property {string|number} [extension] } */
|
||||
/** @typedef {Object} CustomerEmail { @property {string} address } */
|
||||
/** @typedef {Object} CustomerBirthDate { @property {'P'|'S'} [type] @property {string} date } */
|
||||
/** @typedef {Object} CustomerSSN { @property {'P'|'S'} [type] @property {string} ssn } */
|
||||
/** @typedef {Object} CustomerDriver { @property {'P'|'S'} [type] @property {string} licenseNumber @property {string} [licenseState] @property {string} [licenseExpDate] } */
|
||||
/** @typedef {Object} CustomerChild { @property {string} name } */
|
||||
/** @typedef {Object} CustomerPersonal { @property {'M'|'F'|'U'} [gender] @property {string} [otherName] @property {string} [anniversaryDate] @property {string} [employerName] @property {string} [employerPhone] @property {string} [occupation] @property {string} [optOut] @property {string} [optOutUse] @property {CustomerBirthDate[]} [birthDates] @property {CustomerSSN[]} [ssns] @property {CustomerDriver} [driver] @property {CustomerChild[]} [children] } */
|
||||
/** @typedef {Object} CustomerDmsFollowup { @property {string} type @property {string} value } */
|
||||
/** @typedef {Object} CustomerDmsInfo { @property {string} [taxExemptNum] @property {string} [salesTerritory] @property {string} [deliveryRoute] @property {string} [salesmanNum] @property {string} [lastContactMethod] @property {CustomerDmsFollowup[]} [followups] } */
|
||||
/** @typedef {Object} InsertCustomerPayload
|
||||
* @property {'I'|'B'} [ibFlag] Individual/Business flag (auto-inferred if omitted)
|
||||
* @property {'R'|'W'|'I'|'Retail'|'Wholesale'|'Internal'} [customerType] Customer type
|
||||
* @property {string} [createdBy] Username or identifier creating the record
|
||||
* @property {string} [customerName] Business name (alias for lastName when business)
|
||||
* @property {string} [lastName] Last name or business name
|
||||
* @property {string} [firstName] First name (required for individuals)
|
||||
* @property {string} [midName] Middle name
|
||||
* @property {string} [salut] Salutation
|
||||
* @property {string} [suffix] Suffix
|
||||
* @property {CustomerAddress[]} [addresses] Addresses list (line1 required per address)
|
||||
* @property {CustomerPhone[]} [phones] Phone numbers list (number required per phone)
|
||||
* @property {CustomerEmail[]} [emails] Email list (first entry used)
|
||||
* @property {CustomerPersonal} [personal] Personal details
|
||||
* @property {CustomerDmsInfo} [dms] DMS-specific supplemental info
|
||||
*/
|
||||
/** @typedef {InsertCustomerPayload & { nameRecId: string|number }} UpdateCustomerPayload */
|
||||
/** @typedef {Object} CustomerResponseData { @property {string|undefined} dmsRecKey @property {string|undefined} status @property {string|undefined} statusCode } */
|
||||
|
||||
/** @typedef {Object} ServiceVehicleDetail { @property {string} [licNo] } */
|
||||
/** @typedef {Object} ServiceVehicleWarranty { @property {string} [contractNumber] @property {string} [expirationDate] @property {string|number} [expirationMileage] } */
|
||||
/** @typedef {Object} ServiceVehicleAdvisorContactInfo { @property {string|number} nameRecId } */
|
||||
/** @typedef {Object} ServiceVehicleServInfo { @property {string|number} customerNo @property {string|number} [salesmanNo] @property {string|number} [inServiceDate] @property {string|number} [mileage] @property {string} [teamCode] @property {ServiceVehicleWarranty} [vehExtWarranty] @property {{contactInfo?: ServiceVehicleAdvisorContactInfo}} [advisor] } */
|
||||
/** @typedef {Object} InsertServiceVehiclePayload { @property {string} vin @property {string} [modelDesc] @property {string} [carline] @property {string} [extClrDesc] @property {string} [intClrDesc] @property {string} [trimDesc] @property {string} [bodyStyle] @property {string} [engineDesc] @property {string} [transDesc] @property {string|number} [year] @property {string|number} [odometer] @property {string} [odometerUnits] @property {ServiceVehicleDetail} [vehicleDetail] @property {ServiceVehicleServInfo} vehicleServInfo } */
|
||||
/** @typedef {Object} ServiceVehicleResponseData { @property {string|undefined} status @property {string|undefined} statusCode } */
|
||||
|
||||
/** @typedef {Object} RepairOrderEstimate { @property {string|number} [parts] @property {string|number} [labor] @property {string|number} [total] @property {string} [estimateType] } */
|
||||
/** @typedef {Object} RepairOrderTax { @property {'All'|'Cust'|'Intr'|'Warr'} [payType] @property {string} [taxCode] @property {string|number} [txblGrossAmt] @property {string|number} [grossTaxAmt] } */
|
||||
/** @typedef {Object} RepairOrderLaborBill { @property {'All'|'Cust'|'Intr'|'Warr'} [payType] @property {string|number} [jobTotalHrs] @property {string|number} [billTime] @property {string|number} [billRate] } */
|
||||
/** @typedef {Object} RepairOrderLaborCCC { @property {string} [cause] @property {string} [complaint] @property {string} [correction] } */
|
||||
/** @typedef {Object} RepairOrderLaborAmount { @property {'All'|'Cust'|'Intr'|'Warr'} [payType] @property {string} [amtType] @property {string|number} [custPrice] @property {string|number} [totalAmt] } */
|
||||
/** @typedef {Object} RepairOrderLaborOp { @property {string} [opCode] @property {string|number} [jobNo] @property {'T'|'N'} [custTxblNtxblFlag] @property {'T'|'N'} [warrTxblNtxblFlag] @property {'T'|'N'} [intrTxblNtxblFlag] @property {string} [custPayTypeFlag] @property {string} [warrPayTypeFlag] @property {string} [intrPayTypeFlag] @property {string} [vlrCode] @property {RepairOrderLaborBill} [bill] @property {RepairOrderLaborCCC} [ccc] @property {RepairOrderLaborAmount} [amount] } */
|
||||
/** @typedef {Object} RepairOrderLabor { @property {RepairOrderLaborOp[]} [ops] } */
|
||||
/** @typedef {Object} RepairOrderPartLine { @property {string} [partNo] @property {string} [partNoDesc] @property {string|number} [partQty] @property {string|number} [sale] @property {string|number} [cost] @property {string} [addDeleteFlag] } */
|
||||
/** @typedef {Object} RepairOrderPartJob { @property {string} [opCode] @property {string|number} [jobNo] @property {RepairOrderPartLine[]} [lines] } */
|
||||
/** @typedef {Object} RepairOrderPart { @property {RepairOrderPartJob[]} [jobs] } */
|
||||
/** @typedef {Object} RepairOrderGGLineAmount { @property {'All'|'Cust'|'Intr'|'Warr'} [payType] @property {string} [amtType] @property {string|number} [custPrice] @property {string|number} [dlrCost] } */
|
||||
/** @typedef {Object} RepairOrderGGLine { @property {string} [breakOut] @property {'G'|'P'|'S'|'F'} [itemType] @property {string} [itemDesc] @property {string|number} [custQty] @property {string|number} [warrQty] @property {string|number} [intrQty] @property {'T'|'N'} [custTxblNtxblFlag] @property {'T'|'N'} [warrTxblNtxblFlag] @property {'T'|'N'} [intrTxblNtxblFlag] @property {string} [custPayTypeFlag] @property {string} [warrPayTypeFlag] @property {string} [intrPayTypeFlag] @property {RepairOrderGGLineAmount} [amount] } */
|
||||
/** @typedef {Object} RepairOrderGGOp { @property {string} [opCode] @property {string|number} [jobNo] @property {RepairOrderGGLine[]} [lines] } */
|
||||
/** @typedef {Object} RepairOrderGG { @property {string|number} [roNo] @property {RepairOrderGGOp[]} [ops] } */
|
||||
/** @typedef {Object} RepairOrderMiscLine { @property {string} [miscCode] @property {'T'|'N'} [custTxblNtxblFlag] @property {'T'|'N'} [warrTxblNtxblFlag] @property {'T'|'N'} [intrTxblNtxblFlag] @property {string} [custPayTypeFlag] @property {string} [warrPayTypeFlag] @property {string} [intrPayTypeFlag] @property {string|number} [codeAmt] } */
|
||||
/** @typedef {Object} RepairOrderMiscOp { @property {string} [opCode] @property {string|number} [jobNo] @property {RepairOrderMiscLine[]} [lines] } */
|
||||
/** @typedef {Object} RepairOrderMisc { @property {string|number} [roNo] @property {RepairOrderMiscOp[]} [ops] } */
|
||||
/** @typedef {Object} CreateRepairOrderPayload
|
||||
* @property {string|number} customerNo Customer number (CustNo)
|
||||
* @property {string|number} departmentType Department type (DeptType)
|
||||
* @property {string} vin Vehicle VIN (Vin)
|
||||
* @property {string|number} outsdRoNo External RO identifier
|
||||
* @property {string|number} [advisorNo] Advisor number
|
||||
* @property {string|number} [tagNo] Tag number
|
||||
* @property {string|number} [mileageIn] Mileage in value
|
||||
* @property {string} [roComment] Repair order comment
|
||||
* @property {RepairOrderEstimate} [estimate] Repair order estimate block
|
||||
* @property {RepairOrderTax} [tax] Tax block
|
||||
* @property {RepairOrderLabor} [rolabor] Labor operations
|
||||
* @property {RepairOrderPart} [ropart] Parts jobs/lines
|
||||
* @property {RepairOrderGG} [rogg] General goods lines
|
||||
* @property {RepairOrderMisc} [romisc] Miscellaneous charges
|
||||
*/
|
||||
/** @typedef {Object} UpdateRepairOrderPayload { @property {'Y'|'N'} finalUpdate @property {string|number} outsdRoNo @property {string|number} [roNo] @property {string|number} [customerNo] @property {string|number} [tagNo] @property {string|number} [departmentType] @property {string} [vin] @property {string|number} [mileageIn] @property {string|number} [mileageOut] @property {string} [roComment] @property {RepairOrderEstimate} [estimate] @property {RepairOrderTax} [tax] @property {RepairOrderLabor} [rolabor] @property {RepairOrderPart} [ropart] @property {RepairOrderGG} [rogg] @property {RepairOrderMisc} [romisc] } */
|
||||
/** @typedef {Object} RepairOrderData { @property {string|undefined} status @property {string|undefined} date @property {string|undefined} time @property {string|undefined} outsdRoNo @property {string|undefined} dmsRoNo @property {string|undefined} errorMessage } */
|
||||
|
||||
/** @typedef {Object} GetAdvisorsParams { @property {'S'|'P'|'B'|'SERVICE'|'PARTS'|'BODY'|'BODYSHOP'|'BODY SHOP'} department @property {string|number} [advisorNumber] @property {number} [maxResults] } */
|
||||
/** @typedef {Object} AdvisorRow { @property {string|number|undefined} advisorId @property {string|undefined} firstName @property {string|undefined} lastName @property {'S'|'P'|'B'|undefined} department } */
|
||||
/** @typedef {Object} GetPartsParams { @property {string|number} roNumber } */
|
||||
/** @typedef {Object} PartRow { @property {string|undefined} partNumber @property {string|undefined} partDescription @property {string|number|undefined} quantityOrdered @property {string|number|undefined} quantityShipped @property {string|number|undefined} price @property {string|number|undefined} cost @property {string|undefined} processedFlag @property {string|undefined} addOrDelete } */
|
||||
|
||||
// Marker exports (no runtime impact)
|
||||
export const _extendedTypesDoc = null;
|
||||
1035
server/rr/lib/types/types.d.ts
vendored
Normal file
1035
server/rr/lib/types/types.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user