Merged in release/2025-09-12 (pull request #2554)
DO NOT MERGE - Release/2025-09-12 into master-AIO -IO-3255, IO-3310, IO-3352, IO-3355
This commit is contained in:
308
client/package-lock.json
generated
308
client/package-lock.json
generated
@@ -9,48 +9,49 @@
|
||||
"version": "0.2.1",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-browser": "^2.23.1",
|
||||
"@amplitude/analytics-browser": "^2.23.5",
|
||||
"@ant-design/pro-layout": "^7.22.6",
|
||||
"@apollo/client": "^3.13.9",
|
||||
"@emotion/is-prop-valid": "^1.3.1",
|
||||
"@emotion/is-prop-valid": "^1.4.0",
|
||||
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
||||
"@firebase/analytics": "^0.10.17",
|
||||
"@firebase/app": "^0.14.1",
|
||||
"@firebase/app": "^0.14.2",
|
||||
"@firebase/auth": "^1.10.8",
|
||||
"@firebase/firestore": "^4.8.0",
|
||||
"@firebase/firestore": "^4.9.1",
|
||||
"@firebase/messaging": "^0.12.22",
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"@sentry/cli": "^2.52.0",
|
||||
"@reduxjs/toolkit": "^2.9.0",
|
||||
"@sentry/cli": "^2.53.0",
|
||||
"@sentry/react": "^9.43.0",
|
||||
"@sentry/vite-plugin": "^4.1.1",
|
||||
"@sentry/vite-plugin": "^4.3.0",
|
||||
"@splitsoftware/splitio-react": "^2.3.1",
|
||||
"@tanem/react-nprogress": "^5.0.53",
|
||||
"antd": "^5.27.1",
|
||||
"antd": "^5.27.3",
|
||||
"apollo-link-logger": "^2.0.1",
|
||||
"apollo-link-sentry": "^4.4.0",
|
||||
"autosize": "^6.0.1",
|
||||
"axios": "^1.11.0",
|
||||
"classnames": "^2.5.1",
|
||||
"css-box-model": "^1.2.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"dayjs": "^1.11.18",
|
||||
"dayjs-business-days2": "^1.3.0",
|
||||
"dinero.js": "^1.9.1",
|
||||
"dotenv": "^17.2.1",
|
||||
"dotenv": "^17.2.2",
|
||||
"env-cmd": "^10.1.0",
|
||||
"exifr": "^7.1.3",
|
||||
"graphql": "^16.11.0",
|
||||
"i18next": "^25.4.0",
|
||||
"i18next": "^25.5.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"libphonenumber-js": "^1.12.13",
|
||||
"libphonenumber-js": "^1.12.15",
|
||||
"lightningcss": "^1.30.1",
|
||||
"logrocket": "^9.0.2",
|
||||
"markerjs2": "^2.32.6",
|
||||
"memoize-one": "^6.0.0",
|
||||
"normalize-url": "^8.0.2",
|
||||
"object-hash": "^3.0.0",
|
||||
"phone": "^3.1.67",
|
||||
"posthog-js": "^1.260.2",
|
||||
"posthog-js": "^1.261.7",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^9.2.2",
|
||||
"raf-schd": "^4.0.3",
|
||||
@@ -62,7 +63,7 @@
|
||||
"react-drag-listview": "^2.0.0",
|
||||
"react-grid-gallery": "^1.0.1",
|
||||
"react-grid-layout": "1.3.4",
|
||||
"react-i18next": "^15.7.1",
|
||||
"react-i18next": "^15.7.3",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-image-lightbox": "^5.1.4",
|
||||
"react-markdown": "^10.1.0",
|
||||
@@ -81,7 +82,7 @@
|
||||
"redux-saga": "^1.3.0",
|
||||
"redux-state-sync": "^3.1.4",
|
||||
"reselect": "^5.1.1",
|
||||
"sass": "^1.90.0",
|
||||
"sass": "^1.92.0",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"styled-components": "^6.1.19",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
@@ -110,7 +111,6 @@
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^15.15.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"lightningcss": "^1.30.1",
|
||||
"memfs": "^4.36.3",
|
||||
"os-browserify": "^0.3.0",
|
||||
"playwright": "^1.55.0",
|
||||
@@ -141,28 +141,28 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@amplitude/analytics-browser": {
|
||||
"version": "2.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.23.1.tgz",
|
||||
"integrity": "sha512-TYsh7ORT9UoEF3JpmWVpyyRyeE4k8SS+6TNgEoCRj4ZtjiiWKP1CE7lEspgVBjWdSCUqS1o85Cte7c2mkj+SiA==",
|
||||
"version": "2.23.5",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.23.5.tgz",
|
||||
"integrity": "sha512-R1N506rifI3/axSTM3EQkVjCgeJsmhybRONOdnA3MCJwOIC77UVEOIzTVNjnAAzgBSxDNTCy6ejGgBf3PgzBog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-core": "^2.21.1",
|
||||
"@amplitude/analytics-core": "^2.22.1",
|
||||
"@amplitude/analytics-remote-config": "^0.4.0",
|
||||
"@amplitude/plugin-autocapture-browser": "^1.10.1",
|
||||
"@amplitude/plugin-network-capture-browser": "^1.5.1",
|
||||
"@amplitude/plugin-page-view-tracking-browser": "^2.3.42",
|
||||
"@amplitude/plugin-web-vitals-browser": "^0.1.0-beta.17",
|
||||
"@amplitude/plugin-autocapture-browser": "^1.11.1",
|
||||
"@amplitude/plugin-network-capture-browser": "^1.5.4",
|
||||
"@amplitude/plugin-page-view-tracking-browser": "^2.3.45",
|
||||
"@amplitude/plugin-web-vitals-browser": "^0.1.0-beta.20",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@amplitude/analytics-client-common": {
|
||||
"version": "2.3.36",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-2.3.36.tgz",
|
||||
"integrity": "sha512-4MmuUuX8V9HOCrZ3VMQ3v3lkdksKQxswsO6mpm4YJvznty16+AaaupajubHik5GmmK8MV89ZqG0yLQLKiQm4yg==",
|
||||
"version": "2.3.39",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-2.3.39.tgz",
|
||||
"integrity": "sha512-Dt31IIalME8whTXLgnKPLh9HbHTr8dC9F51reS1gngXAkOTErzAvbBl6UIc09bjqHWmimsRYgi6nflubnqwvMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-connector": "^1.4.8",
|
||||
"@amplitude/analytics-core": "^2.21.1",
|
||||
"@amplitude/analytics-core": "^2.22.1",
|
||||
"@amplitude/analytics-types": "^2.10.0",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
@@ -174,9 +174,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@amplitude/analytics-core": {
|
||||
"version": "2.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.21.1.tgz",
|
||||
"integrity": "sha512-4lfjUDl4VF4H+O9uZJsf6hlmOlVte+CJI45i8gV8vh9jUEn0/Ad3Cyeu2D9p2dUtLUgKVcXglqkoSpxPzhGWFw==",
|
||||
"version": "2.22.1",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.22.1.tgz",
|
||||
"integrity": "sha512-nzlulhS7jYQc91wOc392avBLDAiPZmIBuJ1apA640YlleX/egVxKgZVYHH3Ge4ZNkaxoESwUb4mf2R+ZI0fXxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-connector": "^1.6.4",
|
||||
@@ -202,12 +202,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@amplitude/plugin-autocapture-browser": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.10.1.tgz",
|
||||
"integrity": "sha512-fLsad4xnxkiZ62mEFxze5SgNyxbc6qk7FMlzUPCpgkPhdbJkiogajTonEnRi+p5HU2Ze8K242gsfnR66xLEU1Q==",
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.11.1.tgz",
|
||||
"integrity": "sha512-6nus1nXlH1ru/yjx07yk1cyjc9scAsE9dO4f0xxH8xpHlYQ4yVCuYApcguIpogISlPiySAxSZ+4WDreLrpQiDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-core": "^2.21.1",
|
||||
"@amplitude/analytics-core": "^2.22.1",
|
||||
"@amplitude/analytics-remote-config": "^0.6.3",
|
||||
"rxjs": "^7.8.1",
|
||||
"tslib": "^2.4.1"
|
||||
@@ -241,23 +241,23 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@amplitude/plugin-network-capture-browser": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.5.1.tgz",
|
||||
"integrity": "sha512-45KD4wo+7dfFIi3Q7w3u6x3R9FQdYifSZPyDG02V7YYdOjmRFC0K4Jzx0fpmbYqsl4BQDwe4q2DC6eDPKYDn3A==",
|
||||
"version": "1.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.5.4.tgz",
|
||||
"integrity": "sha512-GRvi44tNx2TdHQ/dnC9DLqwsaBE1gC/bmHNaudTbp/nwIM8nVCAxZaXaXJEUouK7WBAamr7a3WmFruecqCeOlA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-core": "^2.21.1",
|
||||
"@amplitude/analytics-core": "^2.22.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@amplitude/plugin-page-view-tracking-browser": {
|
||||
"version": "2.3.42",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.3.42.tgz",
|
||||
"integrity": "sha512-MSO5hOSXdPXAUSW3vFqUz08/MrAfzn4TU1uyYL0q1MZz63bEwxppVaMnwgx1NfkyYf4zlWn0KZ6PREhXeWL0YA==",
|
||||
"version": "2.3.45",
|
||||
"resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.3.45.tgz",
|
||||
"integrity": "sha512-L2JH/TDTdjfexkY5hHVS3dCb4+q5H1jeIKhXUcBQ/Wx91asLY9BsH91J4bo9EK4J4Al8jVRwqJz0tIQ17qW9RQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-client-common": "^2.3.36",
|
||||
"@amplitude/analytics-client-common": "^2.3.39",
|
||||
"@amplitude/analytics-types": "^2.10.0",
|
||||
"tslib": "^2.4.1"
|
||||
}
|
||||
@@ -2534,9 +2534,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@emotion/is-prop-valid": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz",
|
||||
"integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==",
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz",
|
||||
"integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@emotion/memoize": "^0.9.0"
|
||||
@@ -3280,9 +3280,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@firebase/app": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.1.tgz",
|
||||
"integrity": "sha512-jxTrDbxnGoX7cGz7aP9E7v9iKvBbQfZ8Gz4TH3SfrrkcyIojJM3+hJnlbGnGxHrABts844AxRcg00arMZEyA6Q==",
|
||||
"version": "0.14.2",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.2.tgz",
|
||||
"integrity": "sha512-Ecx2ig/JLC9ayIQwZHqm41Tzlf4c1WUuFhFUZB1y+JIJqDRE579x7Uil7tKT8MwDpOPwrK5ZtpxdSsrfy/LF8Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@firebase/component": "0.7.0",
|
||||
@@ -3333,9 +3333,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@firebase/firestore": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.9.0.tgz",
|
||||
"integrity": "sha512-5zl0+/h1GvlCSLt06RMwqFsd7uqRtnNZt4sW99k2rKRd6k/ECObIWlEnvthm2cuOSnUmwZknFqtmd1qyYSLUuQ==",
|
||||
"version": "4.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.9.1.tgz",
|
||||
"integrity": "sha512-PYVUTkhC9y8pydrqC3O1Oc4AMfkGSWdmuH9xgPJjiEbpUIUPQ4J8wJhyuash+o2u+axmyNRFP8ULNUKb+WzBzQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@firebase/component": "0.7.0",
|
||||
@@ -4090,6 +4090,12 @@
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@posthog/core": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.0.2.tgz",
|
||||
"integrity": "sha512-hWk3rUtJl2crQK0WNmwg13n82hnTwB99BT99/XI5gZSvIlYZ1TPmMZE8H2dhJJ98J/rm9vYJ/UXNzw3RV5HTpQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
@@ -4387,9 +4393,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz",
|
||||
"integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==",
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz",
|
||||
"integrity": "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
@@ -5055,6 +5061,7 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.1.1.tgz",
|
||||
"integrity": "sha512-HUpqrCK7zDVojTV6KL6BO9ZZiYrEYQqvYQrscyMsq04z+WCupXaH6YEliiNRvreR8DBJgdsG3lBRpebhUGmvfA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
@@ -5080,6 +5087,7 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.1.1.tgz",
|
||||
"integrity": "sha512-Hx9RgXaD1HEYmL5aYoWwCKkVvPp4iklwfD9mvmdpQtcwLg6b6oLnPVDQaOry1ak6Pxt8smlrWcKy4IiKASlvig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.18.5",
|
||||
@@ -5099,6 +5107,7 @@
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -5108,9 +5117,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/cli": {
|
||||
"version": "2.52.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.52.0.tgz",
|
||||
"integrity": "sha512-PXyo7Yv7+rVMSBGZfI/eFEzzhiKedTs25sDCjz4a3goAZ/F5R5tn3MKq30pnze5wNnoQmLujAa0uUjfNcWP+uQ==",
|
||||
"version": "2.53.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.53.0.tgz",
|
||||
"integrity": "sha512-n2ZNb+5Z6AZKQSI0SusQ7ZzFL637mfw3Xh4C3PEyVSn9LiF683fX0TTq8OeGmNZQS4maYfS95IFD+XpydU0dEA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
@@ -5127,20 +5136,20 @@
|
||||
"node": ">= 10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@sentry/cli-darwin": "2.52.0",
|
||||
"@sentry/cli-linux-arm": "2.52.0",
|
||||
"@sentry/cli-linux-arm64": "2.52.0",
|
||||
"@sentry/cli-linux-i686": "2.52.0",
|
||||
"@sentry/cli-linux-x64": "2.52.0",
|
||||
"@sentry/cli-win32-arm64": "2.52.0",
|
||||
"@sentry/cli-win32-i686": "2.52.0",
|
||||
"@sentry/cli-win32-x64": "2.52.0"
|
||||
"@sentry/cli-darwin": "2.53.0",
|
||||
"@sentry/cli-linux-arm": "2.53.0",
|
||||
"@sentry/cli-linux-arm64": "2.53.0",
|
||||
"@sentry/cli-linux-i686": "2.53.0",
|
||||
"@sentry/cli-linux-x64": "2.53.0",
|
||||
"@sentry/cli-win32-arm64": "2.53.0",
|
||||
"@sentry/cli-win32-i686": "2.53.0",
|
||||
"@sentry/cli-win32-x64": "2.53.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/cli-darwin": {
|
||||
"version": "2.52.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.52.0.tgz",
|
||||
"integrity": "sha512-ieQs/p4yTHT27nBzy0wtAb8BSISfWlpXdgsACcwXimYa36NJRwyCqgOXUaH/BYiTdwWSHpuANbUHGJW6zljzxw==",
|
||||
"version": "2.53.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.53.0.tgz",
|
||||
"integrity": "sha512-NNPfpILMwKgpHiyJubHHuauMKltkrgLQ5tvMdxNpxY60jBNdo5VJtpESp4XmXlnidzV4j1z61V4ozU6ttDgt5Q==",
|
||||
"license": "BSD-3-Clause",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -5151,9 +5160,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/cli-linux-arm": {
|
||||
"version": "2.52.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.52.0.tgz",
|
||||
"integrity": "sha512-tWMLU+hj+iip5Akx+S76biAOE1eMMWTDq8c0MqMv/ahHgb6/HiVngMcUsp59Oz3EczJGbTkcnS3vRTDodEcMDw==",
|
||||
"version": "2.53.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.53.0.tgz",
|
||||
"integrity": "sha512-NdRzQ15Ht83qG0/Lyu11ciy/Hu/oXbbtJUgwzACc7bWvHQA8xEwTsehWexqn1529Kfc5EjuZ0Wmj3MHmp+jOWw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -5169,9 +5178,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/cli-linux-arm64": {
|
||||
"version": "2.52.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.52.0.tgz",
|
||||
"integrity": "sha512-RxT5uzxjCkcvplmx0bavJIEYerRex2Rg/2RAVBdVvWLKFOcmeerTn/VVxPZVuDIVMVyjlZsteWPYwfUm+Ia3wQ==",
|
||||
"version": "2.53.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.53.0.tgz",
|
||||
"integrity": "sha512-xY/CZ1dVazsSCvTXzKpAgXaRqfljVfdrFaYZRUaRPf1ZJRGa3dcrivoOhSIeG/p5NdYtMvslMPY9Gm2MT0M83A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -5187,9 +5196,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/cli-linux-i686": {
|
||||
"version": "2.52.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.52.0.tgz",
|
||||
"integrity": "sha512-sKcJmIg7QWFtlNU5Bs5OZprwdIzzyYMRpFkWioPZ4TE82yvP1+2SAX31VPUlTx+7NLU6YVEWNwvSxh8LWb7iOw==",
|
||||
"version": "2.53.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.53.0.tgz",
|
||||
"integrity": "sha512-0REmBibGAB4jtqt9S6JEsFF4QybzcXHPcHtJjgMi5T0ueh952uG9wLzjSxQErCsxTKF+fL8oG0Oz5yKBuCwCCQ==",
|
||||
"cpu": [
|
||||
"x86",
|
||||
"ia32"
|
||||
@@ -5206,9 +5215,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/cli-linux-x64": {
|
||||
"version": "2.52.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.52.0.tgz",
|
||||
"integrity": "sha512-aPZ7bP02zGkuEqTiOAm4np/ggfgtzrq4ti1Xze96Csi/DV3820SCfLrPlsvcvnqq7x69IL9cI3kXjdEpgrfGxw==",
|
||||
"version": "2.53.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.53.0.tgz",
|
||||
"integrity": "sha512-9UGJL+Vy5N/YL1EWPZ/dyXLkShlNaDNrzxx4G7mTS9ywjg+BIuemo6rnN7w43K1NOjObTVO6zY0FwumJ1pCyLg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -5224,9 +5233,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/cli-win32-arm64": {
|
||||
"version": "2.52.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.52.0.tgz",
|
||||
"integrity": "sha512-90hrB5XdwJVhRpCmVrEcYoKW8nl5/V9OfVvOGeKUPvUkApLzvsInK74FYBZEVyAn1i/NdUv+Xk9q2zqUGK1aLQ==",
|
||||
"version": "2.53.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.53.0.tgz",
|
||||
"integrity": "sha512-G1kjOjrjMBY20rQcJV2GA8KQE74ufmROCDb2GXYRfjvb1fKAsm4Oh8N5+Tqi7xEHdjQoLPkE4CNW0aH68JSUDQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -5240,9 +5249,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/cli-win32-i686": {
|
||||
"version": "2.52.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.52.0.tgz",
|
||||
"integrity": "sha512-HXlSE4CaLylNrELx4KVmOQjV5bURCNuky6sjCWiTH7HyDqHEak2Rk8iLE0JNLj5RETWMvmaZnZZFfmyGlY1opg==",
|
||||
"version": "2.53.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.53.0.tgz",
|
||||
"integrity": "sha512-qbGTZUzesuUaPtY9rPXdNfwLqOZKXrJRC1zUFn52hdo6B+Dmv0m/AHwRVFHZP53Tg1NCa8bDei2K/uzRN0dUZw==",
|
||||
"cpu": [
|
||||
"x86",
|
||||
"ia32"
|
||||
@@ -5257,9 +5266,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/cli-win32-x64": {
|
||||
"version": "2.52.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.52.0.tgz",
|
||||
"integrity": "sha512-hJT0C3FwHk1Mt9oFqcci88wbO1D+yAWUL8J29HEGM5ZAqlhdh7sAtPDIC3P2LceUJOjnXihow47Bkj62juatIQ==",
|
||||
"version": "2.53.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.53.0.tgz",
|
||||
"integrity": "sha512-1TXYxYHtwgUq5KAJt3erRzzUtPqg7BlH9T7MdSPHjJatkrr/kwZqnVe2H6Arr/5NH891vOlIeSPHBdgJUAD69g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -5320,18 +5329,58 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/vite-plugin": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-4.1.1.tgz",
|
||||
"integrity": "sha512-kNIZiqRbFHJHzV0QF1RyuwMprwK2Lk354qs98P7DduU1TkzrNG3+2f8liYJaiYCrsjDvJlPHyVFBDF9IRhJGdA==",
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-4.3.0.tgz",
|
||||
"integrity": "sha512-MeTAHMmTOgBPMAjeW7/ONyXwgScZdaFFtNiALKcAODnVqC7eoHdSRIWeH5mkLr2Dvs7nqtBaDpKxRjUBgfm9LQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/bundler-plugin-core": "4.1.1",
|
||||
"@sentry/bundler-plugin-core": "4.3.0",
|
||||
"unplugin": "1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/vite-plugin/node_modules/@sentry/babel-plugin-component-annotate": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-4.3.0.tgz",
|
||||
"integrity": "sha512-OuxqBprXRyhe8Pkfyz/4yHQJc5c3lm+TmYWSSx8u48g5yKewSQDOxkiLU5pAk3WnbLPy8XwU/PN+2BG0YFU9Nw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/vite-plugin/node_modules/@sentry/bundler-plugin-core": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-4.3.0.tgz",
|
||||
"integrity": "sha512-dmR4DJhJ4jqVWGWppuTL2blNFqOZZnt4aLkewbD1myFG3KVfUx8CrMQWEmGjkgPOtj5TO6xH9PyTJjXC6o5tnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.18.5",
|
||||
"@sentry/babel-plugin-component-annotate": "4.3.0",
|
||||
"@sentry/cli": "^2.51.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"find-up": "^5.0.0",
|
||||
"glob": "^9.3.2",
|
||||
"magic-string": "0.30.8",
|
||||
"unplugin": "1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/vite-plugin/node_modules/dotenv": {
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/webpack-plugin": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-4.1.1.tgz",
|
||||
@@ -6148,9 +6197,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/antd": {
|
||||
"version": "5.27.1",
|
||||
"resolved": "https://registry.npmjs.org/antd/-/antd-5.27.1.tgz",
|
||||
"integrity": "sha512-jGMSdBN7hAMvPV27B4RhzZfL6n6yu8yDbo7oXrlJasaOqB7bSDPcjdEy1kXy3JPsny/Qazb1ykzRI4EfcByAPQ==",
|
||||
"version": "5.27.3",
|
||||
"resolved": "https://registry.npmjs.org/antd/-/antd-5.27.3.tgz",
|
||||
"integrity": "sha512-Jewp1ek1iyqoAyjWyPgzc2kioZ+7S3jh39a+tld/j4ucnuf/cBk4omfyIdhLz49pVNsaEcRp5LtJOSQPFwPgpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "^7.2.1",
|
||||
@@ -6192,7 +6241,7 @@
|
||||
"rc-slider": "~11.1.8",
|
||||
"rc-steps": "~6.0.1",
|
||||
"rc-switch": "~4.1.0",
|
||||
"rc-table": "~7.51.1",
|
||||
"rc-table": "~7.52.6",
|
||||
"rc-tabs": "~15.7.0",
|
||||
"rc-textarea": "~1.10.2",
|
||||
"rc-tooltip": "~6.4.0",
|
||||
@@ -7897,9 +7946,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.13",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
|
||||
"version": "1.11.18",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz",
|
||||
"integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dayjs-business-days2": {
|
||||
@@ -8074,7 +8123,6 @@
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -8197,9 +8245,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz",
|
||||
"integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==",
|
||||
"version": "17.2.2",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz",
|
||||
"integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@@ -9846,9 +9894,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "25.4.0",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.4.0.tgz",
|
||||
"integrity": "sha512-UH5aiamXsO3cfrZFurCHiB6YSs3C+s+XY9UaJllMMSbmaoXILxFgqDEZu4NbfzJFjmUo3BNMa++Rjkr3ofjfLw==",
|
||||
"version": "25.5.2",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.5.2.tgz",
|
||||
"integrity": "sha512-lW8Zeh37i/o0zVr+NoCHfNnfvVw+M6FQbRp36ZZ/NyHDJ3NJVpp2HhAUyU9WafL5AssymNoOjMRB48mmx2P6Hw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -10992,16 +11040,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/libphonenumber-js": {
|
||||
"version": "1.12.13",
|
||||
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.13.tgz",
|
||||
"integrity": "sha512-QZXnR/OGiDcBjF4hGk0wwVrPcZvbSSyzlvkjXv5LFfktj7O2VZDrt4Xs8SgR/vOFco+qk1i8J43ikMXZoTrtPw==",
|
||||
"version": "1.12.15",
|
||||
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.15.tgz",
|
||||
"integrity": "sha512-TMDCtIhWUDHh91wRC+wFuGlIzKdPzaTUHHVrIZ3vPUEoNaXFLrsIQ1ZpAeZeXApIF6rvDksMTvjrIQlLKaYxqQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
||||
"integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
@@ -11033,7 +11080,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -11054,7 +11100,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -11075,7 +11120,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -11096,7 +11140,6 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -11117,7 +11160,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -11138,7 +11180,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -11159,7 +11200,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -11180,7 +11220,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -11201,7 +11240,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -11222,7 +11260,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -13152,11 +13189,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.260.2",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.260.2.tgz",
|
||||
"integrity": "sha512-2Q+QUz9j9+uG16wp0WcOEbezVsLZCobZyTX8NvWPMGKyPaf2lOsjbPjznsq5JiIt324B6NAqzpWYZTzvhn9k9Q==",
|
||||
"version": "1.261.7",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.261.7.tgz",
|
||||
"integrity": "sha512-Fjpbz6VfIMsEbKIN/UyTWhU1DGgVIngqoRjPGRolemIMOVzTfI77OZq8WwiBhMug+rU+wNhGCQhC41qRlR5CxA==",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"@posthog/core": "1.0.2",
|
||||
"core-js": "^3.38.1",
|
||||
"fflate": "^0.4.8",
|
||||
"preact": "^10.19.3",
|
||||
@@ -13892,9 +13930,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rc-table": {
|
||||
"version": "7.51.1",
|
||||
"resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.51.1.tgz",
|
||||
"integrity": "sha512-5iq15mTHhvC42TlBLRCoCBLoCmGlbRZAlyF21FonFnS/DIC8DeRqnmdyVREwt2CFbPceM0zSNdEeVfiGaqYsKw==",
|
||||
"version": "7.52.7",
|
||||
"resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.52.7.tgz",
|
||||
"integrity": "sha512-yuZfnTpuHwRa4JH+F28wQfGeDzqtgIDvLBBJk5sFncXQjTExhtBNc6dPfVo5pL5SjabJEoejefs6wsrAKfhDoQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.10.1",
|
||||
@@ -14204,16 +14242,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "15.7.1",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.7.1.tgz",
|
||||
"integrity": "sha512-o4VsKh30fy7p0z5ACHuyWqB6xu9WpQIQy2/ZcbCqopNnrnTVOPn/nAv9uYP4xYAWg99QMpvZ9Bu/si3eGurzGw==",
|
||||
"version": "15.7.3",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.7.3.tgz",
|
||||
"integrity": "sha512-AANws4tOE+QSq/IeMF/ncoHlMNZaVLxpa5uUGW1wjike68elVYr0018L9xYoqBr1OFO7G7boDPrbn0HpMCJxTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.27.6",
|
||||
"html-parse-stringify": "^3.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 23.4.0",
|
||||
"i18next": ">= 25.4.1",
|
||||
"react": ">= 16.8.0",
|
||||
"typescript": "^5"
|
||||
},
|
||||
@@ -15134,9 +15172,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.90.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.90.0.tgz",
|
||||
"integrity": "sha512-9GUyuksjw70uNpb1MTYWsH9MQHOHY6kwfnkafC24+7aOMZn9+rVMBxRbLvw756mrBFbIsFg6Xw9IkR2Fnn3k+Q==",
|
||||
"version": "1.92.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.92.0.tgz",
|
||||
"integrity": "sha512-KDNI0BxgIRDAfJgzNm5wuy+4yOCIZyrUbjSpiU/JItfih+KGXAVefKL53MTml054MmBA3DDKIBMSI/7XLxZJ3A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
|
||||
@@ -8,48 +8,48 @@
|
||||
"private": true,
|
||||
"proxy": "http://localhost:4000",
|
||||
"dependencies": {
|
||||
"@amplitude/analytics-browser": "^2.23.1",
|
||||
"@amplitude/analytics-browser": "^2.23.5",
|
||||
"@ant-design/pro-layout": "^7.22.6",
|
||||
"@apollo/client": "^3.13.9",
|
||||
"@emotion/is-prop-valid": "^1.3.1",
|
||||
"@emotion/is-prop-valid": "^1.4.0",
|
||||
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
||||
"@firebase/analytics": "^0.10.17",
|
||||
"@firebase/app": "^0.14.1",
|
||||
"@firebase/app": "^0.14.2",
|
||||
"@firebase/auth": "^1.10.8",
|
||||
"@firebase/firestore": "^4.8.0",
|
||||
"@firebase/firestore": "^4.9.1",
|
||||
"@firebase/messaging": "^0.12.22",
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"@sentry/cli": "^2.52.0",
|
||||
"@reduxjs/toolkit": "^2.9.0",
|
||||
"@sentry/cli": "^2.53.0",
|
||||
"@sentry/react": "^9.43.0",
|
||||
"@sentry/vite-plugin": "^4.1.1",
|
||||
"@sentry/vite-plugin": "^4.3.0",
|
||||
"@splitsoftware/splitio-react": "^2.3.1",
|
||||
"@tanem/react-nprogress": "^5.0.53",
|
||||
"antd": "^5.27.1",
|
||||
"antd": "^5.27.3",
|
||||
"apollo-link-logger": "^2.0.1",
|
||||
"apollo-link-sentry": "^4.4.0",
|
||||
"autosize": "^6.0.1",
|
||||
"axios": "^1.11.0",
|
||||
"classnames": "^2.5.1",
|
||||
"css-box-model": "^1.2.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"dayjs": "^1.11.18",
|
||||
"dayjs-business-days2": "^1.3.0",
|
||||
"dinero.js": "^1.9.1",
|
||||
"dotenv": "^17.2.1",
|
||||
"dotenv": "^17.2.2",
|
||||
"env-cmd": "^10.1.0",
|
||||
"exifr": "^7.1.3",
|
||||
"graphql": "^16.11.0",
|
||||
"i18next": "^25.4.0",
|
||||
"i18next": "^25.5.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"immutability-helper": "^3.1.1",
|
||||
"libphonenumber-js": "^1.12.13",
|
||||
"libphonenumber-js": "^1.12.15",
|
||||
"logrocket": "^9.0.2",
|
||||
"markerjs2": "^2.32.6",
|
||||
"memoize-one": "^6.0.0",
|
||||
"normalize-url": "^8.0.2",
|
||||
"object-hash": "^3.0.0",
|
||||
"phone": "^3.1.67",
|
||||
"posthog-js": "^1.260.2",
|
||||
"posthog-js": "^1.261.7",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^9.2.2",
|
||||
"raf-schd": "^4.0.3",
|
||||
@@ -57,11 +57,12 @@
|
||||
"react-big-calendar": "^1.19.4",
|
||||
"react-color": "^2.19.3",
|
||||
"react-cookie": "^8.0.1",
|
||||
"lightningcss": "^1.30.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-drag-listview": "^2.0.0",
|
||||
"react-grid-gallery": "^1.0.1",
|
||||
"react-grid-layout": "1.3.4",
|
||||
"react-i18next": "^15.7.1",
|
||||
"react-i18next": "^15.7.3",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-image-lightbox": "^5.1.4",
|
||||
"react-markdown": "^10.1.0",
|
||||
@@ -80,7 +81,7 @@
|
||||
"redux-saga": "^1.3.0",
|
||||
"redux-state-sync": "^3.1.4",
|
||||
"reselect": "^5.1.1",
|
||||
"sass": "^1.90.0",
|
||||
"sass": "^1.92.0",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"styled-components": "^6.1.19",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
@@ -152,7 +153,6 @@
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^15.15.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"lightningcss": "^1.30.1",
|
||||
"memfs": "^4.36.3",
|
||||
"os-browserify": "^0.3.0",
|
||||
"playwright": "^1.55.0",
|
||||
|
||||
@@ -20,35 +20,27 @@ export function JobTotalsCashDiscount({ bodyshop, amountDinero }) {
|
||||
const notification = useNotification();
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
if (amountDinero && bodyshop) {
|
||||
setLoading(true);
|
||||
let response;
|
||||
try {
|
||||
response = await axios.post("/intellipay/checkfee", {
|
||||
bodyshop: { id: bodyshop.id, imexshopid: bodyshop.imexshopid, state: bodyshop.state },
|
||||
amount: Dinero(amountDinero).toFormat("0.00")
|
||||
});
|
||||
if (!amountDinero || !bodyshop) return;
|
||||
|
||||
if (response?.data?.error) {
|
||||
notification.open({
|
||||
type: "error",
|
||||
message:
|
||||
response.data?.error ||
|
||||
"Error encountered when contacting IntelliPay service to determine cash discounted price."
|
||||
});
|
||||
} else {
|
||||
setFee(response.data?.fee || 0);
|
||||
}
|
||||
} catch (error) {
|
||||
notification.open({
|
||||
type: "error",
|
||||
message:
|
||||
error.response?.data?.error ||
|
||||
"Error encountered when contacting IntelliPay service to determine cash discounted price."
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoading(true);
|
||||
const errorMessage = "Error encountered when contacting IntelliPay service to determine cash discounted price.";
|
||||
|
||||
try {
|
||||
const { id, imexshopid, state } = bodyshop;
|
||||
const { data } = await axios.post("/intellipay/checkfee", {
|
||||
bodyshop: { id, imexshopid, state },
|
||||
amount: Dinero(amountDinero).toUnit()
|
||||
});
|
||||
|
||||
if (data?.error) {
|
||||
notification.open({ type: "error", message: data.error || errorMessage });
|
||||
} else {
|
||||
setFee(data?.fee ?? 0);
|
||||
}
|
||||
} catch (error) {
|
||||
notification.open({ type: "error", message: error.response?.data?.error || errorMessage });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [amountDinero, bodyshop, notification]);
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
||||
<Checkbox
|
||||
checked={!!job.estimate_sent_approval}
|
||||
onChange={(e) => handleCheckboxChange("estimate_sent_approval", e.target.checked)}
|
||||
disabled={disabled}
|
||||
disabled={disabled || isPartsEntry}
|
||||
>
|
||||
{job.estimate_sent_approval && (
|
||||
<span style={{ color: "#888" }}>
|
||||
@@ -192,7 +192,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
||||
<Checkbox
|
||||
checked={!!job.estimate_approved}
|
||||
onChange={(e) => handleCheckboxChange("estimate_approved", e.target.checked)}
|
||||
disabled={disabled}
|
||||
disabled={disabled || isPartsEntry}
|
||||
>
|
||||
{job.estimate_approved && (
|
||||
<span style={{ color: "#888" }}>
|
||||
@@ -237,7 +237,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
||||
<Card
|
||||
style={{ height: "100%" }}
|
||||
title={
|
||||
disabled ? (
|
||||
disabled || isPartsEntry ? (
|
||||
<>{ownerTitle.length > 0 ? ownerTitle : t("owner.labels.noownerinfo")}</>
|
||||
) : (
|
||||
<Link to={`/manage/owners/${job.owner.id}`}>
|
||||
@@ -248,14 +248,14 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
||||
>
|
||||
<div>
|
||||
<DataLabel key="2" label={t("jobs.fields.ownr_ph1")}>
|
||||
{disabled ? (
|
||||
{disabled || isPartsEntry ? (
|
||||
<PhoneNumberFormatter>{job.ownr_ph1}</PhoneNumberFormatter>
|
||||
) : (
|
||||
<ChatOpenButton phone={job.ownr_ph1} jobid={job.id} />
|
||||
)}
|
||||
</DataLabel>
|
||||
<DataLabel key="22" label={t("jobs.fields.ownr_ph2")}>
|
||||
{disabled ? (
|
||||
{disabled || isPartsEntry ? (
|
||||
<PhoneNumberFormatter>{job.ownr_ph2}</PhoneNumberFormatter>
|
||||
) : (
|
||||
<ChatOpenButton phone={job.ownr_ph2} jobid={job.id} />
|
||||
@@ -267,7 +267,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
||||
} ${job.ownr_st || ""} ${job.ownr_zip || ""}`}
|
||||
</DataLabel>
|
||||
<DataLabel key="4" label={t("owners.fields.ownr_ea")}>
|
||||
{disabled ? (
|
||||
{disabled || isPartsEntry ? (
|
||||
<>{job.ownr_ea || ""}</>
|
||||
) : job.ownr_ea ? (
|
||||
<a href={`mailto:${job.ownr_ea}`}>{job.ownr_ea}</a>
|
||||
@@ -317,7 +317,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
||||
<DataLabel label={t("jobs.labels.relatedros")}>
|
||||
<JobsRelatedRos jobid={job.id} job={job} disabled={disabled} />
|
||||
</DataLabel>
|
||||
{job.vehicle && job.vehicle.notes && (
|
||||
{job.vehicle?.notes && (
|
||||
<DataLabel
|
||||
label={t("vehicles.fields.notes")}
|
||||
valueStyle={{ whiteSpace: "pre-wrap" }}
|
||||
@@ -327,7 +327,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail, is
|
||||
{job.vehicle.notes}
|
||||
</DataLabel>
|
||||
)}
|
||||
{job.vehicle && job.vehicle.v_paint_codes && (
|
||||
{job.vehicle?.v_paint_codes && (
|
||||
<DataLabel label={t("vehicles.fields.v_paint_codes", { number: "" })}>
|
||||
<span style={{ whiteSpace: "pre" }}>
|
||||
{Object.keys(job.vehicle.v_paint_codes)
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { Space, Tag } from "antd";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectIsPartsEntry } from "../../redux/application/application.selectors.js";
|
||||
import { connect } from "react-redux";
|
||||
import getPartsBasePath from "../../utils/getPartsBasePath.js";
|
||||
|
||||
export default function JobsRelatedRos({ job, disabled }) {
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
isPartsEntry: selectIsPartsEntry
|
||||
});
|
||||
|
||||
function JobsRelatedRos({ job, disabled, isPartsEntry }) {
|
||||
if (!(job?.vehicle && job.vehicle.jobs)) return null;
|
||||
const basePath = getPartsBasePath(isPartsEntry);
|
||||
|
||||
return (
|
||||
<Space wrap>
|
||||
{job.vehicle.jobs
|
||||
@@ -12,7 +22,7 @@ export default function JobsRelatedRos({ job, disabled }) {
|
||||
{disabled ? (
|
||||
<>{`${j.ro_number || "N/A"}${j.clm_no ? ` | ${j.clm_no}` : ""}${j.status ? ` | ${j.status}` : ""}`}</>
|
||||
) : (
|
||||
<Link to={`/manage/jobs/${j?.id}`}>{`${j.ro_number || "N/A"}${
|
||||
<Link to={`${basePath}/jobs/${j?.id}`}>{`${j.ro_number || "N/A"}${
|
||||
j.clm_no ? ` | ${j.clm_no}` : ""
|
||||
}${j.status ? ` | ${j.status}` : ""}`}</Link>
|
||||
)}
|
||||
@@ -21,3 +31,4 @@ export default function JobsRelatedRos({ job, disabled }) {
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
export default connect(mapStateToProps)(JobsRelatedRos);
|
||||
|
||||
@@ -227,15 +227,21 @@ export function PartsOrderListTableComponent({
|
||||
sorter: (a, b) => a.order_date - b.order_date,
|
||||
sortOrder: state.sortedInfo.columnKey === "order_date" && state.sortedInfo.order,
|
||||
render: (text, record) => <DateFormatter>{record.order_date}</DateFormatter>
|
||||
},
|
||||
{
|
||||
}
|
||||
];
|
||||
|
||||
if (!isPartsEntry) {
|
||||
columns.push({
|
||||
title: t("parts_orders.fields.return"),
|
||||
dataIndex: "return",
|
||||
key: "return",
|
||||
sorter: (a, b) => a.return - b.return,
|
||||
sortOrder: state.sortedInfo.columnKey === "return" && state.sortedInfo.order,
|
||||
render: (text, record) => <Checkbox checked={record.return} />
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
columns.push(
|
||||
{
|
||||
title: t("parts_orders.fields.deliver_by"),
|
||||
dataIndex: "deliver_by",
|
||||
@@ -256,7 +262,7 @@ export function PartsOrderListTableComponent({
|
||||
render: (text, record) => recordActions(record, true),
|
||||
id: "parts-order-list-table-actions"
|
||||
}
|
||||
];
|
||||
);
|
||||
|
||||
const handleTableChange = (pagination, filters, sorter) => {
|
||||
setState({ ...state, filteredInfo: filters, sortedInfo: sorter });
|
||||
|
||||
@@ -11,16 +11,27 @@ 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";
|
||||
import { selectIsPartsEntry } from "../../redux/application/application.selectors.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
bodyshop: selectBodyshop,
|
||||
isPartsEntry: selectIsPartsEntry
|
||||
});
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PartsOrderModalComponent);
|
||||
|
||||
export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState, isReturn, preferredMake, job, form }) {
|
||||
export function PartsOrderModalComponent({
|
||||
bodyshop,
|
||||
vendorList,
|
||||
sendTypeState,
|
||||
isReturn,
|
||||
preferredMake,
|
||||
job,
|
||||
form,
|
||||
isPartsEntry
|
||||
}) {
|
||||
const [sendType, setSendType] = sendTypeState;
|
||||
|
||||
const {
|
||||
@@ -83,7 +94,7 @@ export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState,
|
||||
</Space>
|
||||
</Tag>
|
||||
)}
|
||||
{!isReturn && (
|
||||
{!isReturn && !isPartsEntry && (
|
||||
<Form.Item
|
||||
name="removefrompartsqueue"
|
||||
label={t("parts_orders.labels.removefrompartsqueue")}
|
||||
@@ -92,7 +103,7 @@ export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState,
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
)}
|
||||
{OEConnection.treatment === "on" && !isReturn && (
|
||||
{OEConnection.treatment === "on" && !isReturn && !isPartsEntry && (
|
||||
<Form.Item name="is_quote" label={t("parts_orders.labels.is_quote")} valuePropName="checked">
|
||||
<Checkbox />
|
||||
</Form.Item>
|
||||
@@ -249,7 +260,7 @@ export function PartsOrderModalComponent({ bodyshop, vendorList, sendTypeState,
|
||||
<Radio disabled={is_quote} value={"p"}>
|
||||
{t("parts_orders.labels.print")}
|
||||
</Radio>
|
||||
{OEConnection.treatment === "on" && !isReturn && (
|
||||
{OEConnection.treatment === "on" && !isReturn && !isPartsEntry && (
|
||||
<Radio value={"oec"}>{t("parts_orders.labels.oec")}</Radio>
|
||||
)}
|
||||
</Radio.Group>
|
||||
|
||||
@@ -40,27 +40,26 @@ export function ScheduleCalendarWrapperComponent({
|
||||
const currentView = search.view || defaultView || "week";
|
||||
|
||||
const handleEventPropStyles = (event) => {
|
||||
const hasColor = Boolean(event?.color?.hex || event?.color);
|
||||
const { color, block, arrived } = event ?? {};
|
||||
const hasColor = Boolean(color?.hex || color);
|
||||
const useBg = currentView !== "agenda";
|
||||
|
||||
// Prioritize explicit blocked-day background to ensure red in all themes
|
||||
let bg;
|
||||
if (useBg) {
|
||||
if (event?.block) {
|
||||
bg = "var(--event-block-bg)";
|
||||
} else if (hasColor) {
|
||||
bg = event?.color?.hex ?? event?.color;
|
||||
} else {
|
||||
bg = "var(--event-bg-fallback)";
|
||||
}
|
||||
bg = block
|
||||
? "var(--event-block-bg)"
|
||||
: arrived
|
||||
? "var(--event-arrived-bg)"
|
||||
: (color?.hex ?? color ?? "var(--event-bg-fallback)");
|
||||
}
|
||||
|
||||
const usedFallback = !hasColor && !event?.block; // only mark as fallback when not blocked
|
||||
const usedFallback = !hasColor && !block && !arrived; // only mark as fallback when not blocked or arrived
|
||||
|
||||
const classes = [
|
||||
"imex-event",
|
||||
event.arrived && "imex-event-arrived",
|
||||
event.block && "imex-event-block",
|
||||
arrived && "imex-event-arrived",
|
||||
block && "imex-event-block",
|
||||
usedFallback && "imex-event-fallback"
|
||||
]
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -23,13 +23,24 @@ export default function ShopInfoContainer() {
|
||||
});
|
||||
const notification = useNotification();
|
||||
|
||||
const combinedFeatureConfig = {
|
||||
...FEATURE_CONFIGS.general,
|
||||
...FEATURE_CONFIGS.responsibilitycenters
|
||||
};
|
||||
const combineFeatureConfigs = (...configs) =>
|
||||
(configs || [])
|
||||
.filter(Boolean)
|
||||
.flatMap((cfg) => Object.entries(cfg))
|
||||
.reduce((acc, [featureName, fieldPaths]) => {
|
||||
if (!Array.isArray(fieldPaths)) return acc;
|
||||
acc[featureName] = [...(acc[featureName] ?? []), ...fieldPaths];
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const combinedFeatureConfig = combineFeatureConfigs(FEATURE_CONFIGS.general, FEATURE_CONFIGS.responsibilitycenters);
|
||||
|
||||
// Use form data preservation for all shop-info features
|
||||
const { createSubmissionHandler } = useFormDataPreservation(form, data?.bodyshops[0], combinedFeatureConfig);
|
||||
const { createSubmissionHandler, preserveHiddenFormData } = useFormDataPreservation(
|
||||
form,
|
||||
data?.bodyshops[0],
|
||||
combinedFeatureConfig
|
||||
);
|
||||
|
||||
const handleFinish = createSubmissionHandler((values) => {
|
||||
setSaveLoading(true);
|
||||
@@ -51,8 +62,11 @@ export default function ShopInfoContainer() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) form.resetFields();
|
||||
}, [form, data]);
|
||||
if (!data) return;
|
||||
form.resetFields();
|
||||
// After reset, re-apply hidden field preservation so values aren't wiped
|
||||
preserveHiddenFormData();
|
||||
}, [data, form, preserveHiddenFormData]);
|
||||
|
||||
if (error) return <AlertComponent message={error.message} type="error" />;
|
||||
if (loading) return <LoadingSpinner />;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { HasFeatureAccess } from "./../feature-wrapper/feature-wrapper.component";
|
||||
|
||||
/**
|
||||
@@ -8,73 +8,57 @@ import { HasFeatureAccess } from "./../feature-wrapper/feature-wrapper.component
|
||||
* @param {Object} featureConfig - Configuration object defining which features and their associated fields to preserve
|
||||
*/
|
||||
export const useFormDataPreservation = (form, bodyshop, featureConfig) => {
|
||||
const getNestedValue = (obj, path) => {
|
||||
return path.reduce((current, key) => current?.[key], obj);
|
||||
};
|
||||
|
||||
// Safe nested getters/setters using path arrays
|
||||
const getNestedValue = (obj, path) => path?.reduce((acc, key) => acc?.[key], obj);
|
||||
const setNestedValue = (obj, path, value) => {
|
||||
const lastKey = path[path.length - 1];
|
||||
const parentPath = path.slice(0, -1);
|
||||
|
||||
const parent = parentPath.reduce((current, key) => {
|
||||
if (!current[key]) current[key] = {};
|
||||
return current[key];
|
||||
const parent = path.slice(0, -1).reduce((curr, key) => {
|
||||
if (!curr[key] || typeof curr[key] !== "object") curr[key] = {};
|
||||
return curr[key];
|
||||
}, obj);
|
||||
|
||||
parent[lastKey] = value;
|
||||
};
|
||||
|
||||
const preserveHiddenFormData = useCallback(() => {
|
||||
const preservationData = {};
|
||||
let hasDataToPreserve = false;
|
||||
|
||||
// Paths for features that are NOT accessible
|
||||
const disabledPaths = useMemo(() => {
|
||||
const result = [];
|
||||
if (!featureConfig) return result;
|
||||
Object.entries(featureConfig).forEach(([featureName, fieldPaths]) => {
|
||||
const hasAccess = HasFeatureAccess({ featureName, bodyshop });
|
||||
if (hasAccess || !Array.isArray(fieldPaths)) return;
|
||||
fieldPaths.forEach((p) => Array.isArray(p) && p.length && result.push(p));
|
||||
});
|
||||
return result;
|
||||
}, [featureConfig, bodyshop]);
|
||||
|
||||
if (!hasAccess) {
|
||||
fieldPaths.forEach((fieldPath) => {
|
||||
const currentValues = form.getFieldsValue();
|
||||
let value = getNestedValue(currentValues, fieldPath);
|
||||
const preserveHiddenFormData = useCallback(() => {
|
||||
const currentValues = form.getFieldsValue();
|
||||
const preservationData = {};
|
||||
let hasAny = false;
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
value = getNestedValue(bodyshop, fieldPath);
|
||||
}
|
||||
|
||||
if (value !== undefined && value !== null) {
|
||||
setNestedValue(preservationData, fieldPath, value);
|
||||
hasDataToPreserve = true;
|
||||
}
|
||||
});
|
||||
disabledPaths.forEach((path) => {
|
||||
let value = getNestedValue(currentValues, path);
|
||||
if (value == null) value = getNestedValue(bodyshop, path);
|
||||
if (value != null) {
|
||||
setNestedValue(preservationData, path, value);
|
||||
hasAny = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasDataToPreserve) {
|
||||
form.setFieldsValue(preservationData);
|
||||
}
|
||||
}, [form, featureConfig, bodyshop]);
|
||||
if (hasAny) form.setFieldsValue(preservationData);
|
||||
}, [form, bodyshop, disabledPaths]);
|
||||
|
||||
const getCompleteFormValues = () => {
|
||||
const currentFormValues = form.getFieldsValue();
|
||||
const completeValues = { ...currentFormValues };
|
||||
const currentValues = form.getFieldsValue();
|
||||
const complete = { ...currentValues };
|
||||
|
||||
Object.entries(featureConfig).forEach(([featureName, fieldPaths]) => {
|
||||
const hasAccess = HasFeatureAccess({ featureName, bodyshop });
|
||||
|
||||
if (!hasAccess) {
|
||||
fieldPaths.forEach((fieldPath) => {
|
||||
let value = getNestedValue(currentFormValues, fieldPath);
|
||||
if (value === undefined || value === null) {
|
||||
value = getNestedValue(bodyshop, fieldPath);
|
||||
}
|
||||
|
||||
if (value !== undefined && value !== null) {
|
||||
setNestedValue(completeValues, fieldPath, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
disabledPaths.forEach((path) => {
|
||||
let value = getNestedValue(currentValues, path);
|
||||
if (value == null) value = getNestedValue(bodyshop, path);
|
||||
if (value != null) setNestedValue(complete, path, value);
|
||||
});
|
||||
|
||||
return completeValues;
|
||||
return complete;
|
||||
};
|
||||
|
||||
const createSubmissionHandler = (originalHandler) => {
|
||||
@@ -103,8 +87,8 @@ export const FEATURE_CONFIGS = {
|
||||
["md_responsibility_centers", "profits"],
|
||||
["md_responsibility_centers", "defaults"],
|
||||
["md_responsibility_centers", "dms_defaults"],
|
||||
["md_responsibility_centers", "taxes", "itemexemptcode"],
|
||||
["md_responsibility_centers", "taxes", "invoiceexemptcode"],
|
||||
["md_responsibility_centers", "taxes"],
|
||||
["md_responsibility_centers", "cieca_pfl"],
|
||||
["md_responsibility_centers", "ar"],
|
||||
["md_responsibility_centers", "refund"],
|
||||
["md_responsibility_centers", "sales_tax_codes"],
|
||||
|
||||
@@ -14,16 +14,18 @@ import VendorsPhonebookAdd from "../vendors-phonebook-add/vendors-phonebook-add.
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { selectIsPartsEntry } from "../../redux/application/application.selectors.js";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
bodyshop: selectBodyshop,
|
||||
isPartsEntry: selectIsPartsEntry
|
||||
});
|
||||
const mapDispatchToProps = () => ({
|
||||
//setUserLanguage: language => dispatch(setUserLanguage(language))
|
||||
});
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(VendorsFormComponent);
|
||||
|
||||
export function VendorsFormComponent({ bodyshop, form, formLoading, handleDelete, selectedvendor }) {
|
||||
export function VendorsFormComponent({ bodyshop, form, formLoading, handleDelete, selectedvendor, isPartsEntry }) {
|
||||
const { t } = useTranslation();
|
||||
const client = useApolloClient();
|
||||
|
||||
@@ -57,8 +59,7 @@ export function VendorsFormComponent({ bodyshop, form, formLoading, handleDelete
|
||||
>
|
||||
{t("general.actions.delete")}
|
||||
</Button>
|
||||
|
||||
<VendorsPhonebookAdd form={form} disabled={form.isFieldsTouched()} />
|
||||
{!isPartsEntry && <VendorsPhonebookAdd form={form} disabled={form.isFieldsTouched()} />}
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
@@ -148,12 +149,18 @@ export function VendorsFormComponent({ bodyshop, form, formLoading, handleDelete
|
||||
</Form.Item>
|
||||
</LayoutFormRow>
|
||||
<LayoutFormRow grow>
|
||||
<Form.Item label={t("vendors.fields.discount")} name="discount">
|
||||
<InputNumber min={0} max={1} precision={2} step={0.01} />
|
||||
</Form.Item>
|
||||
<Form.Item label={t("vendors.fields.due_date")} name="due_date">
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
{!isPartsEntry && (
|
||||
<>
|
||||
<Form.Item label={t("vendors.fields.discount")} name="discount">
|
||||
<InputNumber min={0} max={1} precision={2} step={0.01} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={t("vendors.fields.due_date")} name="due_date">
|
||||
<InputNumber min={0} />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{
|
||||
// <Form.Item
|
||||
// label={t("vendors.fields.cost_center")}
|
||||
@@ -173,7 +180,7 @@ export function VendorsFormComponent({ bodyshop, form, formLoading, handleDelete
|
||||
|
||||
<Form.Item
|
||||
name="tags"
|
||||
label={t("vendor.fields.tags")}
|
||||
label={t("vendors.fields.tags")}
|
||||
rules={[
|
||||
{
|
||||
//message: t("general.validation.required"),
|
||||
|
||||
@@ -11,7 +11,6 @@ import { createStructuredSelector } from "reselect";
|
||||
import FormFieldsChanged from "../../components/form-fields-changed-alert/form-fields-changed-alert.component.jsx";
|
||||
import JobsLinesContainer from "../../components/job-detail-lines/job-lines.container.jsx";
|
||||
import JobLineUpsertModalContainer from "../../components/job-lines-upsert-modal/job-lines-upsert-modal.container.jsx";
|
||||
import JobProfileDataWarning from "../../components/job-profile-data-warning/job-profile-data-warning.component.jsx";
|
||||
import JobsChangeStatus from "../../components/jobs-change-status/jobs-change-status.component.jsx";
|
||||
import JobsDetailHeaderActions from "../../components/jobs-detail-header-actions/jobs-detail-header-actions.component.jsx";
|
||||
import JobsDetailHeader from "../../components/jobs-detail-header/jobs-detail-header.component.jsx";
|
||||
@@ -133,9 +132,8 @@ export function SimplifiedPartsJobDetailComponent({ setPrintCenterContext, jobRO
|
||||
<JobLineUpsertModalContainer />
|
||||
|
||||
<PageHeader title={<Space>{job.ro_number || t("general.labels.na")}</Space>} extra={menuExtra} />
|
||||
<JobsDetailHeader job={job} disabled={true} />
|
||||
<JobsDetailHeader job={job} />
|
||||
<Divider type="horizontal" />
|
||||
<JobProfileDataWarning job={job} />
|
||||
<FormFieldsChanged form={form} />
|
||||
<Tabs
|
||||
defaultActiveKey={search.tab}
|
||||
|
||||
@@ -1249,7 +1249,8 @@
|
||||
"sizelimit": "The selected items exceed the size limit.",
|
||||
"sub_status": {
|
||||
"expired": "The subscription for this shop has expired. Please contact Sales to reactivate.",
|
||||
"trial-expired": "The trial for this shop has expired. Please contact Sales to reactivate."
|
||||
"trial-expired": "The trial for this shop has expired. Please contact Sales to reactivate.",
|
||||
"undefined": "The subscription for this shop is removed. Please contact Sales to reactivate."
|
||||
},
|
||||
"submit-for-testing": "Error submitting Job for testing."
|
||||
},
|
||||
|
||||
@@ -1249,7 +1249,8 @@
|
||||
"sizelimit": "",
|
||||
"sub_status": {
|
||||
"expired": "",
|
||||
"trial-expired": ""
|
||||
"trial-expired": "",
|
||||
"undefined": ""
|
||||
},
|
||||
"submit-for-testing": ""
|
||||
},
|
||||
|
||||
@@ -1249,7 +1249,8 @@
|
||||
"sizelimit": "",
|
||||
"sub_status": {
|
||||
"expired": "",
|
||||
"trial-expired": ""
|
||||
"trial-expired": "",
|
||||
"undefined": ""
|
||||
},
|
||||
"submit-for-testing": ""
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@ services:
|
||||
- redis-node-1-data:/data
|
||||
- redis-lock:/redis-lock
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
test: [ "CMD", "redis-cli", "ping" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
@@ -39,7 +39,7 @@ services:
|
||||
- redis-node-2-data:/data
|
||||
- redis-lock:/redis-lock
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
test: [ "CMD", "redis-cli", "ping" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
@@ -57,7 +57,7 @@ services:
|
||||
- redis-node-3-data:/data
|
||||
- redis-lock:/redis-lock
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
test: [ "CMD", "redis-cli", "ping" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
@@ -85,7 +85,7 @@ services:
|
||||
ports:
|
||||
- "4566:4566"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
|
||||
test: [ "CMD", "curl", "-f", "http://localhost:4566/_localstack/health" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
@@ -118,6 +118,7 @@ services:
|
||||
aws --endpoint-url=http://localstack:4566 logs create-log-group --log-group-name development --region ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-large-log --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket imex-job-totals --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
aws --endpoint-url=http://localstack:4566 s3api create-bucket --bucket parts-estimates --create-bucket-configuration LocationConstraint=ca-central-1
|
||||
"
|
||||
# Node App: The Main IMEX API
|
||||
node-app:
|
||||
|
||||
@@ -959,6 +959,7 @@
|
||||
- enforce_referral
|
||||
- entegral_configuration
|
||||
- entegral_id
|
||||
- external_shop_id
|
||||
- features
|
||||
- federal_tax_id
|
||||
- id
|
||||
@@ -1012,6 +1013,8 @@
|
||||
- prodtargethrs
|
||||
- production_config
|
||||
- region_config
|
||||
- rr_configuration
|
||||
- rr_dealerid
|
||||
- schedule_end_time
|
||||
- schedule_start_time
|
||||
- scoreboard_target
|
||||
@@ -1035,7 +1038,6 @@
|
||||
- use_fippa
|
||||
- use_paint_scale_data
|
||||
- uselocalmediaserver
|
||||
- external_shop_id
|
||||
- website
|
||||
- workingdays
|
||||
- zip_post
|
||||
@@ -1068,6 +1070,7 @@
|
||||
- enforce_conversion_category
|
||||
- enforce_conversion_csr
|
||||
- enforce_referral
|
||||
- external_shop_id
|
||||
- federal_tax_id
|
||||
- id
|
||||
- inhousevendorid
|
||||
@@ -1113,6 +1116,7 @@
|
||||
- phone
|
||||
- prodtargethrs
|
||||
- production_config
|
||||
- rr_configuration
|
||||
- schedule_end_time
|
||||
- schedule_start_time
|
||||
- scoreboard_target
|
||||
@@ -1131,7 +1135,6 @@
|
||||
- use_fippa
|
||||
- use_paint_scale_data
|
||||
- uselocalmediaserver
|
||||
- external_shop_id
|
||||
- website
|
||||
- workingdays
|
||||
- zip_post
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."bodyshops" add column "rr_configuration" jsonb
|
||||
-- null default jsonb_build_object();
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."bodyshops" add column "rr_configuration" jsonb
|
||||
null default jsonb_build_object();
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Could not auto-generate a down migration.
|
||||
-- Please write an appropriate down migration for the SQL below:
|
||||
-- alter table "public"."bodyshops" add column "rr_dealierid" text
|
||||
-- null;
|
||||
@@ -0,0 +1,2 @@
|
||||
alter table "public"."bodyshops" add column "rr_dealierid" text
|
||||
null;
|
||||
@@ -0,0 +1 @@
|
||||
alter table "public"."bodyshops" rename column "rr_dealerid" to "rr_dealierid";
|
||||
@@ -0,0 +1 @@
|
||||
alter table "public"."bodyshops" rename column "rr_dealierid" to "rr_dealerid";
|
||||
858
package-lock.json
generated
858
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
32
package.json
32
package.json
@@ -18,14 +18,14 @@
|
||||
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-cloudwatch-logs": "^3.876.0",
|
||||
"@aws-sdk/client-elasticache": "^3.876.0",
|
||||
"@aws-sdk/client-s3": "^3.876.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.876.0",
|
||||
"@aws-sdk/client-ses": "^3.876.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.876.0",
|
||||
"@aws-sdk/lib-storage": "^3.876.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.876.0",
|
||||
"@aws-sdk/client-cloudwatch-logs": "^3.882.0",
|
||||
"@aws-sdk/client-elasticache": "^3.882.0",
|
||||
"@aws-sdk/client-s3": "^3.882.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.882.0",
|
||||
"@aws-sdk/client-ses": "^3.882.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.882.0",
|
||||
"@aws-sdk/lib-storage": "^3.882.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.882.0",
|
||||
"@opensearch-project/opensearch": "^2.13.0",
|
||||
"@socket.io/admin-ui": "^0.5.1",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
@@ -33,18 +33,18 @@
|
||||
"aws4": "^1.13.2",
|
||||
"axios": "^1.11.0",
|
||||
"better-queue": "^3.8.12",
|
||||
"bullmq": "^5.58.2",
|
||||
"bullmq": "^5.58.5",
|
||||
"chart.js": "^4.5.0",
|
||||
"cloudinary": "^2.7.0",
|
||||
"compression": "^1.8.1",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"crisp-status-reporter": "^1.2.2",
|
||||
"dd-trace": "^5.64.0",
|
||||
"dd-trace": "^5.65.0",
|
||||
"dinero.js": "^1.9.1",
|
||||
"dotenv": "^17.2.1",
|
||||
"dotenv": "^17.2.2",
|
||||
"express": "^4.21.1",
|
||||
"firebase-admin": "^13.4.0",
|
||||
"firebase-admin": "^13.5.0",
|
||||
"graphql": "^16.11.0",
|
||||
"graphql-request": "^6.1.0",
|
||||
"intuit-oauth": "^4.2.0",
|
||||
@@ -62,12 +62,12 @@
|
||||
"query-string": "7.1.3",
|
||||
"recursive-diff": "^1.0.9",
|
||||
"rimraf": "^6.0.1",
|
||||
"skia-canvas": "^3.0.4",
|
||||
"skia-canvas": "^3.0.6",
|
||||
"soap": "^1.3.0",
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-adapter": "^2.5.5",
|
||||
"ssh2-sftp-client": "^11.0.0",
|
||||
"twilio": "^5.8.0",
|
||||
"twilio": "^5.9.0",
|
||||
"uuid": "^11.1.0",
|
||||
"winston": "^3.17.0",
|
||||
"winston-cloudwatch": "^6.3.0",
|
||||
@@ -76,8 +76,8 @@
|
||||
"yazl": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.34.0",
|
||||
"eslint": "^9.34.0",
|
||||
"@eslint/js": "^9.35.0",
|
||||
"eslint": "^9.35.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^15.15.0",
|
||||
"mock-require": "^3.0.3",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const admin = require("firebase-admin");
|
||||
const client = require("../../../graphql-client/graphql-client").client;
|
||||
|
||||
const {
|
||||
DELETE_SHOP,
|
||||
DELETE_VENDORS_BY_SHOP,
|
||||
@@ -18,145 +17,154 @@ const {
|
||||
|
||||
/**
|
||||
* Deletes a Firebase user by UID.
|
||||
* @param uid
|
||||
* @param {string} uid - The Firebase user ID
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const deleteFirebaseUser = async (uid) => {
|
||||
if (!uid) throw new Error("User UID is required");
|
||||
return admin.auth().deleteUser(uid);
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes all vendors associated with a shop.
|
||||
* @param shopId
|
||||
* @param {string} shopId - The shop ID
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const deleteVendorsByShop = async (shopId) => {
|
||||
if (!shopId) throw new Error("Shop ID is required");
|
||||
await client.request(DELETE_VENDORS_BY_SHOP, { shopId });
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes a bodyshop from the database.
|
||||
* @param shopId
|
||||
* @param {string} shopId - The shop ID
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const deleteBodyshop = async (shopId) => {
|
||||
if (!shopId) throw new Error("Shop ID is required");
|
||||
await client.request(DELETE_SHOP, { id: shopId });
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch job ids for a given shop
|
||||
* @param shopId
|
||||
* @param {string} shopId - The shop ID
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
const getJobIdsForShop = async (shopId) => {
|
||||
if (!shopId) throw new Error("Shop ID is required");
|
||||
const resp = await client.request(GET_JOBS_BY_SHOP, { shopId });
|
||||
return resp.jobs.map((j) => j.id);
|
||||
return resp.jobs?.map((j) => j.id) || [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete joblines for the given job ids
|
||||
* @param jobIds {string[]}
|
||||
* @param {string[]} jobIds - Array of job IDs
|
||||
* @returns {Promise<number>} affected rows
|
||||
*/
|
||||
const deleteJoblinesForJobs = async (jobIds) => {
|
||||
if (!jobIds.length) return 0;
|
||||
if (!jobIds?.length) return 0;
|
||||
const resp = await client.request(DELETE_JOBLINES_BY_JOB_IDS, { jobIds });
|
||||
return resp.delete_joblines.affected_rows;
|
||||
return resp.delete_joblines?.affected_rows || 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete jobs for the given job ids
|
||||
* @param jobIds {string[]}
|
||||
* @param {string[]} jobIds - Array of job IDs
|
||||
* @returns {Promise<number>} affected rows
|
||||
*/
|
||||
const deleteJobsByIds = async (jobIds) => {
|
||||
if (!jobIds.length) return 0;
|
||||
if (!jobIds?.length) return 0;
|
||||
const resp = await client.request(DELETE_JOBS_BY_IDS, { jobIds });
|
||||
return resp.delete_jobs.affected_rows;
|
||||
return resp.delete_jobs?.affected_rows || 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles deprovisioning a shop for parts management.
|
||||
* @param req
|
||||
* @param res
|
||||
* @param {Object} req - Express request object
|
||||
* @param {Object} res - Express response object
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
const partsManagementDeprovisioning = async (req, res) => {
|
||||
const { logger } = req;
|
||||
const body = req.body;
|
||||
const { shopId } = req.body;
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
if (process.env.NODE_ENV === "production" || process.env.HOSTNAME?.endsWith("compute.internal")) {
|
||||
return res.status(403).json({ error: "Deprovisioning not allowed in production environment." });
|
||||
}
|
||||
|
||||
try {
|
||||
if (!body.shopId) {
|
||||
if (!shopId) {
|
||||
throw { status: 400, message: "shopId is required." };
|
||||
}
|
||||
|
||||
// Fetch bodyshop and check external_shop_id
|
||||
const shopResp = await client.request(GET_BODYSHOP, { id: body.shopId });
|
||||
const shopResp = await client.request(GET_BODYSHOP, { id: shopId });
|
||||
const shop = shopResp.bodyshops_by_pk;
|
||||
if (!shop) {
|
||||
throw { status: 404, message: `Bodyshop with id ${body.shopId} not found.` };
|
||||
throw { status: 404, message: `Bodyshop with id ${shopId} not found.` };
|
||||
}
|
||||
if (!shop.external_shop_id) {
|
||||
throw { status: 400, message: "Cannot delete bodyshop without external_shop_id." };
|
||||
}
|
||||
|
||||
logger.log("admin-delete-shop", "debug", null, null, {
|
||||
shopId: body.shopId,
|
||||
shopId,
|
||||
shopname: shop.shopname,
|
||||
ioadmin: true
|
||||
});
|
||||
|
||||
// Get vendors
|
||||
const vendorsResp = await client.request(GET_VENDORS, { shopId: body.shopId });
|
||||
const deletedVendors = vendorsResp.vendors.map((v) => v.name);
|
||||
const vendorsResp = await client.request(GET_VENDORS, { shopId });
|
||||
const deletedVendors = vendorsResp.vendors?.map((v) => v.name) || [];
|
||||
|
||||
// Get associated users
|
||||
const assocResp = await client.request(GET_ASSOCIATED_USERS, { shopId: body.shopId });
|
||||
const associatedUsers = assocResp.associations.map((assoc) => ({
|
||||
authId: assoc.user.authid,
|
||||
email: assoc.user.email
|
||||
}));
|
||||
const assocResp = await client.request(GET_ASSOCIATED_USERS, { shopId });
|
||||
const associatedUsers =
|
||||
assocResp.associations?.map((assoc) => ({
|
||||
authId: assoc.user?.authid,
|
||||
email: assoc.user?.email
|
||||
})) || [];
|
||||
|
||||
// Delete associations for the shop
|
||||
const assocDeleteResp = await client.request(DELETE_ASSOCIATIONS_BY_SHOP, { shopId: body.shopId });
|
||||
const associationsDeleted = assocDeleteResp.delete_associations.affected_rows;
|
||||
const assocDeleteResp = await client.request(DELETE_ASSOCIATIONS_BY_SHOP, { shopId });
|
||||
const associationsDeleted = assocDeleteResp.delete_associations?.affected_rows || 0;
|
||||
|
||||
// For each user, check if they have remaining associations; if not, delete user and Firebase account
|
||||
// Delete users with no remaining associations
|
||||
const deletedUsers = [];
|
||||
for (const user of associatedUsers) {
|
||||
const countResp = await client.request(GET_USER_ASSOCIATIONS_COUNT, { userEmail: user.email });
|
||||
// Determine which users now have zero associations and should be deleted (defer deletion until end)
|
||||
const emailsToAuthId = associatedUsers.reduce((acc, u) => {
|
||||
acc[u.email] = u.authId;
|
||||
return acc;
|
||||
}, {});
|
||||
const emailsToDelete = [];
|
||||
await client.request(DELETE_USER, { email: user.email });
|
||||
await deleteFirebaseUser(user.authId);
|
||||
deletedUsers.push(user.email);
|
||||
if (!user.email || !user.authId) continue;
|
||||
try {
|
||||
const countResp = await client.request(GET_USER_ASSOCIATIONS_COUNT, { userEmail: user.email });
|
||||
const assocCount = countResp.associations_aggregate?.aggregate?.count || 0;
|
||||
if (assocCount === 0) {
|
||||
await client.request(DELETE_USER, { email: user.email });
|
||||
await deleteFirebaseUser(user.authId);
|
||||
deletedUsers.push(user.email);
|
||||
}
|
||||
} catch (userError) {
|
||||
logger.log("admin-delete-user-error", "warn", null, null, {
|
||||
email: user.email,
|
||||
error: userError.message || userError
|
||||
});
|
||||
}
|
||||
}
|
||||
emailsToDelete.push(user.email);
|
||||
const jobIds = await getJobIdsForShop(body.shopId);
|
||||
|
||||
// Delete jobs and joblines
|
||||
const jobIds = await getJobIdsForShop(shopId);
|
||||
const joblinesDeleted = await deleteJoblinesForJobs(jobIds);
|
||||
const jobsDeleted = await deleteJobsByIds(jobIds);
|
||||
|
||||
// Delete any audit trail entries tied to this bodyshop to avoid FK violations
|
||||
const auditResp = await client.request(DELETE_AUDIT_TRAIL_BY_SHOP, { shopId: body.shopId });
|
||||
const auditDeleted = auditResp.delete_audit_trail.affected_rows;
|
||||
// Delete audit trail
|
||||
const auditResp = await client.request(DELETE_AUDIT_TRAIL_BY_SHOP, { shopId });
|
||||
const auditDeleted = auditResp.delete_audit_trail?.affected_rows || 0;
|
||||
|
||||
// Delete vendors
|
||||
await deleteVendorsByShop(body.shopId);
|
||||
|
||||
// Delete shop
|
||||
await deleteBodyshop(body.shopId);
|
||||
// Delete vendors and shop
|
||||
await deleteVendorsByShop(shopId);
|
||||
await deleteBodyshop(shopId);
|
||||
|
||||
// Summary log
|
||||
logger.log("admin-delete-shop-summary", "info", null, null, {
|
||||
shopId: body.shopId,
|
||||
shopId,
|
||||
shopname: shop.shopname,
|
||||
associationsDeleted,
|
||||
deletedUsers,
|
||||
@@ -167,31 +175,20 @@ const partsManagementDeprovisioning = async (req, res) => {
|
||||
});
|
||||
|
||||
return res.status(200).json({
|
||||
message: `Bodyshop ${body.shopId} and associated resources deleted successfully.`,
|
||||
deletedShop: { id: body.shopId, name: shop.shopname },
|
||||
message: `Bodyshop ${shopId} and associated resources deleted successfully.`,
|
||||
deletedShop: { id: shopId, name: shop.shopname },
|
||||
deletedAssociationsCount: associationsDeleted,
|
||||
deletedUsers: deletedUsers,
|
||||
deletedVendors: deletedVendors,
|
||||
deletedUsers,
|
||||
deletedVendors,
|
||||
deletedJoblinesCount: joblinesDeleted,
|
||||
deletedJobsCount: jobsDeleted,
|
||||
deletedAuditTrailCount: auditDeleted
|
||||
});
|
||||
// Now delete users that have no remaining associations and their Firebase accounts
|
||||
const deletedUsers = [];
|
||||
for (const email of emailsToDelete) {
|
||||
await client.request(DELETE_USER, { email });
|
||||
const authId = emailsToAuthId[email];
|
||||
if (authId) {
|
||||
await deleteFirebaseUser(authId);
|
||||
}
|
||||
deletedUsers.push(email);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.log("admin-delete-shop-error", "error", null, null, {
|
||||
message: err.message,
|
||||
detail: err.detail || err
|
||||
detail: err.detail || err.stack || err
|
||||
});
|
||||
|
||||
return res.status(err.status || 500).json({ error: err.message || "Internal server error" });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
// no-dd-sa:javascript-code-style/assignment-name
|
||||
// CamelCase is used for GraphQL and database fields.
|
||||
|
||||
const client = require("../../../graphql-client/graphql-client").client;
|
||||
const { extractPartsTaxRates } = require("./lib/extractPartsTaxRates");
|
||||
const { parseXml, normalizeXmlObject } = require("../partsManagementUtils");
|
||||
const opCodes = require("./lib/opCodes.json");
|
||||
// New imports for S3 XML archival
|
||||
const { uploadFileToS3 } = require("../../../utils/s3");
|
||||
const InstanceMgr = require("../../../utils/instanceMgr").default;
|
||||
|
||||
// GraphQL Queries and Mutations
|
||||
const {
|
||||
@@ -12,10 +13,28 @@ const {
|
||||
INSERT_OWNER,
|
||||
INSERT_JOB_WITH_LINES
|
||||
} = require("../partsManagement.queries");
|
||||
const { v4: uuidv4 } = require("uuid");
|
||||
|
||||
// Defaults
|
||||
const FALLBACK_DEFAULT_JOB_STATUS = "Open";
|
||||
|
||||
const ESTIMATE_XML_BUCKET =
|
||||
process.env?.NODE_ENV === "development"
|
||||
? "parts-estimates" // local/dev shared bucket name
|
||||
: InstanceMgr({
|
||||
imex: `imex-webest-xml`,
|
||||
rome: `rome-webest-xml`
|
||||
});
|
||||
|
||||
const buildEstimateXmlKey = (rq) => {
|
||||
const refClaimNum = rq.RefClaimNum;
|
||||
const shopId = rq.ShopID;
|
||||
|
||||
const ts = new Date().toISOString().replace(/:/g, "-");
|
||||
const safeClaim = (refClaimNum || "no-claim").toString().replace(/[^A-Za-z0-9_-]/g, "_");
|
||||
return `addRequest/${shopId}/${safeClaim}/${ts}-${uuidv4()}.xml`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the default order status for a bodyshop.
|
||||
* @param {string} shopId - The bodyshop UUID.
|
||||
@@ -65,6 +84,7 @@ const extractJobData = (rq) => {
|
||||
const ci = rq.ClaimInfo || {};
|
||||
|
||||
return {
|
||||
driveable: !!rq.VehicleInfo?.Condition?.DrivableInd,
|
||||
shopId: rq.ShopID || rq.shopId,
|
||||
// status: ci.ClaimStatus || null, Proper, setting it default for now
|
||||
refClaimNum: rq.RefClaimNum,
|
||||
@@ -107,8 +127,7 @@ const extractOwnerData = (rq, shopId) => {
|
||||
: [ownerOrClaimant.ContactInfo?.Communications || {}];
|
||||
|
||||
for (const c of comms) {
|
||||
// TODO: Should document this logic. 1 and 2 don't
|
||||
// typically indicate type in EMS. This makes sense, but good to document.
|
||||
// -- Document
|
||||
if (c.CommQualifier === "CP") ownr_ph1 = c.CommPhone;
|
||||
if (c.CommQualifier === "WP") ownr_ph2 = c.CommPhone;
|
||||
if (c.CommQualifier === "EM") ownr_ea = c.CommEmail;
|
||||
@@ -167,7 +186,7 @@ const extractEstimatorData = (rq) => {
|
||||
// : [adjParty.ContactInfo?.Communications || {}];
|
||||
//
|
||||
// return {
|
||||
// //TODO: I dont think we display agt_ct_* fields in app. Have they typically been sending data here?
|
||||
// //TODO (FUTURE): I dont think we display agt_ct_* fields in app. Have they typically been sending data here?
|
||||
// agt_ct_fn: adjParty.PersonInfo?.PersonName?.FirstName || null,
|
||||
// agt_ct_ln: adjParty.PersonInfo?.PersonName?.LastName || null,
|
||||
// agt_ct_ph: adjComms.find((c) => c.CommQualifier === "CP")?.CommPhone || null,
|
||||
@@ -188,8 +207,8 @@ const extractEstimatorData = (rq) => {
|
||||
//
|
||||
// return {
|
||||
// servicing_dealer: rfParty.OrgInfo?.CompanyName || null,
|
||||
// // TODO: The servicing dealer fields are a relic from synergy for a few folks
|
||||
// // TODO: I suspect RF data could be ignored since they are the RF.
|
||||
// // TODO (Future): The servicing dealer fields are a relic from synergy for a few folks
|
||||
// // TODO (Future): I suspect RF data could be ignored since they are the RF.
|
||||
// servicing_dealer_contact:
|
||||
// rfComms.find((c) => c.CommQualifier === "WP" || c.CommQualifier === "FX")?.CommPhone || null
|
||||
// };
|
||||
@@ -294,10 +313,9 @@ const extractVehicleData = (rq, shopId) => {
|
||||
v_color: exterior.Color?.ColorName || null,
|
||||
v_bstyle: desc.BodyStyle || null,
|
||||
v_engine: desc.EngineDesc || null,
|
||||
// TODO Need to confirm with exact data, but this is typically a list of options. Not used AFAIK.
|
||||
v_options: desc.SubModelDesc || null,
|
||||
// TODO (for future) Need to confirm with exact data, but this is typically a list of options. Not used AFAIK.
|
||||
// v_options: desc.SubModelDesc || null,
|
||||
v_type: desc.FuelType || null,
|
||||
// TODO there is a separate driveable flag on the job.
|
||||
v_cond: rq.VehicleInfo?.Condition?.DrivableInd,
|
||||
v_trimcode: desc.TrimCode || null,
|
||||
v_tone: exterior.Tone || null,
|
||||
@@ -345,30 +363,34 @@ const extractJobLines = (rq) => {
|
||||
const lineOut = { ...base };
|
||||
|
||||
// Manual line flag coercion
|
||||
if (line.ManualLineInd !== undefined) {
|
||||
lineOut.manual_line =
|
||||
line.ManualLineInd === true ||
|
||||
line.ManualLineInd === 1 ||
|
||||
line.ManualLineInd === "1" ||
|
||||
// TODO: manual line tracks manual in IO or not, this woudl presumably always be false
|
||||
(typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y");
|
||||
} else {
|
||||
lineOut.manual_line = null;
|
||||
}
|
||||
// if (line.ManualLineInd !== undefined) {
|
||||
// lineOut.manual_line =
|
||||
// line.ManualLineInd === true ||
|
||||
// line.ManualLineInd === 1 ||
|
||||
// line.ManualLineInd === "1" ||
|
||||
// // TODO (FUTURE): manual line tracks manual in IO or not, this woudl presumably always be false
|
||||
// (typeof line.ManualLineInd === "string" && line.ManualLineInd.toUpperCase() === "Y");
|
||||
// } else {
|
||||
// lineOut.manual_line = null;
|
||||
// }
|
||||
|
||||
// Is set to false because anything coming from the DMS is considered not a manual line, it becomes
|
||||
// a manual line once it is edited in OUR system.
|
||||
lineOut.manual_line = false;
|
||||
|
||||
// Parts (preferred) or Sublet (fallback when no PartInfo)
|
||||
const hasPart = Object.keys(partInfo).length > 0;
|
||||
const hasSublet = Object.keys(subletInfo).length > 0;
|
||||
|
||||
if (hasPart) {
|
||||
const price = parseFloat(partInfo.PartPrice || partInfo.ListPrice || 0);
|
||||
lineOut.part_type = partInfo.PartType || null ? String(partInfo.PartType).toUpperCase() : null;
|
||||
lineOut.part_qty = parseFloat(partInfo.Quantity || 0) || 1;
|
||||
//TODO: if aftermarket part, we have alt_part_no to capture.
|
||||
lineOut.oem_partno = partInfo.OEMPartNum || partInfo.PartNum || null;
|
||||
//TODO: the Db and act price often are different. These should map back to their EMS equivalents.
|
||||
lineOut.db_price = isNaN(price) ? 0 : price;
|
||||
lineOut.act_price = isNaN(price) ? 0 : price;
|
||||
lineOut.oem_partno = partInfo.OEMPartNum;
|
||||
lineOut.alt_partno = partInfo?.NonOEM?.NonOEMPartNum;
|
||||
|
||||
// THIS NEEDS TO BE CHANGED IN CHANGE REQUEST
|
||||
lineOut.act_price = parseFloat(partInfo?.PartPrice || 0);
|
||||
lineOut.db_price = parseFloat(partInfo?.OEMPartPrice || 0);
|
||||
|
||||
// Tax flag from PartInfo.TaxableInd when provided
|
||||
if (
|
||||
@@ -384,8 +406,10 @@ const extractJobLines = (rq) => {
|
||||
(typeof partInfo.TaxableInd === "string" && partInfo.TaxableInd.toUpperCase() === "Y");
|
||||
}
|
||||
}
|
||||
//TODO: Some nuance here. Usually a part and sublet amount shouldnt be on the same line, but they theoretically
|
||||
// could.May require additional discussion.
|
||||
|
||||
//TODO (FUTURE): Some nuance here. Usually a part and sublet amount shouldnt be on the same line, but they theoretically
|
||||
// could. May require additional discussion.
|
||||
// EMS - > Misc Amount, calibration for example, painting, etc
|
||||
else if (hasSublet) {
|
||||
const amt = parseFloat(subletInfo.SubletAmount || 0);
|
||||
lineOut.part_type = "PAS"; // Sublet as parts-as-service
|
||||
@@ -400,18 +424,22 @@ const extractJobLines = (rq) => {
|
||||
(!!laborInfo.LaborType && String(laborInfo.LaborType).length > 0) ||
|
||||
(!isNaN(hrs) && hrs !== 0) ||
|
||||
(!isNaN(amt) && amt !== 0);
|
||||
|
||||
if (hasLabor) {
|
||||
lineOut.mod_lbr_ty = laborInfo.LaborType || null;
|
||||
lineOut.mod_lb_hrs = isNaN(hrs) ? 0 : hrs;
|
||||
//TODO: can add lbr_op_desc according to mapping available in new partner.
|
||||
lineOut.lbr_op = laborInfo.LaborOperation || null;
|
||||
const opCodeKey =
|
||||
typeof laborInfo.LaborOperation === "string" ? laborInfo.LaborOperation.trim().toUpperCase() : null;
|
||||
lineOut.op_code_desc = opCodes?.[opCodeKey]?.desc || null;
|
||||
|
||||
lineOut.lbr_amt = isNaN(amt) ? 0 : amt;
|
||||
}
|
||||
|
||||
//TODO: what's the BMS logic for this? Body and refinish operations can often happen to the same part,
|
||||
//TODO (FUTURE): what's the BMS logic for this? Body and refinish operations can often happen to the same part,
|
||||
// but most systems output a second line for the refinish labor.
|
||||
//TODO: 2nd line may include a duplicate of the part price, but that can be removed. This is the case for CCC.
|
||||
//TODO (FUTURE): 2nd line may include a duplicate of the part price, but that can be removed. This is the case for CCC.
|
||||
// Refinish labor (if present) recorded on the same line using secondary labor fields
|
||||
|
||||
const rHrs = parseFloat(refinishInfo.LaborHours || 0);
|
||||
const rAmt = parseFloat(refinishInfo.LaborAmt || 0);
|
||||
const hasRefinish =
|
||||
@@ -421,9 +449,9 @@ const extractJobLines = (rq) => {
|
||||
!isNaN(rAmt) ||
|
||||
!!refinishInfo.LaborOperation);
|
||||
if (hasRefinish) {
|
||||
lineOut.lbr_typ_j = refinishInfo.LaborType || "LAR"; //TODO: _j fields indicate judgement, and are bool type.
|
||||
lineOut.lbr_hrs_j = isNaN(rHrs) ? 0 : rHrs; //TODO: _j fields indicate judgement, and are bool type.
|
||||
lineOut.lbr_op_j = refinishInfo.LaborOperation || null; //TODO: _j fields indicate judgement, and are bool type.
|
||||
lineOut.lbr_typ_j = !!refinishInfo?.LaborAmtJudgmentInd;
|
||||
lineOut.lbr_hrs_j = !!refinishInfo?.LaborHoursJudgmentInd;
|
||||
lineOut.lbr_op_j = !!refinishInfo.LaborOperationJudgmentInd;
|
||||
// Aggregate refinish labor amount into the total labor amount for the line
|
||||
if (!isNaN(rAmt)) {
|
||||
lineOut.lbr_amt = (Number.isFinite(lineOut.lbr_amt) ? lineOut.lbr_amt : 0) + rAmt;
|
||||
@@ -494,9 +522,10 @@ const insertOwner = async (ownerInput, logger) => {
|
||||
// }
|
||||
// const total = parts + labor;
|
||||
//
|
||||
// //TODO: clm_total is the 100% full amount of the repair including deductible, betterment and taxes. Typically provided by the source system.
|
||||
// //TODO (FUTURE): clm_total is the 100% full amount of the repair including deductible, betterment and taxes. Typically provided by the source system.
|
||||
// return Number.isFinite(total) && total > 0 ? total : 0;
|
||||
// };
|
||||
// //TODO (FUTURE): clm_total is the 100% full amount of the repair including deductible,
|
||||
// // betterment and taxes. Typically provided by the source system.
|
||||
|
||||
/**
|
||||
* Handles the VehicleDamageEstimateAddRq XML request from parts management.
|
||||
@@ -506,17 +535,10 @@ const insertOwner = async (ownerInput, logger) => {
|
||||
*/
|
||||
const vehicleDamageEstimateAddRq = async (req, res) => {
|
||||
const { logger } = req;
|
||||
|
||||
const rawXml = typeof req.body === "string" ? req.body : Buffer.isBuffer(req.body) ? req.body.toString("utf8") : "";
|
||||
try {
|
||||
// Parse XML
|
||||
const payload = await parseXml(req.body, logger);
|
||||
const rq = normalizeXmlObject(payload.VehicleDamageEstimateAddRq);
|
||||
if (!rq) {
|
||||
logger.log("parts-missing-root", "error");
|
||||
return res.status(400).send("Missing <VehicleDamageEstimateAddRq>");
|
||||
}
|
||||
|
||||
// Extract job data
|
||||
const {
|
||||
shopId,
|
||||
refClaimNum,
|
||||
@@ -534,39 +556,22 @@ const vehicleDamageEstimateAddRq = async (req, res) => {
|
||||
scheduled_completion,
|
||||
clm_no,
|
||||
policy_no,
|
||||
ded_amt
|
||||
// status,
|
||||
ded_amt,
|
||||
driveable
|
||||
} = extractJobData(rq);
|
||||
|
||||
if (!shopId) {
|
||||
throw { status: 400, message: "Missing <ShopID> in XML" };
|
||||
}
|
||||
|
||||
// Get default status
|
||||
const defaultStatus = await getDefaultJobStatus(shopId, logger);
|
||||
|
||||
// Extract additional data
|
||||
const parts_tax_rates = extractPartsTaxRates(rq.ProfileInfo);
|
||||
const ownerData = extractOwnerData(rq, shopId);
|
||||
const estimatorData = extractEstimatorData(rq);
|
||||
// const adjusterData = extractAdjusterData(rq);
|
||||
// const repairFacilityData = extractRepairFacilityData(rq);
|
||||
const vehicleData = extractVehicleData(rq, shopId);
|
||||
const lossInfo = extractLossInfo(rq);
|
||||
const joblinesData = extractJobLines(rq);
|
||||
const insuranceData = extractInsuranceData(rq);
|
||||
|
||||
// Derive clm_total: prefer RepairTotalsInfo SummaryTotals GRAND TOTAL; else sum from lines
|
||||
// const grandTotal = extractGrandTotal(rq);
|
||||
// const computedTotal = grandTotal ?? computeLinesTotal(joblinesData);
|
||||
|
||||
// Find or create relationships
|
||||
const ownerid = await insertOwner(ownerData, logger);
|
||||
const vehicleid = await findExistingVehicle(shopId, vehicleData.v_vin, logger);
|
||||
|
||||
// Build job input
|
||||
const jobInput = {
|
||||
shopid: shopId,
|
||||
driveable,
|
||||
converted: true,
|
||||
ownerid,
|
||||
ro_number: refClaimNum,
|
||||
@@ -578,7 +583,7 @@ const vehicleDamageEstimateAddRq = async (req, res) => {
|
||||
parts_tax_rates,
|
||||
clm_no,
|
||||
status: defaultStatus,
|
||||
clm_total: 0, // computedTotal || null,
|
||||
clm_total: 0,
|
||||
policy_no,
|
||||
ded_amt,
|
||||
comment,
|
||||
@@ -588,14 +593,10 @@ const vehicleDamageEstimateAddRq = async (req, res) => {
|
||||
asgn_date,
|
||||
scheduled_in,
|
||||
scheduled_completion,
|
||||
// Inline insurance/loss/contacts
|
||||
...insuranceData,
|
||||
...lossInfo,
|
||||
...ownerData,
|
||||
...estimatorData,
|
||||
// ...adjusterData,
|
||||
// ...repairFacilityData,
|
||||
// Inline vehicle data
|
||||
v_vin: vehicleData.v_vin,
|
||||
v_model_yr: vehicleData.v_model_yr,
|
||||
v_model_desc: vehicleData.v_model_desc,
|
||||
@@ -606,10 +607,23 @@ const vehicleDamageEstimateAddRq = async (req, res) => {
|
||||
...(vehicleid ? { vehicleid } : { vehicle: { data: vehicleData } }),
|
||||
joblines: { data: joblinesData }
|
||||
};
|
||||
|
||||
// Insert job
|
||||
const { insert_jobs_one: newJob } = await client.request(INSERT_JOB_WITH_LINES, { job: jobInput });
|
||||
|
||||
// Upload AFTER job creation to include job id in filename
|
||||
(async () => {
|
||||
try {
|
||||
const key = buildEstimateXmlKey(rq);
|
||||
await uploadFileToS3({
|
||||
bucketName: ESTIMATE_XML_BUCKET,
|
||||
key,
|
||||
content: rawXml || "",
|
||||
contentType: "application/xml"
|
||||
});
|
||||
logger.log("parts-estimate-xml-uploaded", "info", shopId, newJob.id, { key, bytes: rawXml?.length || 0 });
|
||||
} catch (e) {
|
||||
logger.log("parts-estimate-xml-upload-failed", "warn", shopId, null, { error: e?.message });
|
||||
}
|
||||
})();
|
||||
return res.status(200).json({ success: true, jobId: newJob.id });
|
||||
} catch (err) {
|
||||
logger.log("parts-route-error", "error", null, null, { error: err });
|
||||
|
||||
@@ -4,17 +4,22 @@
|
||||
const client = require("../../../graphql-client/graphql-client").client;
|
||||
const { parseXml, normalizeXmlObject } = require("../partsManagementUtils");
|
||||
const { extractPartsTaxRates } = require("./lib/extractPartsTaxRates");
|
||||
const opCodes = require("./lib/opCodes.json");
|
||||
const { uploadFileToS3 } = require("../../../utils/s3");
|
||||
const InstanceMgr = require("../../../utils/instanceMgr").default;
|
||||
|
||||
const {
|
||||
GET_JOB_BY_ID,
|
||||
UPDATE_JOB_BY_ID,
|
||||
SOFT_DELETE_JOBLINES_BY_IDS,
|
||||
INSERT_JOBLINES,
|
||||
GET_JOBLINES_NOTES_BY_JOBID_UNQSEQ
|
||||
GET_JOBLINES_NOTES_BY_JOBID_UNQSEQ,
|
||||
GET_JOBLINE_IDS_BY_JOBID_UNQSEQ,
|
||||
UPDATE_JOBLINE_BY_PK,
|
||||
INSERT_JOBLINES
|
||||
} = require("../partsManagement.queries");
|
||||
|
||||
/**
|
||||
* Finds a job by shop ID and claim number.
|
||||
* Finds a job by shop ID and job ID.
|
||||
* @param shopId
|
||||
* @param jobId
|
||||
* @param logger
|
||||
@@ -32,38 +37,37 @@ const findJob = async (shopId, jobId, logger) => {
|
||||
|
||||
/**
|
||||
* Extracts updated job data from the request payload.
|
||||
* Mirrors AddRq for parts_tax_rates + driveable when present.
|
||||
* @param rq
|
||||
* @returns {{comment: (number|((comment: Comment, helper: postcss.Helpers) => (Promise<void> | void))|string|null), clm_no: null, status: (*|null), policy_no: (*|null)}}
|
||||
*/
|
||||
const extractUpdatedJobData = (rq) => {
|
||||
const doc = rq.DocumentInfo || {};
|
||||
const claim = rq.ClaimInfo || {};
|
||||
//TODO: In the full BMS world, much more can change, this will need to be expanded
|
||||
// before it can be considered an generic BMS importer, currently it is bespoke to webest
|
||||
const policyNo = claim.PolicyInfo?.PolicyInfo?.PolicyNum || claim.PolicyInfo?.PolicyNum || null;
|
||||
|
||||
const out = {
|
||||
comment: doc.Comment || null,
|
||||
clm_no: claim.ClaimNum || null,
|
||||
// TODO: Commented out so they do not blow over with 'Auth Cust'
|
||||
// status: claim.ClaimStatus || null,
|
||||
// TODO (future): status omitted intentionally to avoid overwriting with 'Auth Cust'
|
||||
policy_no: policyNo
|
||||
};
|
||||
|
||||
// If ProfileInfo provided in ChangeRq, update parts_tax_rates to stay in sync with AddRq behavior
|
||||
if (rq.ProfileInfo) {
|
||||
out.parts_tax_rates = extractPartsTaxRates(rq.ProfileInfo);
|
||||
}
|
||||
|
||||
if (rq.VehicleInfo?.Condition?.DrivableInd !== undefined) {
|
||||
out.driveable = !!rq.VehicleInfo.Condition.DrivableInd;
|
||||
}
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts updated job lines from the request payload without splitting parts and labor:
|
||||
* - Keep part and labor on the same jobline
|
||||
* - Aggregate RefinishLabor into secondary labor fields and add its amount to lbr_amt
|
||||
* - SUBLET-only lines become PAS part_type with act_price = SubletAmount
|
||||
* Accepts currentJobLineNotes map for notes merging.
|
||||
* Build jobline payloads for updates/inserts (no split between parts & labor).
|
||||
* - Refinish labor aggregated into lbr_* secondary fields and lbr_amt.
|
||||
* - SUBLET-only -> PAS line with act_price = SubletAmount.
|
||||
* - Notes merged with current DB value by unq_seq.
|
||||
*/
|
||||
const extractUpdatedJobLines = (addsChgs = {}, jobId, currentJobLineNotes = {}) => {
|
||||
const linesIn = Array.isArray(addsChgs.DamageLineInfo) ? addsChgs.DamageLineInfo : [addsChgs.DamageLineInfo || {}];
|
||||
@@ -87,56 +91,39 @@ const extractUpdatedJobLines = (addsChgs = {}, jobId, currentJobLineNotes = {})
|
||||
unq_seq: parseInt(line.UniqueSequenceNum || 0, 10),
|
||||
status: line.LineStatusCode || null,
|
||||
line_desc: line.LineDesc || null,
|
||||
// notes will be set below
|
||||
manual_line: line.ManualLineInd !== undefined ? coerceManual(line.ManualLineInd) : null
|
||||
manual_line: false
|
||||
// manual_line: line.ManualLineInd !== undefined ? coerceManual(line.ManualLineInd) : null
|
||||
};
|
||||
|
||||
const lineOut = { ...base };
|
||||
|
||||
// --- Notes merge logic ---
|
||||
// --- Notes merge ---
|
||||
const unqSeq = lineOut.unq_seq;
|
||||
const currentNotes = currentJobLineNotes?.[unqSeq] || null;
|
||||
const newNotes = line.LineMemo || null;
|
||||
if (newNotes && currentNotes) {
|
||||
if (currentNotes === newNotes) {
|
||||
lineOut.notes = currentNotes;
|
||||
} else if (currentNotes.includes(newNotes)) {
|
||||
lineOut.notes = currentNotes;
|
||||
} else {
|
||||
lineOut.notes = `${currentNotes} | ${newNotes}`;
|
||||
}
|
||||
} else if (newNotes) {
|
||||
lineOut.notes = newNotes;
|
||||
} else if (currentNotes) {
|
||||
lineOut.notes = currentNotes;
|
||||
} else {
|
||||
lineOut.notes = null;
|
||||
}
|
||||
// --- End notes merge logic ---
|
||||
if (currentNotes === newNotes || currentNotes.includes(newNotes)) lineOut.notes = currentNotes;
|
||||
else lineOut.notes = `${currentNotes} | ${newNotes}`;
|
||||
} else if (newNotes) lineOut.notes = newNotes;
|
||||
else if (currentNotes) lineOut.notes = currentNotes;
|
||||
else lineOut.notes = null;
|
||||
// --- end notes merge ---
|
||||
|
||||
const hasPart = Object.keys(partInfo).length > 0;
|
||||
const hasSublet = Object.keys(subletInfo).length > 0;
|
||||
|
||||
if (hasPart) {
|
||||
const price = parseFloat(partInfo.PartPrice || partInfo.ListPrice || 0);
|
||||
lineOut.part_type = partInfo.PartType ? String(partInfo.PartType).toUpperCase() : null;
|
||||
lineOut.part_qty = parseFloat(partInfo.Quantity || 0) || 1;
|
||||
lineOut.oem_partno = partInfo.OEMPartNum || partInfo.PartNum || null;
|
||||
lineOut.db_price = isNaN(price) ? 0 : price;
|
||||
lineOut.act_price = isNaN(price) ? 0 : price;
|
||||
lineOut.oem_partno = partInfo.OEMPartNum;
|
||||
lineOut.alt_partno = partInfo?.NonOEM?.NonOEMPartNum;
|
||||
lineOut.part_type = partInfo.PartType || null ? String(partInfo.PartType).toUpperCase() : null;
|
||||
|
||||
// Optional: taxability flag for parts
|
||||
if (
|
||||
partInfo.TaxableInd !== undefined &&
|
||||
(typeof partInfo.TaxableInd === "string" ||
|
||||
typeof partInfo.TaxableInd === "number" ||
|
||||
typeof partInfo.TaxableInd === "boolean")
|
||||
) {
|
||||
lineOut.tax_part =
|
||||
partInfo.TaxableInd === true ||
|
||||
partInfo.TaxableInd === 1 ||
|
||||
partInfo.TaxableInd === "1" ||
|
||||
(typeof partInfo.TaxableInd === "string" && partInfo.TaxableInd.toUpperCase() === "Y");
|
||||
lineOut.act_price = parseFloat(partInfo?.PartPrice || 0);
|
||||
lineOut.db_price = parseFloat(partInfo?.OEMPartPrice || 0);
|
||||
|
||||
if (partInfo.TaxableInd !== undefined) {
|
||||
const t = partInfo.TaxableInd;
|
||||
lineOut.tax_part = t === true || t === 1 || t === "1" || (typeof t === "string" && t.toUpperCase() === "Y");
|
||||
}
|
||||
} else if (hasSublet) {
|
||||
const amt = parseFloat(subletInfo.SubletAmount || 0);
|
||||
@@ -145,7 +132,7 @@ const extractUpdatedJobLines = (addsChgs = {}, jobId, currentJobLineNotes = {})
|
||||
lineOut.act_price = isNaN(amt) ? 0 : amt;
|
||||
}
|
||||
|
||||
// Primary labor on same line
|
||||
// Primary labor
|
||||
const hrs = parseFloat(laborInfo.LaborHours || 0);
|
||||
const amt = parseFloat(laborInfo.LaborAmt || 0);
|
||||
const hasLabor =
|
||||
@@ -155,11 +142,15 @@ const extractUpdatedJobLines = (addsChgs = {}, jobId, currentJobLineNotes = {})
|
||||
if (hasLabor) {
|
||||
lineOut.mod_lbr_ty = laborInfo.LaborType || null;
|
||||
lineOut.mod_lb_hrs = isNaN(hrs) ? 0 : hrs;
|
||||
lineOut.lbr_op = laborInfo.LaborOperation || null;
|
||||
|
||||
const opCodeKey =
|
||||
typeof laborInfo.LaborOperation === "string" ? laborInfo.LaborOperation.trim().toUpperCase() : null;
|
||||
lineOut.op_code_desc = opCodeKey && opCodes?.[opCodeKey]?.desc ? opCodes[opCodeKey].desc : null;
|
||||
|
||||
lineOut.lbr_amt = isNaN(amt) ? 0 : amt;
|
||||
}
|
||||
|
||||
// Refinish labor on same line using secondary fields; aggregate amount into lbr_amt
|
||||
// Refinish (secondary fields, add amount)
|
||||
const rHrs = parseFloat(refinishInfo.LaborHours || 0);
|
||||
const rAmt = parseFloat(refinishInfo.LaborAmt || 0);
|
||||
const hasRefinish =
|
||||
@@ -172,9 +163,7 @@ const extractUpdatedJobLines = (addsChgs = {}, jobId, currentJobLineNotes = {})
|
||||
lineOut.lbr_typ_j = refinishInfo.LaborType || "LAR";
|
||||
lineOut.lbr_hrs_j = isNaN(rHrs) ? 0 : rHrs;
|
||||
lineOut.lbr_op_j = refinishInfo.LaborOperation || null;
|
||||
if (!isNaN(rAmt)) {
|
||||
lineOut.lbr_amt = (Number.isFinite(lineOut.lbr_amt) ? lineOut.lbr_amt : 0) + rAmt;
|
||||
}
|
||||
if (!isNaN(rAmt)) lineOut.lbr_amt = (Number.isFinite(lineOut.lbr_amt) ? lineOut.lbr_amt : 0) + rAmt;
|
||||
if (refinishInfo.PaintStagesNum !== undefined) lineOut.paint_stg = refinishInfo.PaintStagesNum;
|
||||
if (refinishInfo.PaintTonesNum !== undefined) lineOut.paint_tone = refinishInfo.PaintTonesNum;
|
||||
}
|
||||
@@ -186,85 +175,186 @@ const extractUpdatedJobLines = (addsChgs = {}, jobId, currentJobLineNotes = {})
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts deletion IDs from the deletions object, also removing any derived labor/refinish lines
|
||||
* by including offsets (base + 400000, base + 500000).
|
||||
* Expand deletion IDs to include derived labor/refinish offsets.
|
||||
*/
|
||||
const extractDeletions = (deletions = {}) => {
|
||||
const items = Array.isArray(deletions.DamageLineInfo) ? deletions.DamageLineInfo : [deletions.DamageLineInfo || {}];
|
||||
const baseSeqs = items.map((line) => parseInt(line.UniqueSequenceNum, 10)).filter((id) => Number.isInteger(id));
|
||||
|
||||
const allSeqs = [];
|
||||
for (const u of baseSeqs) {
|
||||
allSeqs.push(u, u + 400000, u + 500000);
|
||||
}
|
||||
// De-dup
|
||||
for (const u of baseSeqs) allSeqs.push(u, u + 400000, u + 500000);
|
||||
return Array.from(new Set(allSeqs));
|
||||
};
|
||||
|
||||
// S3 bucket + key builder (mirrors AddRq but with changeRequest prefix)
|
||||
const ESTIMATE_XML_BUCKET =
|
||||
process.env?.NODE_ENV === "development"
|
||||
? "parts-estimates"
|
||||
: InstanceMgr({
|
||||
imex: `imex-webest-xml`,
|
||||
rome: `rome-webest-xml`
|
||||
});
|
||||
|
||||
const buildEstimateXmlKey = (rq) => {
|
||||
const shopId = rq.ShopID;
|
||||
const jobId = rq.JobID;
|
||||
const ts = new Date().toISOString().replace(/:/g, "-");
|
||||
return `changeRequest/${shopId}/${jobId}/${ts}.xml`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles VehicleDamageEstimateChgRq requests.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns {Promise<*>}
|
||||
* Convert a full jobline object into a jobs_set_input for update_by_pk (omit immutable fields).
|
||||
*/
|
||||
const toJoblineSetInput = (jl) => {
|
||||
const {
|
||||
// immutable identity fields:
|
||||
// jobid,
|
||||
// unq_seq,
|
||||
// everything else:
|
||||
line_no,
|
||||
status,
|
||||
line_desc,
|
||||
manual_line,
|
||||
notes,
|
||||
part_qty,
|
||||
oem_partno,
|
||||
alt_partno,
|
||||
part_type,
|
||||
act_price,
|
||||
db_price,
|
||||
tax_part,
|
||||
mod_lbr_ty,
|
||||
mod_lb_hrs,
|
||||
op_code_desc,
|
||||
lbr_amt,
|
||||
lbr_typ_j,
|
||||
lbr_hrs_j,
|
||||
lbr_op_j,
|
||||
paint_stg,
|
||||
paint_tone
|
||||
} = jl;
|
||||
|
||||
return {
|
||||
line_no,
|
||||
status,
|
||||
line_desc,
|
||||
manual_line,
|
||||
notes,
|
||||
part_qty,
|
||||
oem_partno,
|
||||
alt_partno,
|
||||
part_type,
|
||||
act_price,
|
||||
db_price,
|
||||
tax_part,
|
||||
mod_lbr_ty,
|
||||
mod_lb_hrs,
|
||||
op_code_desc,
|
||||
lbr_amt,
|
||||
lbr_typ_j,
|
||||
lbr_hrs_j,
|
||||
lbr_op_j,
|
||||
paint_stg,
|
||||
paint_tone
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles VehicleDamageEstimateChgRq requests:
|
||||
* - Update core job fields
|
||||
* - For lines: update by PK if existing; otherwise bulk insert
|
||||
* - Soft-delete only explicit deletions (exclude any updated seqs)
|
||||
*/
|
||||
const partsManagementVehicleDamageEstimateChgRq = async (req, res) => {
|
||||
const { logger } = req;
|
||||
|
||||
const rawXml = typeof req.body === "string" ? req.body : Buffer.isBuffer(req.body) ? req.body.toString("utf8") : "";
|
||||
try {
|
||||
const payload = await parseXml(req.body, logger);
|
||||
const rq = normalizeXmlObject(payload.VehicleDamageEstimateChgRq);
|
||||
if (!rq) return res.status(400).send("Missing <VehicleDamageEstimateChgRq>");
|
||||
|
||||
const shopId = rq.ShopID;
|
||||
const jobId = rq.JobID;
|
||||
const shopId = rq.ShopID;
|
||||
|
||||
if (!shopId || !jobId) return res.status(400).send("Missing ShopID or JobID");
|
||||
// Fire-and-forget archival on valid request
|
||||
(async () => {
|
||||
try {
|
||||
const key = buildEstimateXmlKey(rq);
|
||||
await uploadFileToS3({
|
||||
bucketName: ESTIMATE_XML_BUCKET,
|
||||
key,
|
||||
content: rawXml || "",
|
||||
contentType: "application/xml"
|
||||
});
|
||||
logger.log("parts-estimate-xml-uploaded", "info", jobId, null, { key, bytes: rawXml?.length || 0 });
|
||||
} catch (e) {
|
||||
logger.log("parts-estimate-xml-upload-failed", "warn", jobId, null, { error: e?.message });
|
||||
}
|
||||
})();
|
||||
|
||||
const job = await findJob(shopId, jobId, logger);
|
||||
|
||||
if (!job) return res.status(404).send("Job not found");
|
||||
|
||||
// --- Get updated lines and their unq_seq ---
|
||||
// --- Updated seqs from incoming changes ---
|
||||
const linesIn = Array.isArray(rq.AddsChgs?.DamageLineInfo)
|
||||
? rq.AddsChgs.DamageLineInfo
|
||||
: [rq.AddsChgs?.DamageLineInfo || {}];
|
||||
|
||||
const updatedSeqs = Array.from(
|
||||
new Set((linesIn || []).map((l) => parseInt(l?.UniqueSequenceNum || 0, 10)).filter((v) => Number.isInteger(v)))
|
||||
);
|
||||
|
||||
// --- Fetch current notes for merge ---
|
||||
let currentJobLineNotes = {};
|
||||
if (updatedSeqs.length > 0) {
|
||||
const resp = await client.request(GET_JOBLINES_NOTES_BY_JOBID_UNQSEQ, { jobid: job.id, unqSeqs: updatedSeqs });
|
||||
if (resp?.joblines) {
|
||||
for (const jl of resp.joblines) {
|
||||
currentJobLineNotes[jl.unq_seq] = jl.notes;
|
||||
}
|
||||
for (const jl of resp.joblines) currentJobLineNotes[jl.unq_seq] = jl.notes;
|
||||
}
|
||||
}
|
||||
// --- End fetch current notes ---
|
||||
|
||||
const updatedJobData = extractUpdatedJobData(rq);
|
||||
const updatedLines = extractUpdatedJobLines(rq.AddsChgs, job.id, currentJobLineNotes);
|
||||
const deletedLineIds = extractDeletions(rq.Deletions);
|
||||
|
||||
await client.request(UPDATE_JOB_BY_ID, { id: job.id, job: updatedJobData });
|
||||
|
||||
//TODO: for changed lines, are they deleted and then reinserted?
|
||||
//TODO: Updated lines should get an upsert to update things like desc, price, etc.
|
||||
if (deletedLineIds?.length || updatedSeqs?.length) {
|
||||
const allToDelete = Array.from(new Set([...(deletedLineIds || []), ...(updatedSeqs || [])]));
|
||||
if (allToDelete.length) {
|
||||
await client.request(SOFT_DELETE_JOBLINES_BY_IDS, { jobid: job.id, unqSeqs: allToDelete });
|
||||
//TODO: appears to soft delete updated lines as well.
|
||||
// --- Look up existing rows (by natural key) to decide update vs insert ---
|
||||
let existingIdByUnqSeq = {};
|
||||
if (updatedSeqs.length > 0) {
|
||||
const existing = await client.request(GET_JOBLINE_IDS_BY_JOBID_UNQSEQ, { jobid: job.id, unqSeqs: updatedSeqs });
|
||||
if (existing?.joblines) {
|
||||
for (const row of existing.joblines) existingIdByUnqSeq[row.unq_seq] = row.id;
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedLines.length > 0) {
|
||||
// Insert fresh versions after deletion so we don’t depend on a unique constraint
|
||||
await client.request(INSERT_JOBLINES, {
|
||||
joblines: updatedLines
|
||||
});
|
||||
const toUpdate = [];
|
||||
const toInsert = [];
|
||||
for (const jl of updatedLines) {
|
||||
const id = existingIdByUnqSeq[jl.unq_seq];
|
||||
if (id) toUpdate.push({ id, _set: toJoblineSetInput(jl) });
|
||||
else toInsert.push(jl);
|
||||
}
|
||||
|
||||
// Build deletions list and exclude any seqs we are updating (avoid accidental removal)
|
||||
const deletedLineIdsAll = extractDeletions(rq.Deletions);
|
||||
const deletionSeqs = deletedLineIdsAll.filter((u) => !updatedSeqs.includes(u));
|
||||
|
||||
// Mutations:
|
||||
const updateJobPromise = client.request(UPDATE_JOB_BY_ID, { id: job.id, job: updatedJobData });
|
||||
|
||||
const softDeletePromise = deletionSeqs.length
|
||||
? client.request(SOFT_DELETE_JOBLINES_BY_IDS, { jobid: job.id, unqSeqs: deletionSeqs })
|
||||
: Promise.resolve({});
|
||||
|
||||
// Update each existing row by primary key (parallelized)
|
||||
const perRowUpdatesPromise =
|
||||
toUpdate.length > 0
|
||||
? Promise.all(toUpdate.map(({ id, _set }) => client.request(UPDATE_JOBLINE_BY_PK, { id, jl: _set })))
|
||||
: Promise.resolve([]);
|
||||
|
||||
// Insert brand-new rows in bulk
|
||||
const insertPromise =
|
||||
toInsert.length > 0 ? client.request(INSERT_JOBLINES, { joblines: toInsert }) : Promise.resolve({});
|
||||
|
||||
await Promise.all([updateJobPromise, softDeletePromise, perRowUpdatesPromise, insertPromise]);
|
||||
|
||||
logger.log("parts-job-changed", "info", job.id, null);
|
||||
return res.status(200).json({ success: true, jobId: job.id });
|
||||
} catch (err) {
|
||||
|
||||
@@ -246,6 +246,58 @@ const DELETE_PARTS_ORDERS_BY_JOB_IDS = `
|
||||
}
|
||||
`;
|
||||
|
||||
const UPSERT_JOBLINES = `
|
||||
mutation UpsertJoblines($joblines: [joblines_insert_input!]!) {
|
||||
insert_joblines(
|
||||
objects: $joblines,
|
||||
on_conflict: {
|
||||
constraint: joblines_jobid_unq_seq_key,
|
||||
update_columns: [
|
||||
status,
|
||||
line_desc,
|
||||
notes,
|
||||
manual_line,
|
||||
part_qty,
|
||||
oem_partno,
|
||||
alt_partno,
|
||||
part_type,
|
||||
act_price,
|
||||
db_price,
|
||||
tax_part,
|
||||
mod_lbr_ty,
|
||||
mod_lb_hrs,
|
||||
op_code_desc,
|
||||
lbr_amt,
|
||||
lbr_typ_j,
|
||||
lbr_hrs_j,
|
||||
lbr_op_j,
|
||||
paint_stg,
|
||||
paint_tone
|
||||
]
|
||||
}
|
||||
) {
|
||||
affected_rows
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Get jobline IDs for the incoming unq_seq values (only non-removed)
|
||||
const GET_JOBLINE_IDS_BY_JOBID_UNQSEQ = `
|
||||
query GetJoblineIdsByJobIdUnqSeq($jobid: uuid!, $unqSeqs: [Int!]!) {
|
||||
joblines(where: { jobid: { _eq: $jobid }, unq_seq: { _in: $unqSeqs }, removed: { _neq: true } }) {
|
||||
id
|
||||
unq_seq
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// Update a single jobline by primary key
|
||||
const UPDATE_JOBLINE_BY_PK = `
|
||||
mutation UpdateJoblineByPk($id: uuid!, $jl: joblines_set_input!) {
|
||||
update_joblines_by_pk(pk_columns: { id: $id }, _set: $jl) { id }
|
||||
}
|
||||
`;
|
||||
|
||||
module.exports = {
|
||||
GET_BODYSHOP_STATUS,
|
||||
GET_VEHICLE_BY_SHOP_VIN,
|
||||
@@ -272,8 +324,10 @@ module.exports = {
|
||||
DELETE_AUDIT_TRAIL_BY_SHOP,
|
||||
GET_JOBLINES_NOTES_BY_JOBID_UNQSEQ,
|
||||
GET_JOB_BY_ID,
|
||||
// newly added exports
|
||||
CLEAR_TASKS_PARTSORDER_LINKS_BY_JOBIDS,
|
||||
DELETE_PARTS_ORDER_LINES_BY_JOB_IDS,
|
||||
DELETE_PARTS_ORDERS_BY_JOB_IDS
|
||||
DELETE_PARTS_ORDERS_BY_JOB_IDS,
|
||||
UPSERT_JOBLINES,
|
||||
GET_JOBLINE_IDS_BY_JOBID_UNQSEQ,
|
||||
UPDATE_JOBLINE_BY_PK
|
||||
};
|
||||
|
||||
@@ -252,35 +252,27 @@ const generatePaymentUrl = async (req, res) => {
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const checkFee = async (req, res) => {
|
||||
const logResponseMeta = {
|
||||
bodyshop: {
|
||||
id: req.body?.bodyshop?.id,
|
||||
imexshopid: req.body?.bodyshop?.imexshopid,
|
||||
name: req.body?.bodyshop?.shopname,
|
||||
state: req.body?.bodyshop?.state
|
||||
},
|
||||
amount: req.body?.amount
|
||||
};
|
||||
const { bodyshop = {}, amount } = req.body || {};
|
||||
const { id, imexshopid, shopname, state } = bodyshop;
|
||||
const logResponseMeta = { bodyshop: { id, imexshopid, name: shopname, state }, amount };
|
||||
|
||||
logger.log("intellipay-checkfee-request-received", "DEBUG", req.user?.email, null, logResponseMeta);
|
||||
|
||||
if (!isNumber(req.body?.amount) || req.body?.amount <= 0) {
|
||||
if (!isNumber(amount) || amount <= 0) {
|
||||
logger.log("intellipay-checkfee-skip", "DEBUG", req.user?.email, null, {
|
||||
message: "Amount is zero or undefined, skipping fee check.",
|
||||
...logResponseMeta
|
||||
});
|
||||
|
||||
return res.json({ fee: 0 });
|
||||
}
|
||||
|
||||
const shopCredentials = await getShopCredentials(req.body.bodyshop);
|
||||
const shopCredentials = await getShopCredentials(bodyshop);
|
||||
|
||||
if (shopCredentials?.error) {
|
||||
logger.log("intellipay-checkfee-credentials-error", "ERROR", req.user?.email, null, {
|
||||
message: shopCredentials.error?.message,
|
||||
...logResponseMeta
|
||||
});
|
||||
|
||||
return res.status(400).json({ error: shopCredentials.error?.message, ...logResponseMeta });
|
||||
}
|
||||
|
||||
@@ -292,13 +284,10 @@ const checkFee = async (req, res) => {
|
||||
{
|
||||
method: "fee",
|
||||
...shopCredentials,
|
||||
amount: req.body.amount,
|
||||
paymenttype: `CC`,
|
||||
amount: String(amount), // Type cast to string as required by API
|
||||
paymenttype: "CC",
|
||||
cardnum: "4111111111111111", // Required for compatibility with API
|
||||
state:
|
||||
req.body.bodyshop?.state && req.body.bodyshop.state.length === 2
|
||||
? req.body.bodyshop.state.toUpperCase()
|
||||
: "ZZ"
|
||||
state: state?.toUpperCase() || "ZZ"
|
||||
},
|
||||
{ sort: false } // Ensure query string order is preserved
|
||||
),
|
||||
@@ -310,46 +299,24 @@ const checkFee = async (req, res) => {
|
||||
...logResponseMeta
|
||||
});
|
||||
|
||||
const response = await axios(options);
|
||||
const { data } = await axios(options);
|
||||
|
||||
if (response.data?.error) {
|
||||
logger.log("intellipay-checkfee-api-error", "ERROR", req.user?.email, null, {
|
||||
message: response.data?.error,
|
||||
...logResponseMeta
|
||||
});
|
||||
|
||||
return res.status(400).json({
|
||||
error: response.data?.error,
|
||||
type: "intellipay-checkfee-api-error",
|
||||
...logResponseMeta
|
||||
});
|
||||
if (data?.error || data < 0) {
|
||||
const errorType = data?.error ? "intellipay-checkfee-api-error" : "intellipay-checkfee-negative-fee";
|
||||
const errorMessage = data?.error
|
||||
? data?.error
|
||||
: "Fee amount negative. Check API credentials & account configuration.";
|
||||
logger.log(errorType, "ERROR", req.user?.email, null, { message: errorMessage, data, ...logResponseMeta });
|
||||
return res.status(400).json({ error: errorMessage, type: errorType, data, ...logResponseMeta });
|
||||
}
|
||||
|
||||
if (response.data < 0) {
|
||||
logger.log("intellipay-checkfee-negative-fee", "ERROR", req.user?.email, null, {
|
||||
message: "Fee amount returned is negative.",
|
||||
...logResponseMeta
|
||||
});
|
||||
|
||||
return res.json({
|
||||
error: "Fee amount negative. Check API credentials & account configuration.",
|
||||
...logResponseMeta,
|
||||
type: "intellipay-checkfee-negative-fee"
|
||||
});
|
||||
}
|
||||
|
||||
logger.log("intellipay-checkfee-success", "DEBUG", req.user?.email, null, {
|
||||
fee: response.data,
|
||||
...logResponseMeta
|
||||
});
|
||||
|
||||
return res.json({ fee: response.data, ...logResponseMeta });
|
||||
logger.log("intellipay-checkfee-success", "DEBUG", req.user?.email, null, { fee: data, ...logResponseMeta });
|
||||
return res.json({ fee: data, ...logResponseMeta });
|
||||
} catch (error) {
|
||||
logger.log("intellipay-checkfee-error", "ERROR", req.user?.email, null, {
|
||||
message: error?.message,
|
||||
...logResponseMeta
|
||||
});
|
||||
|
||||
return res.status(500).json({ error: error?.message, logResponseMeta });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const logger = require("../../server/utils/logger");
|
||||
|
||||
// Pull secrets from env
|
||||
const { VSSTA_INTEGRATION_SECRET, PARTS_MANAGEMENT_INTEGRATION_SECRET } = process.env;
|
||||
@@ -11,7 +12,7 @@ if (typeof VSSTA_INTEGRATION_SECRET === "string" && VSSTA_INTEGRATION_SECRET.len
|
||||
|
||||
router.post("/vssta", vsstaMiddleware, vsstaIntegration);
|
||||
} else {
|
||||
console.warn("VSSTA_INTEGRATION_SECRET is not set — skipping /vssta integration route");
|
||||
logger.logger.warn("VSSTA_INTEGRATION_SECRET is not set — skipping /vssta integration route");
|
||||
}
|
||||
|
||||
// Only load Parts Management routes if that secret is set
|
||||
@@ -45,14 +46,17 @@ if (typeof PARTS_MANAGEMENT_INTEGRATION_SECRET === "string" && PARTS_MANAGEMENT_
|
||||
);
|
||||
|
||||
// Deprovisioning route
|
||||
router.post("/parts-management/deprovision", partsManagementIntegrationMiddleware, partsManagementDeprovisioning);
|
||||
if (process.env.NODE_ENV !== "production" && !process.env.HOSTNAME?.endsWith("compute.internal")) {
|
||||
logger.logger.warn("Parts Management Deprovisioning route has been loaded.");
|
||||
router.post("/parts-management/deprovision", partsManagementIntegrationMiddleware, partsManagementDeprovisioning);
|
||||
}
|
||||
|
||||
/**
|
||||
* Route to handle Parts Management Provisioning
|
||||
*/
|
||||
router.post("/parts-management/provision", partsManagementIntegrationMiddleware, partsManagementProvisioning);
|
||||
} else {
|
||||
console.warn("PARTS_MANAGEMENT_INTEGRATION_SECRET is not set — skipping /parts-management/provision route");
|
||||
logger.logger.warn("PARTS_MANAGEMENT_INTEGRATION_SECRET is not set — skipping /parts-management/provision route");
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
|
||||
Reference in New Issue
Block a user