Compare commits

..

2 Commits

Author SHA1 Message Date
Dave Richer
18da11f593 - Merge master-aio, bump packages.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-08-19 12:27:30 -04:00
Dave Richer
136d52ec0b - Merge Master, update packages.
Signed-off-by: Dave Richer <dave@imexsystems.ca>
2024-07-22 14:47:44 -04:00
97 changed files with 6316 additions and 7891 deletions

View File

@@ -5,7 +5,6 @@ orbs:
aws-s3: circleci/aws-s3@4.0.0
aws-cli: circleci/aws-cli@4.0
eb: circleci/aws-elastic-beanstalk@2.0.1
jira: circleci/jira@2.1.0
jobs:
imex-api-deploy:
docker:
@@ -19,12 +18,6 @@ jobs:
eb status --verbose
eb deploy
eb status
- jira/notify:
environment: Production (ImEX) - API
environment_type: production
job_type: deployment
pipeline_id: << pipeline.id >>
pipeline_number: << pipeline.number >>
imex-hasura-migrate:
docker:
@@ -40,16 +33,11 @@ jobs:
- run:
name: Execute migration
command: |
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
npm install hasura-cli -g
hasura migrate apply --endpoint https://db.imex.online/ --admin-secret << parameters.secret >>
hasura metadata apply --endpoint https://db.imex.online/ --admin-secret << parameters.secret >>
hasura metadata reload --endpoint https://db.imex.online/ --admin-secret << parameters.secret >>
- jira/notify:
environment: Production (ImEX) - Hasura
environment_type: production
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
imex-app-build:
docker:
- image: cimg/node:18.18.2
@@ -74,7 +62,6 @@ jobs:
to: "s3://imex-online-production/"
arguments: "--exclude '*.map'"
imex-app-beta-build:
docker:
- image: cimg/node:18.18.2
@@ -99,12 +86,6 @@ jobs:
from: dist
to: "s3://imex-online-beta/"
arguments: "--exclude '*.map'"
- jira/notify:
environment: Production (ImEX) - Front End
environment_type: production
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
rome-api-deploy:
docker:
@@ -118,12 +99,7 @@ jobs:
eb status --verbose
eb deploy
eb status
- jira/notify:
environment: Production (Rome) - API
environment_type: production
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
rome-hasura-migrate:
docker:
- image: cimg/node:18.18.2
@@ -138,16 +114,11 @@ jobs:
- run:
name: Execute migration
command: |
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
npm install hasura-cli -g
hasura migrate apply --endpoint https://db.romeonline.io/ --admin-secret << parameters.secret >>
hasura metadata apply --endpoint https://db.romeonline.io/ --admin-secret << parameters.secret >>
hasura metadata reload --endpoint https://db.romeonline.io/ --admin-secret << parameters.secret >>
- jira/notify:
environment: Production (Rome) - Hasura
environment_type: production
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
rome-app-build:
docker:
- image: cimg/node:18.18.2
@@ -172,12 +143,6 @@ jobs:
from: dist
to: "s3://rome-online-production/"
arguments: "--exclude '*.map'"
- jira/notify:
environment: Production (Rome) - Front End
environment_type: production
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
promanager-app-build:
docker:
@@ -203,12 +168,6 @@ jobs:
from: dist
to: "s3://promanager-production/"
arguments: "--exclude '*.map'"
- jira/notify:
environment: Production (ProManager) - Front End
environment_type: production
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
test-rome-hasura-migrate:
docker:
@@ -224,16 +183,10 @@ jobs:
- run:
name: Execute migration
command: |
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
npm install hasura-cli -g
hasura migrate apply --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >>
hasura metadata apply --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >>
hasura metadata reload --endpoint https://db.test.romeonline.io/ --admin-secret << parameters.secret >>
- jira/notify:
environment: Test (Rome) - Hasura
environment_type: testing
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
test-rome-app-build:
docker:
@@ -259,12 +212,6 @@ jobs:
from: dist
to: "s3://rome-online-test/"
arguments: "--exclude '*.map'"
- jira/notify:
environment: Test (Rome) - Front End
environment_type: testing
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
test-promanager-app-build:
docker:
@@ -290,12 +237,6 @@ jobs:
from: dist
to: "s3://promanager-testing/"
arguments: "--exclude '*.map'"
- jira/notify:
environment: Test (ProManager) - Front End
environment_type: testing
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
test-hasura-migrate:
docker:
@@ -311,16 +252,10 @@ jobs:
- run:
name: Execute migration
command: |
curl -L https://github.com/hasura/graphql-engine/raw/stable/cli/get.sh | bash
npm install hasura-cli -g
hasura migrate apply --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >>
hasura metadata apply --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >>
hasura metadata reload --endpoint https://db.test.bodyshop.app/ --admin-secret << parameters.secret >>
- jira/notify:
environment: Test (ImEX) - Hasura
environment_type: testing
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
imex-test-app-build:
docker:
@@ -367,12 +302,7 @@ jobs:
from: dist
to: "s3://imex-online-test-beta/"
arguments: "--exclude '*.map'"
- jira/notify:
environment: Test (ImEX) - Front End
environment_type: testing
pipeline_id: << pipeline.id >>
job_type: deployment
pipeline_number: << pipeline.number >>
admin-app-build:
docker:

View File

@@ -1,20 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDWzCCAkOgAwIBAgIUD/QBSAXy/AlJ/cS4DaPWJLpChxgwDQYJKoZIhvcNAQEL
BQAwPTELMAkGA1UEBhMCQ0ExCzAJBgNVBAgMAk9OMSEwHwYDVQQKDBhJbnRlcm5l
dCBXaWRnaXRzIFB0eSBMdGQwHhcNMjQwOTA5MTU0MjA1WhcNMjUwOTA5MTU0MjA1
WjA9MQswCQYDVQQGEwJDQTELMAkGA1UECAwCT04xITAfBgNVBAoMGEludGVybmV0
IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AKSd0l7NJCNBwvtPU+dVPQkteg0AfC3sGqRnZMQteCRVa2oIgC4NoF3A9BK/yHbF
ZF25OnXTck5vzc8yb3v73ndfTD9ASKNoiaZE84/GFBsxqlKR8cs0qVwzuAsdijMv
vlMPNlMRyE1Rb7nR6HXGkPXNyxgMko03NXPkvIje9zRudm0Lf8L4q/hPyPkS7Mrm
/uQfAAJe+xFcupkEX2XY7r0x1C+z6E8lA1UcuhK3SHdW7CWYqp1vU5/dnnUiXwCa
GiC6Y1bCJB0pDAVISzy3JUDdINZdiqGR+y8ho3pstChf2mp/76s3N9eG9KA/qaFK
BrGk2PvCoZ8/Aj1aMsRYFHECAwEAAaNTMFEwHQYDVR0OBBYEFDLJ2fbWP4VUJgOp
PSs+NGHcVgRmMB8GA1UdIwQYMBaAFDLJ2fbWP4VUJgOpPSs+NGHcVgRmMA8GA1Ud
EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBABfv5ut/y03atq0NMB0jeDY4
AvW4ukk0k1svyqxFZCw9o7m2lHb/IjmVrZG1Sj4JWrrSv0s02ccb26/t6vazNa5L
Powe3eyfHgfjTZJmgs8hyeMwKS0wWk/SPuu9JDhIJakiquqD+UVBGkHpP+XYvhDv
vhS2XRlW+aEjpUmr1oCyyrc6WbzrYRNadqEsn/AxwcMyUbht3Ugjkg+OpidcTIQp
5lv63waKo6I1vQofzBQ3L7JYsKo8kC0vAP7wkLxvzBii335uZJzzpFYFVOyVNezi
dJdazPbRYbXz4LjltdEn/SNfRuKX8ZRiN2OSo7OfSrZaMTS87SfCSFJGgQM8Yrk=
-----END CERTIFICATE-----

View File

@@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCkndJezSQjQcL7
T1PnVT0JLXoNAHwt7BqkZ2TELXgkVWtqCIAuDaBdwPQSv8h2xWRduTp103JOb83P
Mm97+953X0w/QEijaImmRPOPxhQbMapSkfHLNKlcM7gLHYozL75TDzZTEchNUW+5
0eh1xpD1zcsYDJKNNzVz5LyI3vc0bnZtC3/C+Kv4T8j5EuzK5v7kHwACXvsRXLqZ
BF9l2O69MdQvs+hPJQNVHLoSt0h3VuwlmKqdb1Of3Z51Il8AmhogumNWwiQdKQwF
SEs8tyVA3SDWXYqhkfsvIaN6bLQoX9pqf++rNzfXhvSgP6mhSgaxpNj7wqGfPwI9
WjLEWBRxAgMBAAECggEAUNpHYlLFxh9dokujPUMreF+Cy/IKDBAkQc2au5RNpyLh
YDIOqw/8TTAhcTgLQPLQygvZP9f8E7RsVLFD+pSJ/v2qmIJ9au1Edor1Sg+S/oxV
SLrwFMunx2aLpcH7iAqSI3+cQg7A3+D4zD7iOz6tIl3Su9wo+v073tFhHKTOrEwv
Qgr9Jf3viIiKV1ym+uQEVQndocfsj46FnFpXTQ2qs7kAF6FgAOLDGfQQwzkiqEBD
NsqsDmbYIx6foZL+DEz1ZVO2M5B+xxpbNK82KwuQilVpimW8ui4LZHCe+RIFzt9+
BK6KGlLpSEwTFliivI3nahy18JzskZsfyah0CPZlQQKBgQDVv+A0qIPGvOP3Sx+9
HyeQCV23SkvvSvw8p8pMB0gvwv63YdJ7N/rJzBGS6YUHFWWZZgEeTgkJ6VJvoe0r
8JL1el9uSUa7f0eayjmFBOGuzpktNVdIn2Tg7A9MWA4JqPNNC69RMOh86ewGD4J3
a8Hz2a1bHxAmy/AZt2ukypY6eQKBgQDFJ7kqeOPkRBz9WbALRgVIXo8YWf5di0sQ
r0HC03GAISHQ725A2IFBPHJWeqj0jaMiIZD0y+Obgp7KAskrJaLfsd7Ug775kFfw
oUI9UAl6kRuPKvm3BaVAm46SQm+56VsgxTi73YN0NUp75THHZgAJjepF9zSpVJxq
VY9DjEGruQKBgQCQCpGIatcCol/tUg69X7VFd0pULhkl1J5OMbQ9r9qRdRI5eg5h
QsQaIQ7mtb8TmvOwf/DY/zVQHI+U8sXlCmW+TwzoQTENQSR7xzMj1LpRFqBaustr
AR72A537kItFLzll/i3SxOam5uxK2UDOQSuerF4KPdCglGXkrpo3nt3F4QKBgQCa
RArPAOjQo7tLQfJN3+wiRFsTYtd1uphx5bA/EdOtvj8HjVFnzADXWsTchf3N3UXY
XwtdgGwIMpys1KEz8a8P+c2x26SDAj7NOmDqOMYx8Xju/WGHpBM6Cn30U6e4gK+d
ZLSPyzQgqdIuP5hDvbwpvbGiLVw3Ys1BJtGCuSxpgQJ/eHnRiuSi5Zq5jGg+GpA+
FEEc9NCy772rR+4uzEOqyIwqewffqzSuVWuIsY/8MP3fh+NDxl/mU6cB5QVeD54Z
JZUKwmpM26muiM6WvVnM4ExPdSGA2+l4pZjby/KKd6F/w0tgZ1jb9Pb2/0vN3qVA
Y4U4XNTMt2fxUACqiL4SHA==
-----END PRIVATE KEY-----

View File

@@ -1,5 +1,5 @@
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.imex.online/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.imex.online/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
VITE_APP_GA_CODE=231099835
VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
@@ -8,7 +8,7 @@ VITE_APP_CLOUDINARY_API_KEY=957865933348715
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
VITE_APP_AXIOS_BASE_API_URL=/api/
VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_INSTANCE=IMEX

View File

@@ -1,5 +1,5 @@
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.imex.online/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.imex.online/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
VITE_APP_GA_CODE=231099835
VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
@@ -8,7 +8,7 @@ VITE_APP_CLOUDINARY_API_KEY=957865933348715
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BG3tzU7L2BXlGZ_3VLK4PNaRceoEXEnmHfxcVbRMF5o5g05ejslhVPki9kBM9cBBT-08Ad9kN3HSpS6JmrWD6h4'
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
VITE_APP_AXIOS_BASE_API_URL=/api/
VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_INSTANCE=PROMANAGER

View File

@@ -1,15 +1,14 @@
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.imex.online/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.imex.online/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT=https://db.dev.bodyshop.app/v1/graphql
VITE_APP_GRAPHQL_ENDPOINT_WS=wss://db.dev.bodyshop.app/v1/graphql
VITE_APP_GA_CODE=231099835
# VITE_APP_FIREBASE_CONFIG={ "apiKey": "AIzaSyAuLQR9SV5LsVxjU8wh9hvFLdhcAHU6cxE", "authDomain": "rome-prod-1.firebaseapp.com", "projectId": "rome-prod-1", "storageBucket": "rome-prod-1.appspot.com", "messagingSenderId": "147786367145", "appId": "1:147786367145:web:9d4cba68071c3f29a8a9b8", "measurementId": "G-G8Z9DRHTZS"}
VITE_APP_FIREBASE_CONFIG={"apiKey":"AIzaSyDPLT8GiDHDR1R4nI66Qi0BY1aYviDPioc","authDomain":"imex-dev.firebaseapp.com","databaseURL":"https://imex-dev.firebaseio.com","projectId":"imex-dev","storageBucket":"imex-dev.appspot.com","messagingSenderId":"759548147434","appId":"1:759548147434:web:e8239868a48ceb36700993","measurementId":"G-K5XRBVVB4S"}
VITE_APP_FIREBASE_CONFIG={ "apiKey": "AIzaSyAuLQR9SV5LsVxjU8wh9hvFLdhcAHU6cxE", "authDomain": "rome-prod-1.firebaseapp.com", "projectId": "rome-prod-1", "storageBucket": "rome-prod-1.appspot.com", "messagingSenderId": "147786367145", "appId": "1:147786367145:web:9d4cba68071c3f29a8a9b8", "measurementId": "G-G8Z9DRHTZS"}
VITE_APP_CLOUDINARY_ENDPOINT_API=https://api.cloudinary.com/v1_1/io-test
VITE_APP_CLOUDINARY_ENDPOINT=https://res.cloudinary.com/io-test
VITE_APP_CLOUDINARY_API_KEY=957865933348715
VITE_APP_CLOUDINARY_THUMB_TRANSFORMATIONS=c_fill,h_250,w_250
VITE_APP_FIREBASE_PUBLIC_VAPID_KEY='BP1B7ZTYpn-KMt6nOxlld6aS8Skt3Q7ZLEqP0hAvGHxG4UojPYiXZ6kPlzZkUC5jH-EcWXomTLtmadAIxurfcHo'
VITE_APP_STRIPE_PUBLIC_KEY=pk_test_51GqB4TJl3nQjrZ0wCQWAxAhlNF8jKe0tipIa6ExBaxwJGitwvFsIZUEua4dUzaMIAuXp4qwYHXx7lgjyQSwP0Pe900vzm38C7g
VITE_APP_AXIOS_BASE_API_URL=/api/
VITE_APP_AXIOS_BASE_API_URL=http://localhost:4000
VITE_APP_REPORTS_SERVER_URL=https://reports3.test.imex.online
VITE_APP_SPLIT_API=ts615lqgnmk84thn72uk18uu5pgce6e0l4rc
VITE_APP_COUNTRY=USA

1
client/.gitignore vendored
View File

@@ -1,4 +1,3 @@
# Sentry Config File
.sentryclirc
/dev-dist

View File

@@ -12,6 +12,6 @@ module.exports = defineConfig({
setupNodeEvents(on, config) {
return require("./cypress/plugins/index.js")(on, config);
},
baseUrl: "https://localhost:3000"
baseUrl: "http://localhost:3000"
}
});

2625
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,22 +8,22 @@
"private": true,
"proxy": "http://localhost:4000",
"dependencies": {
"@ant-design/pro-layout": "^7.20.0",
"@apollo/client": "^3.11.8",
"@ant-design/pro-layout": "^7.19.12",
"@apollo/client": "^3.11.4",
"@emotion/is-prop-valid": "^1.3.0",
"@fingerprintjs/fingerprintjs": "^4.5.0",
"@fingerprintjs/fingerprintjs": "^4.4.3",
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.2.7",
"@sentry/cli": "^2.36.1",
"@sentry/cli": "^2.33.1",
"@sentry/react": "^7.114.0",
"@splitsoftware/splitio-react": "^1.13.0",
"@splitsoftware/splitio-react": "^1.12.1",
"@tanem/react-nprogress": "^5.0.51",
"@vitejs/plugin-react": "^4.3.1",
"antd": "^5.20.6",
"antd": "^5.20.2",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^3.3.0",
"autosize": "^6.0.1",
"axios": "^1.7.7",
"axios": "^1.7.4",
"classnames": "^2.5.1",
"css-box-model": "^1.2.1",
"dayjs": "^1.11.12",
@@ -32,12 +32,12 @@
"dotenv": "^16.4.5",
"env-cmd": "^10.1.0",
"exifr": "^7.1.3",
"firebase": "^10.13.1",
"firebase": "^10.13.0",
"graphql": "^16.9.0",
"i18next": "^23.15.1",
"i18next": "^23.14.0",
"i18next-browser-languagedetector": "^8.0.0",
"immutability-helper": "^3.1.1",
"libphonenumber-js": "^1.11.8",
"libphonenumber-js": "^1.11.7",
"logrocket": "^8.1.2",
"markerjs2": "^2.32.1",
"memoize-one": "^6.0.0",
@@ -47,7 +47,7 @@
"query-string": "^9.1.0",
"raf-schd": "^4.0.3",
"react": "^18.3.1",
"react-big-calendar": "^1.13.4",
"react-big-calendar": "^1.13.3",
"react-color": "^2.19.3",
"react-cookie": "^7.2.0",
"react-dom": "^18.3.1",
@@ -58,15 +58,15 @@
"react-icons": "^5.3.0",
"react-image-lightbox": "^5.1.4",
"react-markdown": "^9.0.1",
"react-number-format": "^5.4.2",
"react-number-format": "^5.4.0",
"react-popopo": "^2.1.9",
"react-product-fruits": "^2.2.61",
"react-redux": "^9.1.2",
"react-resizable": "^3.0.5",
"react-router-dom": "^6.26.2",
"react-router-dom": "^6.26.1",
"react-sticky": "^6.0.3",
"react-virtualized": "^9.22.5",
"react-virtuoso": "^4.10.3",
"react-virtuoso": "^4.10.1",
"recharts": "^2.12.7",
"redux": "^5.0.1",
"redux-actions": "^3.0.3",
@@ -74,9 +74,9 @@
"redux-saga": "^1.3.0",
"redux-state-sync": "^3.1.4",
"reselect": "^5.1.1",
"sass": "^1.78.0",
"sass": "^1.77.8",
"socket.io-client": "^4.7.5",
"styled-components": "^6.1.13",
"styled-components": "^6.1.12",
"subscriptions-transport-ws": "^0.11.0",
"use-memo-one": "^1.1.3",
"userpilot": "^1.3.5",
@@ -90,9 +90,6 @@
"start:imex": "dotenvx run --env-file=.env.development.imex -- vite",
"start:rome": "dotenvx run --env-file=.env.development.rome -- vite",
"start:promanager": "dotenvx run --env-file=.env.development.promanager -- vite",
"preview:imex": "dotenvx run --env-file=.env.development.imex -- vite preview",
"preview:rome": "dotenvx run --env-file=.env.development.rome -- vite preview",
"preview:promanager": "dotenvx run --env-file=.env.development.promanager -- vite preview",
"build:test:imex": "env-cmd -f .env.test.imex npm run build",
"build:test:rome": "env-cmd -f .env.test.rome npm run build",
"build:test:promanager": "env-cmd -f .env.test.promanager npm run build",
@@ -133,16 +130,15 @@
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@babel/preset-react": "^7.24.7",
"@dotenvx/dotenvx": "^1.14.0",
"@dotenvx/dotenvx": "^1.7.0",
"@emotion/babel-plugin": "^11.12.0",
"@emotion/react": "^11.13.3",
"@sentry/webpack-plugin": "^2.22.4",
"@emotion/react": "^11.13.0",
"@sentry/webpack-plugin": "^2.22.2",
"@testing-library/cypress": "^10.0.2",
"browserslist": "^4.23.3",
"browserslist-to-esbuild": "^2.1.1",
"chalk": "^5.3.0",
"cross-env": "^7.0.3",
"cypress": "^13.14.2",
"cypress": "^13.13.3",
"eslint": "^8.57.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-cypress": "^2.15.1",
@@ -151,12 +147,12 @@
"react-error-overlay": "6.0.11",
"redux-logger": "^3.0.6",
"source-map-explorer": "^2.5.3",
"vite": "^5.4.3",
"vite": "^5.4.1",
"vite-plugin-babel": "^1.2.0",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-legacy": "^2.1.0",
"vite-plugin-node-polyfills": "^0.22.0",
"vite-plugin-pwa": "^0.20.1",
"vite-plugin-style-import": "^2.0.0",
"workbox-window": "^7.1.0"
"vite-plugin-style-import": "^2.0.0"
}
}

View File

@@ -18,10 +18,10 @@ import { checkUserSession } from "../redux/user/user.actions";
import { selectBodyshop, selectCurrentEula, selectCurrentUser } from "../redux/user/user.selectors";
import PrivateRoute from "../components/PrivateRoute";
import "./App.styles.scss";
import handleBeta from "../utils/handleBeta";
import Eula from "../components/eula/eula.component";
import InstanceRenderMgr from "../utils/instanceRenderMgr";
import ProductFruitsWrapper from "./ProductFruitsWrapper.jsx";
import { SocketProvider } from "../contexts/SocketIO/socketContext.jsx";
const ResetPassword = lazy(() => import("../pages/reset-password/reset-password.component"));
const ManagePage = lazy(() => import("../pages/manage/manage.page.container"));
@@ -108,6 +108,8 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
return <LoadingSpinner message={t("general.labels.loggingin")} />;
}
handleBeta();
if (!online) {
return (
<Result
@@ -202,9 +204,7 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
path="/manage/*"
element={
<ErrorBoundary>
<SocketProvider bodyshop={bodyshop}>
<PrivateRoute isAuthorized={currentUser.authorized} />
</SocketProvider>
<PrivateRoute isAuthorized={currentUser.authorized} />
</ErrorBoundary>
}
>
@@ -214,9 +214,7 @@ export function App({ bodyshop, checkUserSession, currentUser, online, setOnline
path="/tech/*"
element={
<ErrorBoundary>
<SocketProvider bodyshop={bodyshop}>
<PrivateRoute isAuthorized={currentUser.authorized} />
</SocketProvider>
<PrivateRoute isAuthorized={currentUser.authorized} />
</ErrorBoundary>
}
>

View File

@@ -14,6 +14,7 @@ import dayjs from "../../utils/day";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import AlertComponent from "../alert/alert.component";
import BillFormLinesExtended from "../bill-form-lines-extended/bill-form-lines-extended.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import JobSearchSelect from "../job-search-select/job-search-select.component";
@@ -21,7 +22,6 @@ import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
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";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -276,7 +276,7 @@ export function BillFormComponent({
})
]}
>
<DateTimePicker isDateOnly disabled={disabled} />
<FormDatePicker disabled={disabled} />
</Form.Item>
<Form.Item
label={t("bills.fields.is_credit_memo")}

View File

