Compare commits
54 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d52426f5f5 | ||
|
|
2c508cf1a1 | ||
|
|
16a91c772a | ||
|
|
5c47088b11 | ||
|
|
8e5dc4fa71 | ||
|
|
39c3729f6d | ||
|
|
e3d854e02b | ||
|
|
618acf2acf | ||
|
|
2cf2b70293 | ||
|
|
0541afceb8 | ||
|
|
28ed3f9936 | ||
|
|
6afa50332b | ||
|
|
8c8c68867d | ||
|
|
8ee52598e8 | ||
|
|
c822028174 | ||
|
|
36b82c6195 | ||
|
|
079dffce4d | ||
|
|
831802f5af | ||
|
|
7bd5190bf2 | ||
|
|
83860152a9 | ||
|
|
1e10493615 | ||
|
|
9d81c68a4d | ||
|
|
985d066978 | ||
|
|
6ad9e27d1d | ||
|
|
19ebdda5b3 | ||
|
|
4602dd1183 | ||
|
|
6005eaee6a | ||
|
|
6d59e3994f | ||
|
|
f770b2f1b1 | ||
|
|
b014744940 | ||
|
|
714c90c25e | ||
|
|
9a3a971da6 | ||
|
|
96cba0aaab | ||
|
|
c069600cfd | ||
|
|
186cbf2c97 | ||
|
|
392988ae11 | ||
|
|
2e33b79eb9 | ||
|
|
d4f718c44c | ||
|
|
fa99ef7b37 | ||
|
|
c4aff1b516 | ||
|
|
61276bb2d1 | ||
|
|
8b89e2eb9d | ||
|
|
9ab41308e7 | ||
|
|
f76052ec9b | ||
|
|
b8841e3ded | ||
|
|
a49b3f6496 | ||
|
|
3e17ec3cf8 | ||
|
|
76c0c7c41e | ||
|
|
025b986f60 | ||
|
|
6e6addd62f | ||
|
|
266c3acf34 | ||
|
|
ca18291425 | ||
|
|
a9814c1eb1 | ||
|
|
12c87ed689 |
363
client/package-lock.json
generated
363
client/package-lock.json
generated
@@ -13,21 +13,21 @@
|
|||||||
"@apollo/client": "^3.13.6",
|
"@apollo/client": "^3.13.6",
|
||||||
"@emotion/is-prop-valid": "^1.3.1",
|
"@emotion/is-prop-valid": "^1.3.1",
|
||||||
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
||||||
"@firebase/analytics": "^0.10.13",
|
"@firebase/analytics": "^0.10.16",
|
||||||
"@firebase/app": "^0.12.1",
|
"@firebase/app": "^0.13.0",
|
||||||
"@firebase/auth": "^1.10.2",
|
"@firebase/auth": "^1.10.5",
|
||||||
"@firebase/firestore": "^4.7.12",
|
"@firebase/firestore": "^4.7.15",
|
||||||
"@firebase/messaging": "^0.12.18",
|
"@firebase/messaging": "^0.12.21",
|
||||||
"@jsreport/browser-client": "^3.1.0",
|
"@jsreport/browser-client": "^3.1.0",
|
||||||
"@reduxjs/toolkit": "^2.8.1",
|
"@reduxjs/toolkit": "^2.8.2",
|
||||||
"@sentry/cli": "^2.45.0",
|
"@sentry/cli": "^2.45.0",
|
||||||
"@sentry/react": "^9.17.0",
|
"@sentry/react": "^9.22.0",
|
||||||
"@sentry/vite-plugin": "^3.4.0",
|
"@sentry/vite-plugin": "^3.5.0",
|
||||||
"@splitsoftware/splitio-react": "^2.1.1",
|
"@splitsoftware/splitio-react": "^2.1.1",
|
||||||
"@tanem/react-nprogress": "^5.0.53",
|
"@tanem/react-nprogress": "^5.0.53",
|
||||||
"antd": "^5.25.0",
|
"antd": "^5.25.2",
|
||||||
"apollo-link-logger": "^2.0.1",
|
"apollo-link-logger": "^2.0.1",
|
||||||
"apollo-link-sentry": "^4.2.0",
|
"apollo-link-sentry": "^4.3.0",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
"normalize-url": "^8.0.1",
|
"normalize-url": "^8.0.1",
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"query-string": "^9.1.2",
|
"query-string": "^9.2.0",
|
||||||
"raf-schd": "^4.0.3",
|
"raf-schd": "^4.0.3",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-big-calendar": "^1.18.0",
|
"react-big-calendar": "^1.18.0",
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
"redux-saga": "^1.3.0",
|
"redux-saga": "^1.3.0",
|
||||||
"redux-state-sync": "^3.1.4",
|
"redux-state-sync": "^3.1.4",
|
||||||
"reselect": "^5.1.1",
|
"reselect": "^5.1.1",
|
||||||
"sass": "^1.86.3",
|
"sass": "^1.89.0",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"styled-components": "^6.1.18",
|
"styled-components": "^6.1.18",
|
||||||
"subscriptions-transport-ws": "^0.11.0",
|
"subscriptions-transport-ws": "^0.11.0",
|
||||||
@@ -90,12 +90,12 @@
|
|||||||
"@ant-design/icons": "^6.0.0",
|
"@ant-design/icons": "^6.0.0",
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-react": "^7.27.1",
|
"@babel/preset-react": "^7.27.1",
|
||||||
"@dotenvx/dotenvx": "^1.43.0",
|
"@dotenvx/dotenvx": "^1.44.1",
|
||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@eslint/js": "^9.26.0",
|
"@eslint/js": "^9.27.0",
|
||||||
"@playwright/test": "^1.51.1",
|
"@playwright/test": "^1.51.1",
|
||||||
"@sentry/webpack-plugin": "^3.4.0",
|
"@sentry/webpack-plugin": "^3.5.0",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
"memfs": "^4.17.1",
|
"memfs": "^4.17.2",
|
||||||
"os-browserify": "^0.3.0",
|
"os-browserify": "^0.3.0",
|
||||||
"playwright": "^1.51.1",
|
"playwright": "^1.51.1",
|
||||||
"react-error-overlay": "^6.1.0",
|
"react-error-overlay": "^6.1.0",
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
"vite-plugin-node-polyfills": "^0.23.0",
|
"vite-plugin-node-polyfills": "^0.23.0",
|
||||||
"vite-plugin-pwa": "^1.0.0",
|
"vite-plugin-pwa": "^1.0.0",
|
||||||
"vite-plugin-style-import": "^2.0.0",
|
"vite-plugin-style-import": "^2.0.0",
|
||||||
"vitest": "^3.1.3",
|
"vitest": "^3.1.4",
|
||||||
"workbox-window": "^7.3.0"
|
"workbox-window": "^7.3.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -2587,9 +2587,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@dotenvx/dotenvx": {
|
"node_modules/@dotenvx/dotenvx": {
|
||||||
"version": "1.43.0",
|
"version": "1.44.1",
|
||||||
"resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.43.0.tgz",
|
"resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.44.1.tgz",
|
||||||
"integrity": "sha512-Z8XjM75aWZ/ekUzBjlr/OqQsLWtJY4nVtruxopAt+FlYHfY0/gKl85nD16aEqbTkU53kJcm5psID0L2/sQMmuw==",
|
"integrity": "sha512-j1QImCqf/XJmhIjC1OPpgiZV9g370HG9MNT9s/CDwCKsoYzNCPEKK+GfsidahJx7yIlBbm+4dPLlGec+bKn7oA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2912,13 +2912,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/js": {
|
"node_modules/@eslint/js": {
|
||||||
"version": "9.26.0",
|
"version": "9.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz",
|
||||||
"integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==",
|
"integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://eslint.org/donate"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@fingerprintjs/fingerprintjs": {
|
"node_modules/@fingerprintjs/fingerprintjs": {
|
||||||
@@ -2931,15 +2934,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@firebase/analytics": {
|
"node_modules/@firebase/analytics": {
|
||||||
"version": "0.10.13",
|
"version": "0.10.16",
|
||||||
"resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.13.tgz",
|
"resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.16.tgz",
|
||||||
"integrity": "sha512-X+6wMOPgA9l0AeeMdMcMfaCP4XKPvrhx55MGuMrfHvUrOvFKldpzBum7KkoGJMoexKmqmKP+mCmJMY9Fb8K6Hw==",
|
"integrity": "sha512-cMtp19He7Fd6uaj/nDEul+8JwvJsN8aRSJyuA1QN3QrKvfDDp+efjVurJO61sJpkVftw9O9nNMdhFbRcTmTfRQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@firebase/component": "0.6.14",
|
"@firebase/component": "0.6.17",
|
||||||
"@firebase/installations": "0.6.14",
|
"@firebase/installations": "0.6.17",
|
||||||
"@firebase/logger": "0.4.4",
|
"@firebase/logger": "0.4.4",
|
||||||
"@firebase/util": "1.11.1",
|
"@firebase/util": "1.12.0",
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
@@ -2947,14 +2950,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@firebase/app": {
|
"node_modules/@firebase/app": {
|
||||||
"version": "0.12.1",
|
"version": "0.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.13.0.tgz",
|
||||||
"integrity": "sha512-ASExOlmmjRMdwOQ65Oj6R9JBqa7iiT1/LgZjtbU7FqxoJZNWHrt39NJ/z2bjyYDdAHX8jkY7muFqzahScCXgfA==",
|
"integrity": "sha512-Vj3MST245nq+V5UmmfEkB3isIgPouyUr8yGJlFeL9Trg/umG5ogAvrjAYvQ8gV7daKDoQSRnJKWI2JFpQqRsuQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@firebase/component": "0.6.14",
|
"@firebase/component": "0.6.17",
|
||||||
"@firebase/logger": "0.4.4",
|
"@firebase/logger": "0.4.4",
|
||||||
"@firebase/util": "1.11.1",
|
"@firebase/util": "1.12.0",
|
||||||
"idb": "7.1.1",
|
"idb": "7.1.1",
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
},
|
},
|
||||||
@@ -2963,14 +2966,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@firebase/auth": {
|
"node_modules/@firebase/auth": {
|
||||||
"version": "1.10.2",
|
"version": "1.10.5",
|
||||||
"resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.10.5.tgz",
|
||||||
"integrity": "sha512-HHudcj3CJyXpoMKslNOVHGSNJdAUjvy5xBA/G/uPb32QFqvx5F3EW9RDYvve2IHEN7Vpc1QTkk/28J32x83UGA==",
|
"integrity": "sha512-6wF/NdMTwObL4RNQePunuzMr9O3gyftisvFZFFKf57D2HONXo87YymogRV8d+Z7SLA0rcNBN1gLJVk2D0y97gA==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@firebase/component": "0.6.14",
|
"@firebase/component": "0.6.17",
|
||||||
"@firebase/logger": "0.4.4",
|
"@firebase/logger": "0.4.4",
|
||||||
"@firebase/util": "1.11.1",
|
"@firebase/util": "1.12.0",
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -2987,12 +2990,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@firebase/component": {
|
"node_modules/@firebase/component": {
|
||||||
"version": "0.6.14",
|
"version": "0.6.17",
|
||||||
"resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.14.tgz",
|
"resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.17.tgz",
|
||||||
"integrity": "sha512-kf/zAT8GQJ9nYoHuj0mv7twp1QzifKYrO+GsmsVHHM+Hi9KkmI7E3B3J0CtihHpb34vinl4gbJrYJ2p2wfvc9A==",
|
"integrity": "sha512-M6DOg7OySrKEFS8kxA3MU5/xc37fiOpKPMz6cTsMUcsuKB6CiZxxNAvgFta8HGRgEpZbi8WjGIj6Uf+TpOhyzg==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@firebase/util": "1.11.1",
|
"@firebase/util": "1.12.0",
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -3000,14 +3003,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@firebase/firestore": {
|
"node_modules/@firebase/firestore": {
|
||||||
"version": "4.7.12",
|
"version": "4.7.15",
|
||||||
"resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.7.12.tgz",
|
"resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.7.15.tgz",
|
||||||
"integrity": "sha512-50KRdSp8xA7+G0wfWxlnCoEN951mt8BVdLMxeP57Rehj2DqIb41q6Fc6JH0dfQ4TlMqWua1YfVY1jPEAaHVF9w==",
|
"integrity": "sha512-FgWTmkNBEXdKCoN2ngBNjrMaXuBx6QwjiZZVnOGg+VjUmiBq5gAqlDIW5bZY6i/NYvLUrWugdqIs7y9GHEqwww==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@firebase/component": "0.6.14",
|
"@firebase/component": "0.6.17",
|
||||||
"@firebase/logger": "0.4.4",
|
"@firebase/logger": "0.4.4",
|
||||||
"@firebase/util": "1.11.1",
|
"@firebase/util": "1.12.0",
|
||||||
"@firebase/webchannel-wrapper": "1.0.3",
|
"@firebase/webchannel-wrapper": "1.0.3",
|
||||||
"@grpc/grpc-js": "~1.9.0",
|
"@grpc/grpc-js": "~1.9.0",
|
||||||
"@grpc/proto-loader": "^0.7.8",
|
"@grpc/proto-loader": "^0.7.8",
|
||||||
@@ -3021,13 +3024,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@firebase/installations": {
|
"node_modules/@firebase/installations": {
|
||||||
"version": "0.6.14",
|
"version": "0.6.17",
|
||||||
"resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.14.tgz",
|
"resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.17.tgz",
|
||||||
"integrity": "sha512-uE837g9+sv6PfjWPgOfG3JtjZ+hJ7KBHO4UVenVsvhzgOxFkvLjO/bgE7fyvsaD3fOHSXunx3adRIg4eUEMPyA==",
|
"integrity": "sha512-zfhqCNJZRe12KyADtRrtOj+SeSbD1H/K8J24oQAJVv/u02eQajEGlhZtcx9Qk7vhGWF5z9dvIygVDYqLL4o1XQ==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@firebase/component": "0.6.14",
|
"@firebase/component": "0.6.17",
|
||||||
"@firebase/util": "1.11.1",
|
"@firebase/util": "1.12.0",
|
||||||
"idb": "7.1.1",
|
"idb": "7.1.1",
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
},
|
},
|
||||||
@@ -3048,15 +3051,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@firebase/messaging": {
|
"node_modules/@firebase/messaging": {
|
||||||
"version": "0.12.18",
|
"version": "0.12.21",
|
||||||
"resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.18.tgz",
|
"resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.21.tgz",
|
||||||
"integrity": "sha512-2MGhUGoCZloB7ysoYzG/T2nnRmHYLT+AcqYouZuD6APabpkDhF8lHsmSQq4MFSlXhI3DKFOXxjuvbY8ec4C2JQ==",
|
"integrity": "sha512-bYJ2Evj167Z+lJ1ach6UglXz5dUKY1zrJZd15GagBUJSR7d9KfiM1W8dsyL0lDxcmhmA/sLaBYAAhF1uilwN0g==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@firebase/component": "0.6.14",
|
"@firebase/component": "0.6.17",
|
||||||
"@firebase/installations": "0.6.14",
|
"@firebase/installations": "0.6.17",
|
||||||
"@firebase/messaging-interop-types": "0.2.3",
|
"@firebase/messaging-interop-types": "0.2.3",
|
||||||
"@firebase/util": "1.11.1",
|
"@firebase/util": "1.12.0",
|
||||||
"idb": "7.1.1",
|
"idb": "7.1.1",
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
},
|
},
|
||||||
@@ -3071,9 +3074,9 @@
|
|||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@firebase/util": {
|
"node_modules/@firebase/util": {
|
||||||
"version": "1.11.1",
|
"version": "1.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.12.0.tgz",
|
||||||
"integrity": "sha512-RXg4WE8C2LUrvoV/TMGRTu223zZf9Dq9MR8yHZio9nF9TpLnpCPURw9VWWB2WATDl6HfIdWfl2x2SJYtHkN4hw==",
|
"integrity": "sha512-Z4rK23xBCwgKDqmzGVMef+Vb4xso2j5Q8OG0vVL4m4fA5ZjPMYQazu8OJJC3vtQRC3SQ/Pgx/6TPNVsCd70QRw==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3836,9 +3839,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@reduxjs/toolkit": {
|
"node_modules/@reduxjs/toolkit": {
|
||||||
"version": "2.8.1",
|
"version": "2.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz",
|
||||||
"integrity": "sha512-GLjHS13LiBdiuxSJvfWs3+Cx5yt97mCbuVlDteTusS6VRksPhoWviO8L1e3Re1G94m6lkw/l4pjEEyyNaGf19g==",
|
"integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@standard-schema/spec": "^1.0.0",
|
"@standard-schema/spec": "^1.0.0",
|
||||||
@@ -4458,88 +4461,88 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@sentry-internal/browser-utils": {
|
"node_modules/@sentry-internal/browser-utils": {
|
||||||
"version": "9.17.0",
|
"version": "9.22.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.22.0.tgz",
|
||||||
"integrity": "sha512-37n6NXtkUfdK7YiP3L5DJvhA/iusOmnjHQdX1e2VwI6a29xHCl/vRqLR3XNr5K4m+49al+3fWo2ltcKsfV+0xw==",
|
"integrity": "sha512-Ou1tBnVxFAIn8i9gvrWzRotNJQYiu3awNXpsFCw6qFwmiKAVPa6b13vCdolhXnrIiuR77jY1LQnKh9hXpoRzsg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/core": "9.17.0"
|
"@sentry/core": "9.22.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry-internal/feedback": {
|
"node_modules/@sentry-internal/feedback": {
|
||||||
"version": "9.17.0",
|
"version": "9.22.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.22.0.tgz",
|
||||||
"integrity": "sha512-C2jBlGgYVGm8eXK38wlYQyd6NsHKaQlENg5fx8TDFMKWMNmLf6BmnPZ+y73OsFwcUtBz04CwZteybYB2GgYrvQ==",
|
"integrity": "sha512-zgMVkoC61fgi41zLcSZA59vOtKxcLrKBo1ECYhPD1hxEaneNqY5fhXDwlQBw96P5l2yqkgfX6YZtSdU4ejI9yA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/core": "9.17.0"
|
"@sentry/core": "9.22.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry-internal/replay": {
|
"node_modules/@sentry-internal/replay": {
|
||||||
"version": "9.17.0",
|
"version": "9.22.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.22.0.tgz",
|
||||||
"integrity": "sha512-oH4NolXkEpe73eRP9r3K6WpERYItZisYQudsNrtkUBQL5M/uENiE7YTOvL5osD8AWmU0hCKY3Oua+qDi2lB+8g==",
|
"integrity": "sha512-9GOycoKbrclcRXfcbNV8svbmAsOS5R4wXBQmKF4pFLkmFA/lJv9kdZSNYkRvkrxdNfbMIJXP+DV9EqTZcryXig==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry-internal/browser-utils": "9.17.0",
|
"@sentry-internal/browser-utils": "9.22.0",
|
||||||
"@sentry/core": "9.17.0"
|
"@sentry/core": "9.22.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry-internal/replay-canvas": {
|
"node_modules/@sentry-internal/replay-canvas": {
|
||||||
"version": "9.17.0",
|
"version": "9.22.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.22.0.tgz",
|
||||||
"integrity": "sha512-w9AxBJIa+MbxDngvwnqouoJ/ezb7wNjxzFXtmaVtGp7hbC4yme/TOTNtFYg2J/ceQf3GMc8AfW5tsP6zU0R7gg==",
|
"integrity": "sha512-EcG9IMSEalFe49kowBTJObWjof/iHteDwpyuAszsFDdQUYATrVUtwpwN7o52vDYWJud4arhjrQnMamIGxa79eQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry-internal/replay": "9.17.0",
|
"@sentry-internal/replay": "9.22.0",
|
||||||
"@sentry/core": "9.17.0"
|
"@sentry/core": "9.22.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/babel-plugin-component-annotate": {
|
"node_modules/@sentry/babel-plugin-component-annotate": {
|
||||||
"version": "3.4.0",
|
"version": "3.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.5.0.tgz",
|
||||||
"integrity": "sha512-tSzfc3aE7m0PM0Aj7HBDet5llH9AB9oc+tBQ8AvOqUSnWodLrNCuWeQszJ7mIBovD3figgCU3h0cvI6U5cDtsg==",
|
"integrity": "sha512-s2go8w03CDHbF9luFGtBHKJp4cSpsQzNVqgIa9Pfa4wnjipvrK6CxVT4icpLA3YO6kg5u622Yoa5GF3cJdippw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/browser": {
|
"node_modules/@sentry/browser": {
|
||||||
"version": "9.17.0",
|
"version": "9.22.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.22.0.tgz",
|
||||||
"integrity": "sha512-3e/Q5bv06Q+XYV2cKmUgfMfnJtBY8MZKufpcwQ2ab2eMrastqau9KjYeWXapskDm179oPLfzLcDCSlDSTcvqpQ==",
|
"integrity": "sha512-3TeRm74dvX0JdjX0AgkQa+22iUHwHnY+Q6M05NZ+tDeCNHGK/mEBTeqquS1oQX67jWyuvYmG3VV6RJUxtG9Paw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry-internal/browser-utils": "9.17.0",
|
"@sentry-internal/browser-utils": "9.22.0",
|
||||||
"@sentry-internal/feedback": "9.17.0",
|
"@sentry-internal/feedback": "9.22.0",
|
||||||
"@sentry-internal/replay": "9.17.0",
|
"@sentry-internal/replay": "9.22.0",
|
||||||
"@sentry-internal/replay-canvas": "9.17.0",
|
"@sentry-internal/replay-canvas": "9.22.0",
|
||||||
"@sentry/core": "9.17.0"
|
"@sentry/core": "9.22.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/bundler-plugin-core": {
|
"node_modules/@sentry/bundler-plugin-core": {
|
||||||
"version": "3.4.0",
|
"version": "3.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.5.0.tgz",
|
||||||
"integrity": "sha512-X1Q41AsQ6xcT6hB4wYmBDBukndKM/inT4IsR7pdKLi7ICpX2Qq6lisamBAEPCgEvnLpazSFguaiC0uiwMKAdqw==",
|
"integrity": "sha512-zDzPrhJqAAy2VzV4g540qAZH4qxzisstK2+NIJPZUUKztWRWUV2cMHsyUtdctYgloGkLyGpZJBE3RE6dmP/xqQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.18.5",
|
"@babel/core": "^7.18.5",
|
||||||
"@sentry/babel-plugin-component-annotate": "3.4.0",
|
"@sentry/babel-plugin-component-annotate": "3.5.0",
|
||||||
"@sentry/cli": "2.42.2",
|
"@sentry/cli": "2.42.2",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"find-up": "^5.0.0",
|
"find-up": "^5.0.0",
|
||||||
@@ -4899,22 +4902,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/core": {
|
"node_modules/@sentry/core": {
|
||||||
"version": "9.17.0",
|
"version": "9.22.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.22.0.tgz",
|
||||||
"integrity": "sha512-9f1A93/kY9lLH06L1thPx94IhyLjEP3aRxYAtjtBfzId8UtubSpwP92sbxgslodD73R4tURwWJj7nYZ9HLYBUg==",
|
"integrity": "sha512-ixvtKmPF42Y6ckGUbFlB54OWI75H2gO5UYHojO6eXFpS7xO3ZGgV/QH6wb40mWK+0w5XZ0233FuU9VpsuE6mKA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/react": {
|
"node_modules/@sentry/react": {
|
||||||
"version": "9.17.0",
|
"version": "9.22.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-9.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-9.22.0.tgz",
|
||||||
"integrity": "sha512-hJOVUheFoUKr5e4vHxyKiu72FRgqmMTFIUG9myim8PH8mJYDqab7Z7cOt4dsBR86soKanaRB5PJq5jGFuipLfg==",
|
"integrity": "sha512-mI43NnioBYdG5TiXqRlhV1feZs9bnrrl+k5HOHBK7VQtymaXO0fkcsRLZTkdSgLRLMJGasZuvVhq2xK+18QyWQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/browser": "9.17.0",
|
"@sentry/browser": "9.22.0",
|
||||||
"@sentry/core": "9.17.0",
|
"@sentry/core": "9.22.0",
|
||||||
"hoist-non-react-statics": "^3.3.2"
|
"hoist-non-react-statics": "^3.3.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4925,12 +4928,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/vite-plugin": {
|
"node_modules/@sentry/vite-plugin": {
|
||||||
"version": "3.4.0",
|
"version": "3.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-3.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-3.5.0.tgz",
|
||||||
"integrity": "sha512-pUFBGrKsHuc8K6A7B1wU2nx65n9aIzvTlcHX9yZ1qvjEO0cZFih0JCwu1Fcav/yrtT9RMN44L/ugu/kMBHQhjQ==",
|
"integrity": "sha512-jUnpTdpicG8wefamw7eNo2uO+Q3KCbOAiF76xH4gfNHSW6TN2hBfOtmLu7J+ive4c0Al3+NEHz19bIPR0lkwWg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/bundler-plugin-core": "3.4.0",
|
"@sentry/bundler-plugin-core": "3.5.0",
|
||||||
"unplugin": "1.0.1"
|
"unplugin": "1.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4938,13 +4941,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sentry/webpack-plugin": {
|
"node_modules/@sentry/webpack-plugin": {
|
||||||
"version": "3.4.0",
|
"version": "3.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-3.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-3.5.0.tgz",
|
||||||
"integrity": "sha512-i+nAxxniJV5ovijojjTF5n+Yj08Xk8my+vm8+oo0C0I7xcnI2gOKft6B0sJOq01CNbo85X5m/3/edL0PKoWE9w==",
|
"integrity": "sha512-xvclj0QY2HyU7uJLzOlHSrZQBDwfnGKJxp8mmlU4L7CwmK+8xMCqlO7tYZoqE4K/wU3c2xpXql70x8qmvNMxzQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sentry/bundler-plugin-core": "3.4.0",
|
"@sentry/bundler-plugin-core": "3.5.0",
|
||||||
"unplugin": "1.0.1",
|
"unplugin": "1.0.1",
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
@@ -5810,14 +5813,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/expect": {
|
"node_modules/@vitest/expect": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.4.tgz",
|
||||||
"integrity": "sha512-7FTQQuuLKmN1Ig/h+h/GO+44Q1IlglPlR2es4ab7Yvfx+Uk5xsv+Ykk+MEt/M2Yn/xGmzaLKxGw2lgy2bwuYqg==",
|
"integrity": "sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/spy": "3.1.3",
|
"@vitest/spy": "3.1.4",
|
||||||
"@vitest/utils": "3.1.3",
|
"@vitest/utils": "3.1.4",
|
||||||
"chai": "^5.2.0",
|
"chai": "^5.2.0",
|
||||||
"tinyrainbow": "^2.0.0"
|
"tinyrainbow": "^2.0.0"
|
||||||
},
|
},
|
||||||
@@ -5826,13 +5829,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/mocker": {
|
"node_modules/@vitest/mocker": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.4.tgz",
|
||||||
"integrity": "sha512-PJbLjonJK82uCWHjzgBJZuR7zmAOrSvKk1QBxrennDIgtH4uK0TB1PvYmc0XBCigxxtiAVPfWtAdy4lpz8SQGQ==",
|
"integrity": "sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/spy": "3.1.3",
|
"@vitest/spy": "3.1.4",
|
||||||
"estree-walker": "^3.0.3",
|
"estree-walker": "^3.0.3",
|
||||||
"magic-string": "^0.30.17"
|
"magic-string": "^0.30.17"
|
||||||
},
|
},
|
||||||
@@ -5873,9 +5876,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/pretty-format": {
|
"node_modules/@vitest/pretty-format": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.4.tgz",
|
||||||
"integrity": "sha512-i6FDiBeJUGLDKADw2Gb01UtUNb12yyXAqC/mmRWuYl+m/U9GS7s8us5ONmGkGpUUo7/iAYzI2ePVfOZTYvUifA==",
|
"integrity": "sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -5886,13 +5889,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/runner": {
|
"node_modules/@vitest/runner": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.4.tgz",
|
||||||
"integrity": "sha512-Tae+ogtlNfFei5DggOsSUvkIaSuVywujMj6HzR97AHK6XK8i3BuVyIifWAm/sE3a15lF5RH9yQIrbXYuo0IFyA==",
|
"integrity": "sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/utils": "3.1.3",
|
"@vitest/utils": "3.1.4",
|
||||||
"pathe": "^2.0.3"
|
"pathe": "^2.0.3"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -5907,13 +5910,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/snapshot": {
|
"node_modules/@vitest/snapshot": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.4.tgz",
|
||||||
"integrity": "sha512-XVa5OPNTYUsyqG9skuUkFzAeFnEzDp8hQu7kZ0N25B1+6KjGm4hWLtURyBbsIAOekfWQ7Wuz/N/XXzgYO3deWQ==",
|
"integrity": "sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/pretty-format": "3.1.3",
|
"@vitest/pretty-format": "3.1.4",
|
||||||
"magic-string": "^0.30.17",
|
"magic-string": "^0.30.17",
|
||||||
"pathe": "^2.0.3"
|
"pathe": "^2.0.3"
|
||||||
},
|
},
|
||||||
@@ -5939,9 +5942,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/spy": {
|
"node_modules/@vitest/spy": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.4.tgz",
|
||||||
"integrity": "sha512-x6w+ctOEmEXdWaa6TO4ilb7l9DxPR5bwEb6hILKuxfU1NqWT2mpJD9NJN7t3OTfxmVlOMrvtoFJGdgyzZ605lQ==",
|
"integrity": "sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -5952,13 +5955,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/utils": {
|
"node_modules/@vitest/utils": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.4.tgz",
|
||||||
"integrity": "sha512-2Ltrpht4OmHO9+c/nmHtF09HWiyWdworqnHIwjfvDyWjuwKbdkcS9AnhsDn+8E2RM4x++foD1/tNuLPVvWG1Rg==",
|
"integrity": "sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/pretty-format": "3.1.3",
|
"@vitest/pretty-format": "3.1.4",
|
||||||
"loupe": "^3.1.3",
|
"loupe": "^3.1.3",
|
||||||
"tinyrainbow": "^2.0.0"
|
"tinyrainbow": "^2.0.0"
|
||||||
},
|
},
|
||||||
@@ -6088,9 +6091,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/antd": {
|
"node_modules/antd": {
|
||||||
"version": "5.25.0",
|
"version": "5.25.2",
|
||||||
"resolved": "https://registry.npmjs.org/antd/-/antd-5.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/antd/-/antd-5.25.2.tgz",
|
||||||
"integrity": "sha512-p9d8Kuj/bipjNdg9NrTu1VmTrhcwIhURu2NfK6qaBMbb+LRyFdAUoseT+7J4a+5z3jNVjxH5zaYv/45Zf8Coyg==",
|
"integrity": "sha512-7R2nUvlHhey7Trx64+hCtGXOiy+DTUs1Lv5bwbV1LzEIZIhWb0at1AM6V3K108a5lyoR9n7DX3ptlLF7uYV/DQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/colors": "^7.2.0",
|
"@ant-design/colors": "^7.2.0",
|
||||||
@@ -6128,11 +6131,11 @@
|
|||||||
"rc-rate": "~2.13.1",
|
"rc-rate": "~2.13.1",
|
||||||
"rc-resize-observer": "^1.4.3",
|
"rc-resize-observer": "^1.4.3",
|
||||||
"rc-segmented": "~2.7.0",
|
"rc-segmented": "~2.7.0",
|
||||||
"rc-select": "~14.16.6",
|
"rc-select": "~14.16.8",
|
||||||
"rc-slider": "~11.1.8",
|
"rc-slider": "~11.1.8",
|
||||||
"rc-steps": "~6.0.1",
|
"rc-steps": "~6.0.1",
|
||||||
"rc-switch": "~4.1.0",
|
"rc-switch": "~4.1.0",
|
||||||
"rc-table": "~7.50.4",
|
"rc-table": "~7.50.5",
|
||||||
"rc-tabs": "~15.6.1",
|
"rc-tabs": "~15.6.1",
|
||||||
"rc-textarea": "~1.10.0",
|
"rc-textarea": "~1.10.0",
|
||||||
"rc-tooltip": "~6.4.0",
|
"rc-tooltip": "~6.4.0",
|
||||||
@@ -6232,9 +6235,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/apollo-link-sentry": {
|
"node_modules/apollo-link-sentry": {
|
||||||
"version": "4.2.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/apollo-link-sentry/-/apollo-link-sentry-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/apollo-link-sentry/-/apollo-link-sentry-4.3.0.tgz",
|
||||||
"integrity": "sha512-w8EUM4aEw1/VxIB3KOP11T8qz44oWRcbXRd2vJq/qHnfRMKS5HkMerSIYwKN2e8k9H8ubfkwBvStH51CVf4wwg==",
|
"integrity": "sha512-C3WK4iwIzW5vC5BoY3VPdKjm16P6ca/LGKFnxg6PvUuboxPlqs7LHQCYvEsdAxBkoY+8kRXd8Q3+3oU+HHUceA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"deepmerge": "^4.2.2",
|
"deepmerge": "^4.2.2",
|
||||||
@@ -11833,9 +11836,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/memfs": {
|
"node_modules/memfs": {
|
||||||
"version": "4.17.1",
|
"version": "4.17.2",
|
||||||
"resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.2.tgz",
|
||||||
"integrity": "sha512-thuTRd7F4m4dReCIy7vv4eNYnU6XI/tHMLSMMHLiortw/Y0QxqKtinG523U2aerzwYWGi606oBP4oMPy4+edag==",
|
"integrity": "sha512-NgYhCOWgovOXSzvYgUW0LQ7Qy72rWQMGGFJDoWg4G30RHd3z77VbYdtJ4fembJXBy8pMIUA31XNAupobOQlwdg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -13569,9 +13572,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/query-string": {
|
"node_modules/query-string": {
|
||||||
"version": "9.1.2",
|
"version": "9.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/query-string/-/query-string-9.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/query-string/-/query-string-9.2.0.tgz",
|
||||||
"integrity": "sha512-s3UlTyjxRux4KjwWaJsjh1Mp8zoCkSGKirbD9H89pEM9UOZsfpRZpdfzvsy2/mGlLfC3NnYVpy2gk7jXITHEtA==",
|
"integrity": "sha512-YIRhrHujoQxhexwRLxfy3VSjOXmvZRd2nyw1PwL1UUqZ/ys1dEZd1+NSgXkne2l/4X/7OXkigEAuhTX0g/ivJQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"decode-uri-component": "^0.4.1",
|
"decode-uri-component": "^0.4.1",
|
||||||
@@ -14024,9 +14027,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rc-select": {
|
"node_modules/rc-select": {
|
||||||
"version": "14.16.6",
|
"version": "14.16.8",
|
||||||
"resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.6.tgz",
|
"resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.8.tgz",
|
||||||
"integrity": "sha512-YPMtRPqfZWOm2XGTbx5/YVr1HT0vn//8QS77At0Gjb3Lv+Lbut0IORJPKLWu1hQ3u4GsA0SrDzs7nI8JG7Zmyg==",
|
"integrity": "sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.10.1",
|
"@babel/runtime": "^7.10.1",
|
||||||
@@ -14097,9 +14100,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rc-table": {
|
"node_modules/rc-table": {
|
||||||
"version": "7.50.4",
|
"version": "7.50.5",
|
||||||
"resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.50.4.tgz",
|
"resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.50.5.tgz",
|
||||||
"integrity": "sha512-Y+YuncnQqoS5e7yHvfvlv8BmCvwDYDX/2VixTBEhkMDk9itS9aBINp4nhzXFKiBP/frG4w0pS9d9Rgisl0T1Bw==",
|
"integrity": "sha512-FDZu8aolhSYd3v9KOc3lZOVAU77wmRRu44R0Wfb8Oj1dXRUsloFaXMSl6f7yuWZUxArJTli7k8TEOX2mvhDl4A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.10.1",
|
"@babel/runtime": "^7.10.1",
|
||||||
@@ -15375,9 +15378,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/sass": {
|
"node_modules/sass": {
|
||||||
"version": "1.87.0",
|
"version": "1.89.0",
|
||||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.87.0.tgz",
|
"resolved": "https://registry.npmjs.org/sass/-/sass-1.89.0.tgz",
|
||||||
"integrity": "sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw==",
|
"integrity": "sha512-ld+kQU8YTdGNjOLfRWBzewJpU5cwEv/h5yyqlSeJcj6Yh8U4TDA9UA5FPicqDz/xgRPWRSYIQNiFks21TbA9KQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^4.0.0",
|
"chokidar": "^4.0.0",
|
||||||
@@ -17533,9 +17536,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite-node": {
|
"node_modules/vite-node": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.4.tgz",
|
||||||
"integrity": "sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==",
|
"integrity": "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -17731,19 +17734,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vitest": {
|
"node_modules/vitest": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.4.tgz",
|
||||||
"integrity": "sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==",
|
"integrity": "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/expect": "3.1.3",
|
"@vitest/expect": "3.1.4",
|
||||||
"@vitest/mocker": "3.1.3",
|
"@vitest/mocker": "3.1.4",
|
||||||
"@vitest/pretty-format": "^3.1.3",
|
"@vitest/pretty-format": "^3.1.4",
|
||||||
"@vitest/runner": "3.1.3",
|
"@vitest/runner": "3.1.4",
|
||||||
"@vitest/snapshot": "3.1.3",
|
"@vitest/snapshot": "3.1.4",
|
||||||
"@vitest/spy": "3.1.3",
|
"@vitest/spy": "3.1.4",
|
||||||
"@vitest/utils": "3.1.3",
|
"@vitest/utils": "3.1.4",
|
||||||
"chai": "^5.2.0",
|
"chai": "^5.2.0",
|
||||||
"debug": "^4.4.0",
|
"debug": "^4.4.0",
|
||||||
"expect-type": "^1.2.1",
|
"expect-type": "^1.2.1",
|
||||||
@@ -17756,7 +17759,7 @@
|
|||||||
"tinypool": "^1.0.2",
|
"tinypool": "^1.0.2",
|
||||||
"tinyrainbow": "^2.0.0",
|
"tinyrainbow": "^2.0.0",
|
||||||
"vite": "^5.0.0 || ^6.0.0",
|
"vite": "^5.0.0 || ^6.0.0",
|
||||||
"vite-node": "3.1.3",
|
"vite-node": "3.1.4",
|
||||||
"why-is-node-running": "^2.3.0"
|
"why-is-node-running": "^2.3.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -17772,8 +17775,8 @@
|
|||||||
"@edge-runtime/vm": "*",
|
"@edge-runtime/vm": "*",
|
||||||
"@types/debug": "^4.1.12",
|
"@types/debug": "^4.1.12",
|
||||||
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||||
"@vitest/browser": "3.1.3",
|
"@vitest/browser": "3.1.4",
|
||||||
"@vitest/ui": "3.1.3",
|
"@vitest/ui": "3.1.4",
|
||||||
"happy-dom": "*",
|
"happy-dom": "*",
|
||||||
"jsdom": "*"
|
"jsdom": "*"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,21 +12,21 @@
|
|||||||
"@apollo/client": "^3.13.6",
|
"@apollo/client": "^3.13.6",
|
||||||
"@emotion/is-prop-valid": "^1.3.1",
|
"@emotion/is-prop-valid": "^1.3.1",
|
||||||
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
||||||
"@firebase/analytics": "^0.10.13",
|
"@firebase/analytics": "^0.10.16",
|
||||||
"@firebase/app": "^0.12.1",
|
"@firebase/app": "^0.13.0",
|
||||||
"@firebase/auth": "^1.10.2",
|
"@firebase/auth": "^1.10.5",
|
||||||
"@firebase/firestore": "^4.7.12",
|
"@firebase/firestore": "^4.7.15",
|
||||||
"@firebase/messaging": "^0.12.18",
|
"@firebase/messaging": "^0.12.21",
|
||||||
"@jsreport/browser-client": "^3.1.0",
|
"@jsreport/browser-client": "^3.1.0",
|
||||||
"@reduxjs/toolkit": "^2.8.1",
|
"@reduxjs/toolkit": "^2.8.2",
|
||||||
"@sentry/cli": "^2.45.0",
|
"@sentry/cli": "^2.45.0",
|
||||||
"@sentry/react": "^9.17.0",
|
"@sentry/react": "^9.22.0",
|
||||||
"@sentry/vite-plugin": "^3.4.0",
|
"@sentry/vite-plugin": "^3.5.0",
|
||||||
"@splitsoftware/splitio-react": "^2.1.1",
|
"@splitsoftware/splitio-react": "^2.1.1",
|
||||||
"@tanem/react-nprogress": "^5.0.53",
|
"@tanem/react-nprogress": "^5.0.53",
|
||||||
"antd": "^5.25.0",
|
"antd": "^5.25.2",
|
||||||
"apollo-link-logger": "^2.0.1",
|
"apollo-link-logger": "^2.0.1",
|
||||||
"apollo-link-sentry": "^4.2.0",
|
"apollo-link-sentry": "^4.3.0",
|
||||||
"autosize": "^6.0.1",
|
"autosize": "^6.0.1",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
"normalize-url": "^8.0.1",
|
"normalize-url": "^8.0.1",
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"query-string": "^9.1.2",
|
"query-string": "^9.2.0",
|
||||||
"raf-schd": "^4.0.3",
|
"raf-schd": "^4.0.3",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-big-calendar": "^1.18.0",
|
"react-big-calendar": "^1.18.0",
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
"redux-saga": "^1.3.0",
|
"redux-saga": "^1.3.0",
|
||||||
"redux-state-sync": "^3.1.4",
|
"redux-state-sync": "^3.1.4",
|
||||||
"reselect": "^5.1.1",
|
"reselect": "^5.1.1",
|
||||||
"sass": "^1.86.3",
|
"sass": "^1.89.0",
|
||||||
"socket.io-client": "^4.8.1",
|
"socket.io-client": "^4.8.1",
|
||||||
"styled-components": "^6.1.18",
|
"styled-components": "^6.1.18",
|
||||||
"subscriptions-transport-ws": "^0.11.0",
|
"subscriptions-transport-ws": "^0.11.0",
|
||||||
@@ -130,12 +130,12 @@
|
|||||||
"@ant-design/icons": "^6.0.0",
|
"@ant-design/icons": "^6.0.0",
|
||||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"@babel/preset-react": "^7.27.1",
|
"@babel/preset-react": "^7.27.1",
|
||||||
"@dotenvx/dotenvx": "^1.43.0",
|
"@dotenvx/dotenvx": "^1.44.1",
|
||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@eslint/js": "^9.26.0",
|
"@eslint/js": "^9.27.0",
|
||||||
"@playwright/test": "^1.51.1",
|
"@playwright/test": "^1.51.1",
|
||||||
"@sentry/webpack-plugin": "^3.4.0",
|
"@sentry/webpack-plugin": "^3.5.0",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
@@ -148,7 +148,7 @@
|
|||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"jsdom": "^26.0.0",
|
"jsdom": "^26.0.0",
|
||||||
"memfs": "^4.17.1",
|
"memfs": "^4.17.2",
|
||||||
"os-browserify": "^0.3.0",
|
"os-browserify": "^0.3.0",
|
||||||
"playwright": "^1.51.1",
|
"playwright": "^1.51.1",
|
||||||
"react-error-overlay": "^6.1.0",
|
"react-error-overlay": "^6.1.0",
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
"vite-plugin-node-polyfills": "^0.23.0",
|
"vite-plugin-node-polyfills": "^0.23.0",
|
||||||
"vite-plugin-pwa": "^1.0.0",
|
"vite-plugin-pwa": "^1.0.0",
|
||||||
"vite-plugin-style-import": "^2.0.0",
|
"vite-plugin-style-import": "^2.0.0",
|
||||||
"vitest": "^3.1.3",
|
"vitest": "^3.1.4",
|
||||||
"workbox-window": "^7.3.0"
|
"workbox-window": "^7.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,21 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setPartsOrderContext: (context) => dispatch(setModalContext({ context: context, modal: "partsOrder" })),
|
setPartsOrderContext: (context) =>
|
||||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
dispatch(
|
||||||
|
setModalContext({
|
||||||
|
context: context,
|
||||||
|
modal: "partsOrder"
|
||||||
|
})
|
||||||
|
),
|
||||||
|
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||||
|
dispatch(
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid,
|
||||||
|
operation,
|
||||||
|
type
|
||||||
|
})
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditReturn);
|
export default connect(mapStateToProps, mapDispatchToProps)(BillDetailEditReturn);
|
||||||
@@ -69,7 +82,7 @@ export function BillDetailEditReturn({ setPartsOrderContext, insertAuditTrail, b
|
|||||||
<Modal
|
<Modal
|
||||||
open={open}
|
open={open}
|
||||||
onCancel={() => setOpen(false)}
|
onCancel={() => setOpen(false)}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
title={t("bills.actions.return")}
|
title={t("bills.actions.return")}
|
||||||
onOk={() => form.submit()}
|
onOk={() => form.submit()}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default function BillDetailEditcontainer() {
|
|||||||
delete search.billid;
|
delete search.billid;
|
||||||
history({ search: queryString.stringify(search) });
|
history({ search: queryString.stringify(search) });
|
||||||
}}
|
}}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
open={search.billid}
|
open={search.billid}
|
||||||
>
|
>
|
||||||
<BillDetailEditComponent />
|
<BillDetailEditComponent />
|
||||||
|
|||||||
@@ -412,7 +412,7 @@ function BillEnterModalContainer({ billEnterModal, toggleModalVisible, bodyshop,
|
|||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
onFinish={handleFinish}
|
onFinish={handleFinish}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export function ContractsFindModalContainer({ caBcEtfTableModal, toggleModalVisi
|
|||||||
title={t("payments.labels.findermodal")}
|
title={t("payments.labels.findermodal")}
|
||||||
onCancel={() => toggleModalVisible()}
|
onCancel={() => toggleModalVisible()}
|
||||||
onOk={() => toggleModalVisible()}
|
onOk={() => toggleModalVisible()}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
forceRender
|
forceRender
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish}>
|
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish}>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ function CardPaymentModalContainer({ cardPaymentModal, toggleModalVisible, bodys
|
|||||||
</Button>
|
</Button>
|
||||||
]}
|
]}
|
||||||
width="80%"
|
width="80%"
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<CardPaymentModalComponent />
|
<CardPaymentModalComponent />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -34,16 +34,14 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
|||||||
|
|
||||||
SubscribeToTopicForFCMNotification();
|
SubscribeToTopicForFCMNotification();
|
||||||
|
|
||||||
//Register WS handlers
|
// Register WebSocket handlers
|
||||||
if (socket && socket.connected) {
|
if (socket && socket.connected) {
|
||||||
registerMessagingHandlers({ socket, client });
|
registerMessagingHandlers({ socket, client });
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (socket && socket.connected) {
|
|
||||||
unregisterMessagingHandlers({ socket });
|
unregisterMessagingHandlers({ socket });
|
||||||
}
|
};
|
||||||
};
|
}
|
||||||
}, [bodyshop, socket, t, client]);
|
}, [bodyshop, socket, t, client]);
|
||||||
|
|
||||||
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Badge, Card, List, Space, Tag } from "antd";
|
import { Badge, Card, List, Space, Tag } from "antd";
|
||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { Virtuoso } from "react-virtuoso";
|
import { Virtuoso } from "react-virtuoso";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
@@ -10,35 +10,60 @@ import PhoneFormatter from "../../utils/PhoneFormatter";
|
|||||||
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import "./chat-conversation-list.styles.scss";
|
import "./chat-conversation-list.styles.scss";
|
||||||
|
import { useQuery } from "@apollo/client";
|
||||||
|
import { GET_PHONE_NUMBER_OPT_OUTS } from "../../graphql/phone-number-opt-out.queries.js";
|
||||||
|
import { phone } from "phone";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
selectedConversation: selectSelectedConversation
|
selectedConversation: selectSelectedConversation,
|
||||||
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setSelectedConversation: (conversationId) => dispatch(setSelectedConversation(conversationId))
|
setSelectedConversation: (conversationId) => dispatch(setSelectedConversation(conversationId))
|
||||||
});
|
});
|
||||||
|
|
||||||
function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation }) {
|
function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation, bodyshop }) {
|
||||||
// That comma is there for a reason, do not remove it
|
const { t } = useTranslation();
|
||||||
const [, forceUpdate] = useState(false);
|
const [, forceUpdate] = useState(false);
|
||||||
|
|
||||||
// Re-render every minute
|
const phoneNumbers = conversationList.map((item) => phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, ""));
|
||||||
|
|
||||||
|
const { data: optOutData } = useQuery(GET_PHONE_NUMBER_OPT_OUTS, {
|
||||||
|
variables: {
|
||||||
|
bodyshopid: bodyshop.id,
|
||||||
|
phone_numbers: phoneNumbers
|
||||||
|
},
|
||||||
|
skip: !conversationList.length,
|
||||||
|
fetchPolicy: "cache-and-network"
|
||||||
|
});
|
||||||
|
|
||||||
|
const optOutMap = useMemo(() => {
|
||||||
|
const map = new Map();
|
||||||
|
optOutData?.phone_number_opt_out?.forEach((optOut) => {
|
||||||
|
map.set(optOut.phone_number, true);
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}, [optOutData?.phone_number_opt_out]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
forceUpdate((prev) => !prev); // Toggle state to trigger re-render
|
forceUpdate((prev) => !prev);
|
||||||
}, 60000); // 1 minute in milliseconds
|
}, 60000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
return () => clearInterval(interval); // Cleanup on unmount
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Memoize the sorted conversation list
|
const sortedConversationList = useMemo(() => {
|
||||||
const sortedConversationList = React.useMemo(() => {
|
|
||||||
return _.orderBy(conversationList, ["updated_at"], ["desc"]);
|
return _.orderBy(conversationList, ["updated_at"], ["desc"]);
|
||||||
}, [conversationList]);
|
}, [conversationList]);
|
||||||
|
|
||||||
const renderConversation = (index) => {
|
const renderConversation = (index, t) => {
|
||||||
const item = sortedConversationList[index];
|
const item = sortedConversationList[index];
|
||||||
|
const normalizedPhone = phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, "");
|
||||||
|
const hasOptOutEntry = optOutMap.has(normalizedPhone);
|
||||||
|
|
||||||
const cardContentRight = <TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
|
const cardContentRight = <TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
|
||||||
const cardContentLeft =
|
const cardContentLeft =
|
||||||
item.job_conversations.length > 0
|
item.job_conversations.length > 0
|
||||||
@@ -60,7 +85,12 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const cardExtra = <Badge count={item.messages_aggregate.aggregate.count} />;
|
const cardExtra = (
|
||||||
|
<>
|
||||||
|
<Badge count={item.messages_aggregate.aggregate.count} />
|
||||||
|
{hasOptOutEntry && <Tag color="red">{t("messaging.labels.no_consent")}</Tag>}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
const getCardStyle = () =>
|
const getCardStyle = () =>
|
||||||
item.id === selectedConversation
|
item.id === selectedConversation
|
||||||
@@ -73,9 +103,25 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
|
|||||||
onClick={() => setSelectedConversation(item.id)}
|
onClick={() => setSelectedConversation(item.id)}
|
||||||
className={`chat-list-item ${item.id === selectedConversation ? "chat-list-selected-conversation" : ""}`}
|
className={`chat-list-item ${item.id === selectedConversation ? "chat-list-selected-conversation" : ""}`}
|
||||||
>
|
>
|
||||||
<Card style={getCardStyle()} bordered={false} size="small" extra={cardExtra} title={cardTitle}>
|
<Card style={getCardStyle()} variant={true} size="small" extra={cardExtra} title={cardTitle}>
|
||||||
<div style={{ display: "inline-block", width: "70%", textAlign: "left" }}>{cardContentLeft}</div>
|
<div
|
||||||
<div style={{ display: "inline-block", width: "30%", textAlign: "right" }}>{cardContentRight}</div>
|
style={{
|
||||||
|
display: "inline-block",
|
||||||
|
width: "70%",
|
||||||
|
textAlign: "left"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cardContentLeft}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "inline-block",
|
||||||
|
width: "30%",
|
||||||
|
textAlign: "right"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cardContentRight}
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
);
|
);
|
||||||
@@ -85,7 +131,7 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
|
|||||||
<div className="chat-list-container">
|
<div className="chat-list-container">
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
data={sortedConversationList}
|
data={sortedConversationList}
|
||||||
itemContent={(index) => renderConversation(index)}
|
itemContent={(index) => renderConversation(index, t)}
|
||||||
style={{ height: "100%", width: "100%" }}
|
style={{ height: "100%", width: "100%" }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
/* Add spacing and better alignment for items */
|
/* Add spacing and better alignment for items */
|
||||||
.chat-list-item {
|
.chat-list-item {
|
||||||
padding: 0.5rem 0; /* Add spacing between list items */
|
padding: 0.2rem 0; /* Add spacing between list items */
|
||||||
|
|
||||||
.ant-card {
|
.ant-card {
|
||||||
border-radius: 8px; /* Slight rounding for card edges */
|
border-radius: 8px; /* Slight rounding for card edges */
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
|||||||
fetchPolicy: "network-only",
|
fetchPolicy: "network-only",
|
||||||
nextFetchPolicy: "network-only",
|
nextFetchPolicy: "network-only",
|
||||||
variables: {
|
variables: {
|
||||||
jobId: conversation.job_conversations[0] && conversation.job_conversations[0].jobid
|
jobId: conversation.job_conversations[0] && conversation.job_conversations[0]?.jobid
|
||||||
},
|
},
|
||||||
|
|
||||||
skip: !open || !conversation.job_conversations || conversation.job_conversations.length === 0
|
skip: !open || !conversation.job_conversations || conversation.job_conversations.length === 0
|
||||||
@@ -67,14 +67,14 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
|||||||
<>
|
<>
|
||||||
{!bodyshop.uselocalmediaserver && (
|
{!bodyshop.uselocalmediaserver && (
|
||||||
<JobsDocumentImgproxyGalleryExternal
|
<JobsDocumentImgproxyGalleryExternal
|
||||||
jobId={conversation.job_conversations[0].jobid}
|
jobId={conversation.job_conversations[0]?.jobid}
|
||||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{bodyshop.uselocalmediaserver && open && (
|
{bodyshop.uselocalmediaserver && open && (
|
||||||
<JobDocumentsLocalGalleryExternal
|
<JobDocumentsLocalGalleryExternal
|
||||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
jobId={conversation.job_conversations[0] && conversation.job_conversations[0].jobid}
|
jobId={conversation.job_conversations[0] && conversation.job_conversations[0]?.jobid}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@@ -89,7 +89,7 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
|||||||
{bodyshop.uselocalmediaserver && open && (
|
{bodyshop.uselocalmediaserver && open && (
|
||||||
<JobDocumentsLocalGalleryExternal
|
<JobDocumentsLocalGalleryExternal
|
||||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||||
jobId={conversation.job_conversations[0] && conversation.job_conversations[0].jobid}
|
jobId={conversation.job_conversations[0] && conversation.job_conversations[0]?.jobid}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { LoadingOutlined, SendOutlined } from "@ant-design/icons";
|
import { LoadingOutlined, SendOutlined } from "@ant-design/icons";
|
||||||
import { Input, Spin } from "antd";
|
import { Alert, Input, Spin } from "antd";
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
@@ -10,6 +10,9 @@ import { selectIsSending, selectMessage } from "../../redux/messaging/messaging.
|
|||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import ChatMediaSelector from "../chat-media-selector/chat-media-selector.component";
|
import ChatMediaSelector from "../chat-media-selector/chat-media-selector.component";
|
||||||
import ChatPresetsComponent from "../chat-presets/chat-presets.component";
|
import ChatPresetsComponent from "../chat-presets/chat-presets.component";
|
||||||
|
import { useQuery } from "@apollo/client";
|
||||||
|
import { phone } from "phone";
|
||||||
|
import { GET_PHONE_NUMBER_OPT_OUT } from "../../graphql/phone-number-opt-out.queries";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -25,16 +28,24 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSending, message, setMessage }) {
|
function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSending, message, setMessage }) {
|
||||||
const inputArea = useRef(null);
|
const inputArea = useRef(null);
|
||||||
const [selectedMedia, setSelectedMedia] = useState([]);
|
const [selectedMedia, setSelectedMedia] = useState([]);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const normalizedPhone = phone(conversation.phone_num, "CA").phoneNumber.replace(/^\+1/, "");
|
||||||
|
const { data: optOutData } = useQuery(GET_PHONE_NUMBER_OPT_OUT, {
|
||||||
|
variables: { bodyshopid: bodyshop.id, phone_number: normalizedPhone },
|
||||||
|
fetchPolicy: "cache-and-network"
|
||||||
|
});
|
||||||
|
|
||||||
|
const isOptedOut = !!optOutData?.phone_number_opt_out?.[0];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
inputArea.current.focus();
|
inputArea.current.focus();
|
||||||
}, [isSending, setMessage]);
|
}, [isSending, setMessage]);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const handleEnter = () => {
|
const handleEnter = () => {
|
||||||
const selectedImages = selectedMedia.filter((i) => i.isSelected);
|
const selectedImages = selectedMedia.filter((i) => i.isSelected);
|
||||||
if ((message === "" || !message) && selectedImages.length === 0) return;
|
if ((message === "" || !message) && selectedImages.length === 0) return;
|
||||||
|
if (isOptedOut) return; // Prevent sending if phone number is opted out
|
||||||
logImEXEvent("messaging_send_message");
|
logImEXEvent("messaging_send_message");
|
||||||
|
|
||||||
if (selectedImages.length < 11) {
|
if (selectedImages.length < 11) {
|
||||||
@@ -44,7 +55,8 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
|
|||||||
messagingServiceSid: bodyshop.messagingservicesid,
|
messagingServiceSid: bodyshop.messagingservicesid,
|
||||||
conversationid: conversation.id,
|
conversationid: conversation.id,
|
||||||
selectedMedia: selectedImages,
|
selectedMedia: selectedImages,
|
||||||
imexshopid: bodyshop.imexshopid
|
imexshopid: bodyshop.imexshopid,
|
||||||
|
bodyshopid: bodyshop.id
|
||||||
};
|
};
|
||||||
sendMessage(newMessage);
|
sendMessage(newMessage);
|
||||||
setSelectedMedia(
|
setSelectedMedia(
|
||||||
@@ -57,6 +69,7 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="imex-flex-row" style={{ width: "100%" }}>
|
<div className="imex-flex-row" style={{ width: "100%" }}>
|
||||||
|
{isOptedOut && <Alert message={t("messaging.errors.no_consent")} type="warning" style={{ marginBottom: 8 }} />}
|
||||||
<ChatPresetsComponent className="imex-flex-row__margin" />
|
<ChatPresetsComponent className="imex-flex-row__margin" />
|
||||||
<ChatMediaSelector
|
<ChatMediaSelector
|
||||||
conversation={conversation}
|
conversation={conversation}
|
||||||
@@ -71,18 +84,18 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
|
|||||||
ref={inputArea}
|
ref={inputArea}
|
||||||
autoSize={{ minRows: 1, maxRows: 4 }}
|
autoSize={{ minRows: 1, maxRows: 4 }}
|
||||||
value={message}
|
value={message}
|
||||||
disabled={isSending}
|
disabled={isSending || isOptedOut}
|
||||||
placeholder={t("messaging.labels.typeamessage")}
|
placeholder={t("messaging.labels.typeamessage")}
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
onPressEnter={(event) => {
|
onPressEnter={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!!!event.shiftKey) handleEnter();
|
if (!event.shiftKey && !isOptedOut) handleEnter();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<SendOutlined
|
<SendOutlined
|
||||||
className="chat-send-message-button"
|
className="chat-send-message-button"
|
||||||
// disabled={message === "" || !message}
|
disabled={isOptedOut || message === "" || !message}
|
||||||
onClick={handleEnter}
|
onClick={handleEnter}
|
||||||
/>
|
/>
|
||||||
<Spin
|
<Spin
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export function ContractsFindModalContainer({
|
|||||||
title={t("contracts.labels.findermodal")}
|
title={t("contracts.labels.findermodal")}
|
||||||
onCancel={() => toggleModalVisible()}
|
onCancel={() => toggleModalVisible()}
|
||||||
onOk={() => toggleModalVisible()}
|
onOk={() => toggleModalVisible()}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
forceRender
|
forceRender
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish}>
|
<Form form={form} layout="vertical" autoComplete="no" onFinish={handleFinish}>
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ export function EmailOverlayContainer({ emailConfig, modalVisible, toggleEmailOv
|
|||||||
}, [modalVisible]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [modalVisible]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
destroyOnClose={true}
|
destroyOnHidden
|
||||||
open={modalVisible}
|
open={modalVisible}
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
width={"80%"}
|
width={"80%"}
|
||||||
|
|||||||
@@ -81,8 +81,9 @@ export function HasFeatureAccess({ featureName, bodyshop, bypass, debug = false
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
bodyshop?.features?.allAccess ||
|
bodyshop?.features?.allAccess ||
|
||||||
bodyshop?.features?.[featureName] ||
|
(typeof bodyshop?.features?.[featureName] === "boolean"
|
||||||
dayjs(bodyshop?.features[featureName]).isAfter(dayjs())
|
? bodyshop?.features?.[featureName]
|
||||||
|
: dayjs(bodyshop?.features?.[featureName]).isAfter(dayjs()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
|||||||
import LockWrapper from "../lock-wrapper/lock-wrapper.component";
|
import LockWrapper from "../lock-wrapper/lock-wrapper.component";
|
||||||
import NotificationCenterContainer from "../notification-center/notification-center.container.jsx";
|
import NotificationCenterContainer from "../notification-center/notification-center.container.jsx";
|
||||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
||||||
|
|
||||||
// Redux mappings
|
// Redux mappings
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
@@ -98,6 +99,7 @@ function Header({
|
|||||||
const baseTitleRef = useRef(document.title || "");
|
const baseTitleRef = useRef(document.title || "");
|
||||||
const lastSetTitleRef = useRef("");
|
const lastSetTitleRef = useRef("");
|
||||||
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
||||||
|
const isEmployee = useIsEmployee(bodyshop, currentUser);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: unreadData,
|
data: unreadData,
|
||||||
@@ -682,7 +684,7 @@ function Header({
|
|||||||
icon: unreadLoading ? (
|
icon: unreadLoading ? (
|
||||||
<Spin size="small" />
|
<Spin size="small" />
|
||||||
) : (
|
) : (
|
||||||
<Badge offset={[8, 0]} size="small" count={unreadCount}>
|
<Badge offset={[8, 0]} size="small" count={isEmployee ? unreadCount : 0}>
|
||||||
<BellFilled />
|
<BellFilled />
|
||||||
</Badge>
|
</Badge>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export function InventoryUpsertModalContainer({ currentUser, bodyshop, inventory
|
|||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
}}
|
}}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<Form form={form} onFinish={handleFinish} layout="vertical">
|
<Form form={form} onFinish={handleFinish} layout="vertical">
|
||||||
<InventoryUpsertModal form={form} />
|
<InventoryUpsertModal form={form} />
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export function ScheduleEventComponent({
|
|||||||
const [popOverVisible, setPopOverVisible] = useState(false);
|
const [popOverVisible, setPopOverVisible] = useState(false);
|
||||||
|
|
||||||
const [getJobDetails] = useLazyQuery(GET_JOB_BY_PK_QUICK_INTAKE, {
|
const [getJobDetails] = useLazyQuery(GET_JOB_BY_PK_QUICK_INTAKE, {
|
||||||
variables: { id: event.job.id },
|
variables: { id: event.job?.id },
|
||||||
onCompleted: (data) => {
|
onCompleted: (data) => {
|
||||||
if (data?.jobs_by_pk) {
|
if (data?.jobs_by_pk) {
|
||||||
const totalHours =
|
const totalHours =
|
||||||
@@ -83,6 +83,7 @@ export function ScheduleEventComponent({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
fetchPolicy: "network-only"
|
fetchPolicy: "network-only"
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -409,8 +410,10 @@ export function ScheduleEventComponent({
|
|||||||
open={popOverVisible}
|
open={popOverVisible}
|
||||||
onOpenChange={setPopOverVisible}
|
onOpenChange={setPopOverVisible}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
getJobDetails();
|
if (event.job?.id) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
getJobDetails();
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
getPopupContainer={(trigger) => trigger.parentNode}
|
getPopupContainer={(trigger) => trigger.parentNode}
|
||||||
trigger="click"
|
trigger="click"
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export function JobCostingModalContainer({ jobCostingModal, toggleModalVisible }
|
|||||||
}}
|
}}
|
||||||
cancelButtonProps={{ style: { display: "none" } }}
|
cancelButtonProps={{ style: { display: "none" } }}
|
||||||
width="90%"
|
width="90%"
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
{!costingData ? (
|
{!costingData ? (
|
||||||
<LoadingSpinner loading={true} />
|
<LoadingSpinner loading={true} />
|
||||||
|
|||||||
@@ -32,7 +32,13 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
setPrintCenterContext: (context) => dispatch(setModalContext({ context: context, modal: "printCenter" })),
|
setPrintCenterContext: (context) =>
|
||||||
|
dispatch(
|
||||||
|
setModalContext({
|
||||||
|
context: context,
|
||||||
|
modal: "printCenter"
|
||||||
|
})
|
||||||
|
),
|
||||||
insertAuditTrail: ({ jobid, operation, type }) =>
|
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||||
dispatch(
|
dispatch(
|
||||||
insertAuditTrail({
|
insertAuditTrail({
|
||||||
@@ -87,7 +93,7 @@ export function JobDetailCards({ bodyshop, setPrintCenterContext, insertAuditTra
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer open={!!selected} destroyOnClose width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
|
<Drawer open={!!selected} destroyOnHidden width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
|
||||||
{loading ? <LoadingSpinner /> : null}
|
{loading ? <LoadingSpinner /> : null}
|
||||||
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
||||||
{data ? (
|
{data ? (
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ function JobReconciliationModalContainer({ reconciliationModal, toggleModalVisib
|
|||||||
onOk={handleCancel}
|
onOk={handleCancel}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
cancelButtonProps={{ display: "none" }}
|
cancelButtonProps={{ display: "none" }}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
className="imex-reconciliation-modal"
|
className="imex-reconciliation-modal"
|
||||||
>
|
>
|
||||||
{loading && <LoadingSpinner loading={loading} />}
|
{loading && <LoadingSpinner loading={loading} />}
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ export default function JobWatcherToggleComponent({
|
|||||||
handleToggleSelf,
|
handleToggleSelf,
|
||||||
handleRemoveWatcher,
|
handleRemoveWatcher,
|
||||||
handleWatcherSelect,
|
handleWatcherSelect,
|
||||||
handleTeamSelect
|
handleTeamSelect,
|
||||||
|
isEmployee
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -66,22 +67,32 @@ export default function JobWatcherToggleComponent({
|
|||||||
<List>
|
<List>
|
||||||
<List.Item
|
<List.Item
|
||||||
actions={[
|
actions={[
|
||||||
<Button
|
<Tooltip title={!isEmployee ? t("notifications.tooltips.not-employee") : ""} placement="top">
|
||||||
type={isWatching ? "primary" : "default"}
|
<span>
|
||||||
danger={!isWatching}
|
<Button
|
||||||
icon={isWatching ? <EyeOutlined /> : <EyeFilled />}
|
type={isWatching ? "primary" : "default"}
|
||||||
size="medium"
|
danger={!isWatching}
|
||||||
onClick={handleToggleSelf}
|
icon={isWatching ? <EyeOutlined /> : <EyeFilled />}
|
||||||
loading={adding || removing}
|
size="medium"
|
||||||
>
|
onClick={handleToggleSelf}
|
||||||
{isWatching ? t("notifications.labels.unwatch") : t("notifications.labels.watch")}
|
loading={adding || removing}
|
||||||
</Button>
|
disabled={!isEmployee || adding || removing}
|
||||||
|
>
|
||||||
|
{isWatching ? t("notifications.labels.unwatch") : t("notifications.labels.watch")}
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<List.Item.Meta>
|
<List.Item.Meta>
|
||||||
<Text type="secondary" style={{ marginBottom: 8, display: "block" }}>
|
<Text type="secondary" style={{ marginBottom: 8, display: "block" }}>
|
||||||
{t("notifications.labels.watching-issue")}
|
{t("notifications.labels.watching-issue")}
|
||||||
</Text>
|
</Text>
|
||||||
|
{!isEmployee && (
|
||||||
|
<Text type="danger" style={{ marginBottom: 8, display: "block" }}>
|
||||||
|
{t("notifications.tooltips.not-employee")}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</List.Item.Meta>
|
</List.Item.Meta>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
</List>
|
</List>
|
||||||
@@ -98,8 +109,11 @@ export default function JobWatcherToggleComponent({
|
|||||||
<EmployeeSearchSelectComponent
|
<EmployeeSearchSelectComponent
|
||||||
style={{ minWidth: "100%" }}
|
style={{ minWidth: "100%" }}
|
||||||
options={
|
options={
|
||||||
bodyshop?.employees?.filter((e) =>
|
bodyshop?.employees?.filter(
|
||||||
jobWatchers.every((w) => w.user_email !== e.user_email && e.active && e.user_email)
|
(e) =>
|
||||||
|
e.user_email && // Ensure user_email is not null or undefined
|
||||||
|
e.active && // Ensure employee is active
|
||||||
|
jobWatchers.every((w) => w.user_email !== e.user_email) // Ensure not already a watcher
|
||||||
) || []
|
) || []
|
||||||
}
|
}
|
||||||
placeholder={t("notifications.labels.employee-search")}
|
placeholder={t("notifications.labels.employee-search")}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { createStructuredSelector } from "reselect";
|
|||||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
|
||||||
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
import { useSplitTreatments } from "@splitsoftware/splitio-react";
|
||||||
import JobWatcherToggleComponent from "./job-watcher-toggle.component.jsx";
|
import JobWatcherToggleComponent from "./job-watcher-toggle.component.jsx";
|
||||||
|
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -21,13 +22,14 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
|
|||||||
splitKey: bodyshop && bodyshop.imexshopid
|
splitKey: bodyshop && bodyshop.imexshopid
|
||||||
});
|
});
|
||||||
|
|
||||||
const userEmail = currentUser.email;
|
const isEmployee = useIsEmployee(bodyshop, currentUser);
|
||||||
const jobid = job.id;
|
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [selectedWatcher, setSelectedWatcher] = useState(null);
|
const [selectedWatcher, setSelectedWatcher] = useState(null);
|
||||||
const [selectedTeam, setSelectedTeam] = useState(null);
|
const [selectedTeam, setSelectedTeam] = useState(null);
|
||||||
|
|
||||||
|
const userEmail = currentUser.email;
|
||||||
|
const jobid = job.id;
|
||||||
|
|
||||||
// Fetch current watchers with refetch capability
|
// Fetch current watchers with refetch capability
|
||||||
const {
|
const {
|
||||||
data: watcherData,
|
data: watcherData,
|
||||||
@@ -139,13 +141,13 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleToggleSelf = useCallback(async () => {
|
const handleToggleSelf = useCallback(async () => {
|
||||||
if (adding || removing) return;
|
if (adding || removing || !isEmployee) return;
|
||||||
if (isWatching) {
|
if (isWatching) {
|
||||||
await removeWatcher({ variables: { jobid, userEmail } });
|
await removeWatcher({ variables: { jobid, userEmail } });
|
||||||
} else {
|
} else {
|
||||||
await addWatcher({ variables: { jobid, userEmail } });
|
await addWatcher({ variables: { jobid, userEmail } });
|
||||||
}
|
}
|
||||||
}, [isWatching, addWatcher, removeWatcher, jobid, userEmail, adding, removing]);
|
}, [isWatching, addWatcher, removeWatcher, jobid, userEmail, adding, removing, isEmployee]);
|
||||||
|
|
||||||
const handleRemoveWatcher = useCallback(
|
const handleRemoveWatcher = useCallback(
|
||||||
async (email) => {
|
async (email) => {
|
||||||
@@ -187,7 +189,16 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
|
|||||||
setSelectedTeam(null);
|
setSelectedTeam(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await Promise.all(newWatchers.map((email) => addWatcher({ variables: { jobid, userEmail: email } })));
|
await Promise.all(
|
||||||
|
newWatchers.map((email) =>
|
||||||
|
addWatcher({
|
||||||
|
variables: {
|
||||||
|
jobid,
|
||||||
|
userEmail: email
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[jobWatchers, addWatcher, jobid, adding]
|
[jobWatchers, addWatcher, jobid, adding]
|
||||||
);
|
);
|
||||||
@@ -212,6 +223,7 @@ function JobWatcherToggleContainer({ job, currentUser, bodyshop }) {
|
|||||||
handleWatcherSelect={handleWatcherSelect}
|
handleWatcherSelect={handleWatcherSelect}
|
||||||
handleTeamSelect={handleTeamSelect}
|
handleTeamSelect={handleTeamSelect}
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
|
isEmployee={isEmployee} // Pass isEmployee to the component
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import { Col, Row } from "antd";
|
|||||||
import Axios from "axios";
|
import Axios from "axios";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import queryString from "query-string";
|
import queryString from "query-string";
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import {
|
import {
|
||||||
DELETE_AVAILABLE_JOB,
|
DELETE_AVAILABLE_JOB,
|
||||||
@@ -33,7 +34,6 @@ import OwnerFindModalContainer from "../owner-find-modal/owner-find-modal.contai
|
|||||||
import { GetSupplementDelta } from "./jobs-available-supplement.estlines.util";
|
import { GetSupplementDelta } from "./jobs-available-supplement.estlines.util";
|
||||||
import HeaderFields from "./jobs-available-supplement.headerfields";
|
import HeaderFields from "./jobs-available-supplement.headerfields";
|
||||||
import JobsAvailableTableComponent from "./jobs-available-table.component";
|
import JobsAvailableTableComponent from "./jobs-available-table.component";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -195,7 +195,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
|
|||||||
|
|
||||||
await deleteJob({
|
await deleteJob({
|
||||||
variables: { id: estData.id }
|
variables: { id: estData.id }
|
||||||
}).then((r) => {
|
}).then(() => {
|
||||||
refetch();
|
refetch();
|
||||||
setInsertLoading(false);
|
setInsertLoading(false);
|
||||||
});
|
});
|
||||||
@@ -315,7 +315,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
|
|||||||
|
|
||||||
deleteJob({
|
deleteJob({
|
||||||
variables: { id: estData.id }
|
variables: { id: estData.id }
|
||||||
}).then((r) => {
|
}).then(() => {
|
||||||
refetch();
|
refetch();
|
||||||
setInsertLoading(false);
|
setInsertLoading(false);
|
||||||
});
|
});
|
||||||
@@ -372,7 +372,7 @@ export function JobsAvailableContainer({ bodyshop, currentUser, insertAuditTrail
|
|||||||
loadEstData({ variables: { id: record.id } });
|
loadEstData({ variables: { id: record.id } });
|
||||||
modalSearchState[1](record.clm_no);
|
modalSearchState[1](record.clm_no);
|
||||||
setJobModalVisible(true);
|
setJobModalVisible(true);
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -456,7 +456,7 @@ function replaceEmpty(someObj, replaceValue = null) {
|
|||||||
return JSON.parse(temp);
|
return JSON.parse(temp);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function CheckTaxRatesUSA(estData, bodyshop) {
|
async function CheckTaxRatesUSA(estData) {
|
||||||
if (!estData.parts_tax_rates?.PAM) {
|
if (!estData.parts_tax_rates?.PAM) {
|
||||||
estData.parts_tax_rates.PAM = estData.parts_tax_rates.PAC;
|
estData.parts_tax_rates.PAM = estData.parts_tax_rates.PAC;
|
||||||
}
|
}
|
||||||
@@ -568,7 +568,7 @@ async function CheckTaxRates(estData, bodyshop) {
|
|||||||
});
|
});
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
function ResolveCCCLineIssues(estData, bodyshop) {
|
function ResolveCCCLineIssues(estData) {
|
||||||
//Find all misc amounts, populate them to the act price.
|
//Find all misc amounts, populate them to the act price.
|
||||||
//This needs to be done before cleansing unq_seq since some misc prices could move over.
|
//This needs to be done before cleansing unq_seq since some misc prices could move over.
|
||||||
estData.joblines.data.forEach((line) => {
|
estData.joblines.data.forEach((line) => {
|
||||||
@@ -585,6 +585,9 @@ function ResolveCCCLineIssues(estData, bodyshop) {
|
|||||||
// line.notes += ` | ET/UT Update (prev = ${line.mod_lbr_ty})`;
|
// line.notes += ` | ET/UT Update (prev = ${line.mod_lbr_ty})`;
|
||||||
line.mod_lbr_ty = "LAR";
|
line.mod_lbr_ty = "LAR";
|
||||||
}
|
}
|
||||||
|
if (line.mod_lbr_ty === "OTSL") {
|
||||||
|
line.mod_lbr_ty = line.mod_lbr_hrs === 0 ? null : "LAB";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export default connect(
|
|||||||
<Modal
|
<Modal
|
||||||
title={t("jobs.labels.existing_jobs")}
|
title={t("jobs.labels.existing_jobs")}
|
||||||
width={"80%"}
|
width={"80%"}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
okButtonProps={{ disabled: selectedJob ? false : true }}
|
okButtonProps={{ disabled: selectedJob ? false : true }}
|
||||||
{...modalProps}
|
{...modalProps}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -20,7 +20,14 @@ const mapStateToProps = createStructuredSelector({
|
|||||||
});
|
});
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
toggleModalVisible: () => dispatch(toggleModalVisible("noteUpsert")),
|
toggleModalVisible: () => dispatch(toggleModalVisible("noteUpsert")),
|
||||||
insertAuditTrail: ({ jobid, operation, type }) => dispatch(insertAuditTrail({ jobid, operation, type }))
|
insertAuditTrail: ({ jobid, operation, type }) =>
|
||||||
|
dispatch(
|
||||||
|
insertAuditTrail({
|
||||||
|
jobid,
|
||||||
|
operation,
|
||||||
|
type
|
||||||
|
})
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleModalVisible, insertAuditTrail }) {
|
export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleModalVisible, insertAuditTrail }) {
|
||||||
@@ -123,7 +130,7 @@ export function NoteUpsertModalContainer({ currentUser, noteUpsertModal, toggleM
|
|||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
}}
|
}}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<Form form={form} onFinish={handleFinish} layout="vertical">
|
<Form form={form} onFinish={handleFinish} layout="vertical">
|
||||||
<NoteUpsertModalComponent form={form} />
|
<NoteUpsertModalComponent form={form} />
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Virtuoso } from "react-virtuoso";
|
import { Virtuoso } from "react-virtuoso";
|
||||||
import { Badge, Button, Space, Spin, Switch, Tooltip, Typography } from "antd";
|
import { Alert, Badge, Button, Space, Spin, Switch, Tooltip, Typography } from "antd";
|
||||||
import { CheckCircleFilled, CheckCircleOutlined, EyeFilled, EyeOutlined } from "@ant-design/icons";
|
import { CheckCircleFilled, CheckCircleOutlined, EyeFilled, EyeOutlined } from "@ant-design/icons";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import "./notification-center.styles.scss";
|
import "./notification-center.styles.scss";
|
||||||
import day from "../../utils/day.js";
|
import day from "../../utils/day.js";
|
||||||
import { forwardRef, useRef, useEffect } from "react";
|
import { forwardRef, useEffect, useRef } from "react";
|
||||||
import { DateTimeFormat } from "../../utils/DateFormatter.jsx";
|
import { DateTimeFormat } from "../../utils/DateFormatter.jsx";
|
||||||
|
|
||||||
const { Text, Title } = Typography;
|
const { Text, Title } = Typography;
|
||||||
@@ -26,7 +26,8 @@ const NotificationCenterComponent = forwardRef(
|
|||||||
markAllRead,
|
markAllRead,
|
||||||
loadMore,
|
loadMore,
|
||||||
onNotificationClick,
|
onNotificationClick,
|
||||||
unreadCount
|
unreadCount,
|
||||||
|
isEmployee
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
@@ -93,7 +94,12 @@ const NotificationCenterComponent = forwardRef(
|
|||||||
) : (
|
) : (
|
||||||
<EyeOutlined className="notification-toggle-icon" />
|
<EyeOutlined className="notification-toggle-icon" />
|
||||||
)}
|
)}
|
||||||
<Switch checked={showUnreadOnly} onChange={(checked) => toggleUnreadOnly(checked)} size="small" />
|
<Switch
|
||||||
|
checked={showUnreadOnly}
|
||||||
|
onChange={(checked) => toggleUnreadOnly(checked)}
|
||||||
|
size="small"
|
||||||
|
disabled={!isEmployee}
|
||||||
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t("notifications.labels.mark-all-read")}>
|
<Tooltip title={t("notifications.labels.mark-all-read")}>
|
||||||
@@ -106,14 +112,20 @@ const NotificationCenterComponent = forwardRef(
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Virtuoso
|
{!isEmployee ? (
|
||||||
ref={virtuosoRef}
|
<div style={{ padding: 10 }}>
|
||||||
style={{ height: "400px", width: "100%" }}
|
<Alert message={t("notifications.labels.employee-notification")} type="warning" />
|
||||||
data={notifications}
|
</div>
|
||||||
totalCount={notifications.length}
|
) : (
|
||||||
endReached={loadMore}
|
<Virtuoso
|
||||||
itemContent={renderNotification}
|
ref={virtuosoRef}
|
||||||
/>
|
style={{ height: "400px", width: "100%" }}
|
||||||
|
data={notifications}
|
||||||
|
totalCount={notifications.length}
|
||||||
|
endReached={loadMore}
|
||||||
|
itemContent={renderNotification}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import { connect } from "react-redux";
|
|||||||
import NotificationCenterComponent from "./notification-center.component";
|
import NotificationCenterComponent from "./notification-center.component";
|
||||||
import { GET_NOTIFICATIONS } from "../../graphql/notifications.queries";
|
import { GET_NOTIFICATIONS } from "../../graphql/notifications.queries";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors.js";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors.js";
|
||||||
import day from "../../utils/day.js";
|
import day from "../../utils/day.js";
|
||||||
import { INITIAL_NOTIFICATIONS, useSocket } from "../../contexts/SocketIO/useSocket.js";
|
import { INITIAL_NOTIFICATIONS, useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||||
|
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
||||||
|
|
||||||
// This will be used to poll for notifications when the socket is disconnected
|
// This will be used to poll for notifications when the socket is disconnected
|
||||||
const NOTIFICATION_POLL_INTERVAL_SECONDS = 60;
|
const NOTIFICATION_POLL_INTERVAL_SECONDS = 60;
|
||||||
@@ -17,17 +18,18 @@ const NOTIFICATION_POLL_INTERVAL_SECONDS = 60;
|
|||||||
* @param onClose
|
* @param onClose
|
||||||
* @param bodyshop
|
* @param bodyshop
|
||||||
* @param unreadCount
|
* @param unreadCount
|
||||||
|
* @param currentUser
|
||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }) => {
|
const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount, currentUser }) => {
|
||||||
const [showUnreadOnly, setShowUnreadOnly] = useState(false);
|
const [showUnreadOnly, setShowUnreadOnly] = useState(false);
|
||||||
const [notifications, setNotifications] = useState([]);
|
const [notifications, setNotifications] = useState([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { isConnected, markNotificationRead, markAllNotificationsRead } = useSocket();
|
const { isConnected, markNotificationRead, markAllNotificationsRead } = useSocket();
|
||||||
const notificationRef = useRef(null);
|
const notificationRef = useRef(null);
|
||||||
|
|
||||||
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
const userAssociationId = bodyshop?.associations?.[0]?.id;
|
||||||
|
const isEmployee = useIsEmployee(bodyshop, currentUser);
|
||||||
|
|
||||||
const baseWhereClause = useMemo(() => {
|
const baseWhereClause = useMemo(() => {
|
||||||
return { associationid: { _eq: userAssociationId } };
|
return { associationid: { _eq: userAssociationId } };
|
||||||
@@ -51,7 +53,7 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
fetchPolicy: "cache-and-network",
|
fetchPolicy: "cache-and-network",
|
||||||
notifyOnNetworkStatusChange: true,
|
notifyOnNetworkStatusChange: true,
|
||||||
pollInterval: isConnected ? 0 : day.duration(NOTIFICATION_POLL_INTERVAL_SECONDS, "seconds").asMilliseconds(),
|
pollInterval: isConnected ? 0 : day.duration(NOTIFICATION_POLL_INTERVAL_SECONDS, "seconds").asMilliseconds(),
|
||||||
skip: !userAssociationId,
|
skip: !userAssociationId || !isEmployee,
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
console.error(`Error polling Notifications: ${err?.message || ""}`);
|
console.error(`Error polling Notifications: ${err?.message || ""}`);
|
||||||
setTimeout(() => refetch(), day.duration(2, "seconds").asMilliseconds());
|
setTimeout(() => refetch(), day.duration(2, "seconds").asMilliseconds());
|
||||||
@@ -71,7 +73,7 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
}, [visible, onClose]);
|
}, [visible, onClose]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.notifications) {
|
if (data?.notifications && isEmployee) {
|
||||||
const processedNotifications = data.notifications
|
const processedNotifications = data.notifications
|
||||||
.map((notif) => {
|
.map((notif) => {
|
||||||
let scenarioText;
|
let scenarioText;
|
||||||
@@ -101,11 +103,13 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
})
|
})
|
||||||
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||||||
setNotifications(processedNotifications);
|
setNotifications(processedNotifications);
|
||||||
|
} else if (!isEmployee) {
|
||||||
|
setNotifications([]); // Clear notifications if not an employee
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data, isEmployee]);
|
||||||
|
|
||||||
const loadMore = useCallback(() => {
|
const loadMore = useCallback(() => {
|
||||||
if (!queryLoading && data?.notifications.length) {
|
if (!queryLoading && data?.notifications.length && isEmployee) {
|
||||||
setIsLoading(true); // Show spinner during fetchMore
|
setIsLoading(true); // Show spinner during fetchMore
|
||||||
fetchMore({
|
fetchMore({
|
||||||
variables: { offset: data.notifications.length, where: whereClause },
|
variables: { offset: data.notifications.length, where: whereClause },
|
||||||
@@ -121,13 +125,14 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
})
|
})
|
||||||
.finally(() => setIsLoading(false)); // Hide spinner when done
|
.finally(() => setIsLoading(false)); // Hide spinner when done
|
||||||
}
|
}
|
||||||
}, [data?.notifications?.length, fetchMore, queryLoading, whereClause]);
|
}, [data?.notifications?.length, fetchMore, queryLoading, whereClause, isEmployee]);
|
||||||
|
|
||||||
const handleToggleUnreadOnly = (value) => {
|
const handleToggleUnreadOnly = (value) => {
|
||||||
setShowUnreadOnly(value);
|
setShowUnreadOnly(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMarkAllRead = useCallback(() => {
|
const handleMarkAllRead = useCallback(() => {
|
||||||
|
if (!isEmployee) return; // Do nothing if not an employee
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
markAllNotificationsRead()
|
markAllNotificationsRead()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -147,7 +152,7 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
})
|
})
|
||||||
.catch((e) => console.error(`Error marking all notifications read: ${e?.message || ""}`))
|
.catch((e) => console.error(`Error marking all notifications read: ${e?.message || ""}`))
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
}, [markAllNotificationsRead, userAssociationId, showUnreadOnly]);
|
}, [markAllNotificationsRead, userAssociationId, showUnreadOnly, isEmployee]);
|
||||||
|
|
||||||
const handleNotificationClick = useCallback(
|
const handleNotificationClick = useCallback(
|
||||||
(notificationId) => {
|
(notificationId) => {
|
||||||
@@ -170,17 +175,18 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible && !isConnected) {
|
if (visible && !isConnected && isEmployee) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
refetch()
|
refetch()
|
||||||
.catch((err) => console.error(`Error re-fetching notifications: ${err?.message || ""}`))
|
.catch((err) => console.error(`Error re-fetching notifications: ${err?.message || ""}`))
|
||||||
.finally(() => setIsLoading(false));
|
.finally(() => setIsLoading(false));
|
||||||
}
|
}
|
||||||
}, [visible, isConnected, refetch]);
|
}, [visible, isConnected, refetch, isEmployee]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NotificationCenterComponent
|
<NotificationCenterComponent
|
||||||
ref={notificationRef}
|
ref={notificationRef}
|
||||||
|
isEmployee={isEmployee}
|
||||||
visible={visible}
|
visible={visible}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
notifications={notifications}
|
notifications={notifications}
|
||||||
@@ -196,7 +202,8 @@ const NotificationCenterContainer = ({ visible, onClose, bodyshop, unreadCount }
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop
|
bodyshop: selectBodyshop,
|
||||||
|
currentUser: selectCurrentUser
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, null)(NotificationCenterContainer);
|
export default connect(mapStateToProps, null)(NotificationCenterContainer);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useMutation, useQuery } from "@apollo/client";
|
import { useMutation, useQuery } from "@apollo/client";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Button, Card, Checkbox, Divider, Form, Space, Switch, Table, Typography } from "antd";
|
import { Alert, Button, Card, Checkbox, Divider, Form, Space, Switch, Table, Typography } from "antd";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { selectCurrentUser } from "../../redux/user/user.selectors";
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
import AlertComponent from "../alert/alert.component";
|
import AlertComponent from "../alert/alert.component";
|
||||||
import {
|
import {
|
||||||
QUERY_NOTIFICATION_SETTINGS,
|
QUERY_NOTIFICATION_SETTINGS,
|
||||||
@@ -16,14 +16,16 @@ import LoadingSpinner from "../loading-spinner/loading-spinner.component.jsx";
|
|||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import ColumnHeaderCheckbox from "../notification-settings/column-header-checkbox.component.jsx";
|
import ColumnHeaderCheckbox from "../notification-settings/column-header-checkbox.component.jsx";
|
||||||
|
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifications Settings Form
|
* Notifications Settings Form
|
||||||
* @param currentUser
|
* @param currentUser
|
||||||
|
* @param bodyshop
|
||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
const NotificationSettingsForm = ({ currentUser }) => {
|
const NotificationSettingsForm = ({ currentUser, bodyshop }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [initialValues, setInitialValues] = useState({});
|
const [initialValues, setInitialValues] = useState({});
|
||||||
@@ -31,6 +33,7 @@ const NotificationSettingsForm = ({ currentUser }) => {
|
|||||||
const [autoAddEnabled, setAutoAddEnabled] = useState(false);
|
const [autoAddEnabled, setAutoAddEnabled] = useState(false);
|
||||||
const [initialAutoAdd, setInitialAutoAdd] = useState(false);
|
const [initialAutoAdd, setInitialAutoAdd] = useState(false);
|
||||||
const notification = useNotification();
|
const notification = useNotification();
|
||||||
|
const isEmployee = useIsEmployee(bodyshop, currentUser);
|
||||||
|
|
||||||
// Fetch notification settings and notifications_autoadd
|
// Fetch notification settings and notifications_autoadd
|
||||||
const { loading, error, data } = useQuery(QUERY_NOTIFICATION_SETTINGS, {
|
const { loading, error, data } = useQuery(QUERY_NOTIFICATION_SETTINGS, {
|
||||||
@@ -199,6 +202,11 @@ const NotificationSettingsForm = ({ currentUser }) => {
|
|||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
{!isEmployee && (
|
||||||
|
<div style={{ width: "100%", marginBottom: "10px" }}>
|
||||||
|
<Alert message={t("notifications.labels.employee-notification")} type="warning" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Table dataSource={dataSource} columns={columns} pagination={false} bordered rowKey="key" />
|
<Table dataSource={dataSource} columns={columns} pagination={false} bordered rowKey="key" />
|
||||||
<Divider />
|
<Divider />
|
||||||
</Card>
|
</Card>
|
||||||
@@ -209,11 +217,13 @@ const NotificationSettingsForm = ({ currentUser }) => {
|
|||||||
NotificationSettingsForm.propTypes = {
|
NotificationSettingsForm.propTypes = {
|
||||||
currentUser: PropTypes.shape({
|
currentUser: PropTypes.shape({
|
||||||
email: PropTypes.string.isRequired
|
email: PropTypes.string.isRequired
|
||||||
}).isRequired
|
}).isRequired,
|
||||||
|
bodyshop: PropTypes.object.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser
|
currentUser: selectCurrentUser,
|
||||||
|
bodyshop: selectBodyshop
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps)(NotificationSettingsForm);
|
export default connect(mapStateToProps)(NotificationSettingsForm);
|
||||||
|
|||||||
@@ -333,7 +333,7 @@ export function PartsOrderModalContainer({
|
|||||||
onOk={() => form.submit()}
|
onOk={() => form.submit()}
|
||||||
okButtonProps={{ loading: saving }}
|
okButtonProps={{ loading: saving }}
|
||||||
cancelButtonProps={{ loading: saving }}
|
cancelButtonProps={{ loading: saving }}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
width="75%"
|
width="75%"
|
||||||
forceRender
|
forceRender
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export default function PartsQueueDetailCard() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer open={!!selected} destroyOnClose width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
|
<Drawer open={!!selected} destroyOnHidden width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
|
||||||
{loading ? <LoadingSpinner /> : null}
|
{loading ? <LoadingSpinner /> : null}
|
||||||
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
||||||
{data ? (
|
{data ? (
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export function PartsReceiveModalContainer({ partsReceiveModal, toggleModalVisib
|
|||||||
onCancel={() => toggleModalVisible()}
|
onCancel={() => toggleModalVisible()}
|
||||||
onOk={() => form.submit()}
|
onOk={() => form.submit()}
|
||||||
okButtonProps={{ loading: loading }}
|
okButtonProps={{ loading: loading }}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
forceRender
|
forceRender
|
||||||
width="50%"
|
width="50%"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ function PaymentModalContainer({ paymentModal, toggleModalVisible, bodyshop }) {
|
|||||||
<Modal
|
<Modal
|
||||||
title={!context || (context && !context.id) ? t("payments.labels.new") : t("payments.labels.edit")}
|
title={!context || (context && !context.id) ? t("payments.labels.new") : t("payments.labels.edit")}
|
||||||
open={open}
|
open={open}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
okText={t("general.actions.save")}
|
okText={t("general.actions.save")}
|
||||||
onOk={() => form.submit()}
|
onOk={() => form.submit()}
|
||||||
width="50%"
|
width="50%"
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { useQuery } from "@apollo/client";
|
||||||
|
import { Input, Table } from "antd";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||||
|
import { GET_PHONE_NUMBER_OPT_OUTS } from "../../graphql/phone-number-opt-out.queries";
|
||||||
|
|
||||||
|
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||||
|
import { TimeAgoFormatter } from "../../utils/DateFormatter";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop,
|
||||||
|
currentUser: selectCurrentUser
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = () => ({});
|
||||||
|
|
||||||
|
function PhoneNumberConsentList({ bodyshop, currentUser }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const { loading, data } = useQuery(GET_PHONE_NUMBER_OPT_OUTS, {
|
||||||
|
variables: { bodyshopid: bodyshop.id, search: search ? `%${search}%` : undefined },
|
||||||
|
fetchPolicy: "network-only"
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t("consent.phone_number"),
|
||||||
|
dataIndex: "phone_number",
|
||||||
|
render: (text) => <PhoneNumberFormatter>{text}</PhoneNumberFormatter>,
|
||||||
|
sorter: (a, b) => a.phone_number.localeCompare(b.phone_number)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("consent.created_at"),
|
||||||
|
dataIndex: "created_at",
|
||||||
|
render: (text) => <TimeAgoFormatter>{text}</TimeAgoFormatter>,
|
||||||
|
sorter: (a, b) => new Date(a.created_at) - new Date(b.created_at)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Input.Search
|
||||||
|
placeholder={t("general.labels.search")}
|
||||||
|
onSearch={(value) => setSearch(value)}
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={data?.phone_number_opt_out}
|
||||||
|
loading={loading}
|
||||||
|
rowKey="id"
|
||||||
|
style={{ marginTop: 16 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(PhoneNumberConsentList);
|
||||||
@@ -32,7 +32,7 @@ export function PrintCenterModalContainer({ printCenterModal, toggleModalVisible
|
|||||||
okText={t("general.actions.close")}
|
okText={t("general.actions.close")}
|
||||||
width="90%"
|
width="90%"
|
||||||
title={t("printcenter.labels.title")}
|
title={t("printcenter.labels.title")}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<PrintCenterModalComponent context={context} />
|
<PrintCenterModalComponent context={context} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import { Card, Col, Form, Radio, Row } from "antd";
|
import { Card, Col, Form, Radio, Row } from "antd";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../../redux/user/user.selectors";
|
||||||
|
import { HasFeatureAccess } from "../../feature-wrapper/feature-wrapper.component";
|
||||||
|
|
||||||
const LayoutSettings = ({ t }) => (
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
const LayoutSettings = ({ t, bodyshop }) => (
|
||||||
<Card title={t("production.settings.layout")} style={{ maxWidth: "100%", overflowX: "auto" }}>
|
<Card title={t("production.settings.layout")} style={{ maxWidth: "100%", overflowX: "auto" }}>
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
{[
|
{[
|
||||||
@@ -30,14 +38,18 @@ const LayoutSettings = ({ t }) => (
|
|||||||
{ value: false, label: t("production.labels.wide") }
|
{ value: false, label: t("production.labels.wide") }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
...(HasFeatureAccess({ bodyshop, featureName: "smartscheduling" })
|
||||||
name: "cardcolor",
|
? [
|
||||||
label: t("production.labels.cardcolor"),
|
{
|
||||||
options: [
|
name: "cardcolor",
|
||||||
{ value: true, label: t("production.labels.on") },
|
label: t("production.labels.cardcolor"),
|
||||||
{ value: false, label: t("production.labels.off") }
|
options: [
|
||||||
]
|
{ value: true, label: t("production.labels.on") },
|
||||||
},
|
{ value: false, label: t("production.labels.off") }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
name: "kiosk",
|
name: "kiosk",
|
||||||
label: t("production.labels.kiosk_mode"),
|
label: t("production.labels.kiosk_mode"),
|
||||||
@@ -67,4 +79,4 @@ LayoutSettings.propTypes = {
|
|||||||
t: PropTypes.func.isRequired
|
t: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LayoutSettings;
|
export default connect(mapStateToProps)(LayoutSettings);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import React, { useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||||
import { QUERY_ACTIVE_EMPLOYEES, QUERY_ACTIVE_EMPLOYEES_WITH_EMAIL } from "../../graphql/employees.queries";
|
import { QUERY_ACTIVE_EMPLOYEES, QUERY_ACTIVE_EMPLOYEES_WITH_EMAIL } from "../../graphql/employees.queries";
|
||||||
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
|
import { QUERY_ALL_VENDORS } from "../../graphql/vendors.queries";
|
||||||
import { selectReportCenter } from "../../redux/modals/modals.selectors";
|
import { selectReportCenter } from "../../redux/modals/modals.selectors";
|
||||||
@@ -18,11 +19,10 @@ import EmployeeSearchSelectEmail from "../employee-search-select/employee-search
|
|||||||
import EmployeeSearchSelect from "../employee-search-select/employee-search-select.component";
|
import EmployeeSearchSelect from "../employee-search-select/employee-search-select.component";
|
||||||
import BlurWrapperComponent from "../feature-wrapper/blur-wrapper.component";
|
import BlurWrapperComponent from "../feature-wrapper/blur-wrapper.component";
|
||||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||||
|
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
||||||
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
import VendorSearchSelect from "../vendor-search-select/vendor-search-select.component";
|
||||||
import ReportCenterModalFiltersSortersComponent from "./report-center-modal-filters-sorters-component";
|
import ReportCenterModalFiltersSortersComponent from "./report-center-modal-filters-sorters-component";
|
||||||
import "./report-center-modal.styles.scss";
|
import "./report-center-modal.styles.scss";
|
||||||
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
|
||||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
reportCenterModal: selectReportCenter,
|
reportCenterModal: selectReportCenter,
|
||||||
@@ -389,5 +389,7 @@ const restrictedReports = [
|
|||||||
{ key: "job_costing_ro_date_detail", days: 183 },
|
{ key: "job_costing_ro_date_detail", days: 183 },
|
||||||
{ key: "job_costing_ro_estimator", days: 183 },
|
{ key: "job_costing_ro_estimator", days: 183 },
|
||||||
{ key: "job_lifecycle_date_detail", days: 183 },
|
{ key: "job_lifecycle_date_detail", days: 183 },
|
||||||
{ key: "job_lifecycle_date_summary", days: 183 }
|
{ key: "job_lifecycle_date_summary", days: 183 },
|
||||||
|
{ key: "customer_list", days: 183 },
|
||||||
|
{ key: "customer_list_excel", days: 183 }
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export function ReportCenterModalContainer({ reportCenterModal, toggleModalVisib
|
|||||||
onOk={() => toggleModalVisible()}
|
onOk={() => toggleModalVisible()}
|
||||||
onCancel={() => toggleModalVisible()}
|
onCancel={() => toggleModalVisible()}
|
||||||
cancelButtonProps={{ style: { display: "none" } }}
|
cancelButtonProps={{ style: { display: "none" } }}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
width="80%"
|
width="80%"
|
||||||
>
|
>
|
||||||
<RbacWrapperComponent action="shop:reportcenter">
|
<RbacWrapperComponent action="shop:reportcenter">
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ export function ScheduleJobModalContainer({
|
|||||||
onOk={() => form.submit()}
|
onOk={() => form.submit()}
|
||||||
width={"90%"}
|
width={"90%"}
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
okButtonProps={{
|
okButtonProps={{
|
||||||
loading: loading
|
loading: loading
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export default function ScoreboardJobsList({ scoreBoardlist }) {
|
|||||||
<>
|
<>
|
||||||
<Modal
|
<Modal
|
||||||
open={state.open}
|
open={state.open}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
width="80%"
|
width="80%"
|
||||||
closable={false}
|
closable={false}
|
||||||
cancelButtonProps={{ style: { display: "none" } }}
|
cancelButtonProps={{ style: { display: "none" } }}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Typography } from "antd";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import PhoneNumberConsentList from "../phone-number-consent/phone-number-consent.component";
|
||||||
|
|
||||||
|
const mapStateToProps = createStructuredSelector({
|
||||||
|
bodyshop: selectBodyshop
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapDispatchToProps = (dispatch) => ({});
|
||||||
|
|
||||||
|
function ShopInfoConsentComponent({ bodyshop }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Typography.Title level={4}>{t("settings.title")}</Typography.Title>
|
||||||
|
{<PhoneNumberConsentList bodyshop={bodyshop} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(ShopInfoConsentComponent);
|
||||||
@@ -8,7 +8,7 @@ export default function ShopInfoNotificationsAutoadd({ bodyshop }) {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// Filter employee options to ensure active employees with valid IDs
|
// Filter employee options to ensure active employees with valid IDs
|
||||||
const employeeOptions = bodyshop?.employees?.filter((e) => e.active && e.id && typeof e.id === "string") || [];
|
const employeeOptions = bodyshop?.employees?.filter((e) => e.active && e.user_email && e.id) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ export function TaskUpsertModalContainer({ bodyshop, currentUser, taskUpsert, to
|
|||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
}}
|
}}
|
||||||
okButtonProps={{ disabled: !isTouched }}
|
okButtonProps={{ disabled: !isTouched }}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ export function TechLookupJobsDrawer({ bodyshop, setPrintCenterContext }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer open={!!selected} destroyOnClose width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
|
<Drawer open={!!selected} destroyOnHidden width={drawerPercentage} placement="right" onClose={handleDrawerClose}>
|
||||||
{loading ? <LoadingSpinner /> : null}
|
{loading ? <LoadingSpinner /> : null}
|
||||||
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
{error ? <AlertComponent message={error.message} type="error" /> : null}
|
||||||
{data ? (
|
{data ? (
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export function TimeTicketListTeamPay({ bodyshop, context, actions }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Modal width={"80%"} open={visible} destroyOnClose onOk={handleOk} onCancel={() => setVisible(false)}>
|
<Modal width={"80%"} open={visible} destroyOnHidden onOk={handleOk} onCancel={() => setVisible(false)}>
|
||||||
<Form layout="vertical" form={form} initialValues={{ jobid: jobId }}>
|
<Form layout="vertical" form={form} initialValues={{ jobid: jobId }}>
|
||||||
<LayoutFormRow grow noDivider>
|
<LayoutFormRow grow noDivider>
|
||||||
<Form.Item shouldUpdate>
|
<Form.Item shouldUpdate>
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ export function TimeTicketModalContainer({ timeTicketModal, toggleModalVisible,
|
|||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
id="time-ticket-modal"
|
id="time-ticket-modal"
|
||||||
>
|
>
|
||||||
<Form
|
<Form
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export function TimeTickeTaskModalContainer({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
destroyOnClose
|
destroyOnHidden
|
||||||
open={open}
|
open={open}
|
||||||
onCancel={() => {
|
onCancel={() => {
|
||||||
toggleModalVisible();
|
toggleModalVisible();
|
||||||
|
|||||||
28
client/src/graphql/phone-number-opt-out.queries.js
Normal file
28
client/src/graphql/phone-number-opt-out.queries.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { gql } from "@apollo/client";
|
||||||
|
|
||||||
|
export const GET_PHONE_NUMBER_OPT_OUT = gql`
|
||||||
|
query GET_PHONE_NUMBER_OPT_OUT($bodyshopid: uuid!, $phone_number: String!) {
|
||||||
|
phone_number_opt_out(where: { bodyshopid: { _eq: $bodyshopid }, phone_number: { _eq: $phone_number } }) {
|
||||||
|
id
|
||||||
|
bodyshopid
|
||||||
|
phone_number
|
||||||
|
created_at
|
||||||
|
updated_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const GET_PHONE_NUMBER_OPT_OUTS = gql`
|
||||||
|
query GET_PHONE_NUMBER_OPT_OUTS($bodyshopid: uuid!, $search: String) {
|
||||||
|
phone_number_opt_out(
|
||||||
|
where: { bodyshopid: { _eq: $bodyshopid }, phone_number: { _ilike: $search } }
|
||||||
|
order_by: [{ phone_number: asc }, { updated_at: desc }]
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
bodyshopid
|
||||||
|
phone_number
|
||||||
|
created_at
|
||||||
|
updated_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -10,10 +10,10 @@ import ShopCsiConfig from "../../components/shop-csi-config/shop-csi-config.comp
|
|||||||
import ShopEmployeesContainer from "../../components/shop-employees/shop-employees.container";
|
import ShopEmployeesContainer from "../../components/shop-employees/shop-employees.container";
|
||||||
import ShopInfoContainer from "../../components/shop-info/shop-info.container";
|
import ShopInfoContainer from "../../components/shop-info/shop-info.container";
|
||||||
import ShopInfoUsersComponent from "../../components/shop-users/shop-users.component";
|
import ShopInfoUsersComponent from "../../components/shop-users/shop-users.component";
|
||||||
|
import ShopInfoConsentComponent from "../../components/shop-info/shop-info.consent.component";
|
||||||
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
import { setBreadcrumbs, setSelectedHeader } from "../../redux/application/application.actions";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||||
|
|
||||||
import { HasFeatureAccess } from "../../components/feature-wrapper/feature-wrapper.component";
|
import { HasFeatureAccess } from "../../components/feature-wrapper/feature-wrapper.component";
|
||||||
import ShopTeamsContainer from "../../components/shop-teams/shop-teams.container";
|
import ShopTeamsContainer from "../../components/shop-teams/shop-teams.container";
|
||||||
|
|
||||||
@@ -91,6 +91,14 @@ export function ShopPage({ bodyshop, setSelectedHeader, setBreadcrumbs }) {
|
|||||||
children: <ShopCsiConfig />
|
children: <ShopCsiConfig />
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add Consent Settings tab
|
||||||
|
items.push({
|
||||||
|
key: "consent",
|
||||||
|
label: t("bodyshop.labels.consent_settings"),
|
||||||
|
children: <ShopInfoConsentComponent bodyshop={bodyshop} />
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RbacWrapper action="shop:config">
|
<RbacWrapper action="shop:config">
|
||||||
<Tabs activeKey={search.tab} onChange={(key) => history({ search: `?tab=${key}` })} items={items} />
|
<Tabs activeKey={search.tab} onChange={(key) => history({ search: `?tab=${key}` })} items={items} />
|
||||||
|
|||||||
@@ -105,7 +105,6 @@ const userReducer = (state = INITIAL_STATE, action) => {
|
|||||||
...action.payload //Spread current user details in.
|
...action.payload //Spread current user details in.
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
case UserActionTypes.SET_SHOP_DETAILS:
|
case UserActionTypes.SET_SHOP_DETAILS:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@@ -126,6 +125,7 @@ const userReducer = (state = INITIAL_STATE, action) => {
|
|||||||
...state,
|
...state,
|
||||||
imexshopid: action.payload
|
imexshopid: action.payload
|
||||||
};
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -656,6 +656,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
|
"consent_settings": "Phone Number Opt-Out List",
|
||||||
"2tiername": "Name => RO",
|
"2tiername": "Name => RO",
|
||||||
"2tiersetup": "2 Tier Setup",
|
"2tiersetup": "2 Tier Setup",
|
||||||
"2tiersource": "Source => RO",
|
"2tiersource": "Source => RO",
|
||||||
@@ -2377,7 +2378,8 @@
|
|||||||
"errors": {
|
"errors": {
|
||||||
"invalidphone": "The phone number is invalid. Unable to open conversation. ",
|
"invalidphone": "The phone number is invalid. Unable to open conversation. ",
|
||||||
"noattachedjobs": "No Jobs have been associated to this conversation. ",
|
"noattachedjobs": "No Jobs have been associated to this conversation. ",
|
||||||
"updatinglabel": "Error updating label. {{error}}"
|
"updatinglabel": "Error updating label. {{error}}",
|
||||||
|
"no_consent": "This phone number has not consented to receive messages."
|
||||||
},
|
},
|
||||||
"labels": {
|
"labels": {
|
||||||
"addlabel": "Add a label to this conversation.",
|
"addlabel": "Add a label to this conversation.",
|
||||||
@@ -2393,7 +2395,8 @@
|
|||||||
"selectmedia": "Select Media",
|
"selectmedia": "Select Media",
|
||||||
"sentby": "Sent by {{by}} at {{time}}",
|
"sentby": "Sent by {{by}} at {{time}}",
|
||||||
"typeamessage": "Send a message...",
|
"typeamessage": "Send a message...",
|
||||||
"unarchive": "Unarchive"
|
"unarchive": "Unarchive",
|
||||||
|
"no_consent": "No Consent"
|
||||||
},
|
},
|
||||||
"render": {
|
"render": {
|
||||||
"conversation_list": "Conversation List"
|
"conversation_list": "Conversation List"
|
||||||
@@ -2474,7 +2477,8 @@
|
|||||||
"teams-search": "Search for a Team",
|
"teams-search": "Search for a Team",
|
||||||
"unwatch": "Unwatch",
|
"unwatch": "Unwatch",
|
||||||
"watch": "Watch",
|
"watch": "Watch",
|
||||||
"watching-issue": "Watching"
|
"watching-issue": "Watching",
|
||||||
|
"employee-notification": "Notifications are disabled because you do not have an associated Employee record."
|
||||||
},
|
},
|
||||||
"scenarios": {
|
"scenarios": {
|
||||||
"alternate-transport-changed": "Alternate Transport Changed",
|
"alternate-transport-changed": "Alternate Transport Changed",
|
||||||
@@ -2494,7 +2498,9 @@
|
|||||||
"tasks-updated-created": "Tasks Updated / Created"
|
"tasks-updated-created": "Tasks Updated / Created"
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"job-watchers": "Job Watchers"
|
"job-watchers": "Job Watchers",
|
||||||
|
"not-employee": "You need to be an employee to watch this job. Reach out to your admin to get set up!",
|
||||||
|
"not-employee-notifications": "You must be an employee to receive notifications"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"owner": {
|
"owner": {
|
||||||
@@ -3096,6 +3102,7 @@
|
|||||||
"credits_not_received_date_vendorid": "Credits not Received by Vendor",
|
"credits_not_received_date_vendorid": "Credits not Received by Vendor",
|
||||||
"csi": "CSI Responses",
|
"csi": "CSI Responses",
|
||||||
"customer_list": "Customer List",
|
"customer_list": "Customer List",
|
||||||
|
"customer_list_excel": "Customer List - Excel",
|
||||||
"cycle_time_analysis": "Cycle Time Analysis",
|
"cycle_time_analysis": "Cycle Time Analysis",
|
||||||
"estimates_written_converted": "Estimates Written/Converted",
|
"estimates_written_converted": "Estimates Written/Converted",
|
||||||
"estimator_detail": "Jobs by Estimator (Detail)",
|
"estimator_detail": "Jobs by Estimator (Detail)",
|
||||||
@@ -3859,6 +3866,14 @@
|
|||||||
"validation": {
|
"validation": {
|
||||||
"unique_vendor_name": "You must enter a unique vendor name."
|
"unique_vendor_name": "You must enter a unique vendor name."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"consent": {
|
||||||
|
"phone_number": "Phone Number",
|
||||||
|
"status": "Consent Status",
|
||||||
|
"created_at": "Created At"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"title": "Phone Number Opt-Out List"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2476,7 +2476,8 @@
|
|||||||
"teams-search": "",
|
"teams-search": "",
|
||||||
"unwatch": "",
|
"unwatch": "",
|
||||||
"watch": "",
|
"watch": "",
|
||||||
"watching-issue": ""
|
"watching-issue": "",
|
||||||
|
"employee-notification": ""
|
||||||
},
|
},
|
||||||
"scenarios": {
|
"scenarios": {
|
||||||
"alternate-transport-changed": "",
|
"alternate-transport-changed": "",
|
||||||
@@ -2496,7 +2497,8 @@
|
|||||||
"tasks-updated-created": ""
|
"tasks-updated-created": ""
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"job-watchers": ""
|
"job-watchers": "",
|
||||||
|
"not-employee": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"owner": {
|
"owner": {
|
||||||
@@ -3098,6 +3100,7 @@
|
|||||||
"credits_not_received_date_vendorid": "",
|
"credits_not_received_date_vendorid": "",
|
||||||
"csi": "",
|
"csi": "",
|
||||||
"customer_list": "",
|
"customer_list": "",
|
||||||
|
"customer_list_excel": "",
|
||||||
"cycle_time_analysis": "",
|
"cycle_time_analysis": "",
|
||||||
"estimates_written_converted": "",
|
"estimates_written_converted": "",
|
||||||
"estimator_detail": "",
|
"estimator_detail": "",
|
||||||
|
|||||||
@@ -2476,7 +2476,8 @@
|
|||||||
"teams-search": "",
|
"teams-search": "",
|
||||||
"unwatch": "",
|
"unwatch": "",
|
||||||
"watch": "",
|
"watch": "",
|
||||||
"watching-issue": ""
|
"watching-issue": "",
|
||||||
|
"employee-notification": ""
|
||||||
},
|
},
|
||||||
"scenarios": {
|
"scenarios": {
|
||||||
"alternate-transport-changed": "",
|
"alternate-transport-changed": "",
|
||||||
@@ -2496,7 +2497,8 @@
|
|||||||
"tasks-updated-created": ""
|
"tasks-updated-created": ""
|
||||||
},
|
},
|
||||||
"tooltips": {
|
"tooltips": {
|
||||||
"job-watchers": ""
|
"job-watchers": "",
|
||||||
|
"not-employee": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"owner": {
|
"owner": {
|
||||||
@@ -3098,6 +3100,7 @@
|
|||||||
"credits_not_received_date_vendorid": "",
|
"credits_not_received_date_vendorid": "",
|
||||||
"csi": "",
|
"csi": "",
|
||||||
"customer_list": "",
|
"customer_list": "",
|
||||||
|
"customer_list_excel": "",
|
||||||
"cycle_time_analysis": "",
|
"cycle_time_analysis": "",
|
||||||
"estimates_written_converted": "",
|
"estimates_written_converted": "",
|
||||||
"estimator_detail": "",
|
"estimator_detail": "",
|
||||||
|
|||||||
@@ -2004,6 +2004,18 @@ export const TemplateList = (type, context) => {
|
|||||||
},
|
},
|
||||||
group: "customers"
|
group: "customers"
|
||||||
},
|
},
|
||||||
|
customer_list_excel: {
|
||||||
|
title: i18n.t("reportcenter.templates.customer_list_excel"),
|
||||||
|
subject: i18n.t("reportcenter.templates.customer_list_excel"),
|
||||||
|
key: "customer_list_excel",
|
||||||
|
reporttype: "excel",
|
||||||
|
disabled: false,
|
||||||
|
rangeFilter: {
|
||||||
|
object: i18n.t("reportcenter.labels.objects.jobs"),
|
||||||
|
field: i18n.t("jobs.fields.date_invoiced")
|
||||||
|
},
|
||||||
|
group: "customers"
|
||||||
|
},
|
||||||
exported_gsr_by_ro: {
|
exported_gsr_by_ro: {
|
||||||
title: i18n.t("reportcenter.templates.exported_gsr_by_ro"),
|
title: i18n.t("reportcenter.templates.exported_gsr_by_ro"),
|
||||||
subject: i18n.t("reportcenter.templates.exported_gsr_by_ro"),
|
subject: i18n.t("reportcenter.templates.exported_gsr_by_ro"),
|
||||||
@@ -2241,7 +2253,7 @@ export const TemplateList = (type, context) => {
|
|||||||
field: i18n.t("bills.fields.date")
|
field: i18n.t("bills.fields.date")
|
||||||
},
|
},
|
||||||
group: "purchases"
|
group: "purchases"
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
...(!type || type === "courtesycarcontract"
|
...(!type || type === "courtesycarcontract"
|
||||||
|
|||||||
19
client/src/utils/useIsEmployee.js
Normal file
19
client/src/utils/useIsEmployee.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the user is an employee of the bodyshop
|
||||||
|
* @param bodyshop
|
||||||
|
* @param userOrEmail
|
||||||
|
* @returns {boolean|*}
|
||||||
|
*/
|
||||||
|
export function useIsEmployee(bodyshop, userOrEmail) {
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!bodyshop || !bodyshop.employees) return false;
|
||||||
|
|
||||||
|
// Handle both user object and email string
|
||||||
|
const email = typeof userOrEmail === "string" ? userOrEmail : userOrEmail?.email;
|
||||||
|
if (!email) return false;
|
||||||
|
|
||||||
|
return bodyshop.employees.some((employee) => employee.user_email === email);
|
||||||
|
}, [bodyshop, userOrEmail]);
|
||||||
|
}
|
||||||
@@ -2681,6 +2681,9 @@
|
|||||||
- active:
|
- active:
|
||||||
_eq: true
|
_eq: true
|
||||||
allow_aggregations: true
|
allow_aggregations: true
|
||||||
|
- table:
|
||||||
|
name: integration_log
|
||||||
|
schema: public
|
||||||
- table:
|
- table:
|
||||||
name: inventory
|
name: inventory
|
||||||
schema: public
|
schema: public
|
||||||
@@ -5861,6 +5864,32 @@
|
|||||||
template_engine: Kriti
|
template_engine: Kriti
|
||||||
url: '{{$base_url}}/opensearch'
|
url: '{{$base_url}}/opensearch'
|
||||||
version: 2
|
version: 2
|
||||||
|
- table:
|
||||||
|
name: phone_number_opt_out
|
||||||
|
schema: public
|
||||||
|
object_relationships:
|
||||||
|
- name: bodyshop
|
||||||
|
using:
|
||||||
|
foreign_key_constraint_on: bodyshopid
|
||||||
|
select_permissions:
|
||||||
|
- role: user
|
||||||
|
permission:
|
||||||
|
columns:
|
||||||
|
- phone_number
|
||||||
|
- created_at
|
||||||
|
- updated_at
|
||||||
|
- bodyshopid
|
||||||
|
- id
|
||||||
|
filter:
|
||||||
|
bodyshop:
|
||||||
|
associations:
|
||||||
|
_and:
|
||||||
|
- user:
|
||||||
|
authid:
|
||||||
|
_eq: X-Hasura-User-Id
|
||||||
|
- active:
|
||||||
|
_eq: true
|
||||||
|
comment: ""
|
||||||
- table:
|
- table:
|
||||||
name: phonebook
|
name: phonebook
|
||||||
schema: public
|
schema: public
|
||||||
|
|||||||
@@ -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 "enforce_sms_consent" boolean
|
||||||
|
-- null;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."bodyshops" add column "enforce_sms_consent" boolean
|
||||||
|
null;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE "public"."phone_number_consent";
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
CREATE TABLE "public"."phone_number_consent" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "bodyshopid" uuid NOT NULL, "phone_number" text NOT NULL, "consent_status" boolean NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "consent_updated_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("id") , FOREIGN KEY ("bodyshopid") REFERENCES "public"."bodyshops"("id") ON UPDATE restrict ON DELETE restrict, UNIQUE ("id"), UNIQUE ("bodyshopid", "phone_number"));
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE "public"."phone_number_consent_history";
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
CREATE TABLE "public"."phone_number_consent_history" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "phone_number_consent_id" uuid NOT NULL, "old_value" boolean NOT NULL, "new_value" boolean NOT NULL, "reason" text NOT NULL, "changed_at" timestamptz NOT NULL DEFAULT now(), "changed_by" text NOT NULL, PRIMARY KEY ("id") , FOREIGN KEY ("phone_number_consent_id") REFERENCES "public"."phone_number_consent"("id") ON UPDATE restrict ON DELETE restrict, UNIQUE ("id"));
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."phone_number_consent_history" alter column "old_value" set not null;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."phone_number_consent_history" alter column "old_value" drop not null;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- Could not auto-generate a down migration.
|
||||||
|
-- Please write an appropriate down migration for the SQL below:
|
||||||
|
-- DROP table "public"."phone_number_consent_history";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP table "public"."phone_number_consent_history";
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."phone_number_consent" alter column "consent_status" drop not null;
|
||||||
|
alter table "public"."phone_number_consent" add column "consent_status" bool;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."phone_number_consent" drop column "consent_status" cascade;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
alter table "public"."phone_number_consent" alter column "consent_updated_at" set default now();
|
||||||
|
alter table "public"."phone_number_consent" alter column "consent_updated_at" drop not null;
|
||||||
|
alter table "public"."phone_number_consent" add column "consent_updated_at" timestamptz;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."phone_number_consent" drop column "consent_updated_at" cascade;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."phone_number_opt_out" rename to "phone_number_consent";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."phone_number_consent" rename to "phone_number_opt_out";
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
alter table "public"."bodyshops" alter column "enforce_sms_consent" drop not null;
|
||||||
|
alter table "public"."bodyshops" add column "enforce_sms_consent" bool;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
alter table "public"."bodyshops" drop column "enforce_sms_consent" cascade;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE "public"."integration_log";
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
CREATE TABLE "public"."integration_log" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "bodyshopid" uuid NOT NULL, "email" text NOT NULL, "jobid" uuid, "billid" uuid, "paymentid" uuid, "method" text NOT NULL, "name" text NOT NULL, "status" text NOT NULL, "platform" text NOT NULL, PRIMARY KEY ("id") , FOREIGN KEY ("bodyshopid") REFERENCES "public"."bodyshops"("id") ON UPDATE restrict ON DELETE restrict, FOREIGN KEY ("email") REFERENCES "public"."users"("email") ON UPDATE restrict ON DELETE restrict, FOREIGN KEY ("billid") REFERENCES "public"."bills"("id") ON UPDATE restrict ON DELETE set null, FOREIGN KEY ("jobid") REFERENCES "public"."jobs"("id") ON UPDATE restrict ON DELETE restrict, FOREIGN KEY ("paymentid") REFERENCES "public"."payments"("id") ON UPDATE restrict ON DELETE set null);
|
||||||
|
CREATE OR REPLACE FUNCTION "public"."set_current_timestamp_updated_at"()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
DECLARE
|
||||||
|
_new record;
|
||||||
|
BEGIN
|
||||||
|
_new := NEW;
|
||||||
|
_new."updated_at" = NOW();
|
||||||
|
RETURN _new;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
CREATE TRIGGER "set_public_integration_log_updated_at"
|
||||||
|
BEFORE UPDATE ON "public"."integration_log"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE PROCEDURE "public"."set_current_timestamp_updated_at"();
|
||||||
|
COMMENT ON TRIGGER "set_public_integration_log_updated_at" ON "public"."integration_log"
|
||||||
|
IS 'trigger to set value of column "updated_at" to current timestamp on row update';
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
1716
package-lock.json
generated
1716
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
36
package.json
36
package.json
@@ -16,14 +16,14 @@
|
|||||||
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
|
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-cloudwatch-logs": "^3.804.0",
|
"@aws-sdk/client-cloudwatch-logs": "^3.812.0",
|
||||||
"@aws-sdk/client-elasticache": "^3.804.0",
|
"@aws-sdk/client-elasticache": "^3.812.0",
|
||||||
"@aws-sdk/client-s3": "^3.804.0",
|
"@aws-sdk/client-s3": "^3.812.0",
|
||||||
"@aws-sdk/client-secrets-manager": "^3.804.0",
|
"@aws-sdk/client-secrets-manager": "^3.812.0",
|
||||||
"@aws-sdk/client-ses": "^3.804.0",
|
"@aws-sdk/client-ses": "^3.812.0",
|
||||||
"@aws-sdk/credential-provider-node": "^3.804.0",
|
"@aws-sdk/credential-provider-node": "^3.812.0",
|
||||||
"@aws-sdk/lib-storage": "^3.804.0",
|
"@aws-sdk/lib-storage": "^3.812.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.804.0",
|
"@aws-sdk/s3-request-presigner": "^3.812.0",
|
||||||
"@opensearch-project/opensearch": "^2.13.0",
|
"@opensearch-project/opensearch": "^2.13.0",
|
||||||
"@socket.io/admin-ui": "^0.5.1",
|
"@socket.io/admin-ui": "^0.5.1",
|
||||||
"@socket.io/redis-adapter": "^8.3.0",
|
"@socket.io/redis-adapter": "^8.3.0",
|
||||||
@@ -31,18 +31,18 @@
|
|||||||
"aws4": "^1.13.2",
|
"aws4": "^1.13.2",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"better-queue": "^3.8.12",
|
"better-queue": "^3.8.12",
|
||||||
"bullmq": "^5.52.2",
|
"bullmq": "^5.53.0",
|
||||||
"chart.js": "^4.4.8",
|
"chart.js": "^4.4.8",
|
||||||
"cloudinary": "^2.6.1",
|
"cloudinary": "^2.6.1",
|
||||||
"compression": "^1.8.0",
|
"compression": "^1.8.0",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"cors": "2.8.5",
|
"cors": "^2.8.5",
|
||||||
"crisp-status-reporter": "^1.2.2",
|
"crisp-status-reporter": "^1.2.2",
|
||||||
"dd-trace": "^5.51.0",
|
"dd-trace": "^5.52.0",
|
||||||
"dinero.js": "^1.9.1",
|
"dinero.js": "^1.9.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.21.1",
|
"express": "^4.21.1",
|
||||||
"firebase-admin": "^13.2.0",
|
"firebase-admin": "^13.4.0",
|
||||||
"graphql": "^16.11.0",
|
"graphql": "^16.11.0",
|
||||||
"graphql-request": "^6.1.0",
|
"graphql-request": "^6.1.0",
|
||||||
"intuit-oauth": "^4.2.0",
|
"intuit-oauth": "^4.2.0",
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
"node-persist": "^4.0.4",
|
"node-persist": "^4.0.4",
|
||||||
"nodemailer": "^6.10.0",
|
"nodemailer": "^6.10.0",
|
||||||
"phone": "^3.1.58",
|
"phone": "^3.1.58",
|
||||||
"query-string": "^9.1.2",
|
"query-string": "7.1.3",
|
||||||
"recursive-diff": "^1.0.9",
|
"recursive-diff": "^1.0.9",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"skia-canvas": "^2.0.2",
|
"skia-canvas": "^2.0.2",
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"socket.io-adapter": "^2.5.5",
|
"socket.io-adapter": "^2.5.5",
|
||||||
"ssh2-sftp-client": "^11.0.0",
|
"ssh2-sftp-client": "^11.0.0",
|
||||||
"twilio": "^5.6.0",
|
"twilio": "^5.6.1",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0",
|
||||||
"winston-cloudwatch": "^6.3.0",
|
"winston-cloudwatch": "^6.3.0",
|
||||||
@@ -73,14 +73,14 @@
|
|||||||
"xmlbuilder2": "^3.1.1"
|
"xmlbuilder2": "^3.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.26.0",
|
"@eslint/js": "^9.27.0",
|
||||||
"eslint": "^9.26.0",
|
"eslint": "^9.27.0",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"mock-require": "^3.0.3",
|
"mock-require": "^3.0.3",
|
||||||
"p-limit": "^3.1.0",
|
"p-limit": "^3.1.0",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"supertest": "^7.1.0",
|
"supertest": "^7.1.1",
|
||||||
"vitest": "^3.1.3"
|
"vitest": "^3.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,8 @@ const oauthClient = new OAuthClient({
|
|||||||
clientSecret: process.env.QBO_SECRET,
|
clientSecret: process.env.QBO_SECRET,
|
||||||
environment: process.env.NODE_ENV === "production" ? "production" : "sandbox",
|
environment: process.env.NODE_ENV === "production" ? "production" : "sandbox",
|
||||||
redirectUri: process.env.QBO_REDIRECT_URI,
|
redirectUri: process.env.QBO_REDIRECT_URI,
|
||||||
logging: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//TODO:AIO Add in QBO callbacks.
|
|
||||||
const url = InstanceEndpoints();
|
const url = InstanceEndpoints();
|
||||||
|
|
||||||
exports.default = async (req, res) => {
|
exports.default = async (req, res) => {
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ exports.default = async (req, res) => {
|
|||||||
clientSecret: process.env.QBO_SECRET,
|
clientSecret: process.env.QBO_SECRET,
|
||||||
environment: process.env.NODE_ENV === "production" ? "production" : "sandbox",
|
environment: process.env.NODE_ENV === "production" ? "production" : "sandbox",
|
||||||
redirectUri: process.env.QBO_REDIRECT_URI,
|
redirectUri: process.env.QBO_REDIRECT_URI,
|
||||||
logging: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -149,6 +148,15 @@ async function QueryVendorRecord(oauthClient, qbo_realmId, req, bill) {
|
|||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
logger.LogIntegrationCall({
|
||||||
|
platform: "QBO",
|
||||||
|
method: "POST",
|
||||||
|
name: "QueryVendorRecord",
|
||||||
|
billid: bill.id,
|
||||||
|
status: result.response?.status,
|
||||||
|
bodyshopid: bill.job.shopid,
|
||||||
|
email: req.user.email
|
||||||
|
})
|
||||||
setNewRefreshToken(req.user.email, result);
|
setNewRefreshToken(req.user.email, result);
|
||||||
return (
|
return (
|
||||||
result.json &&
|
result.json &&
|
||||||
@@ -178,6 +186,15 @@ async function InsertVendorRecord(oauthClient, qbo_realmId, req, bill) {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify(Vendor)
|
body: JSON.stringify(Vendor)
|
||||||
});
|
});
|
||||||
|
logger.LogIntegrationCall({
|
||||||
|
platform: "QBO",
|
||||||
|
method: "POST",
|
||||||
|
name: "InsertVendorRecord",
|
||||||
|
billid: bill.id,
|
||||||
|
status: result.response?.status,
|
||||||
|
bodyshopid: bill.job.shopid,
|
||||||
|
email: req.user.email
|
||||||
|
})
|
||||||
setNewRefreshToken(req.user.email, result);
|
setNewRefreshToken(req.user.email, result);
|
||||||
return result && result.json && result.json.Vendor;
|
return result && result.json && result.json.Vendor;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -190,7 +207,7 @@ async function InsertVendorRecord(oauthClient, qbo_realmId, req, bill) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop) {
|
async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop) {
|
||||||
const { accounts, taxCodes, classes } = await QueryMetaData(oauthClient, qbo_realmId, req);
|
const { accounts, taxCodes, classes } = await QueryMetaData(oauthClient, qbo_realmId, req, bill.job.shopid);
|
||||||
|
|
||||||
const lines = bill.billlines.map((il) =>
|
const lines = bill.billlines.map((il) =>
|
||||||
generateBillLine(
|
generateBillLine(
|
||||||
@@ -246,11 +263,11 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop)
|
|||||||
.format("YYYY-MM-DD"),
|
.format("YYYY-MM-DD"),
|
||||||
...(!bill.is_credit_memo &&
|
...(!bill.is_credit_memo &&
|
||||||
bill.vendor.due_date && {
|
bill.vendor.due_date && {
|
||||||
DueDate: moment(bill.date)
|
DueDate: moment(bill.date)
|
||||||
//.tz(bill.job.bodyshop.timezone)
|
//.tz(bill.job.bodyshop.timezone)
|
||||||
.add(bill.vendor.due_date, "days")
|
.add(bill.vendor.due_date, "days")
|
||||||
.format("YYYY-MM-DD")
|
.format("YYYY-MM-DD")
|
||||||
}),
|
}),
|
||||||
DocNumber: bill.invoice_number,
|
DocNumber: bill.invoice_number,
|
||||||
//...(bill.job.class ? { ClassRef: { Id: classes[bill.job.class] } } : {}),
|
//...(bill.job.class ? { ClassRef: { Id: classes[bill.job.class] } } : {}),
|
||||||
...(!(
|
...(!(
|
||||||
@@ -263,8 +280,8 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop)
|
|||||||
: {}),
|
: {}),
|
||||||
...(bodyshop.accountingconfig.qbo_departmentid &&
|
...(bodyshop.accountingconfig.qbo_departmentid &&
|
||||||
bodyshop.accountingconfig.qbo_departmentid.trim() !== "" && {
|
bodyshop.accountingconfig.qbo_departmentid.trim() !== "" && {
|
||||||
DepartmentRef: { value: bodyshop.accountingconfig.qbo_departmentid }
|
DepartmentRef: { value: bodyshop.accountingconfig.qbo_departmentid }
|
||||||
}),
|
}),
|
||||||
PrivateNote: `RO ${bill.job.ro_number || ""}`,
|
PrivateNote: `RO ${bill.job.ro_number || ""}`,
|
||||||
Line: lines
|
Line: lines
|
||||||
};
|
};
|
||||||
@@ -280,6 +297,15 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop)
|
|||||||
},
|
},
|
||||||
body: JSON.stringify(billQbo)
|
body: JSON.stringify(billQbo)
|
||||||
});
|
});
|
||||||
|
logger.LogIntegrationCall({
|
||||||
|
platform: "QBO",
|
||||||
|
method: "POST",
|
||||||
|
name: "InsertBill",
|
||||||
|
billid: bill.id,
|
||||||
|
status: result.response?.status,
|
||||||
|
bodyshopid: bill.job.shopid,
|
||||||
|
email: req.user.email
|
||||||
|
})
|
||||||
setNewRefreshToken(req.user.email, result);
|
setNewRefreshToken(req.user.email, result);
|
||||||
return result && result.json && result.json.Bill;
|
return result && result.json && result.json.Bill;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -327,8 +353,8 @@ const generateBillLine = (
|
|||||||
accountingconfig.qbo && accountingconfig.qbo_usa && region_config.includes("CA_")
|
accountingconfig.qbo && accountingconfig.qbo_usa && region_config.includes("CA_")
|
||||||
? {}
|
? {}
|
||||||
: {
|
: {
|
||||||
value: taxCodes[findTaxCode(billLine.applicable_taxes, ioSalesTaxCodes)]
|
value: taxCodes[findTaxCode(billLine.applicable_taxes, ioSalesTaxCodes)]
|
||||||
},
|
},
|
||||||
AccountRef: {
|
AccountRef: {
|
||||||
value: accounts[account.accountname]
|
value: accounts[account.accountname]
|
||||||
}
|
}
|
||||||
@@ -342,7 +368,7 @@ const generateBillLine = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
async function QueryMetaData(oauthClient, qbo_realmId, req) {
|
async function QueryMetaData(oauthClient, qbo_realmId, req, bodyshopid) {
|
||||||
const accounts = await oauthClient.makeApiCall({
|
const accounts = await oauthClient.makeApiCall({
|
||||||
url: urlBuilder(
|
url: urlBuilder(
|
||||||
qbo_realmId,
|
qbo_realmId,
|
||||||
@@ -354,6 +380,14 @@ async function QueryMetaData(oauthClient, qbo_realmId, req) {
|
|||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
logger.LogIntegrationCall({
|
||||||
|
platform: "QBO",
|
||||||
|
method: "POST",
|
||||||
|
name: "QueryAccountType",
|
||||||
|
status: accounts.response?.status,
|
||||||
|
bodyshopid,
|
||||||
|
email: req.user.email
|
||||||
|
})
|
||||||
setNewRefreshToken(req.user.email, accounts);
|
setNewRefreshToken(req.user.email, accounts);
|
||||||
const taxCodes = await oauthClient.makeApiCall({
|
const taxCodes = await oauthClient.makeApiCall({
|
||||||
url: urlBuilder(qbo_realmId, "query", `select * From TaxCode`),
|
url: urlBuilder(qbo_realmId, "query", `select * From TaxCode`),
|
||||||
@@ -362,7 +396,14 @@ async function QueryMetaData(oauthClient, qbo_realmId, req) {
|
|||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
logger.LogIntegrationCall({
|
||||||
|
platform: "QBO",
|
||||||
|
method: "POST",
|
||||||
|
name: "QueryTaxCode",
|
||||||
|
status: taxCodes.status,
|
||||||
|
bodyshopid,
|
||||||
|
email: req.user.email
|
||||||
|
})
|
||||||
const classes = await oauthClient.makeApiCall({
|
const classes = await oauthClient.makeApiCall({
|
||||||
url: urlBuilder(qbo_realmId, "query", `select * From Class`),
|
url: urlBuilder(qbo_realmId, "query", `select * From Class`),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -370,7 +411,14 @@ async function QueryMetaData(oauthClient, qbo_realmId, req) {
|
|||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
logger.LogIntegrationCall({
|
||||||
|
platform: "QBO",
|
||||||
|
method: "POST",
|
||||||
|
name: "QueryClasses",
|
||||||
|
status: classes.status,
|
||||||
|
bodyshopid,
|
||||||
|
email: req.user.email
|
||||||
|
})
|
||||||
const taxCodeMapping = {};
|
const taxCodeMapping = {};
|
||||||
|
|
||||||
taxCodes.json &&
|
taxCodes.json &&
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ exports.default = async (req, res) => {
|
|||||||
clientSecret: process.env.QBO_SECRET,
|
clientSecret: process.env.QBO_SECRET,
|
||||||
environment: process.env.NODE_ENV === "production" ? "production" : "sandbox",
|
environment: process.env.NODE_ENV === "production" ? "production" : "sandbox",
|
||||||
redirectUri: process.env.QBO_REDIRECT_URI,
|
redirectUri: process.env.QBO_REDIRECT_URI,
|
||||||
logging: true
|
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
//Fetch the API Access Tokens & Set them for the session.
|
//Fetch the API Access Tokens & Set them for the session.
|
||||||
@@ -198,7 +197,8 @@ async function InsertPayment(oauthClient, qbo_realmId, req, payment, parentRef,
|
|||||||
req,
|
req,
|
||||||
payment.job.ro_number,
|
payment.job.ro_number,
|
||||||
false,
|
false,
|
||||||
parentRef
|
parentRef,
|
||||||
|
payment.job.shopid
|
||||||
);
|
);
|
||||||
|
|
||||||
if (invoices && invoices.length !== 1) {
|
if (invoices && invoices.length !== 1) {
|
||||||
@@ -227,20 +227,20 @@ async function InsertPayment(oauthClient, qbo_realmId, req, payment, parentRef,
|
|||||||
PaymentRefNum: payment.transactionid,
|
PaymentRefNum: payment.transactionid,
|
||||||
...(invoices && invoices.length === 1 && invoices[0]
|
...(invoices && invoices.length === 1 && invoices[0]
|
||||||
? {
|
? {
|
||||||
Line: [
|
Line: [
|
||||||
{
|
{
|
||||||
Amount: Dinero({
|
Amount: Dinero({
|
||||||
amount: Math.round(payment.amount * 100)
|
amount: Math.round(payment.amount * 100)
|
||||||
}).toFormat(DineroQbFormat),
|
}).toFormat(DineroQbFormat),
|
||||||
LinkedTxn: [
|
LinkedTxn: [
|
||||||
{
|
{
|
||||||
TxnId: invoices[0].Id,
|
TxnId: invoices[0].Id,
|
||||||
TxnType: "Invoice"
|
TxnType: "Invoice"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
: {})
|
: {})
|
||||||
};
|
};
|
||||||
logger.log("qbo-payments-objectlog", "DEBUG", req.user.email, payment.id, {
|
logger.log("qbo-payments-objectlog", "DEBUG", req.user.email, payment.id, {
|
||||||
@@ -255,6 +255,15 @@ async function InsertPayment(oauthClient, qbo_realmId, req, payment, parentRef,
|
|||||||
},
|
},
|
||||||
body: JSON.stringify(paymentQbo)
|
body: JSON.stringify(paymentQbo)
|
||||||
});
|
});
|
||||||
|
logger.LogIntegrationCall({
|
||||||
|
platform: "QBO",
|
||||||
|
method: "POST",
|
||||||
|
name: "InsertPayment",
|
||||||
|
paymentid: payment.id,
|
||||||
|
status: result.response?.status,
|
||||||
|
bodyshopid: payment.job.shopid,
|
||||||
|
email: req.user.email
|
||||||
|
})
|
||||||
setNewRefreshToken(req.user.email, result);
|
setNewRefreshToken(req.user.email, result);
|
||||||
return result && result.Bill;
|
return result && result.Bill;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -266,7 +275,7 @@ async function InsertPayment(oauthClient, qbo_realmId, req, payment, parentRef,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditMemo, parentTierRef) {
|
async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditMemo, parentTierRef, bodyshopid) {
|
||||||
const invoice = await oauthClient.makeApiCall({
|
const invoice = await oauthClient.makeApiCall({
|
||||||
url: urlBuilder(qbo_realmId, "query", `select * From Invoice where DocNumber like '${ro_number}%'`),
|
url: urlBuilder(qbo_realmId, "query", `select * From Invoice where DocNumber like '${ro_number}%'`),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -274,7 +283,15 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditM
|
|||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
logger.LogIntegrationCall({
|
||||||
|
platform: "QBO",
|
||||||
|
method: "POST",
|
||||||
|
name: "QueryInvoice",
|
||||||
|
|
||||||
|
status: invoice.response?.status,
|
||||||
|
bodyshopid,
|
||||||
|
email: req.user.email
|
||||||
|
})
|
||||||
const paymentMethods = await oauthClient.makeApiCall({
|
const paymentMethods = await oauthClient.makeApiCall({
|
||||||
url: urlBuilder(qbo_realmId, "query", `select * From PaymentMethod`),
|
url: urlBuilder(qbo_realmId, "query", `select * From PaymentMethod`),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -282,6 +299,14 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditM
|
|||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
logger.LogIntegrationCall({
|
||||||
|
platform: "QBO",
|
||||||
|
method: "POST",
|
||||||
|
name: "QueryPaymentMethod",
|
||||||
|
status: paymentMethods.response?.status,
|
||||||
|
bodyshopid,
|
||||||
|
email: req.user.email
|
||||||
|
})
|
||||||
setNewRefreshToken(req.user.email, paymentMethods);
|
setNewRefreshToken(req.user.email, paymentMethods);
|
||||||
|
|
||||||
// const classes = await oauthClient.makeApiCall({
|
// const classes = await oauthClient.makeApiCall({
|
||||||
@@ -325,6 +350,15 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditM
|
|||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
logger.LogIntegrationCall({
|
||||||
|
platform: "QBO",
|
||||||
|
method: "POST",
|
||||||
|
name: "QueryTaxCode",
|
||||||
|
|
||||||
|
status: taxCodes.response?.status,
|
||||||
|
bodyshopid,
|
||||||
|
email: req.user.email
|
||||||
|
})
|
||||||
const items = await oauthClient.makeApiCall({
|
const items = await oauthClient.makeApiCall({
|
||||||
url: urlBuilder(qbo_realmId, "query", `select * From Item`),
|
url: urlBuilder(qbo_realmId, "query", `select * From Item`),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -332,6 +366,14 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditM
|
|||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
logger.LogIntegrationCall({
|
||||||
|
platform: "QBO",
|
||||||
|
method: "POST",
|
||||||
|
name: "QueryItems",
|
||||||
|
status: items.response?.status,
|
||||||
|
bodyshopid,
|
||||||
|
email: req.user.email
|
||||||
|
})
|
||||||
setNewRefreshToken(req.user.email, items);
|
setNewRefreshToken(req.user.email, items);
|
||||||
|
|
||||||
const itemMapping = {};
|
const itemMapping = {};
|
||||||
@@ -377,7 +419,8 @@ async function InsertCreditMemo(oauthClient, qbo_realmId, req, payment, parentRe
|
|||||||
req,
|
req,
|
||||||
payment.job.ro_number,
|
payment.job.ro_number,
|
||||||
true,
|
true,
|
||||||
parentRef
|
parentRef,
|
||||||
|
payment.job.shopid
|
||||||
);
|
);
|
||||||
|
|
||||||
if (invoices && invoices.length !== 1) {
|
if (invoices && invoices.length !== 1) {
|
||||||
@@ -406,14 +449,14 @@ async function InsertCreditMemo(oauthClient, qbo_realmId, req, payment, parentRe
|
|||||||
TaxCodeRef: {
|
TaxCodeRef: {
|
||||||
value:
|
value:
|
||||||
taxCodes[
|
taxCodes[
|
||||||
findTaxCode(
|
findTaxCode(
|
||||||
{
|
{
|
||||||
local: false,
|
local: false,
|
||||||
federal: false,
|
federal: false,
|
||||||
state: false
|
state: false
|
||||||
},
|
},
|
||||||
payment.job.bodyshop.md_responsibility_centers.sales_tax_codes
|
payment.job.bodyshop.md_responsibility_centers.sales_tax_codes
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -432,6 +475,15 @@ async function InsertCreditMemo(oauthClient, qbo_realmId, req, payment, parentRe
|
|||||||
},
|
},
|
||||||
body: JSON.stringify(paymentQbo)
|
body: JSON.stringify(paymentQbo)
|
||||||
});
|
});
|
||||||
|
logger.LogIntegrationCall({
|
||||||
|
platform: "QBO",
|
||||||
|
method: "POST",
|
||||||
|
name: "InsertCreditMemo",
|
||||||
|
paymentid: payment.id,
|
||||||
|
status: result.response?.status,
|
||||||
|
bodyshopid: req.user.bodyshopid,
|
||||||
|
email: req.user.email
|
||||||
|
})
|
||||||
setNewRefreshToken(req.user.email, result);
|
setNewRefreshToken(req.user.email, result);
|
||||||
return result && result.Bill;
|
return result && result.Bill;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ exports.default = async (req, res) => {
|
|||||||
clientSecret: process.env.QBO_SECRET,
|
clientSecret: process.env.QBO_SECRET,
|
||||||
environment: process.env.NODE_ENV === "production" ? "production" : "sandbox",
|
environment: process.env.NODE_ENV === "production" ? "production" : "sandbox",
|
||||||
redirectUri: process.env.QBO_REDIRECT_URI,
|
redirectUri: process.env.QBO_REDIRECT_URI,
|
||||||
logging: true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
//Fetch the API Access Tokens & Set them for the session.
|
//Fetch the API Access Tokens & Set them for the session.
|
||||||
const response = await apiGqlClient.request(queries.GET_QBO_AUTH, {
|
const response = await apiGqlClient.request(queries.GET_QBO_AUTH, {
|
||||||
@@ -233,6 +233,15 @@ async function QueryInsuranceCo(oauthClient, qbo_realmId, req, job) {
|
|||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
logger.LogIntegrationCall({
|
||||||
|
platform: "QBO",
|
||||||
|
method: "POST",
|
||||||
|
name: "QueryCustomer",
|
||||||
|
status: result.response?.status,
|
||||||
|
bodyshopid: job.shopid,
|
||||||
|
jobid: job.id,
|
||||||
|
email: req.user.email
|
||||||
|
})
|
||||||
setNewRefreshToken(req.user.email, result);
|
setNewRefreshToken(req.user.email, result);
|
||||||
return (
|
return (
|
||||||
result.json &&
|
result.json &&
|
||||||
@@ -279,6 +288,15 @@ async function InsertInsuranceCo(oauthClient, qbo_realmId, req, job, bodyshop) {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify(Customer)
|
body: JSON.stringify(Customer)
|
||||||
});
|
});
|
||||||
|
logger.LogIntegrationCall({
|
||||||
|
platform: "QBO",
|
||||||
|
method: "POST",
|
||||||
|
name: "InsertCustomer",
|
||||||
|
status: result.response.status,
|
||||||
|
bodyshopid: job.shopid,
|
||||||
|
jobid: job.id,
|
||||||
|
email: req.user.email
|
||||||
|
})
|
||||||
setNewRefreshToken(req.user.email, result);
|
setNewRefreshToken(req.user.email, result);
|
||||||
return result && result.json.Customer;
|
return result && result.json.Customer;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -305,6 +323,15 @@ async function QueryOwner(oauthClient, qbo_realmId, req, job, isThreeTier, paren
|
|||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
logger.LogIntegrationCall({
|
||||||
|
platform: "QBO",
|
||||||
|
method: "POST",
|
||||||
|
name: "QueryCustomer",
|
||||||
|
status: result.response?.status,
|
||||||
|
bodyshopid: job.shopid,
|
||||||
|
jobid: job.id,
|
||||||
|
email: req.user.email
|
||||||
|
})
|
||||||
setNewRefreshToken(req.user.email, result);
|
setNewRefreshToken(req.user.email, result);
|
||||||
return (
|
return (
|
||||||
result.json &&
|
result.json &&
|
||||||
@@ -331,11 +358,11 @@ async function InsertOwner(oauthClient, qbo_realmId, req, job, isThreeTier, pare
|
|||||||
...(job.ownr_ea ? { PrimaryEmailAddr: { Address: job.ownr_ea.trim() } } : {}),
|
...(job.ownr_ea ? { PrimaryEmailAddr: { Address: job.ownr_ea.trim() } } : {}),
|
||||||
...(isThreeTier
|
...(isThreeTier
|
||||||
? {
|
? {
|
||||||
Job: true,
|
Job: true,
|
||||||
ParentRef: {
|
ParentRef: {
|
||||||
value: parentTierRef.Id
|
value: parentTierRef.Id
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
: {})
|
: {})
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
@@ -347,6 +374,15 @@ async function InsertOwner(oauthClient, qbo_realmId, req, job, isThreeTier, pare
|
|||||||
},
|
},
|
||||||
body: JSON.stringify(Customer)
|
body: JSON.stringify(Customer)
|
||||||
});
|
});
|
||||||
|
logger.LogIntegrationCall({
|
||||||
|
platform: "QBO",
|
||||||
|
method: "POST",
|
||||||
|
name: "InsertCustomer",
|
||||||
|
status: result.response?.status,
|
||||||
|
bodyshopid: job.shopid,
|
||||||
|
jobid: job.id,
|
||||||
|
email: req.user.email
|
||||||
|
})
|
||||||
setNewRefreshToken(req.user.email, result);
|
setNewRefreshToken(req.user.email, result);
|
||||||
return result && result.json.Customer;
|
return result && result.json.Customer;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -372,6 +408,15 @@ async function QueryJob(oauthClient, qbo_realmId, req, job, parentTierRef) {
|
|||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
logger.LogIntegrationCall({
|
||||||
|
platform: "QBO",
|
||||||
|
method: "POST",
|
||||||
|
name: "QueryCustomer",
|
||||||
|
status: result.response?.status,
|
||||||
|
bodyshopid: job.shopid,
|
||||||
|
jobid: job.id,
|
||||||
|
email: req.user.email
|
||||||
|
})
|
||||||
setNewRefreshToken(req.user.email, result);
|
setNewRefreshToken(req.user.email, result);
|
||||||
return (
|
return (
|
||||||
result.json &&
|
result.json &&
|
||||||
@@ -411,6 +456,15 @@ async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) {
|
|||||||
},
|
},
|
||||||
body: JSON.stringify(Customer)
|
body: JSON.stringify(Customer)
|
||||||
});
|
});
|
||||||
|
logger.LogIntegrationCall({
|
||||||
|
platform: "QBO",
|
||||||
|
method: "POST",
|
||||||
|
name: "InsertCustomer",
|
||||||
|
status: result.response?.status,
|
||||||
|
bodyshopid: job.shopid,
|
||||||
|
jobid: job.id,
|
||||||
|
email: req.user.email
|
||||||
|
})
|
||||||
setNewRefreshToken(req.user.email, result);
|
setNewRefreshToken(req.user.email, result);
|
||||||
return result && result.json.Customer;
|
return result && result.json.Customer;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -424,7 +478,7 @@ async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) {
|
|||||||
|
|
||||||
exports.InsertJob = InsertJob;
|
exports.InsertJob = InsertJob;
|
||||||
|
|
||||||
async function QueryMetaData(oauthClient, qbo_realmId, req) {
|
async function QueryMetaData(oauthClient, qbo_realmId, req, bodyshopid, jobid) {
|
||||||
const items = await oauthClient.makeApiCall({
|
const items = await oauthClient.makeApiCall({
|
||||||
url: urlBuilder(qbo_realmId, "query", `select * From Item where active=true maxresults 1000`),
|
url: urlBuilder(qbo_realmId, "query", `select * From Item where active=true maxresults 1000`),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -432,6 +486,15 @@ async function QueryMetaData(oauthClient, qbo_realmId, req) {
|
|||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
logger.LogIntegrationCall({
|
||||||
|
platform: "QBO",
|
||||||
|
method: "POST",
|
||||||
|
name: "QueryItems",
|
||||||
|
status: items.response?.status,
|
||||||
|
bodyshopid,
|
||||||
|
jobid: jobid,
|
||||||
|
email: req.user.email
|
||||||
|
})
|
||||||
setNewRefreshToken(req.user.email, items);
|
setNewRefreshToken(req.user.email, items);
|
||||||
const taxCodes = await oauthClient.makeApiCall({
|
const taxCodes = await oauthClient.makeApiCall({
|
||||||
url: urlBuilder(qbo_realmId, "query", `select * From TaxCode where active=true`),
|
url: urlBuilder(qbo_realmId, "query", `select * From TaxCode where active=true`),
|
||||||
@@ -440,7 +503,15 @@ async function QueryMetaData(oauthClient, qbo_realmId, req) {
|
|||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
logger.LogIntegrationCall({
|
||||||
|
platform: "QBO",
|
||||||
|
method: "POST",
|
||||||
|
name: "QueryTaxCodes",
|
||||||
|
status: taxCodes.response?.status,
|
||||||
|
bodyshopid,
|
||||||
|
jobid: jobid,
|
||||||
|
email: req.user.email
|
||||||
|
})
|
||||||
const classes = await oauthClient.makeApiCall({
|
const classes = await oauthClient.makeApiCall({
|
||||||
url: urlBuilder(qbo_realmId, "query", `select * From Class`),
|
url: urlBuilder(qbo_realmId, "query", `select * From Class`),
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -448,7 +519,15 @@ async function QueryMetaData(oauthClient, qbo_realmId, req) {
|
|||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
logger.LogIntegrationCall({
|
||||||
|
platform: "QBO",
|
||||||
|
method: "POST",
|
||||||
|
name: "QueryClasses",
|
||||||
|
status: classes.response?.status,
|
||||||
|
bodyshopid,
|
||||||
|
jobid: jobid,
|
||||||
|
email: req.user.email
|
||||||
|
})
|
||||||
const taxCodeMapping = {};
|
const taxCodeMapping = {};
|
||||||
|
|
||||||
taxCodes.json &&
|
taxCodes.json &&
|
||||||
@@ -483,7 +562,7 @@ async function QueryMetaData(oauthClient, qbo_realmId, req) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function InsertInvoice(oauthClient, qbo_realmId, req, job, bodyshop, parentTierRef) {
|
async function InsertInvoice(oauthClient, qbo_realmId, req, job, bodyshop, parentTierRef) {
|
||||||
const { items, taxCodes, classes } = await QueryMetaData(oauthClient, qbo_realmId, req);
|
const { items, taxCodes, classes } = await QueryMetaData(oauthClient, qbo_realmId, req, job.shopid, job.id);
|
||||||
const InvoiceLineAdd = CreateInvoiceLines({
|
const InvoiceLineAdd = CreateInvoiceLines({
|
||||||
bodyshop,
|
bodyshop,
|
||||||
jobs_by_pk: job,
|
jobs_by_pk: job,
|
||||||
@@ -499,57 +578,55 @@ async function InsertInvoice(oauthClient, qbo_realmId, req, job, bodyshop, paren
|
|||||||
DocNumber: job.ro_number,
|
DocNumber: job.ro_number,
|
||||||
...(job.class ? { ClassRef: { value: classes[job.class] } } : {}),
|
...(job.class ? { ClassRef: { value: classes[job.class] } } : {}),
|
||||||
CustomerMemo: {
|
CustomerMemo: {
|
||||||
value: `${job.clm_no ? `Claim No: ${job.clm_no}` : ``}${
|
value: `${job.clm_no ? `Claim No: ${job.clm_no}` : ``}${job.po_number ? `PO No: ${job.po_number}` : ``
|
||||||
job.po_number ? `PO No: ${job.po_number}` : ``
|
} Vehicle:${job.v_model_yr || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""
|
||||||
} Vehicle:${job.v_model_yr || ""} ${job.v_make_desc || ""} ${
|
} ${job.v_vin || ""} ${job.plate_no || ""} `.trim()
|
||||||
job.v_model_desc || ""
|
|
||||||
} ${job.v_vin || ""} ${job.plate_no || ""} `.trim()
|
|
||||||
},
|
},
|
||||||
CustomerRef: {
|
CustomerRef: {
|
||||||
value: parentTierRef.Id
|
value: parentTierRef.Id
|
||||||
},
|
},
|
||||||
...(bodyshop.accountingconfig.qbo_departmentid &&
|
...(bodyshop.accountingconfig.qbo_departmentid &&
|
||||||
bodyshop.accountingconfig.qbo_departmentid.trim() !== "" && {
|
bodyshop.accountingconfig.qbo_departmentid.trim() !== "" && {
|
||||||
DepartmentRef: { value: bodyshop.accountingconfig.qbo_departmentid }
|
DepartmentRef: { value: bodyshop.accountingconfig.qbo_departmentid }
|
||||||
}),
|
}),
|
||||||
CustomField: [
|
CustomField: [
|
||||||
...(bodyshop.accountingconfig.ReceivableCustomField1
|
...(bodyshop.accountingconfig.ReceivableCustomField1
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
DefinitionId: "1",
|
DefinitionId: "1",
|
||||||
StringValue: job[bodyshop.accountingconfig.ReceivableCustomField1],
|
StringValue: job[bodyshop.accountingconfig.ReceivableCustomField1],
|
||||||
Type: "StringType"
|
Type: "StringType"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...(bodyshop.accountingconfig.ReceivableCustomField2
|
...(bodyshop.accountingconfig.ReceivableCustomField2
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
DefinitionId: "2",
|
DefinitionId: "2",
|
||||||
StringValue: job[bodyshop.accountingconfig.ReceivableCustomField2],
|
StringValue: job[bodyshop.accountingconfig.ReceivableCustomField2],
|
||||||
Type: "StringType"
|
Type: "StringType"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...(bodyshop.accountingconfig.ReceivableCustomField3
|
...(bodyshop.accountingconfig.ReceivableCustomField3
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
DefinitionId: "3",
|
DefinitionId: "3",
|
||||||
StringValue: job[bodyshop.accountingconfig.ReceivableCustomField3],
|
StringValue: job[bodyshop.accountingconfig.ReceivableCustomField3],
|
||||||
Type: "StringType"
|
Type: "StringType"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
: [])
|
: [])
|
||||||
],
|
],
|
||||||
...(bodyshop.accountingconfig &&
|
...(bodyshop.accountingconfig &&
|
||||||
bodyshop.accountingconfig.qbo &&
|
bodyshop.accountingconfig.qbo &&
|
||||||
bodyshop.accountingconfig.qbo_usa && {
|
bodyshop.accountingconfig.qbo_usa && {
|
||||||
TxnTaxDetail: {
|
TxnTaxDetail: {
|
||||||
TxnTaxCodeRef: {
|
TxnTaxCodeRef: {
|
||||||
value: taxCodes[bodyshop.md_responsibility_centers.taxes.state.accountitem]
|
value: taxCodes[bodyshop.md_responsibility_centers.taxes.state.accountitem]
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
...(bodyshop.accountingconfig.printlater ? { PrintStatus: "NeedToPrint" } : {}),
|
...(bodyshop.accountingconfig.printlater ? { PrintStatus: "NeedToPrint" } : {}),
|
||||||
...(bodyshop.accountingconfig.emaillater && job.ownr_ea ? { EmailStatus: "NeedToSend" } : {}),
|
...(bodyshop.accountingconfig.emaillater && job.ownr_ea ? { EmailStatus: "NeedToSend" } : {}),
|
||||||
@@ -575,6 +652,15 @@ async function InsertInvoice(oauthClient, qbo_realmId, req, job, bodyshop, paren
|
|||||||
},
|
},
|
||||||
body: JSON.stringify(invoiceObj)
|
body: JSON.stringify(invoiceObj)
|
||||||
});
|
});
|
||||||
|
logger.LogIntegrationCall({
|
||||||
|
platform: "QBO",
|
||||||
|
method: "POST",
|
||||||
|
name: "InsertInvoice",
|
||||||
|
status: result.response?.status,
|
||||||
|
bodyshopid: job.shopid,
|
||||||
|
jobid: job.id,
|
||||||
|
email: req.user.email
|
||||||
|
})
|
||||||
setNewRefreshToken(req.user.email, result);
|
setNewRefreshToken(req.user.email, result);
|
||||||
return result && result.json && result.json.Invoice;
|
return result && result.json && result.json.Invoice;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -598,7 +684,7 @@ async function InsertInvoiceMultiPayerInvoice(
|
|||||||
payer,
|
payer,
|
||||||
suffix
|
suffix
|
||||||
) {
|
) {
|
||||||
const { items, taxCodes, classes } = await QueryMetaData(oauthClient, qbo_realmId, req);
|
const { items, taxCodes, classes } = await QueryMetaData(oauthClient, qbo_realmId, req, job.shopid);
|
||||||
const InvoiceLineAdd = createMultiQbPayerLines({
|
const InvoiceLineAdd = createMultiQbPayerLines({
|
||||||
bodyshop,
|
bodyshop,
|
||||||
jobs_by_pk: job,
|
jobs_by_pk: job,
|
||||||
@@ -616,58 +702,56 @@ async function InsertInvoiceMultiPayerInvoice(
|
|||||||
DocNumber: job.ro_number + suffix,
|
DocNumber: job.ro_number + suffix,
|
||||||
...(job.class ? { ClassRef: { value: classes[job.class] } } : {}),
|
...(job.class ? { ClassRef: { value: classes[job.class] } } : {}),
|
||||||
CustomerMemo: {
|
CustomerMemo: {
|
||||||
value: `${job.clm_no ? `Claim No: ${job.clm_no}` : ``}${
|
value: `${job.clm_no ? `Claim No: ${job.clm_no}` : ``}${job.po_number ? `PO No: ${job.po_number}` : ``
|
||||||
job.po_number ? `PO No: ${job.po_number}` : ``
|
} Vehicle:${job.v_model_yr || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""
|
||||||
} Vehicle:${job.v_model_yr || ""} ${job.v_make_desc || ""} ${
|
} ${job.v_vin || ""} ${job.plate_no || ""} `.trim()
|
||||||
job.v_model_desc || ""
|
|
||||||
} ${job.v_vin || ""} ${job.plate_no || ""} `.trim()
|
|
||||||
},
|
},
|
||||||
CustomerRef: {
|
CustomerRef: {
|
||||||
value: parentTierRef.Id
|
value: parentTierRef.Id
|
||||||
},
|
},
|
||||||
...(bodyshop.accountingconfig.qbo_departmentid &&
|
...(bodyshop.accountingconfig.qbo_departmentid &&
|
||||||
bodyshop.accountingconfig.qbo_departmentid.trim() !== "" && {
|
bodyshop.accountingconfig.qbo_departmentid.trim() !== "" && {
|
||||||
DepartmentRef: { value: bodyshop.accountingconfig.qbo_departmentid }
|
DepartmentRef: { value: bodyshop.accountingconfig.qbo_departmentid }
|
||||||
}),
|
}),
|
||||||
CustomField: [
|
CustomField: [
|
||||||
...(bodyshop.accountingconfig.ReceivableCustomField1
|
...(bodyshop.accountingconfig.ReceivableCustomField1
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
DefinitionId: "1",
|
DefinitionId: "1",
|
||||||
StringValue: job[bodyshop.accountingconfig.ReceivableCustomField1],
|
StringValue: job[bodyshop.accountingconfig.ReceivableCustomField1],
|
||||||
Type: "StringType"
|
Type: "StringType"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...(bodyshop.accountingconfig.ReceivableCustomField2
|
...(bodyshop.accountingconfig.ReceivableCustomField2
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
DefinitionId: "2",
|
DefinitionId: "2",
|
||||||
StringValue: job[bodyshop.accountingconfig.ReceivableCustomField2],
|
StringValue: job[bodyshop.accountingconfig.ReceivableCustomField2],
|
||||||
Type: "StringType"
|
Type: "StringType"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...(bodyshop.accountingconfig.ReceivableCustomField3
|
...(bodyshop.accountingconfig.ReceivableCustomField3
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
DefinitionId: "3",
|
DefinitionId: "3",
|
||||||
StringValue: job[bodyshop.accountingconfig.ReceivableCustomField3],
|
StringValue: job[bodyshop.accountingconfig.ReceivableCustomField3],
|
||||||
Type: "StringType"
|
Type: "StringType"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
: [])
|
: [])
|
||||||
],
|
],
|
||||||
...(bodyshop.accountingconfig &&
|
...(bodyshop.accountingconfig &&
|
||||||
bodyshop.accountingconfig.qbo &&
|
bodyshop.accountingconfig.qbo &&
|
||||||
bodyshop.accountingconfig.qbo_usa &&
|
bodyshop.accountingconfig.qbo_usa &&
|
||||||
bodyshop.region_config.includes("CA_") && {
|
bodyshop.region_config.includes("CA_") && {
|
||||||
TxnTaxDetail: {
|
TxnTaxDetail: {
|
||||||
TxnTaxCodeRef: {
|
TxnTaxCodeRef: {
|
||||||
value: taxCodes[bodyshop.md_responsibility_centers.taxes.state.accountitem]
|
value: taxCodes[bodyshop.md_responsibility_centers.taxes.state.accountitem]
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
...(bodyshop.accountingconfig.printlater ? { PrintStatus: "NeedToPrint" } : {}),
|
...(bodyshop.accountingconfig.printlater ? { PrintStatus: "NeedToPrint" } : {}),
|
||||||
...(bodyshop.accountingconfig.emaillater && job.ownr_ea ? { EmailStatus: "NeedToSend" } : {}),
|
...(bodyshop.accountingconfig.emaillater && job.ownr_ea ? { EmailStatus: "NeedToSend" } : {}),
|
||||||
@@ -693,6 +777,15 @@ async function InsertInvoiceMultiPayerInvoice(
|
|||||||
},
|
},
|
||||||
body: JSON.stringify(invoiceObj)
|
body: JSON.stringify(invoiceObj)
|
||||||
});
|
});
|
||||||
|
logger.LogIntegrationCall({
|
||||||
|
platform: "QBO",
|
||||||
|
method: "POST",
|
||||||
|
name: "InsertInvoice",
|
||||||
|
status: result.response?.status,
|
||||||
|
bodyshopid: job.shopid,
|
||||||
|
jobid: job.id,
|
||||||
|
email: req.user.email
|
||||||
|
})
|
||||||
setNewRefreshToken(req.user.email, result);
|
setNewRefreshToken(req.user.email, result);
|
||||||
return result && result.json && result.json.Invoice;
|
return result && result.json && result.json.Invoice;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ require("dotenv").config({
|
|||||||
function urlBuilder(realmId, object, query = null) {
|
function urlBuilder(realmId, object, query = null) {
|
||||||
return `https://${
|
return `https://${
|
||||||
process.env.NODE_ENV === "production" ? "" : "sandbox-"
|
process.env.NODE_ENV === "production" ? "" : "sandbox-"
|
||||||
}quickbooks.api.intuit.com/v3/company/${realmId}/${object}${query ? `?query=${encodeURIComponent(query)}` : ""}`;
|
}quickbooks.api.intuit.com/v3/company/${realmId}/${object}?minorversion=75${query ? `&query=${encodeURIComponent(query)}` : ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function StandardizeName(str) {
|
function StandardizeName(str) {
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ exports.chatter = require("./chatter").default;
|
|||||||
exports.claimscorp = require("./claimscorp").default;
|
exports.claimscorp = require("./claimscorp").default;
|
||||||
exports.kaizen = require("./kaizen").default;
|
exports.kaizen = require("./kaizen").default;
|
||||||
exports.usageReport = require("./usageReport").default;
|
exports.usageReport = require("./usageReport").default;
|
||||||
exports.podium = require("./podium").default;
|
exports.podium = require("./podium").default;
|
||||||
|
exports.emsUpload = require("./emsUpload").default;
|
||||||
22
server/data/emsUpload.js
Normal file
22
server/data/emsUpload.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
const moment = require("moment-timezone");
|
||||||
|
const logger = require("../utils/logger");
|
||||||
|
const s3Client = require("../utils/s3"); // Using the S3 client utilities with LocalStack support
|
||||||
|
|
||||||
|
const emsUpload = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { bodyshopid, ciecaid, clm_no, ownr_ln } = req.body;
|
||||||
|
const presignedUrl = await s3Client.getPresignedUrl({
|
||||||
|
bucketName: process.env.S3_EMS_UPLOAD_BUCKET,
|
||||||
|
key: `${bodyshopid}/${ciecaid}-${clm_no}-${ownr_ln}-${moment().format("YYYY-MM-DD--HH-mm-ss")}.zip`
|
||||||
|
});
|
||||||
|
res.status(200).json({ presignedUrl });
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("ems-upload-presign-error", "ERROR", req?.user?.email, null, {
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack
|
||||||
|
});
|
||||||
|
res.status(500).json({ error: error.message, stack: error.stack });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.default = emsUpload;
|
||||||
@@ -185,7 +185,7 @@ async function uploadViaSFTP(csvObj) {
|
|||||||
await sftp.connect(ftpSetup);
|
await sftp.connect(ftpSetup);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
csvObj.result = await sftp.put(Buffer.from(csvObj.xml), `${csvObj.filename}`);
|
csvObj.result = await sftp.put(Buffer.from(csvObj.csv), `${csvObj.filename}`);
|
||||||
logger.log("podium-sftp-upload", "DEBUG", "api", csvObj.bodyshopid, {
|
logger.log("podium-sftp-upload", "DEBUG", "api", csvObj.bodyshopid, {
|
||||||
imexshopid: csvObj.imexshopid,
|
imexshopid: csvObj.imexshopid,
|
||||||
filename: csvObj.filename,
|
filename: csvObj.filename,
|
||||||
|
|||||||
@@ -222,6 +222,7 @@ query QUERY_JOBS_FOR_RECEIVABLES_EXPORT($ids: [uuid!]!) {
|
|||||||
rate_mash
|
rate_mash
|
||||||
rate_matd
|
rate_matd
|
||||||
class
|
class
|
||||||
|
shopid
|
||||||
ca_bc_pvrt
|
ca_bc_pvrt
|
||||||
ca_customer_gst
|
ca_customer_gst
|
||||||
towing_payable
|
towing_payable
|
||||||
@@ -480,6 +481,7 @@ query QUERY_BILLS_FOR_PAYABLES_EXPORT($bills: [uuid!]!) {
|
|||||||
ownr_ln
|
ownr_ln
|
||||||
ownr_co_nm
|
ownr_co_nm
|
||||||
class
|
class
|
||||||
|
shopid
|
||||||
}
|
}
|
||||||
billlines{
|
billlines{
|
||||||
id
|
id
|
||||||
@@ -530,6 +532,7 @@ exports.QUERY_PAYMENTS_FOR_EXPORT = `
|
|||||||
ownr_fn
|
ownr_fn
|
||||||
ownr_ln
|
ownr_ln
|
||||||
ownr_co_nm
|
ownr_co_nm
|
||||||
|
shopid
|
||||||
bodyshop {
|
bodyshop {
|
||||||
accountingconfig
|
accountingconfig
|
||||||
md_responsibility_centers
|
md_responsibility_centers
|
||||||
@@ -1596,6 +1599,7 @@ query QUERY_JOB_COSTING_DETAILS($id: uuid!) {
|
|||||||
ca_customer_gst
|
ca_customer_gst
|
||||||
dms_allocation
|
dms_allocation
|
||||||
cieca_pfl
|
cieca_pfl
|
||||||
|
cieca_stl
|
||||||
materials
|
materials
|
||||||
joblines(where: { removed: { _eq: false } }) {
|
joblines(where: { removed: { _eq: false } }) {
|
||||||
id
|
id
|
||||||
@@ -1712,6 +1716,7 @@ exports.QUERY_JOB_COSTING_DETAILS_MULTI = ` query QUERY_JOB_COSTING_DETAILS_MULT
|
|||||||
ca_customer_gst
|
ca_customer_gst
|
||||||
dms_allocation
|
dms_allocation
|
||||||
cieca_pfl
|
cieca_pfl
|
||||||
|
cieca_stl
|
||||||
materials
|
materials
|
||||||
joblines(where: {removed: {_eq: false}}) {
|
joblines(where: {removed: {_eq: false}}) {
|
||||||
id
|
id
|
||||||
@@ -2968,3 +2973,11 @@ exports.GET_JOB_WATCHERS_MINIMAL = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports.INSERT_INTEGRATION_LOG = `
|
||||||
|
mutation INSERT_INTEGRATION_LOG($log: integration_log_insert_input!) {
|
||||||
|
insert_integration_log_one(object: $log) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -567,6 +567,29 @@ function GenerateCostingData(job) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (InstanceManager({ imex: false, rome: true })) {
|
||||||
|
const stlTowing = job.cieca_stl?.data.find((c) => c.ttl_type === "OTTW");
|
||||||
|
const stlStorage = job.cieca_stl?.data.find((c) => c.ttl_type === "OTST");
|
||||||
|
|
||||||
|
if (!jobLineTotalsByProfitCenter.additional[defaultProfits["TOW"]])
|
||||||
|
jobLineTotalsByProfitCenter.additional[defaultProfits["TOW"]] = Dinero();
|
||||||
|
|
||||||
|
jobLineTotalsByProfitCenter.additional[defaultProfits["TOW"]] = stlTowing
|
||||||
|
? Dinero({ amount: Math.round(stlTowing.ttl_amt * 100) })
|
||||||
|
: Dinero({
|
||||||
|
amount: Math.round((job.towing_payable || 0) * 100)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!jobLineTotalsByProfitCenter.additional[defaultProfits["STO"]])
|
||||||
|
jobLineTotalsByProfitCenter.additional[defaultProfits["STO"]] = Dinero();
|
||||||
|
|
||||||
|
jobLineTotalsByProfitCenter.additional[defaultProfits["STO"]] = stlStorage
|
||||||
|
? Dinero({ amount: Math.round(stlStorage.ttl_amt * 100) })
|
||||||
|
: Dinero({
|
||||||
|
amount: Math.round((job.storage_payable || 0) * 100)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
//Is it a DMS Setup?
|
//Is it a DMS Setup?
|
||||||
const selectedDmsAllocationConfig =
|
const selectedDmsAllocationConfig =
|
||||||
(job.bodyshop.md_responsibility_centers.dms_defaults &&
|
(job.bodyshop.md_responsibility_centers.dms_defaults &&
|
||||||
|
|||||||
@@ -138,6 +138,9 @@ router.post("/canvastest", validateFirebaseIdTokenMiddleware, canvastest);
|
|||||||
// Alert Check
|
// Alert Check
|
||||||
router.post("/alertcheck", eventAuthorizationMiddleware, alertCheck);
|
router.post("/alertcheck", eventAuthorizationMiddleware, alertCheck);
|
||||||
|
|
||||||
|
//EMS Upload
|
||||||
|
router.post("/emsupload", validateFirebaseIdTokenMiddleware, data.emsUpload);
|
||||||
|
|
||||||
// Redis Cache Routes
|
// Redis Cache Routes
|
||||||
router.post("/bodyshop-cache", eventAuthorizationMiddleware, updateBodyshopCache);
|
router.post("/bodyshop-cache", eventAuthorizationMiddleware, updateBodyshopCache);
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const { status, markConversationRead } = require("../sms/status");
|
|||||||
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
||||||
|
|
||||||
// Twilio Webhook Middleware for production
|
// Twilio Webhook Middleware for production
|
||||||
// TODO: Look into this because it technically is never validating anything
|
// TODO: This is never actually doing anything, we should probably verify
|
||||||
const twilioWebhookMiddleware = twilio.webhook({ validate: process.env.NODE_ENV === "PRODUCTION" });
|
const twilioWebhookMiddleware = twilio.webhook({ validate: process.env.NODE_ENV === "PRODUCTION" });
|
||||||
|
|
||||||
router.post("/receive", twilioWebhookMiddleware, receive);
|
router.post("/receive", twilioWebhookMiddleware, receive);
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
const path = require("path");
|
|
||||||
require("dotenv").config({
|
|
||||||
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
|
||||||
});
|
|
||||||
|
|
||||||
const client = require("../graphql-client/graphql-client").client;
|
const client = require("../graphql-client/graphql-client").client;
|
||||||
const queries = require("../graphql-client/queries");
|
const {
|
||||||
|
FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID,
|
||||||
|
UNARCHIVE_CONVERSATION,
|
||||||
|
CREATE_CONVERSATION,
|
||||||
|
INSERT_MESSAGE
|
||||||
|
} = require("../graphql-client/queries");
|
||||||
const { phone } = require("phone");
|
const { phone } = require("phone");
|
||||||
const { admin } = require("../firebase/firebase-handler");
|
const { admin } = require("../firebase/firebase-handler");
|
||||||
const logger = require("../utils/logger");
|
|
||||||
const InstanceManager = require("../utils/instanceMgr").default;
|
const InstanceManager = require("../utils/instanceMgr").default;
|
||||||
|
|
||||||
exports.receive = async (req, res) => {
|
/**
|
||||||
|
* Receive SMS messages from Twilio and process them
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
|
const receive = async (req, res) => {
|
||||||
const {
|
const {
|
||||||
|
logger,
|
||||||
ioRedis,
|
ioRedis,
|
||||||
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
|
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
|
||||||
} = req;
|
} = req;
|
||||||
@@ -20,7 +26,7 @@ exports.receive = async (req, res) => {
|
|||||||
msid: req.body.SmsMessageSid,
|
msid: req.body.SmsMessageSid,
|
||||||
text: req.body.Body,
|
text: req.body.Body,
|
||||||
image: !!req.body.MediaUrl0,
|
image: !!req.body.MediaUrl0,
|
||||||
image_path: generateMediaArray(req.body)
|
image_path: generateMediaArray(req.body, logger)
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.log("sms-inbound", "DEBUG", "api", null, loggerData);
|
logger.log("sms-inbound", "DEBUG", "api", null, loggerData);
|
||||||
@@ -35,7 +41,7 @@ exports.receive = async (req, res) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Step 1: Find the bodyshop and existing conversation
|
// Step 1: Find the bodyshop and existing conversation
|
||||||
const response = await client.request(queries.FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID, {
|
const response = await client.request(FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID, {
|
||||||
mssid: req.body.MessagingServiceSid,
|
mssid: req.body.MessagingServiceSid,
|
||||||
phone: phone(req.body.From).phoneNumber
|
phone: phone(req.body.From).phoneNumber
|
||||||
});
|
});
|
||||||
@@ -46,7 +52,7 @@ exports.receive = async (req, res) => {
|
|||||||
|
|
||||||
const bodyshop = response.bodyshops[0];
|
const bodyshop = response.bodyshops[0];
|
||||||
|
|
||||||
// Sort conversations by `updated_at` (or `created_at`) and pick the last one
|
// Step 4: Process conversation
|
||||||
const sortedConversations = bodyshop.conversations.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
const sortedConversations = bodyshop.conversations.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
||||||
const existingConversation = sortedConversations.length
|
const existingConversation = sortedConversations.length
|
||||||
? sortedConversations[sortedConversations.length - 1]
|
? sortedConversations[sortedConversations.length - 1]
|
||||||
@@ -57,25 +63,21 @@ exports.receive = async (req, res) => {
|
|||||||
msid: req.body.SmsMessageSid,
|
msid: req.body.SmsMessageSid,
|
||||||
text: req.body.Body,
|
text: req.body.Body,
|
||||||
image: !!req.body.MediaUrl0,
|
image: !!req.body.MediaUrl0,
|
||||||
image_path: generateMediaArray(req.body),
|
image_path: generateMediaArray(req.body, logger),
|
||||||
isoutbound: false,
|
isoutbound: false,
|
||||||
userid: null // Add additional fields as necessary
|
userid: null
|
||||||
};
|
};
|
||||||
|
|
||||||
if (existingConversation) {
|
if (existingConversation) {
|
||||||
// Use the existing conversation
|
|
||||||
conversationid = existingConversation.id;
|
conversationid = existingConversation.id;
|
||||||
|
|
||||||
// Unarchive the conversation if necessary
|
|
||||||
if (existingConversation.archived) {
|
if (existingConversation.archived) {
|
||||||
await client.request(queries.UNARCHIVE_CONVERSATION, {
|
await client.request(UNARCHIVE_CONVERSATION, {
|
||||||
id: conversationid,
|
id: conversationid,
|
||||||
archived: false
|
archived: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Create a new conversation
|
const newConversationResponse = await client.request(CREATE_CONVERSATION, {
|
||||||
const newConversationResponse = await client.request(queries.CREATE_CONVERSATION, {
|
|
||||||
conversation: {
|
conversation: {
|
||||||
bodyshopid: bodyshop.id,
|
bodyshopid: bodyshop.id,
|
||||||
phone_num: phone(req.body.From).phoneNumber,
|
phone_num: phone(req.body.From).phoneNumber,
|
||||||
@@ -86,13 +88,12 @@ exports.receive = async (req, res) => {
|
|||||||
conversationid = createdConversation.id;
|
conversationid = createdConversation.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure `conversationid` is added to the message
|
|
||||||
newMessage.conversationid = conversationid;
|
newMessage.conversationid = conversationid;
|
||||||
|
|
||||||
// Step 3: Insert the message into the conversation
|
// Step 5: Insert the message
|
||||||
const insertresp = await client.request(queries.INSERT_MESSAGE, {
|
const insertresp = await client.request(INSERT_MESSAGE, {
|
||||||
msg: newMessage,
|
msg: newMessage,
|
||||||
conversationid: conversationid
|
conversationid
|
||||||
});
|
});
|
||||||
|
|
||||||
const message = insertresp?.insert_messages?.returning?.[0];
|
const message = insertresp?.insert_messages?.returning?.[0];
|
||||||
@@ -102,8 +103,7 @@ exports.receive = async (req, res) => {
|
|||||||
throw new Error("Conversation data is missing from the response.");
|
throw new Error("Conversation data is missing from the response.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: Notify clients through Redis
|
// Step 6: Notify clients
|
||||||
const broadcastRoom = getBodyshopRoom(conversation.bodyshop.id);
|
|
||||||
const conversationRoom = getBodyshopConversationRoom({
|
const conversationRoom = getBodyshopConversationRoom({
|
||||||
bodyshopId: conversation.bodyshop.id,
|
bodyshopId: conversation.bodyshop.id,
|
||||||
conversationId: conversation.id
|
conversationId: conversation.id
|
||||||
@@ -116,6 +116,8 @@ exports.receive = async (req, res) => {
|
|||||||
msid: message.sid
|
msid: message.sid
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const broadcastRoom = getBodyshopRoom(conversation.bodyshop.id);
|
||||||
|
|
||||||
ioRedis.to(broadcastRoom).emit("new-message-summary", {
|
ioRedis.to(broadcastRoom).emit("new-message-summary", {
|
||||||
...commonPayload,
|
...commonPayload,
|
||||||
existingConversation: !!existingConversation,
|
existingConversation: !!existingConversation,
|
||||||
@@ -131,13 +133,13 @@ exports.receive = async (req, res) => {
|
|||||||
summary: false
|
summary: false
|
||||||
});
|
});
|
||||||
|
|
||||||
// Step 5: Send FCM notification
|
// Step 7: Send FCM notification
|
||||||
const fcmresp = await admin.messaging().send({
|
const fcmresp = await admin.messaging().send({
|
||||||
topic: `${message.conversation.bodyshop.imexshopid}-messaging`,
|
topic: `${message.conversation.bodyshop.imexshopid}-messaging`,
|
||||||
notification: {
|
notification: {
|
||||||
title: InstanceManager({
|
title: InstanceManager({
|
||||||
imex: `ImEX Online Message - ${message.conversation.phone_num}`,
|
imex: `ImEX Online Message - ${message.conversation.phone_num}`,
|
||||||
rome: `Rome Online Message - ${message.conversation.phone_num}`,
|
rome: `Rome Online Message - ${message.conversation.phone_num}`
|
||||||
}),
|
}),
|
||||||
body: message.image_path ? `Image ${message.text}` : message.text
|
body: message.image_path ? `Image ${message.text}` : message.text
|
||||||
},
|
},
|
||||||
@@ -157,11 +159,17 @@ exports.receive = async (req, res) => {
|
|||||||
|
|
||||||
res.status(200).send("");
|
res.status(200).send("");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
handleError(req, e, res, "RECEIVE_MESSAGE");
|
handleError(req, e, res, "RECEIVE_MESSAGE", logger);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateMediaArray = (body) => {
|
/**
|
||||||
|
* Generate media array from the request body
|
||||||
|
* @param body
|
||||||
|
* @param logger
|
||||||
|
* @returns {null|*[]}
|
||||||
|
*/
|
||||||
|
const generateMediaArray = (body, logger) => {
|
||||||
const { NumMedia } = body;
|
const { NumMedia } = body;
|
||||||
if (parseInt(NumMedia) > 0) {
|
if (parseInt(NumMedia) > 0) {
|
||||||
const ret = [];
|
const ret = [];
|
||||||
@@ -174,12 +182,20 @@ const generateMediaArray = (body) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleError = (req, error, res, context) => {
|
/**
|
||||||
|
* Handle error logging and response
|
||||||
|
* @param req
|
||||||
|
* @param error
|
||||||
|
* @param res
|
||||||
|
* @param context
|
||||||
|
* @param logger
|
||||||
|
*/
|
||||||
|
const handleError = (req, error, res, context, logger) => {
|
||||||
logger.log("sms-inbound-error", "ERROR", "api", null, {
|
logger.log("sms-inbound-error", "ERROR", "api", null, {
|
||||||
msid: req.body.SmsMessageSid,
|
msid: req.body.SmsMessageSid,
|
||||||
text: req.body.Body,
|
text: req.body.Body,
|
||||||
image: !!req.body.MediaUrl0,
|
image: !!req.body.MediaUrl0,
|
||||||
image_path: generateMediaArray(req.body),
|
image_path: generateMediaArray(req.body, logger),
|
||||||
messagingServiceSid: req.body.MessagingServiceSid,
|
messagingServiceSid: req.body.MessagingServiceSid,
|
||||||
context,
|
context,
|
||||||
error
|
error
|
||||||
@@ -187,3 +203,7 @@ const handleError = (req, error, res, context) => {
|
|||||||
|
|
||||||
res.status(500).json({ error: error.message || "Internal Server Error" });
|
res.status(500).json({ error: error.message || "Internal Server Error" });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
receive
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
const path = require("path");
|
|
||||||
require("dotenv").config({
|
|
||||||
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
|
||||||
});
|
|
||||||
|
|
||||||
const twilio = require("twilio");
|
const twilio = require("twilio");
|
||||||
const { phone } = require("phone");
|
const { phone } = require("phone");
|
||||||
const queries = require("../graphql-client/queries");
|
const { INSERT_MESSAGE } = require("../graphql-client/queries");
|
||||||
const logger = require("../utils/logger");
|
|
||||||
const client = twilio(process.env.TWILIO_AUTH_TOKEN, process.env.TWILIO_AUTH_KEY);
|
const client = twilio(process.env.TWILIO_AUTH_TOKEN, process.env.TWILIO_AUTH_KEY);
|
||||||
const gqlClient = require("../graphql-client/graphql-client").client;
|
const gqlClient = require("../graphql-client/graphql-client").client;
|
||||||
|
|
||||||
exports.send = async (req, res) => {
|
/**
|
||||||
|
* Send an outbound SMS message
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
const send = async (req, res) => {
|
||||||
const { to, messagingServiceSid, body, conversationid, selectedMedia, imexshopid } = req.body;
|
const { to, messagingServiceSid, body, conversationid, selectedMedia, imexshopid } = req.body;
|
||||||
const {
|
const {
|
||||||
ioRedis,
|
ioRedis,
|
||||||
|
logger,
|
||||||
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
|
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
|
||||||
} = req;
|
} = req;
|
||||||
|
|
||||||
@@ -25,8 +26,8 @@ exports.send = async (req, res) => {
|
|||||||
conversationid,
|
conversationid,
|
||||||
isoutbound: true,
|
isoutbound: true,
|
||||||
userid: req.user.email,
|
userid: req.user.email,
|
||||||
image: req.body.selectedMedia.length > 0,
|
image: selectedMedia.length > 0,
|
||||||
image_path: req.body.selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : []
|
image_path: selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : []
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!to || !messagingServiceSid || (!body && selectedMedia.length === 0) || !conversationid) {
|
if (!to || !messagingServiceSid || (!body && selectedMedia.length === 0) || !conversationid) {
|
||||||
@@ -38,8 +39,8 @@ exports.send = async (req, res) => {
|
|||||||
conversationid,
|
conversationid,
|
||||||
isoutbound: true,
|
isoutbound: true,
|
||||||
userid: req.user.email,
|
userid: req.user.email,
|
||||||
image: req.body.selectedMedia.length > 0,
|
image: selectedMedia.length > 0,
|
||||||
image_path: req.body.selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : []
|
image_path: selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : []
|
||||||
});
|
});
|
||||||
res.status(400).json({ success: false, message: "Missing required parameter(s)." });
|
res.status(400).json({ success: false, message: "Missing required parameter(s)." });
|
||||||
return;
|
return;
|
||||||
@@ -59,12 +60,15 @@ exports.send = async (req, res) => {
|
|||||||
conversationid,
|
conversationid,
|
||||||
isoutbound: true,
|
isoutbound: true,
|
||||||
userid: req.user.email,
|
userid: req.user.email,
|
||||||
image: req.body.selectedMedia.length > 0,
|
image: selectedMedia.length > 0,
|
||||||
image_path: req.body.selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : []
|
image_path: selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : []
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const gqlResponse = await gqlClient.request(queries.INSERT_MESSAGE, { msg: newMessage, conversationid });
|
const gqlResponse = await gqlClient.request(INSERT_MESSAGE, {
|
||||||
|
msg: newMessage,
|
||||||
|
conversationid
|
||||||
|
});
|
||||||
|
|
||||||
logger.log("sms-outbound-success", "DEBUG", req.user.email, null, {
|
logger.log("sms-outbound-success", "DEBUG", req.user.email, null, {
|
||||||
msid: message.sid,
|
msid: message.sid,
|
||||||
@@ -111,3 +115,7 @@ exports.send = async (req, res) => {
|
|||||||
res.status(500).json({ success: false, message: "Failed to send message through Twilio." });
|
res.status(500).json({ success: false, message: "Failed to send message through Twilio." });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
send
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
const path = require("path");
|
|
||||||
require("dotenv").config({
|
|
||||||
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV || "development"}`)
|
|
||||||
});
|
|
||||||
|
|
||||||
const client = require("../graphql-client/graphql-client").client;
|
const client = require("../graphql-client/graphql-client").client;
|
||||||
const queries = require("../graphql-client/queries");
|
const { UPDATE_MESSAGE_STATUS, MARK_MESSAGES_AS_READ } = require("../graphql-client/queries");
|
||||||
const logger = require("../utils/logger");
|
const logger = require("../utils/logger");
|
||||||
|
|
||||||
exports.status = async (req, res) => {
|
/**
|
||||||
|
* Handle the status of an SMS message
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
|
const status = async (req, res) => {
|
||||||
const { SmsSid, SmsStatus } = req.body;
|
const { SmsSid, SmsStatus } = req.body;
|
||||||
const {
|
const {
|
||||||
ioRedis,
|
ioRedis,
|
||||||
@@ -21,7 +22,7 @@ exports.status = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update message status in the database
|
// Update message status in the database
|
||||||
const response = await client.request(queries.UPDATE_MESSAGE_STATUS, {
|
const response = await client.request(UPDATE_MESSAGE_STATUS, {
|
||||||
msid: SmsSid,
|
msid: SmsSid,
|
||||||
fields: { status: SmsStatus }
|
fields: { status: SmsStatus }
|
||||||
});
|
});
|
||||||
@@ -65,7 +66,13 @@ exports.status = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.markConversationRead = async (req, res) => {
|
/**
|
||||||
|
* Mark a conversation as read
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
|
const markConversationRead = async (req, res) => {
|
||||||
const {
|
const {
|
||||||
ioRedis,
|
ioRedis,
|
||||||
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
|
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
|
||||||
@@ -80,7 +87,7 @@ exports.markConversationRead = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await client.request(queries.MARK_MESSAGES_AS_READ, {
|
const response = await client.request(MARK_MESSAGES_AS_READ, {
|
||||||
conversationId
|
conversationId
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -104,3 +111,8 @@ exports.markConversationRead = async (req, res) => {
|
|||||||
res.status(500).json({ error: "Failed to mark conversation as read." });
|
res.status(500).json({ error: "Failed to mark conversation as read." });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
status,
|
||||||
|
markConversationRead
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* @module ioHelpers
|
||||||
|
* @param app
|
||||||
|
* @param api
|
||||||
|
* @param io
|
||||||
|
* @param logger
|
||||||
|
* @returns {{getBodyshopRoom: (function(*): string), getBodyshopConversationRoom: (function({bodyshopId: *, conversationId: *}): string)}}
|
||||||
|
*/
|
||||||
const applyIOHelpers = ({ app, api, io, logger }) => {
|
const applyIOHelpers = ({ app, api, io, logger }) => {
|
||||||
// Global Bodyshop Room
|
// Global Bodyshop Room
|
||||||
const getBodyshopRoom = (bodyshopId) => `bodyshop-broadcast-room:${bodyshopId}`;
|
const getBodyshopRoom = (bodyshopId) => `bodyshop-broadcast-room:${bodyshopId}`;
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ const { uploadFileToS3 } = require("./s3");
|
|||||||
const { v4 } = require("uuid");
|
const { v4 } = require("uuid");
|
||||||
const { InstanceRegion } = require("./instanceMgr");
|
const { InstanceRegion } = require("./instanceMgr");
|
||||||
const getHostNameOrIP = require("./getHostNameOrIP");
|
const getHostNameOrIP = require("./getHostNameOrIP");
|
||||||
|
const client = require("../graphql-client/graphql-client").client;
|
||||||
|
const queries = require("../graphql-client/queries");
|
||||||
|
|
||||||
|
|
||||||
const LOG_LEVELS = {
|
const LOG_LEVELS = {
|
||||||
error: { level: 0, name: "error" },
|
error: { level: 0, name: "error" },
|
||||||
@@ -99,13 +102,11 @@ const createLogger = () => {
|
|||||||
const labelColor = "\x1b[33m"; // Yellow
|
const labelColor = "\x1b[33m"; // Yellow
|
||||||
const separatorColor = "\x1b[35m|\x1b[0m"; // Magenta for separators
|
const separatorColor = "\x1b[35m|\x1b[0m"; // Magenta for separators
|
||||||
|
|
||||||
return `${timestampColor} [${hostnameColor}] [${level}]: ${message} ${
|
return `${timestampColor} [${hostnameColor}] [${level}]: ${message} ${user ? `${separatorColor} ${labelColor}user:\x1b[0m ${JSON.stringify(user)}` : ""
|
||||||
user ? `${separatorColor} ${labelColor}user:\x1b[0m ${JSON.stringify(user)}` : ""
|
} ${record ? `${separatorColor} ${labelColor}record:\x1b[0m ${JSON.stringify(record)}` : ""}${meta
|
||||||
} ${record ? `${separatorColor} ${labelColor}record:\x1b[0m ${JSON.stringify(record)}` : ""}${
|
|
||||||
meta
|
|
||||||
? `\n${separatorColor} ${labelColor}meta:\x1b[0m ${JSON.stringify(meta, null, 2)} ${separatorColor}`
|
? `\n${separatorColor} ${labelColor}meta:\x1b[0m ${JSON.stringify(meta, null, 2)} ${separatorColor}`
|
||||||
: ""
|
: ""
|
||||||
}`;
|
}`;
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -194,9 +195,45 @@ const createLogger = () => {
|
|||||||
winstonLogger.log(logEntry);
|
winstonLogger.log(logEntry);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const LogIntegrationCall = async ({ platform, method, name, jobid, paymentid, billid, status, bodyshopid, email }) => {
|
||||||
|
try {
|
||||||
|
//Insert the record.
|
||||||
|
await client.request(queries.INSERT_INTEGRATION_LOG, {
|
||||||
|
log: {
|
||||||
|
platform,
|
||||||
|
method,
|
||||||
|
name,
|
||||||
|
jobid,
|
||||||
|
paymentid,
|
||||||
|
billid,
|
||||||
|
status: status?.toString() ?? "0",
|
||||||
|
bodyshopid,
|
||||||
|
email
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.trace("Stack", error?.stack);
|
||||||
|
log("integration-log-error", "ERROR", email, null, {
|
||||||
|
message: error?.message,
|
||||||
|
stack: error?.stack,
|
||||||
|
platform,
|
||||||
|
method,
|
||||||
|
name,
|
||||||
|
jobid,
|
||||||
|
paymentid,
|
||||||
|
billid,
|
||||||
|
status,
|
||||||
|
bodyshopid,
|
||||||
|
email
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
log,
|
log,
|
||||||
logger: winstonLogger
|
logger: winstonLogger,
|
||||||
|
LogIntegrationCall
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error setting up enhanced Logger, defaulting to console.: " + e?.message || "");
|
console.error("Error setting up enhanced Logger, defaulting to console.: " + e?.message || "");
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const {
|
|||||||
const { defaultProvider } = require("@aws-sdk/credential-provider-node");
|
const { defaultProvider } = require("@aws-sdk/credential-provider-node");
|
||||||
const { InstanceRegion } = require("./instanceMgr");
|
const { InstanceRegion } = require("./instanceMgr");
|
||||||
const { isString, isEmpty } = require("lodash");
|
const { isString, isEmpty } = require("lodash");
|
||||||
|
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
|
||||||
|
|
||||||
const createS3Client = () => {
|
const createS3Client = () => {
|
||||||
const S3Options = {
|
const S3Options = {
|
||||||
@@ -95,6 +96,17 @@ const createS3Client = () => {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getPresignedUrl = async ({ bucketName, key }) => {
|
||||||
|
const command = new PutObjectCommand({
|
||||||
|
Bucket: bucketName,
|
||||||
|
Key: key,
|
||||||
|
StorageClass: "INTELLIGENT_TIERING"
|
||||||
|
});
|
||||||
|
const presignedUrl = await getSignedUrl(s3Client, command, { expiresIn: 360 });
|
||||||
|
return presignedUrl;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
uploadFileToS3,
|
uploadFileToS3,
|
||||||
downloadFileFromS3,
|
downloadFileFromS3,
|
||||||
@@ -102,8 +114,12 @@ const createS3Client = () => {
|
|||||||
deleteFileFromS3,
|
deleteFileFromS3,
|
||||||
copyFileInS3,
|
copyFileInS3,
|
||||||
fileExistsInS3,
|
fileExistsInS3,
|
||||||
|
getPresignedUrl,
|
||||||
...s3Client
|
...s3Client
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = createS3Client();
|
module.exports = createS3Client();
|
||||||
|
|||||||
Reference in New Issue
Block a user