@@ -8,8 +8,8 @@ import { DateFormatter } from "../../utils/DateFormatter";
import ContractStatusSelector from "../contract-status-select/contract-status-select.component";
import ContractsRatesChangeButton from "../contracts-rates-change-button/contracts-rates-change-button.component";
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormDateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import InputPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
@@ -196,7 +196,7 @@ export default function ContractFormComponent({ form, create = false, selectedJo
}
]}
>
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
{dlExpiresBeforeReturn && (
<Space style={{ color: "tomato" }}>
@@ -274,7 +274,7 @@ export default function ContractFormComponent({ form, create = false, selectedJo
<InputPhone />
</Form.Item>
<Form.Item label={t("contracts.fields.driver_dob")} name="driver_dob">
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
</LayoutFormRow>
<ContractsRatesChangeButton form={form} />

View File

@@ -10,12 +10,16 @@ import { DateFormatter } from "../../utils/DateFormatter";
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
import CourtesyCarReadiness from "../courtesy-car-readiness-select/courtesy-car-readiness-select.component";
import CourtesyCarStatus from "../courtesy-car-status-select/courtesy-car-status-select.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
export default function CourtesyCarCreateFormComponent({ form, saveLoading, newCC }) {
export default function CourtesyCarCreateFormComponent({
form,
saveLoading,
newCC,
}) {
const { t } = useTranslation();
const client = useApolloClient();
@@ -157,16 +161,16 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading, newC
<Input />
</Form.Item>
<Form.Item label={t("courtesycars.fields.purchasedate")} name="purchasedate">
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item label={t("courtesycars.fields.servicestartdate")} name="servicestartdate">
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item label={t("courtesycars.fields.serviceenddate")} name="serviceenddate">
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item label={t("courtesycars.fields.leaseenddate")} name="leaseenddate">
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
</LayoutFormRow>
@@ -224,7 +228,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading, newC
</div>
<div>
<Form.Item label={t("courtesycars.fields.nextservicedate")} name="nextservicedate">
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item shouldUpdate={(p, c) => p.nextservicedate !== c.nextservicedate}>
{() => {
@@ -256,7 +260,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading, newC
</Form.Item>
<div>
<Form.Item label={t("courtesycars.fields.registrationexpires")} name="registrationexpires">
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item shouldUpdate={(p, c) => p.registrationexpires !== c.registrationexpires}>
{() => {
@@ -289,7 +293,7 @@ export default function CourtesyCarCreateFormComponent({ form, saveLoading, newC
}
]}
>
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item shouldUpdate={(p, c) => p.insuranceexpires !== c.insuranceexpires}>
{() => {

View File

@@ -2,7 +2,7 @@ import { Form, InputNumber } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import CourtesyCarFuelSlider from "../courtesy-car-fuel-select/courtesy-car-fuel-select.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
export default function CourtesyCarReturnModalComponent() {
const { t } = useTranslation();
@@ -19,7 +19,7 @@ export default function CourtesyCarReturnModalComponent() {
}
]}
>
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item
label={t("contracts.fields.kmend")}

View File

@@ -1,77 +1,62 @@
import { SyncOutlined } from "@ant-design/icons";
import { Button, Card, Form, Input, Table } from "antd";
import React, { useEffect, useState, useContext } from "react";
import React, { 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 { pageLimit } from "../../utils/config";
import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; // Import SocketContext
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop
});
export default connect(mapStateToProps)(DmsAllocationsSummaryAp);
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function DmsAllocationsSummaryAp({ bodyshop, billids, title }) {
export default connect(mapStateToProps, mapDispatchToProps)(DmsAllocationsSummaryAp);
export function DmsAllocationsSummaryAp({ socket, bodyshop, billids, title }) {
const { t } = useTranslation();
const [allocationsSummary, setAllocationsSummary] = useState([]);
const { socket } = useContext(SocketContext);
useEffect(() => {
if (!socket) return;
const handleSuccess = async (billid) => {
socket.on("ap-export-success", (billid) => {
setAllocationsSummary((allocationsSummary) =>
allocationsSummary.map((a) => {
if (a.billid !== billid) return a;
return { ...a, status: "Successful" };
})
);
});
socket.on("ap-export-failure", ({ billid, error }) => {
allocationsSummary.map((a) => {
if (a.billid !== billid) return a;
return { ...a, status: error };
});
});
try {
await new Promise((resolve, reject) => {
socket.emit("clear-dms-session", (response) => {
if (response && response.status === "ok") {
resolve();
} else {
reject(new Error("Failed to clear DMS session"));
}
});
});
} catch (error) {
console.error("Failed to clear DMS session", error);
}
};
const handleFailure = ({ billid, error }) => {
setAllocationsSummary((allocationsSummary) =>
allocationsSummary.map((a) => {
if (a.billid !== billid) return a;
return { ...a, status: error };
})
);
};
socket.on("ap-export-success", handleSuccess);
socket.on("ap-export-failure", handleFailure);
if (socket.disconnected) socket.connect();
return () => {
socket.off("ap-export-success", handleSuccess);
socket.off("ap-export-failure", handleFailure);
socket.removeListener("ap-export-success");
socket.removeListener("ap-export-failure");
//socket.disconnect();
};
}, [socket]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (socket && socket.connected) {
if (socket.connected) {
socket.emit("pbs-calculate-allocations-ap", billids, (ack) => {
setAllocationsSummary(ack);
socket.allocationsSummary = ack;
});
}
}, [socket, socket.connected, billids]);
console.log(allocationsSummary);
const columns = [
{
title: t("general.labels.status"),
@@ -83,40 +68,35 @@ export function DmsAllocationsSummaryAp({ bodyshop, billids, title }) {
dataIndex: ["Posting", "Reference"],
key: "reference"
},
{
title: t("jobs.fields.dms.lines"),
dataIndex: "Lines",
key: "Lines",
render: (text, record) => (
<table style={{ tableLayout: "auto", width: "100%" }}>
<thead>
<tr>
<th>{t("bills.fields.invoice_number")}</th>
<th>{t("bodyshop.fields.dms.dms_acctnumber")}</th>
<th>{t("jobs.fields.dms.amount")}</th>
<tr>
<th>{t("bills.fields.invoice_number")}</th>
<th>{t("bodyshop.fields.dms.dms_acctnumber")}</th>
<th>{t("jobs.fields.dms.amount")}</th>
</tr>
{record.Posting.Lines.map((l, idx) => (
<tr key={idx}>
<td>{l.InvoiceNumber}</td>
<td>{l.Account}</td>
<td>{l.Amount}</td>
</tr>
</thead>
<tbody>
{record.Posting.Lines.map((l, idx) => (
<tr key={idx}>
<td>{l.InvoiceNumber}</td>
<td>{l.Account}</td>
<td>{l.Amount}</td>
</tr>
))}
</tbody>
))}
</table>
)
}
];
const handleFinish = async (values) => {
if (socket) {
socket.emit("pbs-export-ap", {
billids,
txEnvelope: values
});
}
socket.emit(`pbs-export-ap`, {
billids,
txEnvelope: values
});
};
return (
@@ -125,9 +105,7 @@ export function DmsAllocationsSummaryAp({ bodyshop, billids, title }) {
extra={
<Button
onClick={() => {
if (socket) {
socket.emit("pbs-calculate-allocations-ap", billids, (ack) => setAllocationsSummary(ack));
}
socket.emit("pbs-calculate-allocations-ap", billids, (ack) => setAllocationsSummary(ack));
}}
>
<SyncOutlined />
@@ -146,7 +124,12 @@ export function DmsAllocationsSummaryAp({ bodyshop, billids, title }) {
name="journal"
label={t("jobs.fields.dms.journal")}
initialValue={bodyshop.cdk_configuration && bodyshop.cdk_configuration.default_journal}
rules={[{ required: true }]}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
>
<Input />
</Form.Item>

View File

@@ -1,51 +1,37 @@
import { Button, Checkbox, Col, Table } from "antd";
import React, { useContext, useEffect, useState } from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
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 SocketContext from "../../contexts/SocketIO/socketContext"; // Import Socket Context
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(DmsCustomerSelector);
export function DmsCustomerSelector({ bodyshop }) {
const { t } = useTranslation();
const { socket } = useContext(SocketContext); // Use Socket Context
const [customerList, setCustomerList] = useState([]);
const [customerList, setcustomerList] = useState([]);
const [open, setOpen] = useState(false);
const [selectedCustomer, setSelectedCustomer] = useState(null);
const [dmsType, setDmsType] = useState("cdk");
useEffect(() => {
if (socket) {
const handleCdkSelectCustomer = (customerList) => {
setOpen(true);
setDmsType("cdk");
setCustomerList(customerList);
};
const handlePbsSelectCustomer = (customerList) => {
setOpen(true);
setDmsType("pbs");
setCustomerList(customerList);
};
socket.on("cdk-select-customer", handleCdkSelectCustomer);
socket.on("pbs-select-customer", handlePbsSelectCustomer);
// Clean up listeners on unmount
return () => {
socket.off("cdk-select-customer", handleCdkSelectCustomer);
socket.off("pbs-select-customer", handlePbsSelectCustomer);
};
}
}, [socket]);
socket.on("cdk-select-customer", (customerList, callback) => {
setOpen(true);
setDmsType("cdk");
setcustomerList(customerList);
});
socket.on("pbs-select-customer", (customerList, callback) => {
setOpen(true);
setDmsType("pbs");
setcustomerList(customerList);
});
const onUseSelected = () => {
setOpen(false);
@@ -83,11 +69,17 @@ export function DmsCustomerSelector({ bodyshop }) {
key: "name1",
sorter: (a, b) => alphaSort(a.name1 && a.name1.fullName, b.name1 && b.name1.fullName)
},
{
title: t("jobs.fields.dms.address"),
//dataIndex: ["name2", "fullName"],
key: "address",
render: (record) =>
`${record.address?.addressLine?.[0]}, ${record.address?.city} ${record.address?.stateOrProvince} ${record.address?.postalCode}`
render: (record, value) =>
`${
record.address && record.address.addressLine && record.address.addressLine[0]
}, ${record.address && record.address.city} ${
record.address && record.address.stateOrProvince
} ${record.address && record.address.postalCode}`
}
];
@@ -103,15 +95,15 @@ export function DmsCustomerSelector({ bodyshop }) {
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}`
render: (record, value) => `${record.Address}, ${record.City} ${record.State} ${record.ZipCode}`
}
];
if (!open) return null;
return (
<Col span={24}>
<Table
@@ -133,6 +125,7 @@ export function DmsCustomerSelector({ bodyshop }) {
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);

View File

@@ -4,8 +4,11 @@ import React from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({});
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
@@ -14,7 +17,7 @@ const mapDispatchToProps = (dispatch) => ({
export default connect(mapStateToProps, mapDispatchToProps)(DmsLogEvents);
export function DmsLogEvents({ logs }) {
export function DmsLogEvents({ socket, logs, bodyshop }) {
return (
<Timeline
pending

View File

@@ -24,9 +24,9 @@ import i18n from "../../translations/i18n";
import dayjs from "../../utils/day";
import DmsCdkMakes from "../dms-cdk-makes/dms-cdk-makes.component";
import DmsCdkMakesRefetch from "../dms-cdk-makes/dms-cdk-makes.refetch.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -164,7 +164,7 @@ export function DmsPostForm({ bodyshop, socket, job, logsRef }) {
<Input disabled />
</Form.Item>
<Form.Item name="inservicedate" label={t("jobs.fields.dms.inservicedate")}>
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
</LayoutFormRow>
<Space>

View File

@@ -4,6 +4,7 @@ import Markdown from "react-markdown";
import { createStructuredSelector } from "reselect";
import { selectCurrentEula, selectCurrentUser } from "../../redux/user/user.selectors";
import { connect } from "react-redux";
import { FormDatePicker } from "../form-date-picker/form-date-picker.component";
import { INSERT_EULA_ACCEPTANCE } from "../../graphql/user.queries";
import { useMutation } from "@apollo/client";
import { acceptEula } from "../../redux/user/user.actions";
@@ -11,7 +12,6 @@ import { useTranslation } from "react-i18next";
import day from "../../utils/day";
import "./eula.styles.scss";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const Eula = ({ currentEula, currentUser, acceptEula }) => {
const [formReady, setFormReady] = useState(false);
@@ -216,7 +216,7 @@ const EulaFormComponent = ({ form, handleChange, onFinish, t }) => (
}
]}
>
<DateTimePicker isDateOnly onChange={handleChange} onlyToday aria-label={t("eula.labels.date_accepted")} />
<FormDatePicker onChange={handleChange} onlyToday aria-label={t("eula.labels.date_accepted")} />
</Form.Item>
</Col>
</Row>

View File

@@ -0,0 +1,123 @@
import { DatePicker } from "antd";
import dayjs from "../../utils/day";
import React, { useRef } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(FormDatePicker);
const dateFormat = "MM/DD/YYYY";
export function FormDatePicker({
bodyshop,
value,
onChange,
onBlur,
onlyFuture,
onlyToday,
isDateOnly = true,
...restProps
}) {
const ref = useRef();
const handleChange = (newDate) => {
if (value !== newDate && onChange) {
onChange(isDateOnly ? newDate && newDate.format("YYYY-MM-DD") : newDate);
}
};
const handleKeyDown = (e) => {
if (e.key.toLowerCase() === "t") {
if (onChange) {
onChange(isDateOnly ? dayjs().format("YYYY-MM-DD") : dayjs());
}
} else if (e.key.toLowerCase() === "enter") {
if (ref.current && ref.current.blur) ref.current.blur();
}
};
const handleBlur = (e) => {
const v = e.target.value;
if (!v) return;
const formats = [
"MMDDYY",
"MMDDYYYY",
"MM/DD/YY",
"MM/DD/YYYY",
"M/DD/YY",
"M/DD/YYYY",
"MM/D/YY",
"MM/D/YYYY",
"M/D/YY",
"M/D/YYYY",
"D/MM/YY",
"D/MM/YYYY",
"DD/M/YY",
"DD/M/YYYY",
"D/M/YY",
"D/M/YYYY"
];
let _a;
// Iterate through formats to find the correct one
for (let format of formats) {
_a = dayjs(v, format);
if (v === _a.format(format)) {
break;
}
}
if (_a.isValid() && value && value.isValid && value.isValid()) {
_a.set({
hours: value.hours(),
minutes: value.minutes(),
seconds: value.seconds(),
milliseconds: value.milliseconds()
});
}
if (_a.isValid() && onChange) {
if (onlyFuture) {
if (dayjs().subtract(1, "day").isBefore(_a)) {
onChange(isDateOnly ? _a.format("YYYY-MM-DD") : _a);
} else {
onChange(isDateOnly ? dayjs().format("YYYY-MM-DD") : dayjs());
}
} else {
onChange(isDateOnly ? _a.format("YYYY-MM-DD") : _a);
}
}
};
return (
<div onKeyDown={handleKeyDown}>
<DatePicker
ref={ref}
value={value ? dayjs(value) : null}
onChange={handleChange}
format={dateFormat}
onBlur={onBlur || handleBlur}
showToday={false}
disabledTime
disabledDate={(d) => {
if (onlyToday) {
return !dayjs().isSame(d, "day");
} else if (onlyFuture) {
return dayjs().subtract(1, "day").isAfter(d);
}
}}
{...restProps}
/>
</div>
);
}

View File

@@ -0,0 +1,48 @@
import { DatePicker } from "antd";
import dayjs from "../../utils/day.js";
import React, { useRef } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors.js";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export default connect(mapStateToProps, mapDispatchToProps)(FormDateTimePickerEnhanced);
const dateFormat = "MM/DD/YYYY h:mm a";
export function FormDateTimePickerEnhanced({
bodyshop,
value,
onBlur,
onlyFuture,
onlyToday,
isDateOnly = true,
...restProps
}) {
const ref = useRef();
return (
<div>
<DatePicker
ref={ref}
value={value ? dayjs(value) : null}
format={dateFormat}
onBlur={onBlur}
showToday={false}
disabledDate={(d) => {
if (onlyToday) {
return !dayjs().isSame(d, "day");
} else if (onlyFuture) {
return dayjs().subtract(1, "day").isAfter(d);
}
}}
{...restProps}
/>
</div>
);
}

View File

@@ -1,105 +1,46 @@
import { DatePicker } from "antd";
import PropTypes from "prop-types";
import React, { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import React, { forwardRef } from "react";
//import DatePicker from "react-datepicker";
//import "react-datepicker/src/stylesheets/datepicker.scss";
import { Space, TimePicker } from "antd";
import dayjs from "../../utils/day";
import { fuzzyMatchDate } from "./formats.js";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
//To be used as a form element only.
const DateTimePicker = ({ value, onChange, onBlur, id, onlyFuture, onlyToday, isDateOnly = false, ...restProps }) => {
const [isManualInput, setIsManualInput] = useState(false);
const { t } = useTranslation();
const handleChange = useCallback(
(newDate) => {
if (onChange) {
onChange(newDate || null);
}
setIsManualInput(false);
},
[onChange]
);
const handleBlur = useCallback(
(e) => {
// Bail if this is not a manual input
if (!isManualInput) {
return;
}
// Reset manual input flag
setIsManualInput(false);
const v = e?.target?.value;
if (!v) return;
let parsedDate = isDateOnly ? fuzzyMatchDate(v)?.startOf("day") : fuzzyMatchDate(v);
if (parsedDate && onChange) {
onChange(parsedDate);
}
},
[isManualInput, isDateOnly, onChange]
);
const handleKeyDown = useCallback(
(e) => {
setIsManualInput(true);
if (e.key.toLowerCase() === "t" && onChange) {
e.preventDefault();
setIsManualInput(false);
onChange(dayjs());
} else if (e.key.toLowerCase() === "enter") {
handleBlur(e);
}
},
[onChange, handleBlur]
);
const handleDisabledDate = useCallback(
(current) => {
if (onlyToday) {
return !dayjs().isSame(current, "day");
} else if (onlyFuture) {
return dayjs().subtract(1, "day").isAfter(current);
}
return false;
},
[onlyToday, onlyFuture]
);
const DateTimePicker = ({ value, onChange, onBlur, id, onlyFuture, ...restProps }, ref) => {
// const handleChange = (newDate) => {
// if (value !== newDate && onChange) {
// onChange(newDate);
// }
// };
return (
<div onKeyDown={handleKeyDown} id={id} style={{ width: "100%" }}>
<DatePicker
showTime={
isDateOnly
? false
: {
format: "hh:mm a",
minuteStep: 15,
defaultValue: dayjs(dayjs(), "HH:mm:ss")
}
}
format={isDateOnly ? "MM/DD/YYYY" : "MM/DD/YYYY hh:mm a"}
<Space direction="vertical" style={{ width: "100%" }} id={id}>
<FormDatePicker
{...restProps}
{...(onlyFuture && {
disabledDate: (d) => dayjs().subtract(1, "day").isAfter(d)
})}
value={value}
onBlur={onBlur}
onChange={onChange}
onlyFuture={onlyFuture}
isDateOnly={false}
/>
<TimePicker
value={value ? dayjs(value) : null}
onChange={handleChange}
placeholder={isDateOnly ? t("general.labels.date") : t("general.labels.datetime")}
onBlur={onBlur || handleBlur}
disabledDate={handleDisabledDate}
{...(onlyFuture && {
disabledDate: (d) => dayjs().isAfter(d)
})}
onChange={onChange}
disableSeconds={true}
minuteStep={15}
onBlur={onBlur}
format="hh:mm a"
{...restProps}
/>
</div>
</Space>
);
};
DateTimePicker.propTypes = {
value: PropTypes.any,
onChange: PropTypes.func,
onBlur: PropTypes.func,
id: PropTypes.string,
onlyFuture: PropTypes.bool,
onlyToday: PropTypes.bool,
isDateOnly: PropTypes.bool
};
export default React.memo(DateTimePicker);
export default forwardRef(DateTimePicker);

View File

@@ -1,63 +0,0 @@
import dayjs from "../../utils/day";
const dateFormats = [
"MMDDYYYY",
"MMDDYY",
"M/D/YYYY",
"MM/D/YYYY",
"M/DD/YYYY",
"MM/DD/YYYY",
"M/D/YY",
"MM/D/YY",
"M/DD/YY",
"MM/DD/YY"
];
const timeFormats = ["h:mm A", "h:mmA", "h A", "hA", "hh:mm A", "hh:mm:ss A"];
const dateTimeFormats = [
...["M/D/YYYY", "MM/D/YYYY", "M/DD/YYYY", "MM/DD/YYYY", "M/D/YY", "MM/D/YY", "M/DD/YY", "MM/DD/YY"].flatMap(
(dateFormat) => timeFormats.map((timeFormat) => `${dateFormat} ${timeFormat}`)
),
...["MMDDYYYY", "MMDDYY"].flatMap((dateFormat) => timeFormats.map((timeFormat) => `${dateFormat} ${timeFormat}`)),
"M/D/YYYY",
"MM/D/YYYY",
"M/DD/YYYY",
"MM/DD/YYYY",
"M/D/YY",
"MM/D/YY",
"M/DD/YY",
"MM/DD/YY",
"MMDDYYYY",
"MMDDYY"
];
const sanitizeInput = (input) =>
input
.trim()
.toUpperCase()
.replace(/\s*(am|pm)\s*/i, " $1")
.replaceAll(".", "/")
.replaceAll("-", "/");
export const fuzzyMatchDate = (dateString) => {
const sanitizedInput = sanitizeInput(dateString);
for (const format of dateFormats) {
const parsedDate = dayjs(sanitizedInput, format, true);
if (parsedDate.isValid()) {
return parsedDate;
}
}
for (const format of dateTimeFormats) {
const parsedDateTime = dayjs(sanitizedInput, format, true);
if (parsedDateTime.isValid()) {
return parsedDateTime; // Return the dayjs object
}
}
return null; // If no matching format is found
};

View File

@@ -13,6 +13,7 @@ import Icon, {
FileFilled,
HomeFilled,
ImportOutlined,
InfoCircleOutlined,
LineChartOutlined,
PaperClipOutlined,
PhoneOutlined,
@@ -26,8 +27,8 @@ import Icon, {
UserOutlined
} from "@ant-design/icons";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Layout, Menu } from "antd";
import React from "react";
import { Layout, Menu, Switch, Tooltip } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { BsKanban } from "react-icons/bs";
import { FaCalendarAlt, FaCarCrash, FaCreditCard, FaFileInvoiceDollar, FaTasks } from "react-icons/fa";
@@ -42,6 +43,7 @@ import { selectRecentItems, selectSelectedHeader } from "../../redux/application
import { setModalContext } from "../../redux/modals/modals.actions";
import { signOutStart } from "../../redux/user/user.actions";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import { checkBeta, handleBeta, setBeta } from "../../utils/handleBeta";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
@@ -113,21 +115,19 @@ function Header({
names: ["ImEXPay", "DmsAp", "Simple_Inventory"],
splitKey: bodyshop && bodyshop.imexshopid
});
const [betaSwitch, setBetaSwitch] = useState(false);
const { t } = useTranslation();
const deleteBetaCookie = () => {
const cookieExists = document.cookie.split("; ").some((row) => row.startsWith(`betaSwitchImex=`));
if (cookieExists) {
const domain = window.location.hostname.split(".").slice(-2).join(".");
document.cookie = `betaSwitchImex=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; domain=.${domain}`;
console.log(`betaSwitchImex cookie deleted`);
} else {
console.log(`betaSwitchImex cookie does not exist`);
}
};
useEffect(() => {
const isBeta = checkBeta();
setBetaSwitch(isBeta);
}, []);
deleteBetaCookie();
const betaSwitchChange = (checked) => {
setBeta(checked);
setBetaSwitch(checked);
handleBeta();
};
const accountingChildren = [];
@@ -695,6 +695,31 @@ function Header({
}
];
InstanceRenderManager({
executeFunction: true,
args: [],
imex: () => {
menuItems.push({
key: "beta-switch",
id: "header-beta-switch",
style: { marginLeft: "auto" },
label: (
<Tooltip
title={`A more modern ${InstanceRenderManager({
imex: t("titles.imexonline"),
rome: t("titles.romeonline"),
promanager: t("titles.promanager")
})} is ready for you to try! You can switch back at any time.`}
>
<InfoCircleOutlined />
<span style={{ marginRight: 8 }}>Try the new app</span>
<Switch checked={betaSwitch} onChange={betaSwitchChange} />
</Tooltip>
)
});
}
});
return (
<Layout.Header>
<Menu

View File

@@ -10,8 +10,8 @@ import {
QUERY_SCOREBOARD_ENTRY,
UPDATE_SCOREBOARD_ENTRY
} from "../../graphql/scoreboard.queries";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
export default function ScoreboardAddButton({ job, disabled, ...otherBtnProps }) {
const { t } = useTranslation();
@@ -86,7 +86,7 @@ export default function ScoreboardAddButton({ job, disabled, ...otherBtnProps })
}
]}
>
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item
label={t("scoreboard.fields.bodyhrs")}

View File

@@ -5,6 +5,7 @@ import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
@@ -19,14 +20,7 @@ const mapStateToProps = createStructuredSelector({
});
const mapDispatchToProps = (dispatch) => ({
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(
insertAuditTrail({
jobid,
operation,
type
})
)
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
});
export default connect(mapStateToProps, mapDispatchToProps)(JobsAdminDatesChange);
@@ -93,7 +87,7 @@ export function JobsAdminDatesChange({ insertAuditTrail, job }) {
<FormFieldsChanged form={form} />
<LayoutFormRow header={t("jobs.forms.estdates")}>
<Form.Item label={t("jobs.fields.date_estimated")} name="date_estimated">
<DateTimePicker format="MM/DD/YYYY" isDateOnly />
<FormDatePicker format="MM/DD/YYYY" />
</Form.Item>
<Form.Item label={t("jobs.fields.date_towin")} name="date_towin">
<DateTimePicker />

View File

@@ -1,9 +1,18 @@
import { Collapse, Form, Input, InputNumber, Select, Space, Switch } from "antd";
import {
Collapse,
Form,
Input,
InputNumber,
Select,
Space,
Switch,
} from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component";
import FormItemPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
@@ -20,7 +29,6 @@ import JobsDetailRatesTaxes from "../jobs-detail-rates/jobs-detail-rates.taxes.c
import JobsMarkPstExempt from "../jobs-mark-pst-exempt/jobs-mark-pst-exempt.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
@@ -53,7 +61,10 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
<Form.Item label={t("jobs.fields.policy_no")} name="policy_no">
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.regie_number")} name="regie_number">
<Form.Item
label={t("jobs.fields.regie_number")}
name="regie_number"
>
<Input />
</Form.Item>
<Form.Item label={t("jobs.fields.ins_co_nm")} name="ins_co_nm">
@@ -105,7 +116,7 @@ export function JobsCreateJobsInfo({ bodyshop, form, selected }) {
<FormItemEmail email={getFieldValue("ins_ea")} />
</Form.Item>
<Form.Item label={t("jobs.fields.loss_date")} name="loss_date">
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item label={t("jobs.fields.kmin")} name="kmin">
<Input />

View File

@@ -2,9 +2,9 @@ import { Form, Input } from "antd";
import React, { useContext } from "react";
import { useTranslation } from "react-i18next";
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import JobsCreateVehicleInfoPredefined from "./jobs-create-vehicle-info.predefined.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
export default function JobsCreateVehicleInfoNewComponent({ form }) {
const [state] = useContext(JobCreateContext);
@@ -113,7 +113,7 @@ export default function JobsCreateVehicleInfoNewComponent({ form }) {
<Input disabled={!state.vehicle.new} />
</Form.Item>
<Form.Item label={t("vehicles.fields.v_prod_dt")} name={["vehicle", "data", "v_prod_dt"]}>
<DateTimePicker isDateOnly disabled={!state.vehicle.new} />
<FormDatePicker disabled={!state.vehicle.new} />
</Form.Item>
</LayoutFormRow>
<LayoutFormRow grow>

View File

@@ -5,6 +5,7 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
import FormRow from "../layout-form-row/layout-form-row.component";
@@ -29,7 +30,7 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
<div>
<FormRow header={t("jobs.forms.estdates")}>
<Form.Item label={t("jobs.fields.date_estimated")} name="date_estimated">
<DateTimePicker disabled={jobRO} isDateOnly />
<FormDatePicker disabled={jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.date_open")} name="date_open">
<DateTimePicker disabled={jobRO} />
@@ -44,7 +45,7 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
<FormRow header={t("jobs.forms.scheddates")}>
<Form.Item label={t("jobs.fields.date_scheduled")} name="date_scheduled">
<DateTimePicker disabled={jobRO} isDateOnly />
<FormDatePicker disabled={jobRO} />
</Form.Item>
<Tooltip title={t("jobs.labels.scheduledinchange")}>
<Form.Item label={t("jobs.fields.scheduled_in")} name="scheduled_in">
@@ -84,6 +85,7 @@ export function JobsDetailDatesComponent({ jobRO, job, bodyshop }) {
rules={[
{
required: jobInPostProduction
//message: t("general.validation.required"),
}
]}
>

View File

@@ -5,6 +5,7 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { selectBodyshop } from "../../redux/user/user.selectors";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component";
import FormItemPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
@@ -12,7 +13,6 @@ import Car from "../job-damage-visual/job-damage-visual.component";
import JobsDetailChangeEstimator from "../jobs-detail-change-estimator/jobs-detail-change-estimator.component";
import JobsDetailChangeFileHandler from "../jobs-detail-change-filehandler/jobs-detail-change-filehandler.component";
import FormRow from "../layout-form-row/layout-form-row.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly,
@@ -152,7 +152,7 @@ export function JobsDetailGeneral({ bodyshop, jobRO, job, form }) {
<Input disabled={jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.loss_date")} name="loss_date">
<DateTimePicker isDateOnly disabled={jobRO} />
<FormDatePicker disabled={jobRO} />
</Form.Item>
<Form.Item label={t("jobs.fields.loss_of_use")} name="loss_of_use">
<Input disabled={jobRO} />

View File

@@ -23,7 +23,7 @@ export function PartnerPingComponent({ bodyshop }) {
// Execute the created function directly
checkPartnerStatus(bodyshop);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [bodyshop?.id]);
}, [bodyshop]);
return <></>;
}

View File

@@ -8,8 +8,8 @@ import { logImEXEvent } from "../../firebase/firebase.utils";
import { MUTATION_UPDATE_BO_ETA } from "../../graphql/parts-orders.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { DateFormatter } from "../../utils/DateFormatter";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import { CalendarFilled } from "@ant-design/icons";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -62,7 +62,7 @@ export function PartsOrderBackorderEta({
<div>
<Form form={form} onFinish={handleFinish}>
<Form.Item name="eta">
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Button type="primary" onClick={() => form.submit()}>
{t("general.actions.save")}

View File

@@ -7,7 +7,7 @@ import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { MUTATION_BACKORDER_PART_LINE } from "../../graphql/parts-orders.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -71,7 +71,7 @@ export function PartsOrderLineBackorderButton({ partsOrderStatus, partsLineId, j
<div>
<Form form={form} onFinish={handleFinish}>
<Form.Item name="eta">
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Button type="primary" onClick={() => form.submit()}>
{t("parts_orders.actions.backordered")}

View File

@@ -1,7 +1,8 @@
import { DeleteFilled } from "@ant-design/icons";
import { DeleteFilled, EyeFilled } from "@ant-design/icons";
import { PageHeader } from "@ant-design/pro-layout";
import { useLazyQuery, useMutation } from "@apollo/client";
import { Button, Drawer, Grid, Popconfirm, Space, Table } from "antd";
import queryString from "query-string";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -82,34 +83,47 @@ export function PartsOrderListTableDrawerComponent({
sortedInfo: {}
});
const [billData, setBillData] = useState(null);
const [returnfrombill, setReturnFromBill] = useState();
const [billData, setBillData] = useState();
const search = queryString.parse(useLocation().search);
const selectedpartsorder = search.partsorderid;
const [billQuery] = useLazyQuery(QUERY_BILL_BY_PK);
const [deletePartsOrder] = useMutation(DELETE_PARTS_ORDER);
const parts_orders = billsQuery.data ? billsQuery.data.parts_orders : [];
const { refetch } = billsQuery;
const [billQuery] = useLazyQuery(QUERY_BILL_BY_PK);
const selectedPartsOrderRecord = parts_orders.find((r) => r.id === selectedpartsorder);
useEffect(() => {
const fetchData = async () => {
if (selectedPartsOrderRecord?.returnfrombill) {
try {
const { data } = await billQuery({
variables: { billid: selectedPartsOrderRecord.returnfrombill }
});
setBillData(data);
} catch (error) {
console.error("Error fetching bill data:", error);
}
} else setBillData(null);
};
fetchData();
}, [selectedPartsOrderRecord, billQuery]);
if (returnfrombill === null) {
setBillData(null);
} else {
const fetchData = async () => {
const result = await billQuery({
variables: { billid: returnfrombill }
});
setBillData(result.data);
};
fetchData();
}
}, [returnfrombill, billQuery]);
const recordActions = (record) => (
const recordActions = (record, showView = false) => (
<Space direction="horizontal" wrap>
{showView && (
<Button
onClick={() => {
if (record.returnfrombill) {
setReturnFromBill(record.returnfrombill);
} else {
setReturnFromBill(null);
}
handleOnRowClick(record);
}}
>
<EyeFilled />
</Button>
)}
<Button
disabled={jobRO || record.return || record.vendor.id === bodyshop.inhousevendorid}
onClick={() => {
@@ -119,14 +133,16 @@ export function PartsOrderListTableDrawerComponent({
context: {
jobId: job.id,
job: job,
partsorderlines: record.parts_order_lines.map((pol) => ({
joblineid: pol.job_line_id,
id: pol.id,
line_desc: pol.line_desc,
quantity: pol.quantity,
act_price: pol.act_price,
oem_partno: pol.oem_partno
}))
partsorderlines: record.parts_order_lines.map((pol) => {
return {
joblineid: pol.job_line_id,
id: pol.id,
line_desc: pol.line_desc,
quantity: pol.quantity,
act_price: pol.act_price,
oem_partno: pol.oem_partno
};
})
}
});
}}
@@ -151,6 +167,7 @@ export function PartsOrderListTableDrawerComponent({
disabled={jobRO}
onConfirm={async () => {
//Delete the parts return.!
await deletePartsOrder({
variables: { partsOrderId: record.id },
update(cache) {
@@ -174,6 +191,7 @@ export function PartsOrderListTableDrawerComponent({
disabled={(jobRO ? !record.return : jobRO) || record.vendor.id === bodyshop.inhousevendorid}
onClick={() => {
logImEXEvent("parts_order_receive_bill");
setBillEnterContext({
actions: { refetch: refetch },
context: {
@@ -181,20 +199,24 @@ export function PartsOrderListTableDrawerComponent({
bill: {
vendorid: record.vendor.id,
is_credit_memo: record.return,
billlines: record.parts_order_lines.map((pol) => ({
joblineid: pol.job_line_id || "noline",
line_desc: pol.line_desc,
quantity: pol.quantity,
actual_price: pol.act_price,
cost_center: pol.jobline?.part_type
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
? pol.jobline.part_type !== "PAE"
? pol.jobline.part_type
: null
: responsibilityCenters.defaults &&
(responsibilityCenters.defaults.costs[pol.jobline.part_type] || null)
: null
}))
billlines: record.parts_order_lines.map((pol) => {
return {
joblineid: pol.job_line_id || "noline",
line_desc: pol.line_desc,
quantity: pol.quantity,
actual_price: pol.act_price,
cost_center: pol.jobline?.part_type
? bodyshop.pbs_serialnumber || bodyshop.cdk_dealerid
? pol.jobline.part_type !== "PAE"
? pol.jobline.part_type
: null
: responsibilityCenters.defaults &&
(responsibilityCenters.defaults.costs[pol.jobline.part_type] || null)
: null
};
})
}
}
});
@@ -221,6 +243,8 @@ export function PartsOrderListTableDrawerComponent({
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
};
const selectedPartsOrderRecord = parts_orders.find((r) => r.id === selectedpartsorder);
const rowExpander = (record) => {
const columns = [
{

View File

@@ -6,12 +6,12 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
import PartsOrderModalPriceChange from "./parts-order-modal-price-change.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -74,7 +74,7 @@ export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState,
]}
label={t("parts_orders.fields.deliver_by")}
>
<DateTimePicker isDateOnly onlyFuture />
<FormDatePicker onlyFuture />
</Form.Item>
{job && job.special_coverage_policy && (
<Tag color="tomato">

View File

@@ -5,11 +5,11 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import DatePickerFormItem from "../form-date-picker/form-date-picker.component";
import CurrencyInput from "../form-items-formatted/currency-form-item.component";
import JobSearchSelect from "../job-search-select/job-search-select.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import PaymentFormTotalPayments from "./payment-form.totalpayments.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -77,7 +77,7 @@ export function PaymentFormComponent({ form, bodyshop, disabled }) {
}
]}
>
<DateTimePicker isDateOnly disabled={disabled} />
<DatePickerFormItem disabled={disabled} />
</Form.Item>
</LayoutFormRow>

View File

@@ -6,11 +6,11 @@ import {
PauseCircleOutlined
} from "@ant-design/icons";
import { Card, Col, Row, Space, Tooltip } from "antd";
import Dinero from "dinero.js";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import Dinero from "dinero.js";
import ProductionAlert from "../production-list-columns/production-list-columns.alert.component";
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
@@ -18,8 +18,8 @@ import ProductionSubletsManageComponent from "../production-sublets-manage/produ
import dayjs from "../../utils/day";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
import JobPartsQueueCount from "../job-parts-queue-count/job-parts-queue-count.component";
const cardColor = (ssbuckets, totalHrs) => {
const bucket = ssbuckets.find((bucket) => bucket.gte <= totalHrs && (!bucket.lt || bucket.lt > totalHrs));
@@ -213,13 +213,21 @@ const EstimatorToolTip = ({ metadata, cardSettings }) => {
};
const SubtotalTooltip = ({ metadata, cardSettings, t }) => {
const dineroAmount = Dinero(metadata?.job_totals?.totals?.subtotal ?? Dinero()).toFormat();
const amount = metadata?.job_totals?.totals?.subtotal?.amount;
const dineroAmount = amount ? Dinero({ amount: parseInt(amount * 100) }).toFormat("0,0.00") : null;
return (
cardSettings?.subtotal && (
<Col span={cardSettings.compact ? 24 : 12}>
<EllipsesToolTip title={`${dineroAmount}`} kiosk={cardSettings.kiosk}>
{dineroAmount}
<EllipsesToolTip
title={!!amount ? `${t("production.statistics.currency_symbol")}${dineroAmount}` : null}
kiosk={cardSettings.kiosk}
>
{!!amount ? (
<span>{`${t("production.statistics.currency_symbol")}${dineroAmount}`}</span>
) : (
<span>&nbsp;</span>
)}
</EllipsesToolTip>
</Col>
)

View File

@@ -1,10 +1,9 @@
import dayjs from "../../utils/day";
import { SyncOutlined } from "@ant-design/icons";
import { useApolloClient } from "@apollo/client";
import Board from "./trello-board/index";
import { Button, notification, Skeleton, Space } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import React, { useCallback, useContext, useEffect, useMemo, useState } from "react";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -24,7 +23,6 @@ import cloneDeep from "lodash/cloneDeep";
import isEqual from "lodash/isEqual";
import { defaultFilters, mergeWithDefaults } from "./settings/defaultKanbanSettings.js";
import NoteUpsertModal from "../../components/note-upsert-modal/note-upsert-modal.container";
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -47,136 +45,10 @@ function ProductionBoardKanbanComponent({ data, bodyshop, refetch, insertAuditTr
const [loading, setLoading] = useState(true);
const [isMoving, setIsMoving] = useState(false);
const [orientation, setOrientation] = useState("vertical");
const { socket } = useContext(SocketContext); // Access socket from the context
const { t } = useTranslation();
const client = useApolloClient();
const handleJobUpdated = useCallback(
(updatedJob) => {
setBoardLanes((prevBoardLanes) => {
const updatedLanes = cloneDeep(prevBoardLanes.lanes);
// Find the lane containing the card with the updated job ID
let sourceLane = updatedLanes.find((lane) => lane.cards.some((card) => card.id === updatedJob.id));
if (!sourceLane) {
console.log("Card not found in any lane. Checking for valid status to add it.");
// Find the target lane based on the new status if the card does not exist
const targetLane = updatedLanes.find((lane) => lane.id === updatedJob.status);
if (targetLane && updatedJob.isInProduction) {
// Check if job is in production and status is valid
console.log(`Adding card to lane ${targetLane.title}`);
// Add the new card to the target lane
const newCard = {
id: updatedJob.id,
metadata: { ...updatedJob }
};
targetLane.cards.push(newCard);
// Update the lane title with the new card count
targetLane.title = `${targetLane.title.split(" ")[0]} (${targetLane.cards.length})`;
return { lanes: updatedLanes }; // Return early since the card is added
} else {
console.error("No valid lane or status to add the job to.");
return prevBoardLanes; // Return the previous state if no valid status or lane
}
}
// If the card exists, find it in the source lane
const cardIndex = sourceLane.cards.findIndex((card) => card.id === updatedJob.id);
const currentCard = sourceLane.cards[cardIndex];
// If we somehow can't find the card, return
if (!currentCard) {
console.error("Card not found for the updated job.");
return prevBoardLanes; // Return the previous state if the card is not found
}
// Iterate through the properties of updatedJob and update the corresponding values in currentCard.metadata
Object.keys(updatedJob).forEach((key) => {
// Normalize date fields by comparing their ISO strings or timestamps
if (key === "updated_at") {
const currentCardDate = dayjs(currentCard.metadata[key]).toISOString();
const updatedJobDate = dayjs(updatedJob[key]).toISOString();
if (currentCardDate !== updatedJobDate) {
console.log(`Updating ${key} from ${currentCardDate} to ${updatedJobDate}`);
currentCard.metadata[key] = updatedJob[key]; // Assign the new value if different
}
} else if (key in currentCard.metadata && currentCard.metadata[key] !== updatedJob[key]) {
console.log(`Updating ${key} from ${currentCard.metadata[key]} to ${updatedJob[key]}`);
currentCard.metadata[key] = updatedJob[key];
}
});
// Mark that data has been changed if any field was updated
const isDataChanged = !isEqual(currentCard.metadata, updatedJob);
// Check if the lane (status) has changed
const isLaneChanged = updatedJob.status !== sourceLane.id;
// Case 1: Both data and lane have changed
if (isDataChanged && isLaneChanged) {
console.log("Case 1: Data and Lane Changed");
// Remove the card from the source lane
const [cardToMove] = sourceLane.cards.splice(cardIndex, 1);
// Find the target lane based on the new status
const targetLane = updatedLanes.find((lane) => lane.id === updatedJob.status);
if (targetLane) {
targetLane.cards.push({ ...cardToMove, metadata: { ...currentCard.metadata } });
sourceLane.title = `${sourceLane.title.split(" ")[0]} (${sourceLane.cards.length})`;
targetLane.title = `${targetLane.title.split(" ")[0]} (${targetLane.cards.length})`;
} else {
console.error("Target lane not found for the updated job.");
}
}
// Case 2: Only data has changed
else if (isDataChanged && !isLaneChanged) {
console.log("Case 2: Only Data Changed");
sourceLane.cards[cardIndex] = { ...currentCard, metadata: { ...currentCard.metadata } };
}
// Case 3: Only the lane has changed
else if (!isDataChanged && isLaneChanged) {
console.log("Case 3: Only Lane Changed");
// Remove the card from the source lane
const [cardToMove] = sourceLane.cards.splice(cardIndex, 1);
// Find the target lane based on the new status
const targetLane = updatedLanes.find((lane) => lane.id === updatedJob.status);
if (targetLane) {
targetLane.cards.push(cardToMove);
sourceLane.title = `${sourceLane.title.split(" ")[0]} (${sourceLane.cards.length})`;
targetLane.title = `${targetLane.title.split(" ")[0]} (${targetLane.cards.length})`;
} else {
console.error("Target lane not found for the updated job.");
}
}
return { lanes: updatedLanes };
});
},
[setBoardLanes]
);
useEffect(() => {
// Listen for the job-updated event from the socket
if (socket) {
socket.on("job-updated", handleJobUpdated);
return () => {
socket.off("job-updated", handleJobUpdated);
};
}
}, [socket, handleJobUpdated]);
useEffect(() => {
if (associationSettings) {
setLoading(true);

View File

@@ -28,23 +28,22 @@ function ProductionBoardKanbanContainer({ bodyshop, currentUser }) {
onError: (error) => console.error(`Error fetching jobs in production: ${error.message}`)
});
// const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION, {
// onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`)
// });
const { data: updatedJobs } = useSubscription(SUBSCRIPTION_JOBS_IN_PRODUCTION, {
onError: (error) => console.error(`Error subscribing to jobs in production: ${error.message}`)
});
const { loading: associationSettingsLoading, data: associationSettings } = useQuery(QUERY_KANBAN_SETTINGS, {
variables: { email: currentUser.email },
onError: (error) => console.error(`Error fetching Kanban settings: ${error.message}`)
});
// This provides us the current version of the Lanes from the Redux store
// const currentReducerData = useSelector((state) => (state.trello.lanes ? state.trello : {}));
// useEffect(() => {
// if (updatedJobs && data) {
// refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`));
// }
// }, [updatedJobs, data, refetch]);
useEffect(() => {
if (updatedJobs && data) {
refetch().catch((err) => console.error(`Error re-fetching jobs in production: ${err.message}`));
}
}, [updatedJobs, data, refetch]);
const filteredAssociationSettings = useMemo(() => {
return associationSettings?.associations[0] || null;

View File

@@ -3,7 +3,6 @@ import { Card, Statistic } from "antd";
import { useTranslation } from "react-i18next";
import PropTypes from "prop-types";
import { defaultKanbanSettings, statisticsItems } from "./settings/defaultKanbanSettings.js";
import Dinero from "dinero.js";
export const StatisticType = {
HOURS: "hours",
@@ -33,21 +32,7 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
};
const calculateTotalAmount = (items, key) => {
return items.reduce((acc, item) => acc.add(Dinero(item[key]?.totals?.subtotal ?? Dinero())), Dinero({ amount: 0 }));
};
const calculateReducerTotalAmount = (lanes, key) => {
return lanes.reduce(
(acc, lane) => {
return acc.add(
lane.cards.reduce(
(laneAcc, card) => laneAcc.add(Dinero(card.metadata[key]?.totals?.subtotal ?? Dinero())),
Dinero({ amount: 0 })
)
);
},
Dinero({ amount: 0 })
);
return items.reduce((acc, item) => acc + (item[key]?.totals?.subtotal?.amount || 0), 0);
};
const calculateReducerTotal = (lanes, key, subKey) => {
@@ -58,6 +43,14 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
}, 0);
};
const calculateReducerTotalAmount = (lanes, key) => {
return lanes.reduce((acc, lane) => {
return (
acc + lane.cards.reduce((laneAcc, card) => laneAcc + (card.metadata[key]?.totals?.subtotal?.amount || 0), 0)
);
}, 0);
};
const formatValue = (value, type) => {
if (type === StatisticType.JOBS) {
return value.toFixed(0);
@@ -94,15 +87,9 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
const totalAmountInProduction = useMemo(() => {
if (!cardSettings.totalAmountInProduction) return null;
const total = calculateTotalAmount(data, "job_totals");
return total.toFormat("$0,0.00");
return parseFloat(total.toFixed(2));
}, [data, cardSettings.totalAmountInProduction]);
const totalAmountOnBoard = useMemo(() => {
if (!reducerData || !cardSettings.totalAmountOnBoard) return null;
const total = calculateReducerTotalAmount(reducerData.lanes, "job_totals");
return total.toFormat("$0,0.00");
}, [reducerData, cardSettings.totalAmountOnBoard]);
const totalHrsOnBoard = useMemo(() => {
if (!reducerData || !cardSettings.totalHrsOnBoard) return null;
const total =
@@ -131,6 +118,12 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
[reducerData, cardSettings.jobsOnBoard]
);
const totalAmountOnBoard = useMemo(() => {
if (!reducerData || !cardSettings.totalAmountOnBoard) return null;
const total = calculateReducerTotalAmount(reducerData.lanes, "job_totals");
return parseFloat(total.toFixed(2));
}, [reducerData, cardSettings.totalAmountOnBoard]);
const tasksInProduction = useMemo(() => {
if (!data || !cardSettings.tasksInProduction) return null;
return data.reduce((acc, item) => acc + (item.tasks_aggregate?.aggregate?.count || 0), 0);
@@ -198,6 +191,7 @@ const ProductionStatistics = ({ data, cardSettings, reducerData }) => {
<Statistic
title={t(`production.statistics.${stat.label}`)}
value={formatValue(stat.value, stat.type)}
prefix={stat.type === StatisticType.AMOUNT ? t("production.statistics.currency_symbol") : undefined}
suffix={
stat.type === StatisticType.HOURS
? t("production.statistics.hours")

View File

@@ -28,11 +28,6 @@ import ProductionListColumnCategory from "./production-list-columns.status.categ
import ProductionListColumnStatus from "./production-list-columns.status.component";
import ProductionListColumnTouchTime from "./prodution-list-columns.touchtime.component";
const getEmployeeName = (employeeId, employees) => {
const employee = employees.find((e) => e.id === employeeId);
return employee ? `${employee.first_name} ${employee.last_name}` : "";
};
const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatments }) => {
const { Enhanced_Payroll } = treatments;
return [
@@ -431,8 +426,8 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
sortOrder: state.sortedInfo.columnKey === "employee_body" && state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
getEmployeeName(a.employee_body, bodyshop.employees),
getEmployeeName(b.employee_body, bodyshop.employees)
bodyshop.employees?.find((e) => e.id === a.employee_body)?.first_name,
bodyshop.employees?.find((e) => e.id === b.employee_body)?.first_name
),
render: (text, record) => (
<ProductionListEmployeeAssignment refetch={refetch} record={record} type="employee_body" />
@@ -445,8 +440,8 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
sortOrder: state.sortedInfo.columnKey === "employee_prep" && state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
getEmployeeName(a.employee_prep, bodyshop.employees),
getEmployeeName(b.employee_prep, bodyshop.employees)
bodyshop.employees?.find((e) => e.id === a.employee_prep)?.first_name,
bodyshop.employees?.find((e) => e.id === b.employee_prep)?.first_name
),
render: (text, record) => (
<ProductionListEmployeeAssignment record={record} refetch={refetch} type="employee_prep" />
@@ -465,8 +460,8 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
sortOrder: state.sortedInfo.columnKey === "employee_csr" && state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
getEmployeeName(a.employee_csr, bodyshop.employees),
getEmployeeName(b.employee_csr, bodyshop.employees)
bodyshop.employees?.find((e) => e.id === a.employee_csr)?.first_name,
bodyshop.employees?.find((e) => e.id === b.employee_csr)?.first_name
),
render: (text, record) => (
<ProductionListEmployeeAssignment refetch={refetch} record={record} type="employee_csr" />
@@ -479,8 +474,8 @@ const r = ({ technician, state, activeStatuses, data, bodyshop, refetch, treatme
sortOrder: state.sortedInfo.columnKey === "employee_refinish" && state.sortedInfo.order,
sorter: (a, b) =>
alphaSort(
getEmployeeName(a.employee_refinish, bodyshop.employees),
getEmployeeName(b.employee_refinish, bodyshop.employees)
bodyshop.employees?.find((e) => e.id === a.employee_refinish)?.first_name,
bodyshop.employees?.find((e) => e.id === b.employee_refinish)?.first_name
),
render: (text, record) => (
<ProductionListEmployeeAssignment record={record} refetch={refetch} type="employee_refinish" />

View File

@@ -1,12 +1,12 @@
import { useMutation } from "@apollo/client";
import { Button, Card, Dropdown, Space } from "antd";
import { Button, Card, Dropdown, Space, TimePicker } from "antd";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { DateFormatter } from "../../utils/DateFormatter";
import dayjs from "../../utils/day";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
export default function ProductionListDate({ record, field, time, pastIndicator }) {
const [updateAlert] = useMutation(UPDATE_JOB);
@@ -57,14 +57,22 @@ export default function ProductionListDate({ record, field, time, pastIndicator
label: (
<Card style={{ padding: "1rem" }} onClick={(e) => e.stopPropagation()}>
<Space direction={"vertical"}>
<DateTimePicker
<FormDatePicker
onClick={(e) => e.stopPropagation()}
value={(record[field] && dayjs(record[field])) || null}
onChange={handleChange}
format={time ? "MM/DD/YYYY hh:mm a" : "MM/DD/YYYY"}
format="MM/DD/YYYY"
isDateOnly={!time}
showTime={time ? { format: "hh:mm a", minuteStep: 15 } : false}
/>
{time && (
<TimePicker
onClick={(e) => e.stopPropagation()}
value={(record[field] && dayjs(record[field])) || null}
onChange={handleChange}
minuteStep={15}
format="hh:mm a"
/>
)}
<Button onClick={() => setOpen(false)}>{t("general.actions.close")}</Button>
</Space>
</Card>

View File

@@ -0,0 +1,93 @@
import { useMutation } from "@apollo/client";
import React, { useState } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop } from "../../redux/user/user.selectors";
import { Button, Form, Input, notification, Popover, Space } from "antd";
import { useTranslation } from "react-i18next";
import { UPDATE_SHOP } from "../../graphql/bodyshop.queries";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { isFunction } from "lodash";
const mapStateToProps = createStructuredSelector({
//currentUser: selectCurrentUser
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language))
});
export function ProductionListSaveConfigButton({ columns, bodyshop, tableState, onSave }) {
const [updateShop] = useMutation(UPDATE_SHOP);
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const [form] = Form.useForm();
const { t } = useTranslation();
const handleSaveConfig = async (values) => {
logImEXEvent("production_save_config");
setLoading(true);
const result = await updateShop({
variables: {
id: bodyshop.id,
shop: {
production_config: [
...bodyshop.production_config.filter((b) => b.name !== values.name),
//Assign it to the name
{
name: values.name,
columns: {
columnKeys: columns.map((i) => {
return { key: i.key, width: i.width };
}),
tableState
}
}
]
}
}
});
if (!!!result.errors) {
notification["success"]({ message: t("bodyshop.successes.save") });
if (onSave && isFunction(onSave)) {
onSave();
}
} else {
notification["error"]({
message: t("bodyshop.errors.saving", {
error: JSON.stringify(result.errors)
})
});
}
form.resetFields();
setOpen(false);
setLoading(false);
};
const popMenu = (
<div>
<Form layout="vertical" form={form} onFinish={handleSaveConfig}>
<Form.Item label={t("production.labels.viewname")} name="name" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Space wrap>
<Button type="primary" danger onClick={() => form.submit()} loading={loading}>
{t("general.actions.save")}
</Button>
<Button onClick={() => setOpen(false)}>{t("general.actions.close")}</Button>
</Space>
</Form>
</div>
);
return (
<Popover open={open} content={popMenu}>
<Button loading={loading} onClick={() => setOpen(true)}>
{t("production.actions.saveconfig")}
</Button>
</Popover>
);
}
export default connect(mapStateToProps, mapDispatchToProps)(ProductionListSaveConfigButton);

View File

@@ -1,505 +0,0 @@
import { DeleteOutlined, ExclamationCircleOutlined, PlusOutlined } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Button, Form, Input, Modal, notification, Popconfirm, Popover, Select, Space } from "antd";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_ACTIVE_PROD_LIST_VIEW } from "../../graphql/associations.queries";
import { UPDATE_SHOP } from "../../graphql/bodyshop.queries";
import ProductionListColumns from "../production-list-columns/production-list-columns.data";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { isFunction } from "lodash";
const { confirm } = Modal;
export function ProductionListConfigManager({
refetch,
bodyshop,
technician,
currentUser,
state,
data,
columns,
setColumns,
setState,
onSave,
hasUnsavedChanges,
setHasUnsavedChanges
}) {
const { t } = useTranslation();
const [updateDefaultProdView] = useMutation(UPDATE_ACTIVE_PROD_LIST_VIEW);
const [updateShop] = useMutation(UPDATE_SHOP);
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const [isAddingNewProfile, setIsAddingNewProfile] = useState(false);
const [form] = Form.useForm();
const [activeView, setActiveView] = useState(() => {
const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email);
return assoc && assoc.default_prod_list_view;
});
const defaultState = {
sortedInfo: {
columnKey: "ro_number",
order: null
},
filteredInfo: {}
};
const ensureDefaultState = (state) => {
return {
sortedInfo: state?.sortedInfo || defaultState.sortedInfo,
filteredInfo: state?.filteredInfo || defaultState.filteredInfo,
...state
};
};
const createDefaultView = async () => {
const defaultConfig = {
name: t("production.constants.main_profile"),
columns: {
columnKeys: [
{ key: "ro_number", width: 100 },
{ key: "ownr", width: 100 },
{ key: "vehicle", width: 100 },
{ key: "ins_co_nm", width: 100 },
{ key: "actual_in", width: 100 },
{ key: "scheduled_completion", width: 100 },
{ key: "labhrs", width: 100 },
{ key: "employee_body", width: 100 },
{ key: "larhrs", width: 100 },
{ key: "employee_refinish", width: 100 },
{ key: "tt", width: 100 },
{ key: "status", width: 100 },
{ key: "sublets", width: 100 },
{ key: "viewdetail", width: 100 }
],
tableState: ensureDefaultState(state)
}
};
const result = await updateShop({
variables: {
id: bodyshop.id,
shop: {
production_config: [defaultConfig]
}
}
});
if (!result.errors) {
await updateActiveProdView(t("production.constants.main_profile"));
window.location.reload(); // Reload the page
} else {
notification.error({
message: t("bodyshop.errors.creatingdefaultview", {
error: JSON.stringify(result.errors)
})
});
}
};
const {
treatments: { Enhanced_Payroll }
} = useSplitTreatments({
attributes: {},
names: ["Enhanced_Payroll"],
splitKey: bodyshop.imexshopid
});
const updateActiveProdView = async (viewName) => {
const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email);
if (assoc) {
await updateDefaultProdView({
variables: { assocId: assoc.id, view: viewName },
update(cache) {
cache.modify({
id: cache.identify(bodyshop),
fields: {
associations(existingAssociations) {
return existingAssociations.map((a) => {
if (a.useremail !== currentUser.email) return a;
return { ...a, default_prod_list_view: viewName };
});
}
}
});
}
});
setActiveView(viewName);
setHasUnsavedChanges(false);
}
};
const handleSelect = async (value) => {
if (hasUnsavedChanges) {
confirm({
title: t("general.labels.unsavedchanges"),
icon: <ExclamationCircleOutlined />,
content: t("general.messages.unsavedchangespopup"),
onOk: () => proceedWithSelect(value),
onCancel() {
// Do nothing if canceled
}
});
} else {
await proceedWithSelect(value);
}
};
const proceedWithSelect = async (value) => {
if (value === "add_new") {
setIsAddingNewProfile(true);
setOpen(true);
return;
}
const selectedConfig = bodyshop.production_config.find((pc) => pc.name === value);
// If the selected profile doesn't exist, revert to the main profile
if (!selectedConfig) {
const mainProfileConfig = bodyshop.production_config.find(
(pc) => pc.name === t("production.constants.main_profile")
);
if (mainProfileConfig) {
await updateActiveProdView(t("production.constants.main_profile"));
setColumns(
mainProfileConfig.columns.columnKeys.map((k) => {
return {
...ProductionListColumns({
bodyshop,
refetch,
technician,
state: ensureDefaultState(state),
data: data,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
treatments: { Enhanced_Payroll }
}).find((e) => e.key === k.key),
width: k.width
};
})
);
const newState = ensureDefaultState(mainProfileConfig.columns.tableState);
setState(newState);
if (onSave && isFunction(onSave)) {
onSave();
}
return;
}
}
// If the selected profile exists, proceed as normal
if (selectedConfig) {
const newColumns = selectedConfig.columns.columnKeys.map((k) => {
return {
...ProductionListColumns({
bodyshop,
refetch,
technician,
state: ensureDefaultState(state),
data: data,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
treatments: { Enhanced_Payroll }
}).find((e) => e.key === k.key),
width: k.width
};
});
setColumns(newColumns);
const newState = ensureDefaultState(selectedConfig.columns.tableState);
setState(newState);
await updateActiveProdView(value);
if (onSave && isFunction(onSave)) {
onSave();
}
}
};
const handleTrash = async (name) => {
if (name === t("production.constants.main_profile")) return;
const remainingConfigs = bodyshop.production_config.filter((b) => b.name !== name);
await updateShop({
variables: {
id: bodyshop.id,
shop: {
production_config: remainingConfigs
}
},
awaitRefetchQueries: true
});
if (name === activeView) {
// Only switch profiles if the deleted profile was the active profile
if (remainingConfigs.length > 0) {
const nextConfig = remainingConfigs[0];
await updateActiveProdView(nextConfig.name);
setColumns(
nextConfig.columns.columnKeys.map((k) => {
return {
...ProductionListColumns({
technician,
state: ensureDefaultState(state),
refetch,
data: data,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
treatments: { Enhanced_Payroll }
}).find((e) => e.key === k.key),
width: k.width
};
})
);
setState(ensureDefaultState(nextConfig.columns.tableState));
} else {
await updateActiveProdView(null);
setColumns([]);
setState(defaultState);
}
} else {
// Revert back to the active view and load its columns and state
const activeConfig = bodyshop.production_config.find((pc) => pc.name === activeView);
if (activeConfig) {
await updateActiveProdView(activeView);
setColumns(
activeConfig.columns.columnKeys.map((k) => {
return {
...ProductionListColumns({
technician,
state: ensureDefaultState(state),
refetch,
data: data,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
treatments: { Enhanced_Payroll }
}).find((e) => e.key === k.key),
width: k.width
};
})
);
setState(ensureDefaultState(activeConfig.columns.tableState));
}
}
};
const handleSaveConfig = async (values) => {
logImEXEvent("production_save_config");
setLoading(true);
const profileName = isAddingNewProfile ? values.name : activeView;
const result = await updateShop({
variables: {
id: bodyshop.id,
shop: {
production_config: [
...bodyshop.production_config.filter((b) => b.name !== profileName),
{
name: profileName,
columns: {
columnKeys: columns.map((i) => ({ key: i.key, width: i.width })),
tableState: ensureDefaultState(state)
}
}
]
}
}
});
if (!result.errors) {
notification.success({ message: t("bodyshop.successes.save") });
if (isAddingNewProfile) {
await updateActiveProdView(profileName);
}
if (onSave && isFunction(onSave)) {
onSave();
}
setHasUnsavedChanges(false);
} else {
notification.error({
message: t("bodyshop.errors.saving", {
error: JSON.stringify(result.errors)
})
});
}
form.resetFields();
setOpen(false);
setLoading(false);
setIsAddingNewProfile(false);
};
useEffect(() => {
const validateAndSetDefaultView = () => {
const configExists = bodyshop.production_config.some((pc) => pc.name === activeView);
if (!configExists) {
// If the default view doesn't exist, revert to the main profile
const mainProfileConfig = bodyshop.production_config.find(
(pc) => pc.name === t("production.constants.main_profile")
);
if (mainProfileConfig) {
setActiveView(t("production.constants.main_profile"));
setColumns(
mainProfileConfig.columns.columnKeys.map((k) => {
return {
...ProductionListColumns({
bodyshop,
refetch,
technician,
state: ensureDefaultState(state),
data: data,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
treatments: { Enhanced_Payroll }
}).find((e) => e.key === k.key),
width: k.width
};
})
);
setState(ensureDefaultState(mainProfileConfig.columns.tableState));
updateActiveProdView(t("production.constants.main_profile"));
}
} else {
// If the default view exists, set it as active
setActiveView(activeView);
}
};
if (!bodyshop.production_config || bodyshop.production_config.length === 0) {
createDefaultView().catch((e) => {
console.error("Something went wrong saving the production list view Config.");
});
} else {
validateAndSetDefaultView();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeView, bodyshop.production_config]);
const popMenu = (
<div>
<Form layout="vertical" form={form} onFinish={handleSaveConfig}>
{isAddingNewProfile && (
<Form.Item
label={t("production.labels.viewname")}
name="name"
rules={[
{ required: true, message: t("production.errors.name_required") },
{
validator: (_, value) => {
if (!value) {
return Promise.resolve();
}
const nameExists = bodyshop.production_config.some((pc) => pc.name === value);
if (nameExists) {
return Promise.reject(new Error(t("production.errors.name_exists")));
}
return Promise.resolve();
}
}
]}
>
<Input />
</Form.Item>
)}
<Space wrap>
<Button
type="primary"
danger
onClick={() => form.submit()}
loading={loading}
disabled={form.getFieldsError().some(({ errors }) => errors.length)}
>
{t("general.actions.save")}
</Button>
{!isAddingNewProfile && (
<Button
type="default"
onClick={() => {
setIsAddingNewProfile(true);
setOpen(true);
}}
>
{t("general.actions.saveas")}
</Button>
)}
<Button
onClick={() => {
setIsAddingNewProfile(false);
setOpen(false);
}}
>
{t("general.actions.cancel")}
</Button>
</Space>
</Form>
</div>
);
return (
<Space>
<Button loading={loading} onClick={() => setOpen(true)} disabled={isAddingNewProfile || !hasUnsavedChanges}>
{t("production.actions.saveconfig")}
</Button>
<Popover open={open} content={popMenu} placement="bottom">
<Select
style={{
minWidth: "150px"
}}
onSelect={handleSelect}
placeholder={t("production.labels.selectview")}
optionLabelProp="label"
popupMatchSelectWidth={false}
value={activeView}
disabled={open || isAddingNewProfile} // Disable the Select box when the popover is open or adding a new profile
>
{bodyshop.production_config
.slice()
.sort((a, b) =>
a.name === t("production.constants.main_profile")
? -1
: b.name === t("production.constants.main_profile")
? 1
: 0
) //
.map((config) => (
<Select.Option key={config.name} label={config.name}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<span
style={{
flex: 1,
maxWidth: "80%",
marginRight: "1rem",
textOverflow: "ellipsis"
}}
>
{config.name}
</span>
{config.name !== t("production.constants.main_profile") && (
<Popconfirm
placement="right"
title={t("general.labels.areyousure")}
onConfirm={() => handleTrash(config.name)}
onCancel={(e) => e.stopPropagation()}
>
<DeleteOutlined onClick={(e) => e.stopPropagation()} />
</Popconfirm>
)}
</div>
</Select.Option>
))}
<Select.Option key="add_new" label={t("production.labels.addnewprofile")}>
<div style={{ display: "flex", alignItems: "center" }}>
<PlusOutlined style={{ marginRight: "0.5rem" }} />
{t("production.labels.addnewprofile")}
</div>
</Select.Option>
</Select>
</Popover>
</Space>
);
}

View File

@@ -0,0 +1,172 @@
import { DeleteOutlined } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Popconfirm, Select } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { UPDATE_ACTIVE_PROD_LIST_VIEW } from "../../graphql/associations.queries";
import { UPDATE_SHOP } from "../../graphql/bodyshop.queries";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import ProductionListColumns from "../production-list-columns/production-list-columns.data";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { isFunction } from "lodash";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
technician: selectTechnician,
currentUser: selectCurrentUser
});
export function ProductionListTable({
refetch,
bodyshop,
technician,
currentUser,
state,
data,
setColumns,
setState,
onProfileChange
}) {
const { t } = useTranslation();
const [updateDefaultProdView] = useMutation(UPDATE_ACTIVE_PROD_LIST_VIEW);
const [updateShop] = useMutation(UPDATE_SHOP);
const {
treatments: { Enhanced_Payroll }
} = useSplitTreatments({
attributes: {},
names: ["Enhanced_Payroll"],
splitKey: bodyshop.imexshopid
});
const handleSelect = async (value, option) => {
const newColumns = bodyshop.production_config
.filter((pc) => pc.name === value)[0]
.columns.columnKeys.map((k) => {
return {
...ProductionListColumns({
bodyshop,
refetch,
technician,
state,
data: data,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
treatments: { Enhanced_Payroll }
}).find((e) => e.key === k.key),
width: k.width
};
});
setColumns(newColumns);
const newState = bodyshop.production_config.filter((pc) => pc.name === value)[0].columns.tableState;
setState(newState);
const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email);
if (assoc) {
await updateDefaultProdView({
variables: { assocId: assoc.id, view: value },
update(cache) {
cache.modify({
id: cache.identify(bodyshop),
fields: {
associations(existingAssociations, { readField }) {
return existingAssociations.map((a) => {
if (a.useremail !== currentUser.email) return a;
return { ...a, default_prod_list_view: value };
});
}
}
});
}
});
}
if (onProfileChange && isFunction(onProfileChange)) {
onProfileChange({ value, option, newColumns, newState, assoc });
}
};
const handleTrash = async (name) => {
await updateShop({
variables: {
id: bodyshop.id,
shop: {
production_config: bodyshop.production_config.filter((b) => b.name !== name)
}
},
awaitRefetchQueries: true
});
setColumns(
bodyshop.production_config[0].columns.columnKeys.map((k) => {
return {
...ProductionListColumns({
technician,
state,
refetch,
data: data,
activeStatuses: bodyshop.md_ro_statuses.active_statuses,
treatments: { Enhanced_Payroll }
}).find((e) => e.key === k.key),
width: k.width
};
})
);
setState(bodyshop.production_config[0].columns.tableState);
};
const assoc = bodyshop.associations.find((a) => a.useremail === currentUser.email);
const defaultView = assoc && assoc.default_prod_list_view;
return (
<div style={{ width: "10rem" }}>
<Select
onSelect={handleSelect}
placeholder={t("production.labels.selectview")}
optionLabelProp="label"
popupMatchSelectWidth={false}
defaultValue={defaultView}
>
{bodyshop.production_config.map((config) => (
<Select.Option key={config.name} label={config.name}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center"
}}
>
<span
style={{
flex: 1,
maxWidth: "80%",
marginRight: "1rem",
textOverflow: "ellipsis"
}}
>
{config.name}
</span>
<Popconfirm
placement="right"
title={t("general.labels.areyousure")}
onConfirm={() => handleTrash(config.name)}
>
<DeleteOutlined
onClick={(e) => {
e.stopPropagation();
}}
/>
</Popconfirm>
</div>
</Select.Option>
))}
</Select>
</div>
);
}
export default connect(mapStateToProps, null)(ProductionListTable);

View File

@@ -1,23 +1,24 @@
import { SyncOutlined } from "@ant-design/icons";
import { PageHeader } from "@ant-design/pro-layout";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Button, Dropdown, Input, Space, Statistic, Table } from "antd";
import _ from "lodash";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Button, Dropdown, Input, Space, Statistic, Table } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import ReactDragListView from "react-drag-listview";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import Prompt from "../../utils/prompt.js";
import AlertComponent from "../alert/alert.component.jsx";
import ProductionListColumnsAdd from "../production-list-columns/production-list-columns.add.component";
import ProductionListColumns from "../production-list-columns/production-list-columns.data";
import ProductionListDetail from "../production-list-detail/production-list-detail.component";
import { ProductionListConfigManager } from "./production-list-config-manager.component.jsx";
import ProductionListSaveConfigButton from "../production-list-save-config-button/production-list-save-config-button.component";
import ProductionListPrint from "./production-list-print.component";
import ProductionListTableViewSelect from "./production-list-table-view-select.component";
import ResizeableTitle from "./production-list-table.resizeable.component";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { SyncOutlined } from "@ant-design/icons";
import Prompt from "../../utils/prompt.js";
import _ from "lodash";
import AlertComponent from "../alert/alert.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -43,7 +44,7 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
const initialStateRef = useRef(
(bodyshop.production_config &&
bodyshop.production_config.find((p) => p.name === defaultView)?.columns.tableState) ||
(bodyshop.production_config && bodyshop.production_config[0]?.columns.tableState) || {
bodyshop.production_config[0]?.columns.tableState || {
sortedInfo: {},
filteredInfo: { text: "" }
}
@@ -269,24 +270,24 @@ export function ProductionListTable({ loading, data, refetch, bodyshop, technici
data={data}
onColumnAdd={addColumn}
/>
<ProductionListConfigManager
<ProductionListSaveConfigButton
columns={columns}
setColumns={setColumns}
state={state}
setState={setState}
refetch={refetch}
data={data}
bodyshop={bodyshop}
technician={technician}
currentUser={currentUser}
setHasUnsavedChanges={setHasUnsavedChanges}
hasUnsavedChanges={hasUnsavedChanges}
tableState={state}
onSave={() => {
setHasUnsavedChanges(false);
initialStateRef.current = state;
}}
/>
<ProductionListTableViewSelect
state={state}
setState={setState}
setColumns={setColumns}
onProfileChange={() => {
initialStateRef.current = state;
setHasUnsavedChanges(false);
}}
refetch={refetch}
data={data}
/>
<Input
onChange={(e) => setSearchText(e.target.value)}
placeholder={t("general.labels.search")}

View File

@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
import { getOrderOperatorsByType, getWhereOperatorsByType } from "../../utils/graphQLmodifier";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import { generateInternalReflections } from "./report-center-modal-utils";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
import { FormDatePicker } from "../form-date-picker/form-date-picker.component.jsx";
export default function ReportCenterModalFiltersSortersComponent({ form, bodyshop }) {
return (
@@ -196,8 +196,7 @@ function FiltersSection({ filters, form, bodyshop }) {
// We have a type of date, so we will use a date picker
if (type === "date") {
return (
<DateTimePicker
isDateOnly
<FormDatePicker
disabled={!operator}
onChange={(date) => form.setFieldValue(fieldPath, date)}
/>

View File

@@ -4,7 +4,7 @@ import dayjs from "../../utils/day";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { UPDATE_SCOREBOARD_ENTRY } from "../../graphql/scoreboard.queries";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
export default function ScoreboardEntryEdit({ entry }) {
const [open, setOpen] = useState(false);
@@ -52,7 +52,7 @@ export default function ScoreboardEntryEdit({ entry }) {
}
]}
>
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item
label={t("scoreboard.fields.bodyhrs")}

View File

@@ -5,7 +5,7 @@ import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { INSERT_VACATION } from "../../graphql/employees.queries";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
export default function ShopEmployeeAddVacation({ employee }) {
const { t } = useTranslation();
@@ -64,7 +64,7 @@ export default function ShopEmployeeAddVacation({ employee }) {
}
]}
>
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item
label={t("employees.fields.vacation.end")}
@@ -90,7 +90,7 @@ export default function ShopEmployeeAddVacation({ employee }) {
})
]}
>
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Space wrap>

View File

@@ -21,12 +21,12 @@ import { selectBodyshop } from "../../redux/user/user.selectors";
import CiecaSelect from "../../utils/Ciecaselect";
import { DateFormatter } from "../../utils/DateFormatter";
import AlertComponent from "../alert/alert.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormListMoveArrows from "../form-list-move-arrows/form-list-move-arrows.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import ShopEmployeeAddVacation from "./shop-employees-add-vacation.component";
import queryString from "query-string";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -266,10 +266,10 @@ export function ShopEmployeesFormComponent({ bodyshop }) {
}
]}
>
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item label={t("employees.fields.termination_date")} name="termination_date">
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item
label={t("employees.fields.user_email")}

View File

@@ -30,226 +30,219 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
return (
<RbacWrapper action="shop:rbac">
<LayoutFormRow>
{...HasFeatureAccess({ featureName: "export", bodyshop })
? [
<Form.Item
label={t("bodyshop.fields.rbac.accounting.exportlog")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "accounting:exportlog"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.accounting.payables")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "accounting:payables"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.accounting.payments")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "accounting:payments"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.accounting.receivables")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "accounting:receivables"]}
>
<InputNumber />
</Form.Item>
]
: []}
{...HasFeatureAccess({ featureName: "bills", bodyshop })
? [
<Form.Item
label={t("bodyshop.fields.rbac.bills.delete")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:delete"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.bills.enter")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:enter"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.bills.list")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:list"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.bills.reexport")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:reexport"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.bills.view")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:view"]}
>
<InputNumber />
</Form.Item>
]
: []}
{...HasFeatureAccess({ featureName: "courtesycars", bodyshop })
? [
<Form.Item
label={t("bodyshop.fields.rbac.contracts.create")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "contracts:create"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.contracts.detail")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "contracts:detail"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.contracts.list")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "contracts:list"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.courtesycar.create")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "courtesycar:create"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.courtesycar.detail")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "courtesycar:detail"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.courtesycar.list")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "courtesycar:list"]}
>
<InputNumber />
</Form.Item>
]
: []}
{...HasFeatureAccess({ featureName: "csi", bodyshop })
? [
<Form.Item
label={t("bodyshop.fields.rbac.csi.export")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "csi:export"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.csi.page")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "csi:page"]}
>
<InputNumber />
</Form.Item>
]
: []}
{...HasFeatureAccess({ featureName: "export", bodyshop }) ? [
<Form.Item
label={t("bodyshop.fields.rbac.accounting.exportlog")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "accounting:exportlog"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.accounting.payables")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "accounting:payables"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.accounting.payments")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "accounting:payments"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.accounting.receivables")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "accounting:receivables"]}
>
<InputNumber />
</Form.Item>
]:[]}
{...HasFeatureAccess({ featureName: "bills", bodyshop }) ? [
<Form.Item
label={t("bodyshop.fields.rbac.bills.delete")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:delete"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.bills.enter")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:enter"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.bills.list")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:list"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.bills.reexport")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:reexport"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.bills.view")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "bills:view"]}
>
<InputNumber />
</Form.Item>
]:[]}
{...HasFeatureAccess({ featureName: "courtesycars", bodyshop }) ? [
<Form.Item
label={t("bodyshop.fields.rbac.contracts.create")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "contracts:create"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.contracts.detail")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "contracts:detail"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.contracts.list")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "contracts:list"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.courtesycar.create")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "courtesycar:create"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.courtesycar.detail")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "courtesycar:detail"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.courtesycar.list")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "courtesycar:list"]}
>
<InputNumber />
</Form.Item>
]:[]}
{...HasFeatureAccess({ featureName: "csi", bodyshop }) ? [
<Form.Item
label={t("bodyshop.fields.rbac.csi.export")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "csi:export"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.csi.page")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "csi:page"]}
>
<InputNumber />
</Form.Item>
]:[]}
<Form.Item
label={t("bodyshop.fields.rbac.employees.page")}
rules={[
@@ -262,18 +255,6 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.employee_teams.page")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "employee_teams:page"]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.jobs.admin")}
rules={[
@@ -454,6 +435,31 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.employees.page")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "employees:page"]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.employee_teams.page")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "employee_teams:page"]}
>
<InputNumber />
</Form.Item>
<Form.Item
label={t("bodyshop.fields.rbac.payments.enter")}
rules={[
@@ -516,6 +522,7 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
<InputNumber />
</Form.Item>
)}
<Form.Item
label={t("bodyshop.fields.rbac.production.list")}
rules={[
@@ -554,118 +561,128 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
<InputNumber />
</Form.Item>
)}
{...HasFeatureAccess({ featureName: "timetickets", bodyshop })
? [
<Form.Item
label={t("bodyshop.fields.rbac.shiftclock.view")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "shiftclock:view"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.shop.config")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "shop:config"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.edit")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:edit"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.shiftedit")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:shiftedit"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.editcommitted")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:editcommitted"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.ttapprovals.view")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "ttapprovals:view"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.ttapprovals.approve")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "ttapprovals:approve"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.enter")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:enter"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.list")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:list"]}
>
<InputNumber />
</Form.Item>
]
: []}
{...HasFeatureAccess({ featureName: "timetickets", bodyshop }) ? [
<Form.Item
label={t("bodyshop.fields.rbac.shiftclock.view")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "shiftclock:view"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.shop.config")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "shop:config"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.edit")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:edit"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.shiftedit")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:shiftedit"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.editcommitted")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:editcommitted"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.ttapprovals.view")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "ttapprovals:view"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.ttapprovals.approve")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "ttapprovals:approve"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.enter")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:enter"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.list")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:list"]}
>
<InputNumber />
</Form.Item>,
<Form.Item
label={t("bodyshop.fields.rbac.timetickets.shiftedit")}
rules={[
{
required: true
//message: t("general.validation.required"),
}
]}
name={["md_rbac", "timetickets:shiftedit"]}
>
<InputNumber />
</Form.Item>
]:[]}
<Form.Item
label={t("bodyshop.fields.rbac.shop.vendors")}
rules={[
@@ -740,6 +757,7 @@ export function ShopInfoRbacComponent({ form, bodyshop }) {
<InputNumber />
</Form.Item>
)}
<Form.Item
label={t("bodyshop.fields.rbac.users.editaccess")}
rules={[

View File

@@ -1,13 +1,14 @@
import { Col, Form, Input, Row, Select, Switch } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import { FormDatePicker } from "../form-date-picker/form-date-picker.component.jsx";
import { createStructuredSelector } from "reselect";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
import dayjs from "../../utils/day";
import { connect } from "react-redux";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component.jsx";
import JobSearchSelectComponent from "../job-search-select/job-search-select.component.jsx";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
import { FormDateTimePickerEnhanced } from "../form-date-time-picker-enhanced/form-date-time-picker-enhanced.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -245,8 +246,7 @@ export function TaskUpsertModalComponent({
</Col>
<Col span={8}>
<Form.Item label={t("tasks.fields.due_date")} name="due_date">
<DateTimePicker
isDateOnly
<FormDatePicker
onlyFuture
format="MM/DD/YYYY"
presets={generatePresets(selectedJobDetails)}
@@ -278,7 +278,12 @@ export function TaskUpsertModalComponent({
}
]}
>
<DateTimePicker onlyFuture presets={generatePresets(selectedJobDetails)} />
<FormDateTimePickerEnhanced
onlyFuture
showTime
minuteStep={15}
presets={generatePresets(selectedJobDetails)}
/>
</Form.Item>
</Col>
</Row>

View File

@@ -1,4 +1,5 @@
import { Button, Card, DatePicker, Form, Popover, Radio, Space } from "antd";
import dayjs from "../../utils/day";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -6,12 +7,10 @@ import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../firebase/firebase.utils";
import { selectTechnician } from "../../redux/tech/tech.selectors";
import DatePIckerRanges from "../../utils/DatePickerRanges";
import dayjs from "../../utils/day";
import { GenerateDocument } from "../../utils/RenderTemplate";
import { TemplateList } from "../../utils/TemplateConstants";
const mapStateToProps = createStructuredSelector({
bodyshop: selectTechnician,
technician: selectTechnician
});
const mapDispatchToProps = (dispatch) => ({
@@ -19,7 +18,7 @@ const mapDispatchToProps = (dispatch) => ({
});
export default connect(mapStateToProps, mapDispatchToProps)(TechJobPrintTickets);
export function TechJobPrintTickets({ bodyshop, technician, event, attendacePrint }) {
export function TechJobPrintTickets({ technician, event, attendacePrint }) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
@@ -58,8 +57,7 @@ export function TechJobPrintTickets({ bodyshop, technician, event, attendacePrin
subject:
attendacePrint === true ? Templates.attendance_employee.subject : Templates.timetickets_employee.subject
},
values.sendby,
bodyshop
values.sendby // === "email" ? "e" : "p"
);
} catch (error) {
console.log(error);

View File

@@ -7,9 +7,9 @@ import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { GET_JOB_INFO_DRAW_CALCULATIONS } from "../../graphql/jobs-lines.queries";
import { selectBodyshop } from "../../redux/user/user.selectors";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import JobSearchSelectComponent from "../job-search-select/job-search-select.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -71,7 +71,7 @@ export function TimeTicketListTeamPay({ bodyshop, context, actions }) {
}
]}
>
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
</LayoutFormRow>

View File

@@ -1,6 +1,6 @@
import { EditFilled, SyncOutlined } from "@ant-design/icons";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Button, Card, Checkbox, Space, Table } from "antd";
import dayjs from "../../utils/day";
import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
@@ -10,10 +10,10 @@ import { setModalContext } from "../../redux/modals/modals.actions";
import { selectAuthLevel, selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import { DateFormatter, DateTimeFormatter } from "../../utils/DateFormatter";
import { onlyUnique } from "../../utils/arrayHelper";
import dayjs from "../../utils/day";
import { alphaSort, dateSort } from "../../utils/sorters";
import { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
import RbacWrapper, { HasRbacAccess } from "../rbac-wrapper/rbac-wrapper.component";
import TimeTicketEnterButton from "../time-ticket-enter-button/time-ticket-enter-button.component";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -52,10 +52,6 @@ export function TimeTicketList({
splitKey: bodyshop.imexshopid
});
const canEditCommittedTimeTickets = HasRbacAccess({ bodyshop, authLevel, action: "timetickets:editcommitted" });
const canEditTimeTickets = HasRbacAccess({ bodyshop, authLevel, action: "timetickets:edit" });
const canEditShiftTickets = HasRbacAccess({ bodyshop, authLevel, action: "timetickets:shiftedit" });
const totals = useMemo(() => {
if (timetickets)
return timetickets.reduce(
@@ -69,18 +65,6 @@ export function TimeTicketList({
return { productivehrs: 0, actualhrs: 0 };
}, [timetickets]);
const isDisabled = (record) => {
if (disabled === true || !record.id) return true;
const isShiftTicket = !record.ciecacode;
const isCommitted = record.committed_at;
if (isShiftTicket) {
return !(canEditShiftTickets && (!isCommitted || canEditCommittedTimeTickets));
}
return !(canEditTimeTickets && (!isCommitted || canEditCommittedTimeTickets));
};
const columns = [
...(Enhanced_Payroll.treatment === "on"
? [
@@ -181,7 +165,7 @@ export function TimeTicketList({
key: "memo",
sorter: (a, b) => alphaSort(a.memo, b.memo),
sortOrder: state.sortedInfo.columnKey === "memo" && state.sortedInfo.order,
render: (text, record) => (record.memo?.startsWith("timetickets.labels") ? t(record.memo) : record.memo)
render: (text, record) => (record.clockon || record.clockoff ? t(record.memo) : record.memo)
},
...(Enhanced_Payroll.treatment === "on"
? [
@@ -222,55 +206,76 @@ export function TimeTicketList({
return null;
}
}
}
},
]),
{
title: t("timetickets.fields.created_by"),
dataIndex: "created_by",
key: "created_by",
sorter: (a, b) => alphaSort(a.created_by, b.created_by),
sortOrder: state.sortedInfo.columnKey === "created_by" && state.sortedInfo.order,
render: (text, record) => record.created_by
},
// {
// title: "Pay",
// dataIndex: "pay",
// key: "pay",
// render: (text, record) =>
// Dinero({ amount: Math.round(record.rate * 100) })
// .multiply(record.flat_rate ? record.productivehrs : record.actualhrs)
// .toFormat("$0.00"),
// },
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
render: (text, record) => (
<Space wrap>
{techConsole && (
<TimeTicketEnterButton
actions={{ refetch }}
context={{ id: record.id, timeticket: record }}
disabled={!record.job || disabled}
>
<EditFilled />
</TimeTicketEnterButton>
)}
{!techConsole && (
<TimeTicketEnterButton
actions={{ refetch }}
context={{
id: record.id,
timeticket: record
}}
disabled={isDisabled(record)}
>
<EditFilled />
</TimeTicketEnterButton>
)}
</Space>
)
}
{
title: t("timetickets.fields.created_by"),
dataIndex: "created_by",
key: "created_by",
sorter: (a, b) => alphaSort(a.created_by, b.created_by),
sortOrder: state.sortedInfo.columnKey === "created_by" && state.sortedInfo.order,
render: (text, record) => record.created_by
},
// {
// title: "Pay",
// dataIndex: "pay",
// key: "pay",
// render: (text, record) =>
// Dinero({ amount: Math.round(record.rate * 100) })
// .multiply(record.flat_rate ? record.productivehrs : record.actualhrs)
// .toFormat("$0.00"),
// },
{
title: t("general.labels.actions"),
dataIndex: "actions",
key: "actions",
render: (text, record) => (
<Space wrap>
{techConsole && (
<TimeTicketEnterButton
actions={{ refetch }}
context={{ id: record.id, timeticket: record }}
disabled={!record.job || disabled}
>
<EditFilled />
</TimeTicketEnterButton>
)}
{!techConsole && (
<RbacWrapper
action="timetickets:edit"
noauth={() => {
return <div />;
}}
>
<TimeTicketEnterButton
actions={{ refetch }}
context={{
id: record.id,
timeticket: record
}}
disabled={
HasRbacAccess({
bodyshop,
authLevel: authLevel,
action: "timetickets:editcommitted"
}) &&
HasRbacAccess({
bodyshop,
authLevel: authLevel,
action: "timetickets:shiftedit"
})
? disabled
: !record.jobid
}
>
<EditFilled />
</TimeTicketEnterButton>
</RbacWrapper>
)}
</Space>
)
}
];
const handleTableChange = (pagination, filters, sorter) => {

View File

@@ -1,5 +1,4 @@
import { useLazyQuery } from "@apollo/client";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
import { Form, Input, InputNumber, Select, Switch } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
@@ -8,10 +7,8 @@ import { createStructuredSelector } from "reselect";
import { GET_LINE_TICKET_BY_PK } from "../../graphql/jobs-lines.queries";
import { selectAuthLevel, selectBodyshop } from "../../redux/user/user.selectors";
import EmployeeSearchSelect from "../employee-search-select/employee-search-select.component";
import {
default as DateTimePicker,
default as FormDateTimePicker
} from "../form-date-time-picker/form-date-time-picker.component";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormDateTimePicker from "../form-date-time-picker/form-date-time-picker.component";
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";
@@ -19,6 +16,7 @@ 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";
import TimeTicketList from "../time-ticket-list/time-ticket-list.component";
import { useSplitTreatments } from "@splitsoftware/splitio-react";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -62,8 +60,8 @@ export function TimeTicketModalComponent({
{item.cost_center === "timetickets.labels.shift"
? t(item.cost_center)
: bodyshop.cdk_dealerid || bodyshop.pbs_serialnumber || Enhanced_Payroll.treatment === "on"
? t(`joblines.fields.lbr_types.${item.cost_center.toUpperCase()}`)
: item.cost_center}
? t(`joblines.fields.lbr_types.${item.cost_center.toUpperCase()}`)
: item.cost_center}
</Select.Option>
))}
</Select>
@@ -71,7 +69,13 @@ export function TimeTicketModalComponent({
};
const MemoInput = ({ value, ...props }) => {
return <Input value={value?.startsWith("timetickets.labels") ? t(value) : value} {...props} />;
return (
<Input
value={value?.startsWith("timetickets.") ? t(value) : value}
{...props}
disabled={value?.startsWith("timetickets.") || disabled}
/>
);
};
return (
@@ -107,7 +111,7 @@ export function TimeTicketModalComponent({
}
]}
>
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item
name="employeeid"
@@ -329,9 +333,7 @@ export function LaborAllocationContainer({ jobid, loading, lineTicketData, hideT
timetickets={lineTicketData.timetickets}
adjustments={lineTicketData.jobs_by_pk.lbr_adjustments}
/>
{!hideTimeTickets && (
<TimeTicketList loading={loading} timetickets={jobid ? lineTicketData.timetickets : []} techConsole />
)}
{!hideTimeTickets && <TimeTicketList loading={loading} timetickets={lineTicketData.timetickets} techConsole />}
</div>
);
}

View File

@@ -39,7 +39,7 @@ export default function TimeTicketShiftActive({ timetickets, refetch, isTechCons
renderItem={(ticket) => (
<List.Item>
<Card
title={ticket.memo?.startsWith("timetickets.labels") ? t(ticket.memo) : ticket.memo}
title={t(ticket.memo)}
actions={[
<TechClockOffButton
jobId={ticket.jobid}

View File

@@ -31,7 +31,7 @@ export function UpdateAlert({ updateAvailable }) {
() => {
r.update();
},
30 * 60 * 1000
10 * 60 * 1000
);
}
},

View File

@@ -1,9 +1,9 @@
import { Form, Input } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import FormDatePicker from "../form-date-picker/form-date-picker.component";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import DateTimePicker from "../form-date-time-picker/form-date-time-picker.component.jsx";
export default function VehicleDetailFormComponent({ form, loading }) {
const { t } = useTranslation();
@@ -102,7 +102,7 @@ export default function VehicleDetailFormComponent({ form, loading }) {
<Input />
</Form.Item>
<Form.Item label={t("vehicles.fields.v_prod_dt")} name="v_prod_dt">
<DateTimePicker isDateOnly />
<FormDatePicker />
</Form.Item>
<Form.Item label={t("vehicles.fields.v_paint_codes", { number: 1 })} name={["v_paint_codes", "paint_cd1"]}>

View File

@@ -1,13 +0,0 @@
import React, { createContext } from "react";
import useSocket from "./useSocket"; // Import the custom hook
// Create the SocketContext
const SocketContext = createContext(null);
export const SocketProvider = ({ children, bodyshop }) => {
const { socket, clientId } = useSocket(bodyshop);
return <SocketContext.Provider value={{ socket, clientId }}> {children}</SocketContext.Provider>;
};
export default SocketContext;

View File

@@ -1,75 +0,0 @@
import { useEffect, useState } from "react";
import SocketIO from "socket.io-client";
import { auth } from "../../firebase/firebase.utils";
const useSocket = (bodyshop) => {
const [socket, setSocket] = useState(null);
const [clientId, setClientId] = useState(null); // State to store unique identifier
useEffect(() => {
const handleBodyshopMessage = (message) => {
console.log(`Received message for bodyshop ${bodyshop.id}:`, message);
};
if (bodyshop && bodyshop.id) {
const endpoint = import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "https://localhost:3000";
const socketInstance = SocketIO(endpoint, {
path: "/ws", // Ensure this matches the Vite proxy and backend path
withCredentials: true,
auth: async (callback) => {
const token = auth.currentUser && (await auth.currentUser.getIdToken());
callback({ token });
},
reconnectionAttempts: Infinity, // Try reconnecting forever
reconnectionDelay: 2000, // How long to wait between reconnection attempts
reconnectionDelayMax: 10000 // Maximum delay between attempts
});
setSocket(socketInstance);
// When the socket connects or reconnects, join the bodyshop room
const joinRoomOnConnect = () => {
console.log("Socket connected:", socketInstance.id);
setClientId(socketInstance.id);
if (bodyshop.id) {
socketInstance.emit("join-bodyshop-room", bodyshop.id);
console.log(`Joined bodyshop room: ${bodyshop.id}`);
}
};
// Set up the necessary socket event handlers
socketInstance.on("connect", joinRoomOnConnect);
socketInstance.on("reconnect", (attempt) => {
console.log(`Socket reconnected after ${attempt} attempts`);
});
socketInstance.on("bodyshop-message", handleBodyshopMessage);
socketInstance.on("connect_error", (err) => {
console.error("Socket connection error:", err);
});
socketInstance.on("disconnect", () => {
console.log("Socket disconnected");
});
// Clean up on component unmount or when bodyshop changes
return () => {
if (bodyshop?.id) {
socketInstance.emit("leave-bodyshop-room", bodyshop.id);
}
socketInstance.off("connect", joinRoomOnConnect);
socketInstance.off("bodyshop-message", handleBodyshopMessage);
socketInstance.disconnect();
};
}
}, [bodyshop]);
// Return both socket and clientId
return { socket, clientId };
};
export default useSocket;

View File

@@ -5,6 +5,7 @@ import { getFirestore } from "firebase/firestore";
import { getMessaging, getToken, onMessage } from "firebase/messaging";
import { store } from "../redux/store";
import axios from "axios";
import { checkBeta } from "../utils/handleBeta";
const config = JSON.parse(import.meta.env.VITE_APP_FIREBASE_CONFIG);
initializeApp(config);
@@ -87,7 +88,7 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
operationName: eventName,
variables: additionalParams,
dbevent: false,
env: "master"
env: checkBeta() ? "beta" : "master"
});
// console.log(
// "%c[Analytics]",

View File

@@ -1,16 +1,20 @@
import { Button, Card, Col, notification, Row, Select, Space } from "antd";
import React, { useContext, useEffect, useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { useLocation, useNavigate } from "react-router-dom";
import { createStructuredSelector } from "reselect";
import SocketIO from "socket.io-client";
import DmsAllocationsSummaryApComponent from "../../components/dms-allocations-summary-ap/dms-allocations-summary-ap.component";
import DmsLogEvents from "../../components/dms-log-events/dms-log-events.component";
import { auth } from "../../firebase/firebase.utils";
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
import { selectBodyshop } from "../../redux/user/user.selectors";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import SocketContext from "../../contexts/SocketIO/socketContext";
const mapStateToProps = createStructuredSelector({});
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
@@ -19,9 +23,20 @@ const mapDispatchToProps = (dispatch) => ({
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
export function DmsContainer({ setBreadcrumbs, setSelectedHeader }) {
export const socket = SocketIO(
import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : window.location.origin,
{
path: "/ws",
withCredentials: true,
auth: async (callback) => {
const token = auth.currentUser && (await auth.currentUser.getIdToken());
callback({ token });
}
}
);
export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
const { t } = useTranslation();
const { socket } = useContext(SocketContext);
const [logLevel, setLogLevel] = useState("DEBUG");
const history = useNavigate();
const [logs, setLogs] = useState([]);
@@ -52,43 +67,40 @@ export function DmsContainer({ setBreadcrumbs, setSelectedHeader }) {
}, [t, setBreadcrumbs, setSelectedHeader]);
useEffect(() => {
if (socket) {
const handleConnect = () => socket.emit("set-log-level", logLevel);
const handleReconnect = () => {
setLogs((logs) => [
socket.on("connect", () => socket.emit("set-log-level", logLevel));
socket.on("reconnect", () => {
setLogs((logs) => {
return [
...logs,
{
timestamp: new Date(),
level: "WARNING",
message: "Reconnected to CDK Export Service"
}
]);
};
const handleLogEvent = (payload) => {
setLogs((logs) => [...logs, payload]);
};
const handleExportComplete = () => {
notification.open({
type: "success",
message: t("jobs.labels.dms.apexported")
});
};
];
});
});
socket.on("connect", handleConnect);
socket.on("reconnect", handleReconnect);
socket.on("log-event", handleLogEvent);
socket.on("ap-export-complete", handleExportComplete);
socket.on("log-event", (payload) => {
setLogs((logs) => {
return [...logs, payload];
});
});
if (socket.disconnected) socket.connect();
socket.on("ap-export-complete", (payload) => {
notification.open({
type: "success",
message: t("jobs.labels.dms.apexported")
});
});
return () => {
socket.off("connect", handleConnect);
socket.off("reconnect", handleReconnect);
socket.off("log-event", handleLogEvent);
socket.off("ap-export-complete", handleExportComplete);
};
}
}, [socket, logLevel, t]);
if (socket.disconnected) socket.connect();
return () => {
socket.removeAllListeners();
socket.disconnect();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!state?.billids) {
history(`/manage/accounting/payables`);
@@ -124,20 +136,27 @@ export function DmsContainer({ setBreadcrumbs, setSelectedHeader }) {
<Button
onClick={() => {
setLogs([]);
if (socket) {
socket.emit("clear-dms-session");
}
socket.disconnect();
socket.connect();
}}
>
Clear Session
Reconnect
</Button>
</Space>
}
>
<DmsLogEvents logs={logs} />
<DmsLogEvents socket={socket} logs={logs} />
</Card>
</div>
</Col>
</Row>
);
}
export const determineDmsType = (bodyshop) => {
if (bodyshop.cdk_dealerid) return "cdk";
else {
return "pbs";
}
};

View File

@@ -1,11 +1,12 @@
import { useQuery } from "@apollo/client";
import { Button, Card, Col, notification, Result, Row, Select, Space } from "antd";
import queryString from "query-string";
import React, { useContext, useEffect, useRef, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, useLocation, useNavigate } from "react-router-dom";
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";
@@ -13,12 +14,12 @@ import DmsLogEvents from "../../components/dms-log-events/dms-log-events.compone
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 { 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 InstanceRenderManager from "../../utils/instanceRenderMgr";
import AuditTrailMapping from "../../utils/AuditTrailMappings";
import SocketContext from "../../contexts/SocketIO/socketContext";
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
@@ -27,21 +28,25 @@ const mapStateToProps = createStructuredSelector({
const mapDispatchToProps = (dispatch) => ({
setBreadcrumbs: (breadcrumbs) => dispatch(setBreadcrumbs(breadcrumbs)),
setSelectedHeader: (key) => dispatch(setSelectedHeader(key)),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(
insertAuditTrail({
jobid,
operation,
type
})
)
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
});
export default connect(mapStateToProps, mapDispatchToProps)(DmsContainer);
export const socket = SocketIO(
import.meta.env.PROD ? import.meta.env.VITE_APP_AXIOS_BASE_API_URL : "http://localhost:4000", // for dev testing,
{
path: "/ws",
withCredentials: true,
auth: async (callback) => {
const token = auth.currentUser && (await auth.currentUser.getIdToken());
callback({ token });
}
}
);
export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, insertAuditTrail }) {
const { t } = useTranslation();
const { socket } = useContext(SocketContext);
const [logLevel, setLogLevel] = useState("DEBUG");
const history = useNavigate();
const [logs, setLogs] = useState([]);
@@ -54,7 +59,6 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
fetchPolicy: "network-only",
nextFetchPolicy: "network-only"
});
const logsRef = useRef(null);
useEffect(() => {
@@ -79,73 +83,47 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
}, [t, setBreadcrumbs, setSelectedHeader]);
useEffect(() => {
if (socket) {
const handleConnect = () => {
socket.emit("set-log-level", logLevel);
};
const handleReconnect = () => {
setLogs((logs) => [
socket.on("connect", () => socket.emit("set-log-level", logLevel));
socket.on("reconnect", () => {
setLogs((logs) => {
return [
...logs,
{
timestamp: new Date(),
level: "WARNING",
message: "Reconnected to CDK Export Service"
}
]);
};
];
});
});
socket.on("connect_error", (err) => {
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")
});
insertAuditTrail({
jobid: payload,
operation: AuditTrailMapping.jobexported(),
type: "jobexported"
});
history("/manage/accounting/receivables");
});
const handleConnectError = (err) => {
console.log(`connect_error due to ${err}`, err);
notification.error({ message: err.message });
};
const handleLogEvent = (payload) => {
setLogs((logs) => [...logs, payload]);
};
const handleExportSuccess = async (payload) => {
notification.success({
message: t("jobs.successes.exported")
});
insertAuditTrail({
jobid: payload,
operation: AuditTrailMapping.jobexported(),
type: "jobexported"
});
try {
await new Promise((resolve, reject) => {
socket.emit("clear-dms-session", (response) => {
if (response && response.status === "ok") {
resolve();
} else {
reject(new Error("Failed to clear DMS session"));
}
});
});
} catch (error) {
console.error("Failed to clear DMS session", error);
}
history("/manage/accounting/receivables");
};
socket.on("connect", handleConnect);
socket.on("reconnect", handleReconnect);
socket.on("connect_error", handleConnectError);
socket.on("log-event", handleLogEvent);
socket.on("export-success", handleExportSuccess);
return () => {
socket.off("connect", handleConnect);
socket.off("reconnect", handleReconnect);
socket.off("connect_error", handleConnectError);
socket.off("log-event", handleLogEvent);
socket.off("export-success", handleExportSuccess);
};
}
}, [socket, logLevel, t, insertAuditTrail, history]);
if (socket.disconnected) socket.connect();
return () => {
socket.removeAllListeners();
socket.disconnect();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (loading) return <LoadingSpinner />;
if (error) return <AlertComponent message={error.message} type="error" />;
@@ -201,21 +179,20 @@ export function DmsContainer({ bodyshop, setBreadcrumbs, setSelectedHeader, inse
<Select.Option key="WARNING">WARNING</Select.Option>
<Select.Option key="ERROR">ERROR</Select.Option>
</Select>
4<Button onClick={() => setLogs([])}>Clear Logs</Button>
<Button onClick={() => setLogs([])}>Clear Logs</Button>
<Button
onClick={() => {
setLogs([]);
if (socket) {
socket.emit("clear-dms-session");
}
socket.disconnect();
socket.connect();
}}
>
Clear Session
Reconnect
</Button>
</Space>
}
>
<DmsLogEvents logs={logs} />
<DmsLogEvents socket={socket} logs={logs} />
</Card>
</div>
</Col>

View File

@@ -1,6 +1,6 @@
import { FloatButton, Layout, Spin } from "antd";
// import preval from "preval.macro";
import React, { lazy, Suspense, useContext, useEffect, useState } from "react";
import React, { lazy, Suspense, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { Link, Route, Routes } from "react-router-dom";
@@ -18,12 +18,12 @@ import LoadingSpinner from "../../components/loading-spinner/loading-spinner.com
import PartnerPingComponent from "../../components/partner-ping/partner-ping.component";
import PrintCenterModalContainer from "../../components/print-center-modal/print-center-modal.container";
import ShopSubStatusComponent from "../../components/shop-sub-status/shop-sub-status.component";
import { requestForToken } from "../../firebase/firebase.utils";
import { selectBodyshop, selectInstanceConflict } from "../../redux/user/user.selectors";
import UpdateAlert from "../../components/update-alert/update-alert.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr.js";
import "./manage.page.styles.scss";
import SocketContext from "../../contexts/SocketIO/socketContext.jsx";
const JobsPage = lazy(() => import("../jobs/jobs.page"));
@@ -110,7 +110,17 @@ const mapDispatchToProps = (dispatch) => ({});
export function Manage({ conflict, bodyshop }) {
const { t } = useTranslation();
const [chatVisible] = useState(false);
const { socket, clientId } = useContext(SocketContext);
useEffect(() => {
const widgetId = InstanceRenderManager({
imex: "IABVNO4scRKY11XBQkNr",
rome: "mQdqARMzkZRUVugJ6TdS"
});
window.noticeable.render("widget", widgetId);
requestForToken().catch((error) => {
console.error(`Unable to request for token.`, error);
});
}, []);
useEffect(() => {
document.title = InstanceRenderManager({
@@ -119,7 +129,6 @@ export function Manage({ conflict, bodyshop }) {
promanager: t("titles.promanager")
});
}, [t]);
const AppRouteTable = (
<Suspense
fallback={
@@ -560,16 +569,9 @@ export function Manage({ conflict, bodyshop }) {
else if (bodyshop && bodyshop.sub_status !== "active") PageContent = <ShopSubStatusComponent />;
else PageContent = AppRouteTable;
const broadcastMessage = () => {
if (socket && bodyshop && bodyshop.id) {
console.log(`Broadcasting message to bodyshop ${bodyshop.id}:`);
socket.emit("broadcast-to-bodyshop", bodyshop.id, `Hello from ${clientId}`);
}
};
return (
<>
{import.meta.env.PROD && <ChatAffixContainer bodyshop={bodyshop} chatVisible={chatVisible} />}
<ChatAffixContainer bodyshop={bodyshop} chatVisible={chatVisible} />
<Layout style={{ minHeight: "100vh" }} className="layout-container">
<UpdateAlert />
<HeaderContainer />
@@ -601,8 +603,6 @@ export function Manage({ conflict, bodyshop }) {
</div>
<div id="noticeable-widget" style={{ marginLeft: "1rem" }} />
</div>
<button onClick={broadcastMessage}>Broadcast Message</button>
<Link to="/disclaimer" target="_blank" style={{ color: "#ccc" }}>
Disclaimer & Notices
</Link>

View File

@@ -3,7 +3,6 @@ import { useTranslation } from "react-i18next";
import RbacWrapperComponent from "../../components/rbac-wrapper/rbac-wrapper.component";
import TechLookupJobsList from "../../components/tech-lookup-jobs-list/tech-lookup-jobs-list.component";
import InstanceRenderManager from "../../utils/instanceRenderMgr";
import TechLookupJobsDrawer from "../../components/tech-lookup-jobs-drawer/tech-lookup-jobs-drawer.component";
export default function TechLookupContainer() {
const { t } = useTranslation();
@@ -21,7 +20,6 @@ export default function TechLookupContainer() {
return (
<div>
<RbacWrapperComponent action="jobs:list-active">
<TechLookupJobsDrawer />
<TechLookupJobsList />
</RbacWrapperComponent>
</div>

View File

@@ -9,6 +9,7 @@ import ErrorBoundary from "../../components/error-boundary/error-boundary.compon
import FeatureWrapper from "../../components/feature-wrapper/feature-wrapper.component";
import LoadingSpinner from "../../components/loading-spinner/loading-spinner.component";
import TechHeader from "../../components/tech-header/tech-header.component";
import TechLookupJobsDrawer from "../../components/tech-lookup-jobs-drawer/tech-lookup-jobs-drawer.component";
import TechSider from "../../components/tech-sider/tech-sider.component";
import UpdateAlert from "../../components/update-alert/update-alert.component";
import { selectTechnician } from "../../redux/tech/tech.selectors";
@@ -67,7 +68,7 @@ export function TechPage({ technician }) {
<Layout>
<UpdateAlert />
<TechHeader />
<TechLookupJobsDrawer />
<TaskUpsertModalContainer />
<Content className="tech-content-container">
<ErrorBoundary>

View File

@@ -10,7 +10,7 @@ import {
signInWithEmailAndPassword,
signOut
} from "firebase/auth";
import { arrayUnion, doc, getDoc, setDoc, updateDoc } from "firebase/firestore";
import { doc, getDoc, setDoc } from "firebase/firestore";
import { getToken } from "firebase/messaging";
import i18next from "i18next";
import LogRocket from "logrocket";
@@ -48,7 +48,6 @@ import {
validatePasswordResetSuccess
} from "./user.actions";
import UserActionTypes from "./user.types";
import cleanAxios from "../../utils/CleanAxios";
const fpPromise = FingerprintJS.load();
@@ -178,24 +177,10 @@ export function* setInstanceIdSaga({ payload: uid }) {
// Get the visitor identifier when you need it.
const fp = yield fpPromise;
const result = yield fp.get();
const res = yield cleanAxios.get("https://api.ipify.org/?format=json");
const udoc = yield getDoc(userInstanceRef);
if (!udoc.data()) {
yield setDoc(userInstanceRef, {
timestamp: new Date(),
fingerprint: result.visitorId,
//totalFingerprint: result,
ip: [res.data.ip]
});
} else {
yield updateDoc(userInstanceRef, {
timestamp: new Date(),
fingerprint: result.visitorId,
//totalFingerprint: result,
ip: arrayUnion(res.data.ip)
});
}
yield setDoc(userInstanceRef, {
timestamp: new Date(),
fingerprint: result.visitorId
});
yield put(setLocalFingerprint(result.visitorId));
yield delay(5 * 60 * 1000);

View File

@@ -271,8 +271,7 @@
},
"errors": {
"loading": "Unable to load shop details. Please call technical support.",
"saving": "Error encountered while saving. {{message}}",
"creatingdefaultview": "Error creating default view."
"saving": "Error encountered while saving. {{message}}"
},
"fields": {
"ReceivableCustomField": "QBO Receivable Custom Field {{number}}",
@@ -700,10 +699,7 @@
"workingdays": "Working Days"
},
"successes": {
"save": "Shop configuration saved successfully. ",
"unsavedchanges": "Unsaved changes will be lost. Are you sure you want to continue?",
"areyousure": "Are you sure you want to continue?",
"defaultviewcreated": "Default view created successfully."
"save": "Shop configuration saved successfully. "
},
"validation": {
"centermustexist": "The chosen responsibility center does not exist.",
@@ -1165,8 +1161,7 @@
"tryagain": "Try Again",
"view": "View",
"viewreleasenotes": "See What's Changed",
"remove_alert": "Are you sure you want to dismiss the alert?",
"saveas": "Save As"
"remove_alert": "Are you sure you want to dismiss the alert?"
},
"errors": {
"fcm": "You must allow notification permissions to have real time messaging. Click to try again.",
@@ -1181,7 +1176,6 @@
"vehicle": "Vehicle"
},
"labels": {
"unsavedchanges": "Unsaved changes.",
"actions": "Actions",
"areyousure": "Are you sure?",
"barcode": "Barcode",
@@ -1189,8 +1183,6 @@
"clear": "Clear",
"confirmpassword": "Confirm Password",
"created_at": "Created At",
"date": "Select Date",
"datetime": "Select Date & Time",
"email": "Email",
"errors": "Errors",
"excel": "Excel",
@@ -2739,9 +2731,6 @@
}
},
"production": {
"constants": {
"main_profile": "Default"
},
"options": {
"small": "Small",
"medium": "Medium",
@@ -2791,9 +2780,7 @@
"errors": {
"boardupdate": "Error encountered updating Job. {{message}}",
"removing": "Error removing from production board. {{error}}",
"settings": "Error saving board settings: {{error}}",
"name_exists": "A Profile with this name already exists. Please choose a different name.",
"name_required": "Profile name is required."
"settings": "Error saving board settings: {{error}}"
},
"labels": {
"kiosk_mode": "Kiosk Mode",
@@ -2847,8 +2834,7 @@
"totalhours": "Total Hrs ",
"touchtime": "T/T",
"viewname": "View Name",
"alerts": "Alerts",
"addnewprofile": "Add New Profile"
"alerts": "Alerts"
},
"successes": {
"removed": "Job removed from production."

File diff suppressed because it is too large Load Diff

View File

@@ -271,8 +271,7 @@
},
"errors": {
"loading": "Impossible de charger les détails de la boutique. Veuillez appeler le support technique.",
"saving": "",
"creatingdefaultview": ""
"saving": ""
},
"fields": {
"ReceivableCustomField": "",
@@ -700,10 +699,7 @@
"workingdays": ""
},
"successes": {
"save": "",
"unsavedchanges": "",
"areyousure": "",
"defaultviewcreated": ""
"save": ""
},
"validation": {
"centermustexist": "",
@@ -1165,8 +1161,7 @@
"tryagain": "",
"view": "",
"viewreleasenotes": "",
"remove_alert": "",
"saveas": ""
"remove_alert": ""
},
"errors": {
"fcm": "",
@@ -1181,7 +1176,6 @@
"vehicle": ""
},
"labels": {
"unsavedchanges": "",
"actions": "actes",
"areyousure": "",
"barcode": "code à barre",
@@ -1189,8 +1183,6 @@
"clear": "",
"confirmpassword": "",
"created_at": "",
"date": "",
"datetime": "",
"email": "",
"errors": "",
"excel": "",
@@ -2739,9 +2731,6 @@
}
},
"production": {
"constants": {
"main_profile": ""
},
"options": {
"small": "",
"medium": "",
@@ -2791,9 +2780,7 @@
"errors": {
"boardupdate": "",
"removing": "",
"settings": "",
"name_exists": "",
"name_required": ""
"settings": ""
},
"labels": {
"kiosk_mode": "",
@@ -2847,8 +2834,7 @@
"totalhours": "",
"touchtime": "",
"viewname": "",
"alerts": "",
"addnewprofile": ""
"alerts": ""
},
"successes": {
"removed": ""

View File

@@ -2,9 +2,9 @@ import axios from "axios";
import { auth } from "../firebase/firebase.utils";
import InstanceRenderManager from "./instanceRenderMgr";
axios.defaults.baseURL = import.meta.env.DEV
? "/api/"
: import.meta.env.VITE_APP_AXIOS_BASE_API_URL || "https://api.imex.online/";
axios.defaults.baseURL =
import.meta.env.VITE_APP_AXIOS_BASE_API_URL ||
(import.meta.env.MODE === "production" ? "https://api.imex.online/" : "http://localhost:4000/");
export const axiosAuthInterceptorId = axios.interceptors.request.use(
async (config) => {

View File

@@ -0,0 +1,47 @@
export const BETA_KEY = "betaSwitchImex";
export const checkBeta = () => {
const cookie = document.cookie.split("; ").find((row) => row.startsWith(BETA_KEY));
return cookie ? cookie.split("=")[1] === "true" : false;
};
export const setBeta = (value) => {
const domain = window.location.hostname.split(".").slice(-2).join(".");
document.cookie = `${BETA_KEY}=${value}; path=/; domain=.${domain}`;
};
export const handleBeta = () => {
if (window.location.hostname.startsWith("localhost")) {
console.log("Not on beta or test, so no need to handle beta.");
return;
}
const isBeta = checkBeta();
const currentHostName = window.location.hostname;
// Determine if the host name starts with "beta" or "www.beta"
const isBetaHost = currentHostName.startsWith("beta.");
const isBetaHostWithWWW = currentHostName.startsWith("www.beta.");
if (isBeta) {
// If beta is on and we are not on a beta domain, redirect to the beta version
if (!isBetaHost && !isBetaHostWithWWW) {
const newHostName = currentHostName.startsWith("www.")
? `www.beta.${currentHostName.replace(/^www\./, "")}`
: `beta.${currentHostName}`;
const href = `${window.location.protocol}//${newHostName}${window.location.pathname}${window.location.search}${window.location.hash}`;
window.location.replace(href);
}
// Otherwise, if beta is on and we're already on a beta domain, stay there
} else {
// If beta is off and we are on a beta domain, redirect to the non-beta version
if (isBetaHost || isBetaHostWithWWW) {
const newHostName = currentHostName.replace(/^www\.beta\./, "www.").replace(/^beta\./, "");
const href = `${window.location.protocol}//${newHostName}${window.location.pathname}${window.location.search}${window.location.hash}`;
window.location.replace(href);
}
// Otherwise, if beta is off and we're not on a beta domain, stay there
}
};
export default handleBeta;

View File

@@ -3,21 +3,16 @@ import { promises as fsPromises } from "fs";
import { createRequire } from "module";
import * as path from "path";
import * as url from "url";
import { createLogger, defineConfig } from "vite";
import { defineConfig } from "vite";
import { ViteEjsPlugin } from "vite-plugin-ejs";
import eslint from "vite-plugin-eslint";
import { VitePWA } from "vite-plugin-pwa";
import InstanceRenderManager from "./src/utils/instanceRenderMgr";
import chalk from "chalk";
process.env.VITE_APP_GIT_SHA_DATE = new Date().toLocaleString("en-US", {
timeZone: "America/Los_Angeles"
});
const getFormattedTimestamp = () =>
new Date().toLocaleTimeString("en-US", { hour12: true }).replace("AM", "a.m.").replace("PM", "p.m.");
/** This is a hack around react-virtualized, should be removed when switching to react-virtuoso */
const WRONG_CODE = `import { bpfrpt_proptype_WindowScroller } from "../WindowScroller.js";`;
function reactVirtualizedFix() {
@@ -37,11 +32,6 @@ function reactVirtualizedFix() {
}
};
}
/** End of hack */
export const logger = createLogger("info", {
allowClearScreen: false
});
export default defineConfig({
base: "/",
@@ -109,6 +99,7 @@ export default defineConfig({
reactVirtualizedFix(),
react(),
eslint()
// CompressionPlugin(), //Cloudfront already compresses assets, so not needed.
],
define: {
APP_VERSION: JSON.stringify(process.env.npm_package_version)
@@ -116,57 +107,7 @@ export default defineConfig({
server: {
host: true,
port: 3000,
open: true,
proxy: {
"/ws": {
target: "ws://localhost:4000",
rewriteWsOrigin: true,
secure: false,
ws: true
},
"/api": {
target: "http://localhost:4000",
changeOrigin: true,
secure: false,
ws: false,
rewrite: (path) => {
const replacedValue = path.replace(/^\/api/, "");
logger.info(
`${chalk.grey.bold(getFormattedTimestamp())} ${chalk.cyan.bold("[vite]")} ${chalk.green.bold("[API]")} ${chalk.blue(replacedValue)}`
);
return replacedValue;
}
}
},
https: {
key: await fsPromises.readFile("../certs/key.pem"),
cert: await fsPromises.readFile("../certs/cert.pem"),
allowHTTP1: false // Force HTTP/2
}
},
preview: {
port: 6000,
host: true,
open: true,
https: {
key: await fsPromises.readFile("../certs/key.pem"),
cert: await fsPromises.readFile("../certs/cert.pem"),
allowHTTP1: false // Force HTTP/2
},
proxy: {
"/ws": {
target: "ws://localhost:4000",
rewriteWsOrigin: true,
secure: false,
ws: true
},
"/api": {
target: "http://localhost:4000",
changeOrigin: true,
secure: false,
ws: false
}
}
open: true
},
build: {
rollupOptions: {
@@ -180,18 +121,7 @@ export default defineConfig({
}
},
optimizeDeps: {
include: [
"react",
"react-dom",
"antd",
"@apollo/client",
"@reduxjs/toolkit",
"axios",
"react-router-dom",
"dayjs",
"redux",
"react-redux"
],
include: ["react", "react-dom", "antd", "@apollo/client", "@reduxjs/toolkit", "axios"],
esbuildOptions: {
loader: {
".js": "jsx"

View File

@@ -1,4 +1,4 @@
Must set the environment variables using:
firebase functions:config:set auth.graphql_endpoint="https://db.dev.imex.online/v1/graphql"
firebase functions:config:set auth.graphql_endpoint="https://db.dev.bodyshop.app/v1/graphql"
auth.hasura_secret_admin_key="Dev-BodyShopApp!"

View File

@@ -1,5 +1,5 @@
version: 2
endpoint: https://db.dev.imex.online
endpoint: https://db.dev.bodyshop.app
admin_secret: Dev-BodyShopApp!
metadata_directory: metadata
actions:

View File

@@ -1,27 +1,3 @@
- name: AutoHouse Data Pump
webhook: '{{HASURA_API_URL}}/data/ah'
schedule: 0 6 * * *
include_in_metadata: true
payload: {}
headers:
- name: x-imex-auth
value_from_env: DATAPUMP_AUTH
- name: Claimscorp Data Pump
webhook: '{{HASURA_API_URL}}/data/cc'
schedule: 30 6 * * *
include_in_metadata: true
payload: {}
headers:
- name: x-imex-auth
value_from_env: DATAPUMP_AUTH
- name: Kaizen Data Pump
webhook: '{{HASURA_API_URL}}/data/kaizen'
schedule: 30 5 * * *
include_in_metadata: true
payload: {}
headers:
- name: x-imex-auth
value_from_env: DATAPUMP_AUTH
- name: Task Reminders
webhook: '{{HASURA_API_URL}}/tasks-remind-handler'
schedule: '*/15 * * * *'

View File

@@ -4240,63 +4240,6 @@
- active:
_eq: true
event_triggers:
- name: job_modified
definition:
enable_manual: false
update:
columns:
- clm_no
- v_make_desc
- date_next_contact
- status
- employee_csr
- employee_prep
- clm_total
- suspended
- employee_body
- ro_number
- actual_in
- ownr_co_nm
- v_model_yr
- comment
- job_totals
- v_vin
- ownr_fn
- scheduled_completion
- special_coverage_policy
- v_color
- ca_gst_registrant
- scheduled_delivery
- actual_delivery
- actual_completion
- kanbanparent
- est_ct_fn
- employee_refinish
- ownr_ph1
- date_last_contacted
- alt_transport
- inproduction
- est_ct_ln
- production_vars
- category
- v_model_desc
- date_invoiced
- est_co_nm
- ownr_ln
retry_conf:
interval_sec: 10
num_retries: 0
timeout_sec: 60
webhook_from_env: HASURA_API_URL
headers:
- name: event-secret
value_from_env: EVENT_SECRET
request_transform:
method: POST
query_params: {}
template_engine: Kriti
url: '{{$base_url}}/job/job-updated'
version: 2
- name: job_status_transition
definition:
enable_manual: true

1023
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -19,18 +19,17 @@
"makeitpretty": "prettier --write \"**/*.{css,js,json,jsx,scss}\""
},
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.645.0",
"@aws-sdk/client-ses": "^3.645.0",
"@aws-sdk/credential-provider-node": "^3.645.0",
"@opensearch-project/opensearch": "^2.12.0",
"@socket.io/redis-adapter": "^8.3.0",
"aws4": "^1.13.2",
"axios": "^1.7.7",
"@aws-sdk/client-secrets-manager": "^3.632.0",
"@aws-sdk/client-ses": "^3.632.0",
"@aws-sdk/credential-provider-node": "^3.632.0",
"@opensearch-project/opensearch": "^2.11.0",
"aws4": "^1.13.1",
"axios": "^1.7.4",
"better-queue": "^3.8.12",
"bluebird": "^3.7.2",
"body-parser": "^1.20.3",
"body-parser": "^1.20.2",
"canvas": "^2.11.2",
"chart.js": "^4.4.4",
"chart.js": "^4.4.3",
"cloudinary": "^2.4.0",
"compression": "^1.7.4",
"cookie-parser": "^1.4.6",
@@ -38,8 +37,8 @@
"csrf": "^3.1.0",
"dinero.js": "^1.9.1",
"dotenv": "^16.4.5",
"express": "^4.20.0",
"firebase-admin": "^12.4.0",
"express": "^4.19.2",
"firebase-admin": "^12.3.1",
"graphql": "^16.9.0",
"graphql-request": "^6.1.0",
"graylog2": "^0.2.1",
@@ -50,16 +49,14 @@
"moment": "^2.30.1",
"moment-timezone": "^0.5.45",
"multer": "^1.4.5-lts.1",
"node-mailjet": "^6.0.6",
"node-mailjet": "^6.0.5",
"node-persist": "^4.0.3",
"nodemailer": "^6.9.15",
"nodemailer": "^6.9.14",
"phone": "^3.1.50",
"recursive-diff": "^1.0.9",
"redis": "^4.7.0",
"rimraf": "^6.0.1",
"soap": "^1.1.3",
"soap": "^1.1.1",
"socket.io": "^4.7.5",
"socket.io-adapter": "^2.5.5",
"ssh2-sftp-client": "^10.0.3",
"twilio": "^4.23.0",
"uuid": "^10.0.0",

370
server.js
View File

@@ -1,3 +1,4 @@
// Import core modules
const express = require("express");
const cors = require("cors");
const bodyParser = require("body-parser");
@@ -6,307 +7,104 @@ const compression = require("compression");
const cookieParser = require("cookie-parser");
const http = require("http");
const { Server } = require("socket.io");
const { createClient } = require("redis");
const { createAdapter } = require("@socket.io/redis-adapter");
const logger = require("./server/utils/logger");
// Load environment variables
require("dotenv").config({
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
});
/**
* CORS Origin for Socket.IO
* @type {string[][]}
*/
const SOCKETIO_CORS_ORIGIN = [
"https://test.imex.online",
"https://www.test.imex.online",
"http://localhost:3000",
"https://imex.online",
"https://www.imex.online",
"https://romeonline.io",
"https://www.romeonline.io",
"https://beta.test.romeonline.io",
"https://www.beta.test.romeonline.io",
"https://beta.romeonline.io",
"https://www.beta.romeonline.io",
"https://beta.test.imex.online",
"https://www.beta.test.imex.online",
"https://beta.imex.online",
"https://www.beta.imex.online",
"https://www.test.promanager.web-est.com",
"https://test.promanager.web-est.com",
"https://www.promanager.web-est.com",
"https://www.promanager.web-est.com"
];
// Import custom utilities and handlers
const logger = require("./server/utils/logger");
/**
* Middleware for Express app
* @param app
*/
const applyMiddleware = (app) => {
app.use(compression());
app.use(cookieParser());
app.use(bodyParser.json({ limit: "50mb" }));
app.use(bodyParser.urlencoded({ limit: "50mb", extended: true }));
app.use(cors({ credentials: true, exposedHeaders: ["set-cookie"] }));
// Helper middleware
app.use((req, res, next) => {
req.logger = logger;
next();
});
};
/**
* Route groupings for Express app
* @param app
*/
const applyRoutes = (app) => {
app.use("/", require("./server/routes/miscellaneousRoutes"));
app.use("/notifications", require("./server/routes/notificationsRoutes"));
app.use("/render", require("./server/routes/renderRoutes"));
app.use("/mixdata", require("./server/routes/mixDataRoutes"));
app.use("/accounting", require("./server/routes/accountingRoutes"));
app.use("/qbo", require("./server/routes/qboRoutes"));
app.use("/media", require("./server/routes/mediaRoutes"));
app.use("/sms", require("./server/routes/smsRoutes"));
app.use("/job", require("./server/routes/jobRoutes"));
app.use("/scheduling", require("./server/routes/schedulingRoutes"));
app.use("/utils", require("./server/routes/utilRoutes"));
app.use("/data", require("./server/routes/dataRoutes"));
app.use("/adm", require("./server/routes/adminRoutes"));
app.use("/tech", require("./server/routes/techRoutes"));
app.use("/intellipay", require("./server/routes/intellipayRoutes"));
app.use("/cdk", require("./server/routes/cdkRoutes"));
app.use("/csi", require("./server/routes/csiRoutes"));
app.use("/payroll", require("./server/routes/payrollRoutes"));
// Default route for forbidden access
app.get("/", (req, res) => {
res.status(200).send("Access Forbidden.");
});
};
/**
* Apply Redis to the server
* @param server
* @param app
*/
const applySocketIO = async (server, app) => {
// Redis client setup for Pub/Sub and Key-Value Store
const pubClient = createClient({ url: process.env.REDIS_URL || "redis://localhost:6379" });
const subClient = pubClient.duplicate();
pubClient.on("error", (err) => logger.log(`Redis pubClient error: ${err}`, "ERROR", "redis"));
subClient.on("error", (err) => logger.log(`Redis subClient error: ${err}`, "ERROR", "redis"));
try {
await Promise.all([pubClient.connect(), subClient.connect()]);
logger.log(`[${process.env.NODE_ENV}] Connected to Redis`, "INFO", "redis", "api");
} catch (redisError) {
logger.log("Failed to connect to Redis", "ERROR", "redis", redisError);
// Express app and server setup
const app = express();
const port = process.env.PORT || 5000;
const server = http.createServer(app);
const io = new Server(server, {
path: "/ws",
cors: {
origin: [
"https://test.imex.online",
"https://www.test.imex.online",
"http://localhost:3000",
"https://imex.online",
"https://www.imex.online",
"https://romeonline.io", //Added in all RO and PM routes to simplyify setup.
"https://www.romeonline.io",
"https://beta.test.romeonline.io",
"https://www.beta.test.romeonline.io",
"https://beta.romeonline.io",
"https://www.beta.romeonline.io",
"https://beta.test.imex.online",
"https://www.beta.test.imex.online",
"https://beta.imex.online",
"https://www.beta.imex.online",
"https://www.test.promanager.web-est.com",
"https://test.promanager.web-est.com",
"https://www.promanager.web-est.com",
"https://www.promanager.web-est.com"
],
methods: ["GET", "POST"],
credentials: true,
exposedHeaders: ["set-cookie"]
}
});
exports.io = io;
process.on("SIGINT", async () => {
logger.log("Closing Redis connections...", "INFO", "redis", "api");
await Promise.all([pubClient.disconnect(), subClient.disconnect()]);
process.exit(0);
});
require("./server/web-sockets/web-socket");
const io = new Server(server, {
path: "/ws",
adapter: createAdapter(pubClient, subClient),
cors: {
origin: SOCKETIO_CORS_ORIGIN,
methods: ["GET", "POST"],
credentials: true,
exposedHeaders: ["set-cookie"]
}
});
// Middleware
app.use(compression());
app.use(cookieParser());
app.use(bodyParser.json({ limit: "50mb" }));
app.use(bodyParser.urlencoded({ limit: "50mb", extended: true }));
app.use(cors({ credentials: true, exposedHeaders: ["set-cookie"] }));
app.use((req, res, next) => {
req.pubClient = pubClient;
req.io = io;
next();
});
// Helper middleware
app.use((req, res, next) => {
req.logger = logger;
next();
});
Object.assign(module.exports, { io, pubClient });
// Route groupings
app.use("/", require("./server/routes/miscellaneousRoutes"));
app.use("/notifications", require("./server/routes/notificationsRoutes"));
app.use("/render", require("./server/routes/renderRoutes"));
app.use("/mixdata", require("./server/routes/mixDataRoutes"));
app.use("/accounting", require("./server/routes/accountingRoutes"));
app.use("/qbo", require("./server/routes/qboRoutes"));
app.use("/media", require("./server/routes/mediaRoutes"));
app.use("/sms", require("./server/routes/smsRoutes"));
app.use("/job", require("./server/routes/jobRoutes"));
app.use("/scheduling", require("./server/routes/schedulingRoutes"));
app.use("/utils", require("./server/routes/utilRoutes"));
app.use("/data", require("./server/routes/dataRoutes"));
app.use("/adm", require("./server/routes/adminRoutes"));
app.use("/tech", require("./server/routes/techRoutes"));
app.use("/intellipay", require("./server/routes/intellipayRoutes"));
app.use("/cdk", require("./server/routes/cdkRoutes"));
app.use("/csi", require("./server/routes/csiRoutes"));
app.use("/payroll", require("./server/routes/payrollRoutes"));
return { pubClient, io };
};
// Default route for forbidden access
app.get("/", (req, res) => {
res.status(200).send("Access Forbidden.");
});
/**
* Apply Redis helper functions
* @param pubClient
* @param app
*/
const applyRedisHelpers = (pubClient, app) => {
// Store session data in Redis
const setSessionData = async (socketId, key, value) => {
await pubClient.hSet(`socket:${socketId}`, key, JSON.stringify(value)); // Use Redis pubClient
};
// Retrieve session data from Redis
const getSessionData = async (socketId, key) => {
const data = await pubClient.hGet(`socket:${socketId}`, key);
return data ? JSON.parse(data) : null;
};
// Clear session data from Redis
const clearSessionData = async (socketId) => {
await pubClient.del(`socket:${socketId}`);
};
// Store multiple session data in Redis
const setMultipleSessionData = async (socketId, keyValues) => {
// keyValues is expected to be an object { key1: value1, key2: value2, ... }
const entries = Object.entries(keyValues).map(([key, value]) => [key, JSON.stringify(value)]);
await pubClient.hSet(`socket:${socketId}`, ...entries.flat());
};
// Retrieve multiple session data from Redis
const getMultipleSessionData = async (socketId, keys) => {
const data = await pubClient.hmGet(`socket:${socketId}`, keys);
// Redis returns an object with null values for missing keys, so we parse the non-null ones
return Object.fromEntries(keys.map((key, index) => [key, data[index] ? JSON.parse(data[index]) : null]));
};
const setMultipleFromArraySessionData = async (socketId, keyValueArray) => {
// Use Redis multi/pipeline to batch the commands
const multi = pubClient.multi();
keyValueArray.forEach(([key, value]) => {
multi.hSet(`socket:${socketId}`, key, JSON.stringify(value));
});
await multi.exec(); // Execute all queued commands
};
// Helper function to add an item to the end of the Redis list
const addItemToEndOfList = async (socketId, key, newItem) => {
try {
await pubClient.rPush(`socket:${socketId}:${key}`, JSON.stringify(newItem));
} catch (error) {
console.error(`Error adding item to the end of the list for socket ${socketId}:`, error);
}
};
// Helper function to add an item to the beginning of the Redis list
const addItemToBeginningOfList = async (socketId, key, newItem) => {
try {
await pubClient.lPush(`socket:${socketId}:${key}`, JSON.stringify(newItem));
} catch (error) {
console.error(`Error adding item to the beginning of the list for socket ${socketId}:`, error);
}
};
Object.assign(module.exports, {
setSessionData,
getSessionData,
clearSessionData,
setMultipleSessionData,
getMultipleSessionData,
setMultipleFromArraySessionData,
addItemToEndOfList,
addItemToBeginningOfList,
pubClient
});
app.use((req, res, next) => {
req.sessionUtils = {
setSessionData,
getSessionData,
clearSessionData,
setMultipleSessionData,
getMultipleSessionData,
setMultipleFromArraySessionData,
addItemToEndOfList,
addItemToBeginningOfList,
pubClient
};
next();
});
// // Demo to show how all the helper functions work
// const demoSessionData = async () => {
// const socketId = "testSocketId";
//
// // Store session data using setSessionData
// await exports.setSessionData(socketId, "field1", "Hello, Redis!");
//
// // Retrieve session data using getSessionData
// const field1Value = await exports.getSessionData(socketId, "field1");
// console.log("Retrieved single field value:", field1Value);
//
// // Store multiple session data using setMultipleSessionData
// await exports.setMultipleSessionData(socketId, { field2: "Second Value", field3: "Third Value" });
//
// // Retrieve multiple session data using getMultipleSessionData
// const multipleFields = await exports.getMultipleSessionData(socketId, ["field2", "field3"]);
// console.log("Retrieved multiple field values:", multipleFields);
//
// // Store multiple session data using setMultipleFromArraySessionData
// await exports.setMultipleFromArraySessionData(socketId, [
// ["field4", "Fourth Value"],
// ["field5", "Fifth Value"]
// ]);
//
// // Retrieve and log all fields
// const allFields = await exports.getMultipleSessionData(socketId, [
// "field1",
// "field2",
// "field3",
// "field4",
// "field5"
// ]);
// console.log("Retrieved all field values:", allFields);
//
// // Add item to the end of a Redis list
// await exports.addItemToEndOfList(socketId, "logEvents", { event: "Log Event 1", timestamp: new Date() });
// await exports.addItemToEndOfList(socketId, "logEvents", { event: "Log Event 2", timestamp: new Date() });
//
// // Add item to the beginning of a Redis list
// await exports.addItemToBeginningOfList(socketId, "logEvents", { event: "First Log Event", timestamp: new Date() });
//
// // Retrieve the entire list (using lRange)
// const logEvents = await pubClient.lRange(`socket:${socketId}:logEvents`, 0, -1);
// console.log("Log Events List:", logEvents.map(JSON.parse));
//
// // Clear session data
// await exports.clearSessionData(socketId);
// console.log("Session data cleared.");
// };
//
// if (process.env.NODE_ENV === "development") {
// demoSessionData();
// }
};
/**
* Main function to start the server
* @returns {Promise<void>}
*/
const main = async () => {
const app = express();
const port = process.env.PORT || 5000;
const server = http.createServer(app);
const { pubClient } = await applySocketIO(server, app);
applyRedisHelpers(pubClient, app);
require("./server/web-sockets/web-socket");
applyMiddleware(app);
applyRoutes(app);
try {
await server.listen(port);
logger.log(`[${process.env.NODE_ENV}] Server started on port ${port}`, "INFO", "api");
} catch (error) {
logger.log(`[${process.env.NODE_ENV}] Server failed to start on port ${port}`, "ERROR", "api", error);
}
await server.listen(port);
};
// Start server
main();
main()
.then(() => {
logger.log(`[${process.env.NODE_ENV || "DEVELOPMENT"}] Server started on port ${port}`, "INFO", "api");
})
.catch((error) => {
logger.log(
`[${process.env.NODE_ENV || "DEVELOPMENT"}] Server failed to start on port ${port}`,
"ERROR",
"api",
error
);
});

View File

@@ -12,92 +12,67 @@ const AxiosLib = require("axios").default;
const axios = AxiosLib.create();
const { PBS_ENDPOINTS, PBS_CREDENTIALS } = require("./pbs-constants");
const { CheckForErrors } = require("./pbs-job-export");
const { getSessionData, getMultipleSessionData, setMultipleSessionData } = require("../../../server");
const uuid = require("uuid").v4;
axios.interceptors.request.use((x) => {
const socket = x.socket;
axios.interceptors.request.use(
async (x) => {
const socket = x.socket;
const headers = {
...x.headers.common,
...x.headers[x.method],
...x.headers
};
const printable = `${new Date()} | Request: ${x.method.toUpperCase()} | ${
x.url
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
console.log(printable);
const headers = {
...x.headers.common,
...x.headers[x.method],
...x.headers
};
CdkBase.createJsonEvent(socket, "TRACE", `Raw Request: ${printable}`, x.data);
const printable = `${new Date()} | Request: ${x.method.toUpperCase()} | ${
x.url
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
return x;
});
console.log(printable);
axios.interceptors.response.use((x) => {
const socket = x.config.socket;
// Use await properly here for the async operation
await CdkBase.createJsonEvent(socket, "TRACE", `Raw Request: ${printable}`, x.data);
const printable = `${new Date()} | Response: ${x.status} | ${JSON.stringify(x.data)}`;
console.log(printable);
CdkBase.createJsonEvent(socket, "TRACE", `Raw Response: ${printable}`, x.data);
return x; // Return the modified request
},
(error) => {
return Promise.reject(error); // Proper error handling
}
);
axios.interceptors.response.use(
async (x) => {
const socket = x.config.socket;
const printable = `${new Date()} | Response: ${x.status} | ${JSON.stringify(x.data)}`;
console.log(printable);
// Use await properly here for the async operation
await CdkBase.createJsonEvent(socket, "TRACE", `Raw Response: ${printable}`, x.data);
return x; // Return the modified response
},
(error) => {
return Promise.reject(error); // Proper error handling
}
);
return x;
});
async function PbsCalculateAllocationsAp(socket, billids) {
try {
await CdkBase.createLogEvent(socket, "DEBUG", `Received request to calculate allocations for ${billids}`);
CdkBase.createLogEvent(socket, "DEBUG", `Received request to calculate allocations for ${billids}`);
const { bills, bodyshops } = await QueryBillData(socket, billids);
const bodyshop = bodyshops[0];
await setMultipleSessionData(socket.id, {
bills,
bodyshop
});
const txEnvelope = await getSessionData(socket.id, "txEnvelope");
socket.bodyshop = bodyshop;
socket.bills = bills;
//Each bill will enter it's own top level transaction.
const transactionlist = [];
if (bills.length === 0) {
await CdkBase.createLogEvent(
CdkBase.createLogEvent(
socket,
"ERROR",
`No bills found for export. Ensure they have not already been exported and try again.`
);
}
bills.forEach((bill) => {
//Keep the allocations at the bill level.
const transactionObject = {
SerialNumber: bodyshop.pbs_serialnumber,
SerialNumber: socket.bodyshop.pbs_serialnumber,
billid: bill.id,
Posting: {
Reference: bill.invoice_number,
JournalCode: socket.txEnvelope ? socket.txEnvelope.journal : null,
TransactionDate: moment().tz(socket.bodyshop.timezone).toISOString(), //"0001-01-01T00:00:00.0000000Z",
//Description: "Bulk AP posting.",
//AdditionalInfo: "String",
Reference: bill.invoice_number,
JournalCode: txEnvelope?.journal,
TransactionDate: moment().tz(bodyshop.timezone).toISOString(),
Source: "ImEX Online", // TODO: Resolve this for Rome Online.
Lines: [] // Will be populated with allocation data,
Source: "ImEX Online", //TODO:AIO Resolve this for rome online.
Lines: [] //socket.apAllocations,
}
};
@@ -142,13 +117,13 @@ async function PbsCalculateAllocationsAp(socket, billids) {
};
}
// Add the line amount.
//Add the line amount.
billHash[cc.name] = {
...billHash[cc.name],
Amount: billHash[cc.name].Amount.add(lineDinero)
};
// Does the line have taxes?
//Does the line have taxes?
if (bl.applicable_taxes.federal) {
billHash[bodyshop.md_responsibility_centers.taxes.federal_itc.name] = {
...billHash[bodyshop.md_responsibility_centers.taxes.federal_itc.name],
@@ -170,7 +145,7 @@ async function PbsCalculateAllocationsAp(socket, billids) {
let APAmount = Dinero();
Object.keys(billHash).map((key) => {
if (billHash[key].Amount.getAmount() !== 0) {
if (billHash[key].Amount.getAmount() > 0 || billHash[key].Amount.getAmount() < 0) {
transactionObject.Posting.Lines.push({
...billHash[key],
Amount: billHash[key].Amount.toFormat("0.00")
@@ -194,19 +169,19 @@ async function PbsCalculateAllocationsAp(socket, billids) {
return transactionlist;
} catch (error) {
await CdkBase.createLogEvent(socket, "ERROR", `Error encountered in PbsCalculateAllocationsAp. ${error}`);
CdkBase.createLogEvent(socket, "ERROR", `Error encountered in PbsCalculateAllocationsAp. ${error}`);
}
}
exports.PbsCalculateAllocationsAp = PbsCalculateAllocationsAp;
async function QueryBillData(socket, billids) {
await CdkBase.createLogEvent(socket, "DEBUG", `Querying bill data for id(s) ${billids}`);
CdkBase.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 });
await CdkBase.createLogEvent(socket, "TRACE", `Bill data query result ${JSON.stringify(result, null, 2)}`);
CdkBase.createLogEvent(socket, "TRACE", `Bill data query result ${JSON.stringify(result, null, 2)}`);
return result;
}
@@ -221,49 +196,40 @@ function getCostAccount(billline, respcenters) {
}
exports.PbsExportAp = async function (socket, { billids, txEnvelope }) {
await CdkBase.createLogEvent(socket, "DEBUG", `Exporting selected AP.`);
CdkBase.createLogEvent(socket, "DEBUG", `Exporting selected AP.`);
const apAllocations = await PbsCalculateAllocationsAp(socket, billids);
await setMultipleSessionData(socket.id, {
apAllocations,
txEnvelope
});
for (const allocation of apAllocations) {
//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).catch((err) =>
console.error(`Error running CheckingForErrors in pbs-ap-allocations`)
);
CheckForErrors(socket, AccountPostingChange);
if (AccountPostingChange.WasSuccessful) {
await CdkBase.createLogEvent(socket, "DEBUG", `Marking bill as exported.`);
CdkBase.createLogEvent(socket, "DEBUG", `Marking bill as exported.`);
await MarkApExported(socket, [billid]);
socket.emit("ap-export-success", billid);
} else {
await CdkBase.createLogEvent(socket, "ERROR", `Export was not successful.`);
CdkBase.createLogEvent(socket, "ERROR", `Export was not succesful.`);
socket.emit("ap-export-failure", {
billid,
error: AccountPostingChange.Message
});
}
}
socket.emit("ap-export-complete");
};
async function MarkApExported(socket, billids) {
const { bills, bodyshop } = await getMultipleSessionData(socket.id, ["bills", "bodyshop"]);
await CdkBase.createLogEvent(socket, "DEBUG", `Marking bills as exported for id ${billids}`);
CdkBase.createLogEvent(socket, "DEBUG", `Marking bills as exported for id ${billids}`);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
return await client
const result = await client
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.MARK_BILLS_EXPORTED, {
billids,
@@ -271,11 +237,13 @@ async function MarkApExported(socket, billids) {
exported: true,
exported_at: new Date()
},
logs: bills.map((bill) => ({
bodyshopid: bodyshop.id,
logs: socket.bills.map((bill) => ({
bodyshopid: socket.bodyshop.id,
billid: bill.id,
successful: true,
useremail: socket.user.email
}))
});
return result;
}

View File

@@ -12,169 +12,136 @@ const CalculateAllocations = require("../../cdk/cdk-calculate-allocations").defa
const CdkBase = require("../../web-sockets/web-socket");
const moment = require("moment-timezone");
const Dinero = require("dinero.js");
const { setSessionData, getSessionData, getMultipleSessionData, setMultipleSessionData } = require("../../../server");
const InstanceManager = require("../../utils/instanceMgr").default;
const axios = AxiosLib.create();
axios.interceptors.request.use(
async (x) => {
const socket = x.socket;
axios.interceptors.request.use((x) => {
const socket = x.socket;
const headers = {
...x.headers.common,
...x.headers[x.method],
...x.headers
};
const headers = {
...x.headers.common,
...x.headers[x.method],
...x.headers
};
const printable = `${new Date()} | Request: ${x.method.toUpperCase()} | ${
x.url
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
console.log(printable);
const printable = `${new Date()} | Request: ${x.method.toUpperCase()} | ${
x.url
} | ${JSON.stringify(x.data)} | ${JSON.stringify(headers)}`;
CdkBase.createJsonEvent(socket, "TRACE", `Raw Request: ${printable}`, x.data);
console.log(printable);
return x;
});
await CdkBase.createJsonEvent(socket, "TRACE", `Raw Request: ${printable}`, x.data);
axios.interceptors.response.use((x) => {
const socket = x.config.socket;
return x; // Make sure to return the request object
},
(error) => {
return Promise.reject(error);
}
);
const printable = `${new Date()} | Response: ${x.status} | ${JSON.stringify(x.data)}`;
console.log(printable);
CdkBase.createJsonEvent(socket, "TRACE", `Raw Response: ${printable}`, x.data);
axios.interceptors.response.use(
async (x) => {
const socket = x.config.socket;
const printable = `${new Date()} | Response: ${x.status} | ${JSON.stringify(x.data)}`;
console.log(printable);
await CdkBase.createJsonEvent(socket, "TRACE", `Raw Response: ${printable}`, x.data);
return x; // Make sure to return the response object
},
(error) => {
return Promise.reject(error);
}
);
return x;
});
exports.default = async function (socket, { txEnvelope, jobid }) {
socket.logEvents = [];
socket.recordid = jobid;
socket.txEnvelope = txEnvelope;
try {
await setMultipleSessionData(socket.id, {
recordid: jobid,
txEnvelope
});
await CdkBase.createLogEvent(socket, "DEBUG", `Received Job export request for id ${jobid}`);
CdkBase.createLogEvent(socket, "DEBUG", `Received Job export request for id ${jobid}`);
const JobData = await QueryJobData(socket, jobid);
await setSessionData(socket.id, "JobData", JobData);
await CdkBase.createLogEvent(socket, "DEBUG", `Querying the DMS for the Vehicle Record.`);
// Query for the Vehicle record to get the associated customer
const DmsVeh = await QueryVehicleFromDms(socket);
await setSessionData(socket.id, "DmsVeh", DmsVeh);
let DMSVehCustomer;
socket.JobData = JobData;
CdkBase.createLogEvent(socket, "DEBUG", `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 (DmsVeh?.CustomerRef) {
// Get the associated customer from the Vehicle Record
DMSVehCustomer = await QueryCustomerBycodeFromDms(socket, DmsVeh.CustomerRef);
await setSessionData(socket.id, "DMSVehCustomer", DMSVehCustomer);
if (socket.DmsVeh && socket.DmsVeh.CustomerRef) {
//Get the associated customer from the Vehicle Record.
socket.DMSVehCustomer = await QueryCustomerBycodeFromDms(socket, socket.DmsVeh.CustomerRef);
}
const DMSCustList = await QueryCustomersFromDms(socket);
await setSessionData(socket.id, "DMSCustList", DMSCustList);
socket.DMSCustList = await QueryCustomersFromDms(socket);
socket.emit("pbs-select-customer", [
...(DMSVehCustomer ? [{ ...DMSVehCustomer, vinOwner: true }] : []),
...DMSCustList
...(socket.DMSVehCustomer ? [{ ...socket.DMSVehCustomer, vinOwner: true }] : []),
...socket.DMSCustList
]);
} catch (error) {
await CdkBase.createLogEvent(socket, "ERROR", `Error encountered in PbsJobExport. ${error}`);
CdkBase.createLogEvent(socket, "ERROR", `Error encountered in PbsJobExport. ${error}`);
}
};
exports.PbsSelectedCustomer = async function PbsSelectedCustomer(socket, selectedCustomerId) {
try {
const JobData = await getSessionData(socket.id, "JobData");
if (socket.JobData.bodyshop.pbs_configuration.disablecontactvehicle === false) {
CdkBase.createLogEvent(socket, "DEBUG", `User selected customer ${selectedCustomerId || "NEW"}`);
if (JobData.bodyshop.pbs_configuration.disablecontactvehicle === false) {
await CdkBase.createLogEvent(socket, "DEBUG", `User selected customer ${selectedCustomerId || "NEW"}`);
// Upsert the contact information as per Wafaa's Email
await CdkBase.createLogEvent(
//Upsert the contact information as per Wafaa's Email.
CdkBase.createLogEvent(
socket,
"DEBUG",
`Upserting contact information to DMS for ${
JobData.ownr_fn || ""
} ${JobData.ownr_ln || ""} ${JobData.ownr_co_nm || ""}`
socket.JobData.ownr_fn || ""
} ${socket.JobData.ownr_ln || ""} ${socket.JobData.ownr_co_nm || ""}`
);
const ownerRef = await UpsertContactData(socket, selectedCustomerId);
await CdkBase.createLogEvent(socket, "DEBUG", `Upserting vehicle information to DMS for ${JobData.v_vin}`);
CdkBase.createLogEvent(socket, "DEBUG", `Upserting vehicle information to DMS for ${socket.JobData.v_vin}`);
await UpsertVehicleData(socket, ownerRef.ReferenceId);
} else {
await CdkBase.createLogEvent(
CdkBase.createLogEvent(
socket,
"DEBUG",
`Contact and Vehicle updates disabled. Skipping to accounting data insert.`
);
}
await CdkBase.createLogEvent(socket, "DEBUG", `Inserting account data.`);
await CdkBase.createLogEvent(socket, "DEBUG", `Inserting accounting posting data..`);
CdkBase.createLogEvent(socket, "DEBUG", `Inserting account data.`);
CdkBase.createLogEvent(socket, "DEBUG", `Inserting accounting posting data..`);
const insertResponse = await InsertAccountPostingData(socket);
// TODO: Insert Clear session
if (insertResponse.WasSuccessful) {
await CdkBase.createLogEvent(socket, "DEBUG", `Marking job as exported.`);
await MarkJobExported(socket, JobData.id);
CdkBase.createLogEvent(socket, "DEBUG", `Marking job as exported.`);
await MarkJobExported(socket, socket.JobData.id);
socket.emit("export-success", JobData.id);
socket.emit("export-success", socket.JobData.id);
} else {
await CdkBase.createLogEvent(socket, "ERROR", `Export was not successful.`);
CdkBase.createLogEvent(socket, "ERROR", `Export was not succesful.`);
}
} catch (error) {
await CdkBase.createLogEvent(socket, "ERROR", `Error encountered in PbsSelectedCustomer. ${error}`);
CdkBase.createLogEvent(socket, "ERROR", `Error encountered in CdkSelectedCustomer. ${error}`);
await InsertFailedExportLog(socket, error);
}
};
async function CheckForErrors(socket, response) {
if (response.WasSuccessful === undefined || response.WasSuccessful === true) {
await CdkBase.createLogEvent(socket, "DEBUG", `Successful response from DMS. ${response.Message || ""}`);
CdkBase.createLogEvent(socket, "DEBUG", `Successful response from DMS. ${response.Message || ""}`);
} else {
await CdkBase.createLogEvent(socket, "ERROR", `Error received from DMS: ${response.Message}`);
await CdkBase.createLogEvent(socket, "TRACE", `Error received from DMS: ${JSON.stringify(response)}`);
CdkBase.createLogEvent(socket, "ERROR", `Error received from DMS: ${response.Message}`);
CdkBase.createLogEvent(socket, "TRACE", `Error received from DMS: ${JSON.stringify(response)}`);
}
}
exports.CheckForErrors = CheckForErrors;
async function QueryJobData(socket, jobid) {
await CdkBase.createLogEvent(socket, "DEBUG", `Querying job data for id ${jobid}`);
CdkBase.createLogEvent(socket, "DEBUG", `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 });
await CdkBase.createLogEvent(socket, "TRACE", `Job data query result ${JSON.stringify(result, null, 2)}`);
CdkBase.createLogEvent(socket, "TRACE", `Job data query result ${JSON.stringify(result, null, 2)}`);
return result.jobs_by_pk;
}
async function QueryVehicleFromDms(socket) {
try {
const JobData = await getSessionData(socket.id, "JobData");
if (!JobData.v_vin) return null;
if (!socket.JobData.v_vin) return null;
const { data: VehicleGetResponse, request } = await axios.post(
PBS_ENDPOINTS.VehicleGet,
{
SerialNumber: JobData.bodyshop.pbs_serialnumber,
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
// VehicleId: "00000000000000000000000000000000",
// Year: "String",
// Make: "String",
@@ -182,7 +149,7 @@ async function QueryVehicleFromDms(socket) {
// Trim: "String",
// ModelNumber: "String",
// StockNumber: "String",
VIN: JobData.v_vin
VIN: socket.JobData.v_vin
// LicenseNumber: "String",
// Lot: "String",
// Status: "String",
@@ -199,31 +166,26 @@ async function QueryVehicleFromDms(socket) {
{ auth: PBS_CREDENTIALS, socket }
);
await CheckForErrors(socket, VehicleGetResponse);
CheckForErrors(socket, VehicleGetResponse);
return VehicleGetResponse;
} catch (error) {
await CdkBase.createLogEvent(socket, "ERROR", `Error in QueryVehicleFromDms - ${error}`);
CdkBase.createLogEvent(socket, "ERROR", `Error in QueryVehicleFromDms - ${error}`);
throw new Error(error);
}
}
async function QueryCustomersFromDms(socket) {
try {
// Retrieve JobData from session storage
const JobData = await getSessionData(socket.id, "JobData");
// Make an API call to PBS to query customer details
const { data: CustomerGetResponse } = await axios.post(
PBS_ENDPOINTS.ContactGet,
{
SerialNumber: JobData.bodyshop.pbs_serialnumber,
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
//ContactId: "00000000000000000000000000000000",
// ContactCode: JobData.owner.accountingid,
FirstName: JobData.ownr_fn,
LastName: JobData.ownr_co_nm ? JobData.ownr_co_nm : JobData.ownr_ln,
PhoneNumber: JobData.ownr_ph1,
EmailAddress: JobData.ownr_ea
// ContactCode: socket.JobData.owner.accountingid,
FirstName: socket.JobData.ownr_fn,
LastName: socket.JobData.ownr_co_nm ? socket.JobData.ownr_co_nm : socket.JobData.ownr_ln,
PhoneNumber: socket.JobData.ownr_ph1,
EmailAddress: socket.JobData.ownr_ea
// ModifiedSince: "0001-01-01T00:00:00.0000000Z",
// ModifiedUntil: "0001-01-01T00:00:00.0000000Z",
// ContactIdList: ["00000000000000000000000000000000"],
@@ -235,36 +197,27 @@ async function QueryCustomersFromDms(socket) {
},
{ auth: PBS_CREDENTIALS, socket }
);
// Check for errors in the PBS response
await CheckForErrors(socket, CustomerGetResponse);
// Return the list of contacts from the PBS response
CheckForErrors(socket, CustomerGetResponse);
return CustomerGetResponse && CustomerGetResponse.Contacts;
} catch (error) {
// Log any errors encountered during the API call
await CdkBase.createLogEvent(socket, "ERROR", `Error in QueryCustomersFromDms - ${error}`);
CdkBase.createLogEvent(socket, "ERROR", `Error in QueryCustomersFromDms - ${error}`);
throw new Error(error);
}
}
async function QueryCustomerBycodeFromDms(socket, CustomerRef) {
try {
// Retrieve JobData from session storage
const JobData = await getSessionData(socket.id, "JobData");
// Make an API call to PBS to query customer by ContactId
const { data: CustomerGetResponse } = await axios.post(
PBS_ENDPOINTS.ContactGet,
{
SerialNumber: JobData.bodyshop.pbs_serialnumber,
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
ContactId: CustomerRef
//ContactCode: JobData.owner.accountingid,
//FirstName: JobData.ownr_co_nm
// ? JobData.ownr_co_nm
// : JobData.ownr_fn,
//LastName: JobData.ownr_ln,
//PhoneNumber: JobData.ownr_ph1,
//ContactCode: socket.JobData.owner.accountingid,
//FirstName: socket.JobData.ownr_co_nm
// ? socket.JobData.ownr_co_nm
// : socket.JobData.ownr_fn,
//LastName: socket.JobData.ownr_ln,
//PhoneNumber: socket.JobData.ownr_ph1,
// EmailAddress: "String",
// ModifiedSince: "0001-01-01T00:00:00.0000000Z",
// ModifiedUntil: "0001-01-01T00:00:00.0000000Z",
@@ -277,42 +230,33 @@ async function QueryCustomerBycodeFromDms(socket, CustomerRef) {
},
{ auth: PBS_CREDENTIALS, socket }
);
// Check for errors in the PBS response
await CheckForErrors(socket, CustomerGetResponse);
// Return the list of contacts from the PBS response
CheckForErrors(socket, CustomerGetResponse);
return CustomerGetResponse && CustomerGetResponse.Contacts;
} catch (error) {
// Log any errors encountered during the API call
await CdkBase.createLogEvent(socket, "ERROR", `Error in QueryCustomerBycodeFromDms - ${error}`);
CdkBase.createLogEvent(socket, "ERROR", `Error in QueryCustomersFromDms - ${error}`);
throw new Error(error);
}
}
async function UpsertContactData(socket, selectedCustomerId) {
try {
// Retrieve JobData from session storage
const JobData = await getSessionData(socket.id, "JobData");
// Make an API call to PBS to upsert contact data
const { data: ContactChangeResponse } = await axios.post(
PBS_ENDPOINTS.ContactChange,
{
ContactInfo: {
// Id: JobData.owner.id,
// Id: socket.JobData.owner.id,
...(selectedCustomerId ? { ContactId: selectedCustomerId } : {}),
SerialNumber: JobData.bodyshop.pbs_serialnumber,
Code: JobData.owner.accountingid,
...(JobData.ownr_co_nm
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
Code: socket.JobData.owner.accountingid,
...(socket.JobData.ownr_co_nm
? {
//LastName: JobData.ownr_ln,
FirstName: JobData.ownr_co_nm,
//LastName: socket.JobData.ownr_ln,
FirstName: socket.JobData.ownr_co_nm,
IsBusiness: true
}
: {
LastName: JobData.ownr_ln,
FirstName: JobData.ownr_fn,
LastName: socket.JobData.ownr_ln,
FirstName: socket.JobData.ownr_fn,
IsBusiness: false
}),
@@ -322,20 +266,20 @@ async function UpsertContactData(socket, selectedCustomerId) {
IsInactive: false,
//ApartmentNumber: "String",
Address: JobData.ownr_addr1,
City: JobData.ownr_city,
//County: JobData.ownr_addr1,
State: JobData.ownr_st,
ZipCode: JobData.ownr_zip,
Address: socket.JobData.ownr_addr1,
City: socket.JobData.ownr_city,
//County: socket.JobData.ownr_addr1,
State: socket.JobData.ownr_st,
ZipCode: socket.JobData.ownr_zip,
//BusinessPhone: "String",
//BusinessPhoneExt: "String",
HomePhone: JobData.ownr_ph2,
CellPhone: JobData.ownr_ph1,
HomePhone: socket.JobData.ownr_ph2,
CellPhone: socket.JobData.ownr_ph1,
//BusinessPhoneRawReverse: "String",
//HomePhoneRawReverse: "String",
//CellPhoneRawReverse: "String",
//FaxNumber: "String",
EmailAddress: JobData.ownr_ea
EmailAddress: socket.JobData.ownr_ea
//Notes: "String",
//CriticalMemo: "String",
//BirthDate: "0001-01-01T00:00:00.0000000Z",
@@ -368,43 +312,39 @@ async function UpsertContactData(socket, selectedCustomerId) {
},
{ auth: PBS_CREDENTIALS, socket }
);
await CheckForErrors(socket, ContactChangeResponse);
CheckForErrors(socket, ContactChangeResponse);
return ContactChangeResponse;
} catch (error) {
await CdkBase.createLogEvent(socket, "ERROR", `Error in UpsertContactData - ${error}`);
CdkBase.createLogEvent(socket, "ERROR", `Error in UpsertContactData - ${error}`);
throw new Error(error);
}
}
async function UpsertVehicleData(socket, ownerRef) {
try {
const JobData = await getSessionData(socket.id, "JobData");
const { data: VehicleChangeResponse } = await axios.post(
PBS_ENDPOINTS.VehicleChange,
{
VehicleInfo: {
//Id: "string/00000000-0000-0000-0000-000000000000",
//VehicleId: "00000000000000000000000000000000",
SerialNumber: JobData.bodyshop.pbs_serialnumber,
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
//StockNumber: "String",
VIN: JobData.v_vin,
LicenseNumber: JobData.plate_no,
VIN: socket.JobData.v_vin,
LicenseNumber: socket.JobData.plate_no,
//FleetNumber: "String",
//Status: "String",
OwnerRef: ownerRef, // "00000000000000000000000000000000",
ModelNumber: JobData.vehicle && JobData.vehicle.v_makecode,
Make: JobData.v_make_desc,
Model: JobData.v_model_desc,
Trim: JobData.vehicle && JobData.vehicle.v_trimcode,
ModelNumber: socket.JobData.vehicle && 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,
//VehicleType: "String",
Year: JobData.v_model_yr,
Odometer: JobData.kmout,
Year: socket.JobData.v_model_yr,
Odometer: socket.JobData.kmout,
ExteriorColor: {
Code: JobData.v_color,
Description: JobData.v_color
Code: socket.JobData.v_color,
Description: socket.JobData.v_color
}
// InteriorColor: { Code: "String", Description: "String" },
//Engine: "String",
@@ -525,112 +465,100 @@ async function UpsertVehicleData(socket, ownerRef) {
},
{ auth: PBS_CREDENTIALS, socket }
);
await CheckForErrors(socket, VehicleChangeResponse);
CheckForErrors(socket, VehicleChangeResponse);
return VehicleChangeResponse;
} catch (error) {
await CdkBase.createLogEvent(socket, "ERROR", `Error in UpsertVehicleData - ${error}`);
CdkBase.createLogEvent(socket, "ERROR", `Error in UpsertVehicleData - ${error}`);
throw new Error(error);
}
}
async function InsertAccountPostingData(socket) {
try {
const { JobData, txEnvelope } = await getMultipleSessionData(socket.id, ["JobData", "txEnvelope"]);
const allocations = await CalculateAllocations(socket, JobData.id);
const allocations = await CalculateAllocations(socket, socket.JobData.id);
const wips = [];
allocations.forEach((alloc) => {
// Add the sale item from each allocation if the amount is greater than 0 and not a tax
//Add the sale item from each allocation.
if (alloc.sale.getAmount() > 0 && !alloc.tax) {
const item = {
Account: alloc.profitCenter.dms_acctnumber,
ControlNumber: JobData.ro_number,
ControlNumber: socket.JobData.ro_number,
Amount: alloc.sale.multiply(-1).toFormat("0.00"),
// Comment: "String",
// AdditionalInfo: "String",
InvoiceNumber: JobData.ro_number,
InvoiceDate: moment(JobData.date_invoiced).tz(JobData.bodyshop.timezone).toISOString()
//Comment: "String",
//AdditionalInfo: "String",
InvoiceNumber: socket.JobData.ro_number,
InvoiceDate: moment(socket.JobData.date_invoiced).tz(socket.JobData.bodyshop.timezone).toISOString()
};
wips.push(item);
}
// Add the cost item if the cost amount is greater than 0 and not a tax
//Add the cost Item.
if (alloc.cost.getAmount() > 0 && !alloc.tax) {
const item = {
Account: alloc.costCenter.dms_acctnumber,
ControlNumber: JobData.ro_number,
ControlNumber: socket.JobData.ro_number,
Amount: alloc.cost.toFormat("0.00"),
// Comment: "String",
// AdditionalInfo: "String",
InvoiceNumber: JobData.ro_number,
InvoiceDate: moment(JobData.date_invoiced).tz(JobData.bodyshop.timezone).toISOString()
//Comment: "String",
//AdditionalInfo: "String",
InvoiceNumber: socket.JobData.ro_number,
InvoiceDate: moment(socket.JobData.date_invoiced).tz(socket.JobData.bodyshop.timezone).toISOString()
};
wips.push(item);
const itemWip = {
Account: alloc.costCenter.dms_wip_acctnumber,
ControlNumber: JobData.ro_number,
ControlNumber: socket.JobData.ro_number,
Amount: alloc.cost.multiply(-1).toFormat("0.00"),
// Comment: "String",
// AdditionalInfo: "String",
InvoiceNumber: JobData.ro_number,
InvoiceDate: moment(JobData.date_invoiced).tz(JobData.bodyshop.timezone).toISOString()
//Comment: "String",
//AdditionalInfo: "String",
InvoiceNumber: socket.JobData.ro_number,
InvoiceDate: moment(socket.JobData.date_invoiced).tz(socket.JobData.bodyshop.timezone).toISOString()
};
wips.push(itemWip);
// Add to the WIP account.
//Add to the WIP account.
}
// Add tax-related entries if applicable
if (alloc.tax) {
if (alloc.sale.getAmount() > 0) {
const item2 = {
Account: alloc.profitCenter.dms_acctnumber,
ControlNumber: JobData.ro_number,
ControlNumber: socket.JobData.ro_number,
Amount: alloc.sale.multiply(-1).toFormat("0.00"),
// Comment: "String",
// AdditionalInfo: "String",
InvoiceNumber: JobData.ro_number,
InvoiceDate: moment(JobData.date_invoiced).tz(JobData.bodyshop.timezone).toISOString()
//Comment: "String",
//AdditionalInfo: "String",
InvoiceNumber: socket.JobData.ro_number,
InvoiceDate: moment(socket.JobData.date_invoiced).tz(socket.JobData.bodyshop.timezone).toISOString()
};
wips.push(item2);
}
}
});
// Add payer information
txEnvelope.payers.forEach((payer) => {
socket.txEnvelope.payers.forEach((payer) => {
const item = {
Account: payer.dms_acctnumber,
ControlNumber: payer.controlnumber,
Amount: Dinero({ amount: Math.round(payer.amount * 100) }).toFormat("0.0"),
// Comment: "String",
// AdditionalInfo: "String",
InvoiceNumber: JobData.ro_number,
InvoiceDate: moment(JobData.date_invoiced).tz(JobData.bodyshop.timezone).toISOString()
//Comment: "String",
//AdditionalInfo: "String",
InvoiceNumber: socket.JobData.ro_number,
InvoiceDate: moment(socket.JobData.date_invoiced).tz(socket.JobData.bodyshop.timezone).toISOString()
};
wips.push(item);
});
await setSessionData(socket.id, "transWips", wips);
socket.transWips = wips;
const { data: AccountPostingChange } = await axios.post(
PBS_ENDPOINTS.AccountingPostingChange,
{
SerialNumber: JobData.bodyshop.pbs_serialnumber,
SerialNumber: socket.JobData.bodyshop.pbs_serialnumber,
Posting: {
Reference: JobData.ro_number,
JournalCode: txEnvelope.journal,
TransactionDate: moment(JobData.date_invoiced).tz(JobData.bodyshop.timezone).toISOString(), // "0001-01-01T00:00:00.0000000Z",
Description: txEnvelope.story,
// AdditionalInfo: "String",
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",
Description: socket.txEnvelope.story,
//AdditionalInfo: "String",
Source: InstanceManager({ imex: "ImEX Online", rome: "Rome Online" }),
Lines: wips
}
@@ -638,56 +566,54 @@ async function InsertAccountPostingData(socket) {
{ auth: PBS_CREDENTIALS, socket }
);
await CheckForErrors(socket, AccountPostingChange);
CheckForErrors(socket, AccountPostingChange);
return AccountPostingChange;
} catch (error) {
await CdkBase.createLogEvent(socket, "ERROR", `Error in InsertAccountPostingData - ${error}`);
CdkBase.createLogEvent(socket, "ERROR", `Error in InsertAccountPostingData - ${error}`);
throw new Error(error);
}
}
async function MarkJobExported(socket, jobid) {
await CdkBase.createLogEvent(socket, "DEBUG", `Marking job as exported for id ${jobid}`);
CdkBase.createLogEvent(socket, "DEBUG", `Marking job as exported for id ${jobid}`);
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
const { JobData, transWips } = await getMultipleSessionData(socket.id, ["JobData", "transWips"]);
return await client
const result = await client
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.MARK_JOB_EXPORTED, {
jobId: jobid,
job: {
status: JobData.bodyshop.md_ro_statuses.default_exported || "Exported*",
status: socket.JobData.bodyshop.md_ro_statuses.default_exported || "Exported*",
date_exported: new Date()
},
log: {
bodyshopid: JobData.bodyshop.id,
bodyshopid: socket.JobData.bodyshop.id,
jobid: jobid,
successful: true,
useremail: socket.user.email,
metadata: transWips
metadata: socket.transWips
},
bill: {
exported: true,
exported_at: new Date()
}
});
return result;
}
async function InsertFailedExportLog(socket, error) {
const client = new GraphQLClient(process.env.GRAPHQL_ENDPOINT, {});
const JobData = await getSessionData(socket.id, "JobData");
return await client
const result = await client
.setHeaders({ Authorization: `Bearer ${socket.handshake.auth.token}` })
.request(queries.INSERT_EXPORT_LOG, {
log: {
bodyshopid: JobData.bodyshop.id,
jobid: JobData.id,
bodyshopid: socket.JobData.bodyshop.id,
jobid: socket.JobData.id,
successful: false,
message: [error],
useremail: socket.user.email
}
});
return result;
}

View File

@@ -18,12 +18,12 @@ const { DiscountNotAlreadyCounted } = InstanceManager({
exports.defaultRoute = async function (req, res) {
try {
await CdkBase.createLogEvent(req, "DEBUG", `Received request to calculate allocations for ${req.body.jobid}`);
CdkBase.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: await calculateAllocations(req, jobData) });
return res.status(200).json({ data: calculateAllocations(req, jobData) });
} catch (error) {
console.log(error);
await CdkBase.createLogEvent(req, "ERROR", `Error encountered in CdkCalculateAllocations. ${error}`);
CdkBase.createLogEvent(req, "ERROR", `Error encountered in CdkCalculateAllocations. ${error}`);
res.status(500).json({ error: `Error encountered in CdkCalculateAllocations. ${error}` });
}
};
@@ -31,22 +31,22 @@ exports.defaultRoute = async function (req, res) {
exports.default = async function (socket, jobid) {
try {
const jobData = await QueryJobData(socket, "Bearer " + socket.handshake.auth.token, jobid);
return await calculateAllocations(socket, jobData);
return calculateAllocations(socket, jobData);
} catch (error) {
console.log(error);
await CdkBase.createLogEvent(socket, "ERROR", `Error encountered in CdkCalculateAllocations. ${error}`);
CdkBase.createLogEvent(socket, "ERROR", `Error encountered in CdkCalculateAllocations. ${error}`);
}
};
async function QueryJobData(connectionData, token, jobid) {
await CdkBase.createLogEvent(connectionData, "DEBUG", `Querying job data for id ${jobid}`);
CdkBase.createLogEvent(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 });
await CdkBase.createLogEvent(connectionData, "TRACE", `Job data query result ${JSON.stringify(result, null, 2)}`);
CdkBase.createLogEvent(connectionData, "TRACE", `Job data query result ${JSON.stringify(result, null, 2)}`);
return result.jobs_by_pk;
}
async function calculateAllocations(connectionData, job) {
function calculateAllocations(connectionData, job) {
const { bodyshop } = job;
const taxAllocations = InstanceManager({
@@ -171,7 +171,7 @@ async function calculateAllocations(connectionData, job) {
const selectedDmsAllocationConfig = bodyshop.md_responsibility_centers.dms_defaults.find(
(d) => d.name === job.dms_allocation
);
await CdkBase.createLogEvent(
CdkBase.createLogEvent(
connectionData,
"DEBUG",
`Using DMS Allocation ${selectedDmsAllocationConfig && selectedDmsAllocationConfig.name} for cost export.`
@@ -361,10 +361,9 @@ async function calculateAllocations(connectionData, job) {
}
if (InstanceManager({ rome: true })) {
//profile level adjustments for parts
for (const key of Object.keys(job.job_totals.parts.adjustments)) {
Object.keys(job.job_totals.parts.adjustments).forEach((key) => {
const accountName = selectedDmsAllocationConfig.profits[key];
const otherAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === accountName);
if (otherAccount) {
if (!profitCenterHash[accountName]) profitCenterHash[accountName] = Dinero();
@@ -372,41 +371,31 @@ async function calculateAllocations(connectionData, job) {
Dinero(job.job_totals.parts.adjustments[key])
);
} else {
// Use await correctly here
await CdkBase.createLogEvent(
CdkBase.createLogEvent(
connectionData,
"ERROR",
`Error encountered in CdkCalculateAllocations. Unable to find adjustment account for key ${key}.`
`Error encountered in CdkCalculateAllocations. Unable to find adjustment account. ${error}`
);
}
}
});
//profile level adjustments for labor and materials
for (const key of Object.keys(job.job_totals.rates)) {
if (
job.job_totals.rates[key] &&
job.job_totals.rates[key].adjustment &&
Dinero(job.job_totals.rates[key].adjustment).isZero() === false
) {
Object.keys(job.job_totals.rates).forEach((key) => {
if (job.job_totals.rates[key] && job.job_totals.rates[key].adjustment && Dinero(job.job_totals.rates[key].adjustment).isZero() === false) {
const accountName = selectedDmsAllocationConfig.profits[key.toUpperCase()];
const otherAccount = bodyshop.md_responsibility_centers.profits.find((c) => c.name === accountName);
if (otherAccount) {
if (!profitCenterHash[accountName]) profitCenterHash[accountName] = Dinero();
profitCenterHash[accountName] = profitCenterHash[accountName].add(
Dinero(job.job_totals.rates[key].adjustments)
);
profitCenterHash[accountName] = profitCenterHash[accountName].add(Dinero(job.job_totals.rates[key].adjustments));
} else {
// Await the log event creation here
await CdkBase.createLogEvent(
CdkBase.createLogEvent(
connectionData,
"ERROR",
`Error encountered in CdkCalculateAllocations. Unable to find adjustment account for key ${key}.`
`Error encountered in CdkCalculateAllocations. Unable to find adjustment account. ${error}`
);
}
}
}
});
}
const jobAllocations = _.union(Object.keys(profitCenterHash), Object.keys(costCenterHash)).map((key) => {

View File

@@ -86,7 +86,7 @@ async function GetCdkMakes(req, cdk_dealerid) {
{}
);
await CheckCdkResponseForError(null, soapResponseVehicleSearch);
CheckCdkResponseForError(null, soapResponseVehicleSearch);
const [result, rawResponse, , rawRequest] = soapResponseVehicleSearch;
logger.log("cdk-replace-makes-models-request", "ERROR", req.user.email, null, {
cdk_dealerid,

File diff suppressed because it is too large Load Diff

View File

@@ -15,10 +15,10 @@ exports.CDK_CREDENTIALS = CDK_CREDENTIALS;
const cdkDomain =
process.env.NODE_ENV === "production" ? "https://3pa.dmotorworks.com" : "https://uat-3pa.dmotorworks.com";
async function CheckCdkResponseForError(socket, soapResponse) {
function CheckCdkResponseForError(socket, soapResponse) {
if (!soapResponse[0]) {
//The response was null, this might be ok, it might not.
await CdkBase.createLogEvent(
CdkBase.createLogEvent(
socket,
"WARNING",
`Warning detected in CDK Response - it appears to be null. Stack: ${new Error().stack}`
@@ -31,22 +31,23 @@ async function CheckCdkResponseForError(socket, soapResponse) {
if (Array.isArray(ResultToCheck)) {
ResultToCheck.forEach((result) => checkIndividualResult(socket, result));
} else {
await checkIndividualResult(socket, ResultToCheck);
checkIndividualResult(socket, ResultToCheck);
}
}
exports.CheckCdkResponseForError = CheckCdkResponseForError;
async function checkIndividualResult(socket, ResultToCheck) {
function checkIndividualResult(socket, ResultToCheck) {
if (
ResultToCheck.errorLevel === 0 ||
ResultToCheck.errorLevel === "0" ||
ResultToCheck.code === "success" ||
(!ResultToCheck.code && !ResultToCheck.errorLevel)
) {
)
//TODO: Verify that this is the best way to detect errors.
} else {
await CdkBase.createLogEvent(
return;
else {
CdkBase.createLogEvent(
socket,
"ERROR",
`Error detected in CDK Response - ${JSON.stringify(ResultToCheck, null, 2)}`

View File

@@ -31,12 +31,6 @@ const ftpSetup = {
};
exports.default = async (req, res) => {
// Only process if in production environment.
if (process.env.NODE_ENV !== "production") {
res.sendStatus(403);
return;
}
//Query for the List of Bodyshop Clients.
logger.log("autohouse-start", "DEBUG", "api", null, null);
const { bodyshops } = await client.request(queries.GET_AUTOHOUSE_SHOPS);

View File

@@ -31,12 +31,6 @@ const ftpSetup = {
};
exports.default = async (req, res) => {
// Only process if in production environment.
if (process.env.NODE_ENV !== "production") {
res.sendStatus(403);
return;
}
//Query for the List of Bodyshop Clients.
logger.log("claimscorp-start", "DEBUG", "api", null, null);
const { bodyshops } = await client.request(queries.GET_CLAIMSCORP_SHOPS);

View File

@@ -31,12 +31,6 @@ const ftpSetup = {
};
exports.default = async (req, res) => {
// Only process if in production environment.
if (process.env.NODE_ENV !== "production") {
res.sendStatus(403);
return;
}
//Query for the List of Bodyshop Clients.
logger.log("kaizen-start", "DEBUG", "api", null, null);
const kaizenShopsIDs = ["SUMMIT", "STRATHMORE", "SUNRIDGE", "SHAW"];
@@ -62,8 +56,8 @@ exports.default = async (req, res) => {
try {
const { jobs, bodyshops_by_pk } = await client.request(queries.KAIZEN_QUERY, {
bodyshopid: bodyshop.id,
start: start ? moment(start).startOf("day") : moment().subtract(5, "days").startOf("day"),
...(end && { end: moment(end).endOf("day") })
start: start ? moment(start).startOf("hours") : moment().subtract(2, "hours").startOf("hour"),
...(end && { end: moment(end).endOf("hours") })
});
const kaizenObject = {
@@ -182,19 +176,24 @@ exports.default = async (req, res) => {
} finally {
sftp.end();
}
sendServerEmail({
subject: `Kaizen Report ${moment().format("MM-DD-YY")}`,
text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
Uploaded: ${JSON.stringify(
allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })),
null,
2
)}
`
});
// sendServerEmail({
// subject: `Kaizen Report ${moment().format("MM-DD-YY")}`,
// text: `Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}
// Uploaded: ${JSON.stringify(
// allxmlsToUpload.map((x) => ({ filename: x.filename, count: x.count })),
// null,
// 2
// )}
// `,
// });
res.sendStatus(200);
} catch (error) {
res.status(200).json(error);
sendServerEmail({
subject: `Kaizen Report ${moment().format("MM-DD-YY @ HH:mm:ss")}`,
text: `Errors: JSON.stringify(error)}
All Errors: ${allErrors.map((e) => JSON.stringify(e, null, 2))}`
});
}
};

View File

@@ -41,11 +41,9 @@ const tasksEmailQueueCleanup = async () => {
}
};
// TODO: Consolidate into a group that can be run (multiple events with the same name)
if (process.env.NODE_ENV !== "development") {
// Handling SIGINT (e.g., Ctrl+C)
process.on("SIGINT", async () => {
logger.log("Clearing Tasks Email Queue...", "INFO", "redis", "api");
await tasksEmailQueueCleanup();
process.exit(0);
});

View File

@@ -965,17 +965,22 @@ function CalculateTaxesTotals(job, otherTotals) {
}
});
if (job.adjustment_bottom_line && job.adjustment_bottom_line !== 0) {
for (let tyCounter = 1; tyCounter <= 5; tyCounter++) {
if (IsTrueOrYes(pfp["PAN"][`prt_tx_in${tyCounter}`])) {
//This amount is taxable for this type.
taxableAmountsByTier[`ty${tyCounter}Tax`] = taxableAmountsByTier[`ty${tyCounter}Tax`].add(
Dinero({
amount: Math.round(job.adjustment_bottom_line * 100)
})
);
if (job.adjustment_bottom_line) {
const subtotal_before_adjustment = subtotal.add(Dinero({ amount: Math.round(job.adjustment_bottom_line * -100) }));
const percent_of_adjustment =
Math.round(
subtotal_before_adjustment.toUnit() /
(job.adjustment_bottom_line > 0 ? job.adjustment_bottom_line : job.adjustment_bottom_line * -1)
) / 100;
Object.keys(taxableAmountsByTier).forEach((taxTierKey) => {
taxable_adjustment = taxableAmountsByTier[taxTierKey].multiply(percent_of_adjustment);
if (job.adjustment_bottom_line > 0) {
taxableAmountsByTier[taxTierKey] = taxableAmountsByTier[taxTierKey].add(taxable_adjustment);
} else {
taxableAmountsByTier[taxTierKey] = taxableAmountsByTier[taxTierKey].subtract(taxable_adjustment);
}
}
});
}
const remainingTaxableAmounts = taxableAmountsByTier;

View File

@@ -1,461 +0,0 @@
const { isObject } = require("lodash");
const jobUpdated = async (req, res) => {
const { io, logger } = req;
console.dir(req.body, { depth: null });
if (!req?.body?.event?.data?.new || !isObject(req?.body?.event?.data?.new)) {
logger.log("job-update-error", "ERROR", req.user?.email, null, {
message: `Malformed Job Update request sent from Hasura`,
body: req?.body
});
return res.json({
status: "error",
message: `Malformed Job Update request sent from Hasura`
});
}
logger.log("job-update", "INFO", req.user?.email, null, {
message: `Job updated event received from Hasura`,
jobid: req?.body?.event?.data?.new?.id
});
const updatedJob = req.body.event.data.new;
const bodyshopID = updatedJob.shopid;
// Emit the job-updated event only to the room corresponding to the bodyshop
io.to(bodyshopID).emit("job-updated", updatedJob);
return res.json({ message: "Job updated and event emitted" });
};
module.exports = jobUpdated;
// {
// actual_completion: null,
// actual_delivery: null,
// actual_in: '2024-09-05T23:04:31.439+00:00',
// adj_g_disc: 0,
// adj_strdis: 0,
// adj_towdis: 0,
// adjustment_bottom_line: null,
// agt_addr1: null,
// agt_addr2: null,
// agt_city: null,
// agt_co_id: null,
// agt_co_nm: null,
// agt_ct_fn: null,
// agt_ct_ln: null,
// agt_ct_ph: null,
// agt_ct_phx: null,
// agt_ctry: null,
// agt_ea: null,
// agt_fax: null,
// agt_faxx: null,
// agt_lic_no: null,
// agt_ph1: null,
// agt_ph1x: null,
// agt_ph2: null,
// agt_ph2x: null,
// agt_st: null,
// agt_zip: null,
// alt_transport: 'No car',
// area_of_damage: { impact1: '19', impact2: '0102' },
// asgn_date: '2024-08-06',
// asgn_no: '72361166-99',
// asgn_type: null,
// auto_add_ats: false,
// ca_bc_pvrt: null,
// ca_customer_gst: 500,
// ca_gst_registrant: true,
// cat_no: null,
// category: null,
// cieca_pfl: {},
// cieca_pfo: {},
// cieca_pft: {},
// cieca_stl: {
// data: [
// [Object], [Object], [Object],
// [Object], [Object], [Object],
// [Object], [Object], [Object],
// [Object], [Object], [Object],
// [Object], [Object], [Object],
// [Object], [Object], [Object],
// [Object], [Object], [Object],
// [Object]
// ]
// },
// cieca_ttl: {
// data: {
// g_aa_amt: 0,
// g_bett_amt: 0,
// g_cust_amt: 0,
// g_ded_amt: 0,
// g_rpd_amt: 0,
// g_tax: 854.37,
// g_ttl_amt: 13721.35,
// g_ttl_disc: 0,
// g_upd_amt: 0,
// gst_amt: 612.72,
// n_ttl_amt: 13721.35,
// prev_net: 0,
// supp_amt: 958.53
// }
// },
// ciecaid: '2273515',
// class: null,
// clm_addr1: null,
// clm_addr2: null,
// clm_city: null,
// clm_ct_fn: null,
// clm_ct_ln: null,
// clm_ct_ph: null,
// clm_ct_phx: null,
// clm_ctry: null,
// clm_ea: null,
// clm_fax: null,
// clm_faxx: null,
// clm_no: '72361166-99',
// clm_ofc_id: null,
// clm_ofc_nm: null,
// clm_ph1: null,
// clm_ph1x: null,
// clm_ph2: null,
// clm_ph2x: null,
// clm_st: null,
// clm_title: null,
// clm_total: 13721.3,
// clm_zip: null,
// comment: null,
// completed_tasks: [],
// converted: true,
// created_at: '2024-09-05T22:50:11.37571+00:00',
// cust_pr: 'C',
// date_estimated: null,
// date_exported: null,
// date_invoiced: null,
// date_last_contacted: null,
// date_lost_sale: null,
// date_next_contact: '2024-09-07T23:04:31.439+00:00',
// date_open: '2024-09-05T22:50:12.083+00:00',
// date_rentalresp: null,
// date_repairstarted: null,
// date_scheduled: '2024-09-05T23:04:12.182+00:00',
// date_towin: null,
// date_void: null,
// ded_amt: 0,
// ded_note: null,
// ded_status: 'Y',
// deliverchecklist: null,
// depreciation_taxes: 0,
// dms_allocation: null,
// driveable: true,
// employee_body: null,
// employee_csr: null,
// employee_prep: null,
// employee_refinish: null,
// est_addr1: null,
// est_addr2: null,
// est_city: null,
// est_co_nm: null,
// est_ct_fn: 'Monique',
// est_ct_ln: 'Bruneau',
// est_ctry: null,
// est_ea: 'MONIQUE@STCAUTO.COM',
// est_ph1: null,
// est_st: null,
// est_zip: null,
// federal_tax_rate: 0.05,
// g_bett_amt: 0,
// id: '344fe1e0-4e0c-4f7b-9728-659c850d8192',
// inproduction: true,
// ins_addr1: null,
// ins_addr2: null,
// ins_city: null,
// ins_co_id: null,
// ins_co_nm: 'ICBC',
// ins_ct_fn: null,
// ins_ct_ln: null,
// ins_ct_ph: null,
// ins_ct_phx: null,
// ins_ctry: null,
// ins_ea: null,
// ins_fax: null,
// ins_faxx: null,
// ins_memo: null,
// ins_ph1: null,
// ins_ph1x: null,
// ins_ph2: null,
// ins_ph2x: null,
// ins_st: null,
// ins_title: null,
// ins_zip: null,
// insd_addr1: null,
// insd_addr2: null,
// insd_city: null,
// insd_co_nm: null,
// insd_ctry: null,
// insd_ea: null,
// insd_fax: null,
// insd_faxx: null,
// insd_fn: null,
// insd_ln: null,
// insd_ph1: null,
// insd_ph1x: null,
// insd_ph2: null,
// insd_ph2x: null,
// insd_st: null,
// insd_title: null,
// insd_zip: null,
// intakechecklist: {
// addToProduction: true,
// allow_text_message: false,
// completed_at: '2024-09-05T23:04:31.439Z',
// completed_by: 'allan@imex.dev',
// form: [ [Object], [Object], [Object], [Object], [Object], [Object] ],
// production_vars: {},
// scheduled_completion: '2024-09-25T23:03:55.285Z',
// scheduled_delivery: null
// },
// invoice_allocation: null,
// invoice_date: null,
// invoice_final_note: null,
// iouparent: null,
// job_totals: {
// additional: {
// additionalCostItems: [Array],
// additionalCosts: [Object],
// adjustments: [Object],
// pvrt: [Object],
// shipping: [Object],
// storage: [Object],
// total: [Object],
// towing: [Object]
// },
// parts: { parts: [Object], sublets: [Object] },
// rates: {
// la1: [Object],
// la2: [Object],
// la3: [Object],
// la4: [Object],
// laa: [Object],
// lab: [Object],
// lad: [Object],
// lae: [Object],
// laf: [Object],
// lag: [Object],
// lam: [Object],
// lar: [Object],
// las: [Object],
// lau: [Object],
// mapa: [Object],
// mash: [Object],
// rates_subtotal: [Object],
// subtotal: [Object]
// },
// totals: {
// custPayable: [Object],
// federal_tax: [Object],
// local_tax: [Object],
// net_repairs: [Object],
// statePartsTax: [Object],
// state_tax: [Object],
// subtotal: [Object],
// total_repairs: [Object]
// }
// },
// kanbanparent: '-1',
// kmin: null,
// kmout: null,
// labor_rate_desc: 'EST',
// labor_rate_id: null,
// lbr_adjustments: {},
// local_tax_rate: null,
// loss_cat: 'U',
// loss_date: '2024-06-19',
// loss_desc: 'Animal',
// loss_of_use: null,
// loss_type: 'A',
// lost_sale_reason: null,
// materials: {
// mapa: { cal_maxdlr: 9999.99, cal_opcode: 'OP13' },
// mash: { cal_maxdlr: 9999.99, cal_opcode: 'OP13' }
// },
// other_amount_payable: null,
// owner_owing: 500,
// ownerid: 'f392b24f-e828-47fa-bd17-2e5af8493147',
// ownr_addr1: null,
// ownr_addr2: null,
// ownr_city: null,
// ownr_co_nm: null,
// ownr_ctry: null,
// ownr_ea: null,
// ownr_fax: null,
// ownr_faxx: null,
// ownr_fn: 'Neil',
// ownr_ln: 'Leslie',
// ownr_ph1: null,
// ownr_ph1x: null,
// ownr_ph2: null,
// ownr_ph2x: null,
// ownr_st: null,
// ownr_title: null,
// ownr_zip: null,
// parts_tax_rates: {
// CCC: {},
// CCD: {},
// CCDR: {},
// CCF: {},
// CCM: {},
// PAA: {
// prt_discp: 0,
// prt_mktyp: false,
// prt_mkupp: 0,
// prt_tax_in: true,
// prt_tax_rt: 0.07,
// prt_type: 'PAA'
// },
// PAC: {
// prt_discp: 0,
// prt_mktyp: false,
// prt_mkupp: 0,
// prt_tax_in: true,
// prt_tax_rt: 0.07,
// prt_type: 'PAC'
// },
// PAG: {},
// PAL: {
// prt_discp: 0,
// prt_mktyp: false,
// prt_mkupp: 0,
// prt_tax_in: true,
// prt_tax_rt: 0.07,
// prt_type: 'PAL'
// },
// PAM: {
// prt_discp: 0,
// prt_mktyp: false,
// prt_mkupp: 0,
// prt_tax_in: true,
// prt_tax_rt: 0.07,
// prt_type: 'PAM'
// },
// PAN: {
// prt_discp: 0,
// prt_mktyp: false,
// prt_mkupp: 0,
// prt_tax_in: true,
// prt_tax_rt: 0.07,
// prt_type: 'PAN'
// },
// PAO: {},
// PAP: {},
// PAR: {
// prt_discp: 0,
// prt_mktyp: false,
// prt_mkupp: 0,
// prt_tax_in: true,
// prt_tax_rt: 0.07,
// prt_type: 'PAR'
// },
// PAS: {
// prt_discp: 0,
// prt_mktyp: false,
// prt_mkupp: 0,
// prt_tax_in: true,
// prt_tax_rt: 0.07,
// prt_type: 'PAS'
// },
// PASL: {}
// },
// pay_amt: 0,
// pay_chknm: '0',
// pay_date: null,
// pay_type: null,
// payee_nms: null,
// plate_no: 'B15717',
// plate_st: 'MB',
// po_number: null,
// policy_no: '18292147',
// production_vars: { note: 'testtsdsadsasdasdsadsdsadsd' },
// qb_multiple_payers: null,
// queued_for_parts: false,
// rate_ats: null,
// rate_la1: 0,
// rate_la2: 0,
// rate_la3: null,
// rate_la4: 0,
// rate_laa: 0,
// rate_lab: 88.43,
// rate_lad: null,
// rate_lae: null,
// rate_laf: 97.06,
// rate_lag: 88.43,
// rate_lam: 103.5,
// rate_lar: 88.43,
// rate_las: 88.43,
// rate_lau: 0,
// rate_ma2s: 0,
// rate_ma2t: 0,
// rate_ma3s: 0,
// rate_mabl: null,
// rate_macs: 0,
// rate_mahw: 49.7,
// rate_mapa: 56.67,
// rate_mash: 7.23,
// rate_matd: null,
// referral_source: null,
// referral_source_extra: null,
// regie_number: null,
// remove_from_ar: false,
// ro_number: '10002',
// scheduled_completion: '2024-09-25T23:03:55.285+00:00',
// scheduled_delivery: null,
// scheduled_in: '2024-09-05T22:00:55.2+00:00',
// selling_dealer: null,
// selling_dealer_contact: null,
// servicing_dealer: null,
// servicing_dealer_contact: null,
// shopid: 'bfec8c8c-b7f1-49e0-be4c-524455f4e582',
// special_coverage_policy: false,
// state_tax_rate: null,
// status: 'Repair Plan',
// storage_payable: null,
// suspended: false,
// tax_lbr_rt: 0.07,
// tax_levies_rt: 0.07,
// tax_paint_mat_rt: 0.07,
// tax_predis: 0,
// tax_prethr: 0.07,
// tax_pstthr: 0,
// tax_registration_number: null,
// tax_shop_mat_rt: 0.07,
// tax_str_rt: 0.07,
// tax_sub_rt: 0.07,
// tax_thramt: 0,
// tax_tow_rt: 0,
// theft_ind: false,
// tlos_ind: false,
// towin: false,
// towing_payable: null,
// unit_number: null,
// updated_at: '2024-09-12T19:25:48.492299+00:00',
// v_color: 'Polished Metal Metal',
// v_make_desc: 'Honda',
// v_model_desc: 'Civic',
// v_model_yr: '17',
// v_vin: 'SHHFK7H20HU306419',
// vehicleid: '811ba420-e9db-430b-b341-22e197c8dd0e',
// voided: false
// }
// I have been hit, wewet
// job-transition-update-result {
// type: 'DEBUG',
// env: 'development',
// user: null,
// record: '344fe1e0-4e0c-4f7b-9728-659c850d8192',
// insert_transitions_one: { id: '88e73841-247d-4e31-b31c-f1b2ca8364d9' },
// update_transitions: { affected_rows: 1 }
// }

View File

@@ -14,4 +14,3 @@ exports.costing = require("./job-costing").JobCosting;
exports.costingmulti = require("./job-costing").JobCostingMulti;
exports.statustransition = require("./job-status-transition").statustransition;
exports.lifecycle = require("./job-lifecycle");
exports.jobUpdated = require("./job-updated");

View File

@@ -1,10 +1,11 @@
const express = require("express");
const router = express.Router();
const job = require("../job/job");
const ppc = require("../ccc/partspricechange");
const { partsScan } = require("../parts-scan/parts-scan");
const eventAuthorizationMiddleware = require("../middleware/eventAuthorizationMIddleware");
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
const { totals, statustransition, totalsSsu, costing, lifecycle, costingmulti, jobUpdated } = require("../job/job");
const { totals, statustransition, totalsSsu, costing, lifecycle, costingmulti } = require("../job/job");
const withUserGraphQLClientMiddleware = require("../middleware/withUserGraphQLClientMiddleware");
router.post("/totals", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, totals);
@@ -15,5 +16,5 @@ router.post("/lifecycle", validateFirebaseIdTokenMiddleware, withUserGraphQLClie
router.post("/costingmulti", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, costingmulti);
router.post("/partsscan", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, partsScan);
router.post("/ppc", validateFirebaseIdTokenMiddleware, withUserGraphQLClientMiddleware, ppc.generatePpc);
router.post("/job-updated", eventAuthorizationMiddleware, jobUpdated);
module.exports = router;

View File

@@ -3,79 +3,64 @@ require("dotenv").config({
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
});
const {
io,
setSessionData,
clearSessionData,
getMultipleSessionData,
addItemToEndOfList,
pubClient
} = require("../../server");
const { io } = require("../../server");
const { admin } = require("../firebase/firebase-handler");
const logger = require("../utils/logger");
const { default: CdkJobExport, CdkSelectedCustomer } = require("../cdk/cdk-job-export");
const CdkGetMakes = require("../cdk/cdk-get-makes").default;
const CdkCalculateAllocations = require("../cdk/cdk-calculate-allocations").default;
const { isArray } = require("lodash");
const logger = require("../utils/logger");
const { default: PbsExportJob, PbsSelectedCustomer } = require("../accounting/pbs/pbs-job-export");
const { PbsCalculateAllocationsAp, PbsExportAp } = require("../accounting/pbs/pbs-ap-allocations");
// Middleware for verifying tokens via Firebase authentication
function socketAuthMiddleware(socket, next) {
const token = socket.handshake.auth.token;
if (!token) return next(new Error("Authentication error - no authorization token."));
admin
.auth()
.verifyIdToken(token)
.then((user) => {
socket.user = user;
next();
})
.catch((error) => {
next(new Error("Authentication error", JSON.stringify(error)));
io.use(function (socket, next) {
try {
if (socket.handshake.auth.token) {
admin
.auth()
.verifyIdToken(socket.handshake.auth.token)
.then((user) => {
socket.user = user;
next();
})
.catch((error) => {
next(new Error("Authentication error", JSON.stringify(error)));
});
} else {
next(new Error("Authentication error - no authorization token."));
}
} catch (error) {
console.log("Uncaught connection error:::", error);
logger.log("websocket-connection-error", "error", null, null, {
token: socket.handshake.auth.token,
...error
});
}
next(new Error(`Authentication error ${error}`));
}
});
// Register all socket events for a given socket connection
async function registerSocketEvents(socket) {
await setSessionData(socket.id, "log_level", "TRACE");
await createLogEvent(socket, "DEBUG", `Connected and Authenticated.`);
io.on("connection", (socket) => {
socket.log_level = "TRACE";
createLogEvent(socket, "DEBUG", `Connected and Authenticated.`);
// Register CDK-related socket events
registerCdkEvents(socket);
// Register PBS AR-related socket events
registerPbsArEvents(socket);
// Register PBS AP-related socket events
registerPbsApEvents(socket);
// Register room and broadcasting events
registerRoomAndBroadcastEvents(socket);
// Register event to clear DMS session
registerDmsClearSessionEvent(socket);
// Register Production Board events
registerProductionBoardEvents(socket);
// Handle socket disconnection
socket.on("disconnect", async () => {
await createLogEvent(socket, "DEBUG", `User disconnected.`);
socket.on("set-log-level", (level) => {
socket.log_level = level;
socket.emit("log-event", {
timestamp: new Date(),
level: "INFO",
message: `Updated log level to ${level}`
});
});
}
// CDK-specific socket events
function registerCdkEvents(socket) {
socket.on("cdk-export-job", (jobid) => CdkJobExport(socket, jobid));
socket.on("cdk-selected-customer", async (selectedCustomerId) => {
await createLogEvent(socket, "DEBUG", `User selected customer ID ${selectedCustomerId}`);
CdkSelectedCustomer(socket, selectedCustomerId).catch((err) =>
console.error(`Error in cdk-selected-customer: ${err}`)
);
await setSessionData(socket.id, "selectedCustomer", selectedCustomerId);
///CDK
socket.on("cdk-export-job", (jobid) => {
CdkJobExport(socket, jobid);
});
socket.on("cdk-selected-customer", (selectedCustomerId) => {
createLogEvent(socket, "DEBUG", `User selected customer ID ${selectedCustomerId}`);
socket.selectedCustomerId = selectedCustomerId;
CdkSelectedCustomer(socket, selectedCustomerId);
});
socket.on("cdk-get-makes", async (cdk_dealerid, callback) => {
@@ -83,187 +68,159 @@ function registerCdkEvents(socket) {
const makes = await CdkGetMakes(socket, cdk_dealerid);
callback(makes);
} catch (error) {
await createLogEvent(socket, "ERROR", `Error in cdk-get-makes WS call. ${JSON.stringify(error)}`);
createLogEvent(socket, "ERROR", `Error in cdk-get-makes WS call. ${JSON.stringify(error, null, 2)}`);
}
});
socket.on("cdk-calculate-allocations", async (jobid, callback) => {
const allocations = await CdkCalculateAllocations(socket, jobid);
await createLogEvent(socket, "DEBUG", `Allocations calculated.`);
await createLogEvent(socket, "TRACE", `Allocations details: ${JSON.stringify(allocations)}`);
callback(allocations);
await setSessionData(socket.id, "cdk_allocations", allocations);
});
}
createLogEvent(socket, "DEBUG", `Allocations calculated.`);
createLogEvent(socket, "TRACE", `Allocations calculated. ${JSON.stringify(allocations, null, 2)}`);
// PBS AR-specific socket events
function registerPbsArEvents(socket) {
callback(allocations);
});
//END CDK
//PBS AR
socket.on("pbs-calculate-allocations", async (jobid, callback) => {
const allocations = await CdkCalculateAllocations(socket, jobid);
await createLogEvent(socket, "DEBUG", `PBS AR allocations calculated.`);
await createLogEvent(socket, "TRACE", `Allocations details: ${JSON.stringify(allocations)}`);
createLogEvent(socket, "DEBUG", `Allocations calculated.`);
createLogEvent(socket, "TRACE", `Allocations calculated. ${JSON.stringify(allocations, null, 2)}`);
callback(allocations);
await setSessionData(socket.id, "pbs_allocations", allocations);
});
socket.on("pbs-export-job", (jobid) => PbsExportJob(socket, jobid));
socket.on("pbs-selected-customer", async (selectedCustomerId) => {
await createLogEvent(socket, "DEBUG", `PBS AR selected customer ID ${selectedCustomerId}`);
PbsSelectedCustomer(socket, selectedCustomerId).catch((err) =>
console.error(`Error in pbs-selected-customer: ${err}`)
);
await setSessionData(socket.id, "selectedCustomer", selectedCustomerId);
socket.on("pbs-export-job", (jobid) => {
PbsExportJob(socket, jobid);
});
}
socket.on("pbs-selected-customer", (selectedCustomerId) => {
createLogEvent(socket, "DEBUG", `User selected customer ID ${selectedCustomerId}`);
socket.selectedCustomerId = selectedCustomerId;
PbsSelectedCustomer(socket, selectedCustomerId);
});
//End PBS AR
function registerProductionBoardEvents(socket) {}
// PBS AP-specific socket events
function registerPbsApEvents(socket) {
//PBS AP
socket.on("pbs-calculate-allocations-ap", async (billids, callback) => {
const allocations = await PbsCalculateAllocationsAp(socket, billids);
await createLogEvent(socket, "DEBUG", `PBS AP allocations calculated.`);
await createLogEvent(socket, "TRACE", `Allocations details: ${JSON.stringify(allocations)}`);
createLogEvent(socket, "DEBUG", `AP Allocations calculated.`);
createLogEvent(socket, "TRACE", `Allocations calculated. ${JSON.stringify(allocations, null, 2)}`);
socket.apAllocations = allocations;
callback(allocations);
await setSessionData(socket.id, "pbs_ap_allocations", allocations);
});
socket.on("pbs-export-ap", async ({ billids, txEnvelope }) => {
await setSessionData(socket.id, "pbs_txEnvelope", txEnvelope);
PbsExportAp(socket, {
billids,
txEnvelope
}).catch((err) => console.error(`Error in pbs-export-ap: ${err}`));
});
}
const getRedisKeyForSocket = (socketId) => `socket:${socketId}:rooms`;
// Room management and broadcasting events
function registerRoomAndBroadcastEvents(socket) {
// Rejoin rooms on reconnect
pubClient.lRange(getRedisKeyForSocket(socket.id), 0, -1, (err, rooms) => {
if (rooms && rooms.length > 0) {
rooms.forEach((room) => {
socket.join(room);
createLogEvent(socket, "DEBUG", `Client rejoined bodyshop room: ${room}`);
});
}
socket.on("pbs-export-ap", ({ billids, txEnvelope }) => {
socket.txEnvelope = txEnvelope;
PbsExportAp(socket, { billids, txEnvelope });
});
socket.on("join-bodyshop-room", async (bodyshopUUID) => {
socket.join(bodyshopUUID);
// Store room in Redis
pubClient.rPush(getRedisKeyForSocket(socket.id), bodyshopUUID);
await createLogEvent(socket, "DEBUG", `Client joined bodyshop room: ${bodyshopUUID}`);
});
socket.on("leave-bodyshop-room", async (bodyshopUUID) => {
socket.leave(bodyshopUUID);
// Remove room from Redis
pubClient.lRem(getRedisKeyForSocket(socket.id), 0, bodyshopUUID);
await createLogEvent(socket, "DEBUG", `Client left bodyshop room: ${bodyshopUUID}`);
});
socket.on("broadcast-to-bodyshop", async (bodyshopUUID, message) => {
io.to(bodyshopUUID).emit("bodyshop-message", message);
await createLogEvent(socket, "INFO", `Broadcast message to bodyshop ${bodyshopUUID}`);
});
//END PBS AP
socket.on("disconnect", () => {
// Optional: Cleanup Redis entry on disconnect if needed
createLogEvent(socket, "DEBUG", `Client disconnected: ${socket.id}`);
createLogEvent(socket, "DEBUG", `User disconnected.`);
});
}
// DMS session clearing event
function registerDmsClearSessionEvent(socket) {
socket.on("clear-dms-session", async () => {
await clearSessionData(socket.id); // Clear all session data in Redis
await createLogEvent(socket, "INFO", `DMS session data cleared for socket ${socket.id}`);
});
}
});
// Logging helper functions
async function createLogEvent(socket, level, message) {
const { logLevel, recordid } = await getMultipleSessionData(socket.id, ["log_level", "recordid"]);
if (LogLevelHierarchy(logLevel) >= LogLevelHierarchy(level)) {
const logMessage = { timestamp: new Date(), level, message };
// Log the message to the console and emit it via the socket
console.log(`[WS LOG EVENT] ${level} - ${logMessage.timestamp} - ${socket.user.email} - ${socket.id} - ${message}`);
socket.emit("log-event", logMessage);
// Log the event via the logger
logger.log("ws-log-event", level, socket.user.email, recordid, { wsmessage: message });
// Add the log message to the Redis list using the helper function
await addItemToEndOfList(socket.id, "logEvents", logMessage);
}
}
async function createJsonEvent(socket, level, message, json) {
const { logLevel, recordid } = await getMultipleSessionData(socket.id, ["log_level", "recordid"]);
if (LogLevelHierarchy(logLevel) >= LogLevelHierarchy(level)) {
const logMessage = { timestamp: new Date(), level, message };
// Emit the log message via the socket
socket.emit("log-event", logMessage);
// Log the JSON event via the logger
logger.log("ws-log-event-json", level, socket.user.email, recordid, {
wsmessage: message,
json
function createLogEvent(socket, level, message) {
if (LogLevelHierarchy(socket.log_level) >= LogLevelHierarchy(level)) {
console.log(`[WS LOG EVENT] ${level} - ${new Date()} - ${socket.user.email} - ${socket.id} - ${message}`);
socket.emit("log-event", {
timestamp: new Date(),
level,
message
});
// Use the helper function to append the log event to the Redis list
await addItemToEndOfList(socket.id, "logEvents", logMessage);
logger.log("ws-log-event", level, socket.user.email, socket.recordid, {
wsmessage: message
});
if (socket.logEvents && isArray(socket.logEvents)) {
socket.logEvents.push({
timestamp: new Date(),
level,
message
});
}
// if (level === "ERROR") {
// throw new Error(message);
// }
}
}
async function createXmlEvent(socket, xml, message, isError = false) {
const { logLevel, recordid } = await getMultipleSessionData(socket.id, ["log_level", "recordid"]);
function createJsonEvent(socket, level, message, json) {
if (LogLevelHierarchy(socket.log_level) >= LogLevelHierarchy(level)) {
console.log(`[WS LOG EVENT] ${level} - ${new Date()} - ${socket.user.email} - ${socket.id} - ${message}`);
socket.emit("log-event", {
timestamp: new Date(),
level,
message
});
}
logger.log("ws-log-event-json", level, socket.user.email, socket.recordid, {
wsmessage: message,
json
});
if (LogLevelHierarchy(logLevel) >= LogLevelHierarchy("TRACE")) {
const logMessage = {
if (socket.logEvents && isArray(socket.logEvents)) {
socket.logEvents.push({
timestamp: new Date(),
level,
message
});
}
// if (level === "ERROR") {
// throw new Error(message);
// }
}
function createXmlEvent(socket, xml, message, isError = false) {
if (LogLevelHierarchy(socket.log_level) >= LogLevelHierarchy("TRACE")) {
socket.emit("log-event", {
timestamp: new Date(),
level: isError ? "ERROR" : "TRACE",
message: `${message}: ${xml}`
};
});
}
// Emit the log message via the socket
socket.emit("log-event", logMessage);
logger.log(
isError ? "ws-log-event-xml-error" : "ws-log-event-xml",
isError ? "ERROR" : "TRACE",
socket.user.email,
socket.recordid,
{
wsmessage: message,
xml
}
);
// Log the XML event via the logger
logger.log(
isError ? "ws-log-event-xml-error" : "ws-log-event-xml",
isError ? "ERROR" : "TRACE",
socket.user.email,
recordid,
{ wsmessage: message, xml }
);
// Use the helper function to append the log event to the Redis list
await addItemToEndOfList(socket.id, "logEvents", { ...logMessage, xml });
if (socket.logEvents && isArray(socket.logEvents)) {
socket.logEvents.push({
timestamp: new Date(),
level: isError ? "ERROR" : "TRACE",
message,
xml
});
}
}
// Log level hierarchy
function LogLevelHierarchy(level) {
const levels = { XML: 5, TRACE: 5, DEBUG: 4, INFO: 3, WARNING: 2, ERROR: 1 };
return levels[level] || 3;
switch (level) {
case "XML":
return 5;
case "TRACE":
return 5;
case "DEBUG":
return 4;
case "INFO":
return 3;
case "WARNING":
return 2;
case "ERROR":
return 1;
default:
return 3;
}
}
// Socket.IO Middleware and Connection
io.use(socketAuthMiddleware);
io.on("connection", registerSocketEvents);
// Export logging helpers
exports.createLogEvent = createLogEvent;
exports.createXmlEvent = createXmlEvent;
exports.createJsonEvent = createJsonEvent;