Compare commits
37 Commits
feature/IO
...
feature/IO
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99cf95daf0 | ||
|
|
8c1758ae49 | ||
|
|
2d764921ff | ||
|
|
4859239f55 | ||
|
|
5c64d7185e | ||
|
|
152479bc08 | ||
|
|
2c508cf1a1 | ||
|
|
16a91c772a | ||
|
|
5c47088b11 | ||
|
|
8e5dc4fa71 | ||
|
|
39c3729f6d | ||
|
|
e3d854e02b | ||
|
|
618acf2acf | ||
|
|
2cf2b70293 | ||
|
|
0541afceb8 | ||
|
|
28ed3f9936 | ||
|
|
6afa50332b | ||
|
|
8c8c68867d | ||
|
|
8ee52598e8 | ||
|
|
c822028174 | ||
|
|
36b82c6195 | ||
|
|
079dffce4d | ||
|
|
7bd5190bf2 | ||
|
|
83860152a9 | ||
|
|
1e10493615 | ||
|
|
9d81c68a4d | ||
|
|
985d066978 | ||
|
|
6ad9e27d1d | ||
|
|
19ebdda5b3 | ||
|
|
4602dd1183 | ||
|
|
6d59e3994f | ||
|
|
f770b2f1b1 | ||
|
|
b014744940 | ||
|
|
714c90c25e | ||
|
|
9a3a971da6 | ||
|
|
96cba0aaab | ||
|
|
d4f718c44c |
355
client/package-lock.json
generated
355
client/package-lock.json
generated
@@ -13,19 +13,19 @@
|
||||
"@apollo/client": "^3.13.6",
|
||||
"@emotion/is-prop-valid": "^1.3.1",
|
||||
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
||||
"@firebase/analytics": "^0.10.13",
|
||||
"@firebase/app": "^0.12.1",
|
||||
"@firebase/auth": "^1.10.2",
|
||||
"@firebase/firestore": "^4.7.12",
|
||||
"@firebase/messaging": "^0.12.18",
|
||||
"@firebase/analytics": "^0.10.16",
|
||||
"@firebase/app": "^0.13.0",
|
||||
"@firebase/auth": "^1.10.5",
|
||||
"@firebase/firestore": "^4.7.15",
|
||||
"@firebase/messaging": "^0.12.21",
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@reduxjs/toolkit": "^2.8.1",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"@sentry/cli": "^2.45.0",
|
||||
"@sentry/react": "^9.18.0",
|
||||
"@sentry/vite-plugin": "^3.4.0",
|
||||
"@sentry/react": "^9.22.0",
|
||||
"@sentry/vite-plugin": "^3.5.0",
|
||||
"@splitsoftware/splitio-react": "^2.1.1",
|
||||
"@tanem/react-nprogress": "^5.0.53",
|
||||
"antd": "^5.25.1",
|
||||
"antd": "^5.25.2",
|
||||
"apollo-link-logger": "^2.0.1",
|
||||
"apollo-link-sentry": "^4.3.0",
|
||||
"autosize": "^6.0.1",
|
||||
@@ -49,7 +49,7 @@
|
||||
"normalize-url": "^8.0.1",
|
||||
"object-hash": "^3.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^9.1.2",
|
||||
"query-string": "^9.2.0",
|
||||
"raf-schd": "^4.0.3",
|
||||
"react": "^18.3.1",
|
||||
"react-big-calendar": "^1.18.0",
|
||||
@@ -78,7 +78,7 @@
|
||||
"redux-saga": "^1.3.0",
|
||||
"redux-state-sync": "^3.1.4",
|
||||
"reselect": "^5.1.1",
|
||||
"sass": "^1.88.0",
|
||||
"sass": "^1.89.0",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"styled-components": "^6.1.18",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
@@ -90,12 +90,12 @@
|
||||
"@ant-design/icons": "^6.0.0",
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/preset-react": "^7.27.1",
|
||||
"@dotenvx/dotenvx": "^1.44.0",
|
||||
"@dotenvx/dotenvx": "^1.44.1",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@eslint/js": "^9.26.0",
|
||||
"@eslint/js": "^9.27.0",
|
||||
"@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/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
@@ -108,7 +108,7 @@
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^15.15.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"memfs": "^4.17.1",
|
||||
"memfs": "^4.17.2",
|
||||
"os-browserify": "^0.3.0",
|
||||
"playwright": "^1.51.1",
|
||||
"react-error-overlay": "^6.1.0",
|
||||
@@ -120,7 +120,7 @@
|
||||
"vite-plugin-node-polyfills": "^0.23.0",
|
||||
"vite-plugin-pwa": "^1.0.0",
|
||||
"vite-plugin-style-import": "^2.0.0",
|
||||
"vitest": "^3.1.3",
|
||||
"vitest": "^3.1.4",
|
||||
"workbox-window": "^7.3.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2587,9 +2587,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@dotenvx/dotenvx": {
|
||||
"version": "1.44.0",
|
||||
"resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.44.0.tgz",
|
||||
"integrity": "sha512-18Aa+7KP/L2Kj9lxmT4EJZnsCq/xGIHgzU26rdzsKMhjpeT3YY+qin/dNAnIaVHPZnee7kXpZL55M9htd30r7Q==",
|
||||
"version": "1.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.44.1.tgz",
|
||||
"integrity": "sha512-j1QImCqf/XJmhIjC1OPpgiZV9g370HG9MNT9s/CDwCKsoYzNCPEKK+GfsidahJx7yIlBbm+4dPLlGec+bKn7oA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
@@ -2912,13 +2912,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.26.0.tgz",
|
||||
"integrity": "sha512-I9XlJawFdSMvWjDt6wksMCrgns5ggLNfFwFvnShsleWruvXM514Qxk8V246efTw+eo9JABvVz+u3q2RiAowKxQ==",
|
||||
"version": "9.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz",
|
||||
"integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://eslint.org/donate"
|
||||
}
|
||||
},
|
||||
"node_modules/@fingerprintjs/fingerprintjs": {
|
||||
@@ -2931,15 +2934,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@firebase/analytics": {
|
||||
"version": "0.10.13",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.13.tgz",
|
||||
"integrity": "sha512-X+6wMOPgA9l0AeeMdMcMfaCP4XKPvrhx55MGuMrfHvUrOvFKldpzBum7KkoGJMoexKmqmKP+mCmJMY9Fb8K6Hw==",
|
||||
"version": "0.10.16",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.16.tgz",
|
||||
"integrity": "sha512-cMtp19He7Fd6uaj/nDEul+8JwvJsN8aRSJyuA1QN3QrKvfDDp+efjVurJO61sJpkVftw9O9nNMdhFbRcTmTfRQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@firebase/component": "0.6.14",
|
||||
"@firebase/installations": "0.6.14",
|
||||
"@firebase/component": "0.6.17",
|
||||
"@firebase/installations": "0.6.17",
|
||||
"@firebase/logger": "0.4.4",
|
||||
"@firebase/util": "1.11.1",
|
||||
"@firebase/util": "1.12.0",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -2947,14 +2950,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@firebase/app": {
|
||||
"version": "0.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.12.1.tgz",
|
||||
"integrity": "sha512-ASExOlmmjRMdwOQ65Oj6R9JBqa7iiT1/LgZjtbU7FqxoJZNWHrt39NJ/z2bjyYDdAHX8jkY7muFqzahScCXgfA==",
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.13.0.tgz",
|
||||
"integrity": "sha512-Vj3MST245nq+V5UmmfEkB3isIgPouyUr8yGJlFeL9Trg/umG5ogAvrjAYvQ8gV7daKDoQSRnJKWI2JFpQqRsuQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@firebase/component": "0.6.14",
|
||||
"@firebase/component": "0.6.17",
|
||||
"@firebase/logger": "0.4.4",
|
||||
"@firebase/util": "1.11.1",
|
||||
"@firebase/util": "1.12.0",
|
||||
"idb": "7.1.1",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
@@ -2963,14 +2966,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@firebase/auth": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.10.2.tgz",
|
||||
"integrity": "sha512-HHudcj3CJyXpoMKslNOVHGSNJdAUjvy5xBA/G/uPb32QFqvx5F3EW9RDYvve2IHEN7Vpc1QTkk/28J32x83UGA==",
|
||||
"version": "1.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.10.5.tgz",
|
||||
"integrity": "sha512-6wF/NdMTwObL4RNQePunuzMr9O3gyftisvFZFFKf57D2HONXo87YymogRV8d+Z7SLA0rcNBN1gLJVk2D0y97gA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@firebase/component": "0.6.14",
|
||||
"@firebase/component": "0.6.17",
|
||||
"@firebase/logger": "0.4.4",
|
||||
"@firebase/util": "1.11.1",
|
||||
"@firebase/util": "1.12.0",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2987,12 +2990,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@firebase/component": {
|
||||
"version": "0.6.14",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.14.tgz",
|
||||
"integrity": "sha512-kf/zAT8GQJ9nYoHuj0mv7twp1QzifKYrO+GsmsVHHM+Hi9KkmI7E3B3J0CtihHpb34vinl4gbJrYJ2p2wfvc9A==",
|
||||
"version": "0.6.17",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.17.tgz",
|
||||
"integrity": "sha512-M6DOg7OySrKEFS8kxA3MU5/xc37fiOpKPMz6cTsMUcsuKB6CiZxxNAvgFta8HGRgEpZbi8WjGIj6Uf+TpOhyzg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@firebase/util": "1.11.1",
|
||||
"@firebase/util": "1.12.0",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -3000,14 +3003,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@firebase/firestore": {
|
||||
"version": "4.7.12",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.7.12.tgz",
|
||||
"integrity": "sha512-50KRdSp8xA7+G0wfWxlnCoEN951mt8BVdLMxeP57Rehj2DqIb41q6Fc6JH0dfQ4TlMqWua1YfVY1jPEAaHVF9w==",
|
||||
"version": "4.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.7.15.tgz",
|
||||
"integrity": "sha512-FgWTmkNBEXdKCoN2ngBNjrMaXuBx6QwjiZZVnOGg+VjUmiBq5gAqlDIW5bZY6i/NYvLUrWugdqIs7y9GHEqwww==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@firebase/component": "0.6.14",
|
||||
"@firebase/component": "0.6.17",
|
||||
"@firebase/logger": "0.4.4",
|
||||
"@firebase/util": "1.11.1",
|
||||
"@firebase/util": "1.12.0",
|
||||
"@firebase/webchannel-wrapper": "1.0.3",
|
||||
"@grpc/grpc-js": "~1.9.0",
|
||||
"@grpc/proto-loader": "^0.7.8",
|
||||
@@ -3021,13 +3024,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@firebase/installations": {
|
||||
"version": "0.6.14",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.14.tgz",
|
||||
"integrity": "sha512-uE837g9+sv6PfjWPgOfG3JtjZ+hJ7KBHO4UVenVsvhzgOxFkvLjO/bgE7fyvsaD3fOHSXunx3adRIg4eUEMPyA==",
|
||||
"version": "0.6.17",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.17.tgz",
|
||||
"integrity": "sha512-zfhqCNJZRe12KyADtRrtOj+SeSbD1H/K8J24oQAJVv/u02eQajEGlhZtcx9Qk7vhGWF5z9dvIygVDYqLL4o1XQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@firebase/component": "0.6.14",
|
||||
"@firebase/util": "1.11.1",
|
||||
"@firebase/component": "0.6.17",
|
||||
"@firebase/util": "1.12.0",
|
||||
"idb": "7.1.1",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
@@ -3048,15 +3051,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@firebase/messaging": {
|
||||
"version": "0.12.18",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.18.tgz",
|
||||
"integrity": "sha512-2MGhUGoCZloB7ysoYzG/T2nnRmHYLT+AcqYouZuD6APabpkDhF8lHsmSQq4MFSlXhI3DKFOXxjuvbY8ec4C2JQ==",
|
||||
"version": "0.12.21",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.21.tgz",
|
||||
"integrity": "sha512-bYJ2Evj167Z+lJ1ach6UglXz5dUKY1zrJZd15GagBUJSR7d9KfiM1W8dsyL0lDxcmhmA/sLaBYAAhF1uilwN0g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@firebase/component": "0.6.14",
|
||||
"@firebase/installations": "0.6.14",
|
||||
"@firebase/component": "0.6.17",
|
||||
"@firebase/installations": "0.6.17",
|
||||
"@firebase/messaging-interop-types": "0.2.3",
|
||||
"@firebase/util": "1.11.1",
|
||||
"@firebase/util": "1.12.0",
|
||||
"idb": "7.1.1",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
@@ -3071,9 +3074,9 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@firebase/util": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.11.1.tgz",
|
||||
"integrity": "sha512-RXg4WE8C2LUrvoV/TMGRTu223zZf9Dq9MR8yHZio9nF9TpLnpCPURw9VWWB2WATDl6HfIdWfl2x2SJYtHkN4hw==",
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.12.0.tgz",
|
||||
"integrity": "sha512-Z4rK23xBCwgKDqmzGVMef+Vb4xso2j5Q8OG0vVL4m4fA5ZjPMYQazu8OJJC3vtQRC3SQ/Pgx/6TPNVsCd70QRw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -3836,9 +3839,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.1.tgz",
|
||||
"integrity": "sha512-GLjHS13LiBdiuxSJvfWs3+Cx5yt97mCbuVlDteTusS6VRksPhoWviO8L1e3Re1G94m6lkw/l4pjEEyyNaGf19g==",
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz",
|
||||
"integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
@@ -4458,88 +4461,88 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sentry-internal/browser-utils": {
|
||||
"version": "9.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.18.0.tgz",
|
||||
"integrity": "sha512-TwSlmgYpHhe55JpOcVApkM0XcXZh1/cYuEPKPFgeaaPD8BrQrLJJvwKxnonSWXOhdnkJxi4GgK7j7mw57PS4aA==",
|
||||
"version": "9.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.22.0.tgz",
|
||||
"integrity": "sha512-Ou1tBnVxFAIn8i9gvrWzRotNJQYiu3awNXpsFCw6qFwmiKAVPa6b13vCdolhXnrIiuR77jY1LQnKh9hXpoRzsg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "9.18.0"
|
||||
"@sentry/core": "9.22.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/feedback": {
|
||||
"version": "9.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.18.0.tgz",
|
||||
"integrity": "sha512-QlrB8oQK+5bfhbgK6yHF6rLwLNJ9XuGblTc51yVkm4d4jn4W/HDyaNqMfQF+JXdTiFatl8oz2xdKR8kGK8kXyg==",
|
||||
"version": "9.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.22.0.tgz",
|
||||
"integrity": "sha512-zgMVkoC61fgi41zLcSZA59vOtKxcLrKBo1ECYhPD1hxEaneNqY5fhXDwlQBw96P5l2yqkgfX6YZtSdU4ejI9yA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/core": "9.18.0"
|
||||
"@sentry/core": "9.22.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/replay": {
|
||||
"version": "9.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.18.0.tgz",
|
||||
"integrity": "sha512-2A32FFwrlZtdpBruvpcLEfucu6BpyqOk3F4Bo5smM/5q7u0pa7q5d9FSY5l3nwKEAFAoLGv3hcCb+8wxMm50xA==",
|
||||
"version": "9.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.22.0.tgz",
|
||||
"integrity": "sha512-9GOycoKbrclcRXfcbNV8svbmAsOS5R4wXBQmKF4pFLkmFA/lJv9kdZSNYkRvkrxdNfbMIJXP+DV9EqTZcryXig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry-internal/browser-utils": "9.18.0",
|
||||
"@sentry/core": "9.18.0"
|
||||
"@sentry-internal/browser-utils": "9.22.0",
|
||||
"@sentry/core": "9.22.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry-internal/replay-canvas": {
|
||||
"version": "9.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.18.0.tgz",
|
||||
"integrity": "sha512-3DEyQLmHcYgcwJ8n8eMhI6bhhawPuMc2xTT+Az8gXMqCO/X9ZACpipAmhXFjYP9Ptl+w0Vh3nllJw+gXc/DOsg==",
|
||||
"version": "9.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.22.0.tgz",
|
||||
"integrity": "sha512-EcG9IMSEalFe49kowBTJObWjof/iHteDwpyuAszsFDdQUYATrVUtwpwN7o52vDYWJud4arhjrQnMamIGxa79eQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry-internal/replay": "9.18.0",
|
||||
"@sentry/core": "9.18.0"
|
||||
"@sentry-internal/replay": "9.22.0",
|
||||
"@sentry/core": "9.22.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/babel-plugin-component-annotate": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.4.0.tgz",
|
||||
"integrity": "sha512-tSzfc3aE7m0PM0Aj7HBDet5llH9AB9oc+tBQ8AvOqUSnWodLrNCuWeQszJ7mIBovD3figgCU3h0cvI6U5cDtsg==",
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.5.0.tgz",
|
||||
"integrity": "sha512-s2go8w03CDHbF9luFGtBHKJp4cSpsQzNVqgIa9Pfa4wnjipvrK6CxVT4icpLA3YO6kg5u622Yoa5GF3cJdippw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/browser": {
|
||||
"version": "9.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.18.0.tgz",
|
||||
"integrity": "sha512-0SWfp4J2+mH4lZOcHfyIwt9VoGD7yCGQE1cm0BPcLwKnrVQeXHtUXNYNy8HTHSjTGyoFDhEAYelE/tdA3OLcWQ==",
|
||||
"version": "9.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.22.0.tgz",
|
||||
"integrity": "sha512-3TeRm74dvX0JdjX0AgkQa+22iUHwHnY+Q6M05NZ+tDeCNHGK/mEBTeqquS1oQX67jWyuvYmG3VV6RJUxtG9Paw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry-internal/browser-utils": "9.18.0",
|
||||
"@sentry-internal/feedback": "9.18.0",
|
||||
"@sentry-internal/replay": "9.18.0",
|
||||
"@sentry-internal/replay-canvas": "9.18.0",
|
||||
"@sentry/core": "9.18.0"
|
||||
"@sentry-internal/browser-utils": "9.22.0",
|
||||
"@sentry-internal/feedback": "9.22.0",
|
||||
"@sentry-internal/replay": "9.22.0",
|
||||
"@sentry-internal/replay-canvas": "9.22.0",
|
||||
"@sentry/core": "9.22.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/bundler-plugin-core": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.4.0.tgz",
|
||||
"integrity": "sha512-X1Q41AsQ6xcT6hB4wYmBDBukndKM/inT4IsR7pdKLi7ICpX2Qq6lisamBAEPCgEvnLpazSFguaiC0uiwMKAdqw==",
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.5.0.tgz",
|
||||
"integrity": "sha512-zDzPrhJqAAy2VzV4g540qAZH4qxzisstK2+NIJPZUUKztWRWUV2cMHsyUtdctYgloGkLyGpZJBE3RE6dmP/xqQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"dotenv": "^16.3.1",
|
||||
"find-up": "^5.0.0",
|
||||
@@ -4899,22 +4902,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/core": {
|
||||
"version": "9.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.18.0.tgz",
|
||||
"integrity": "sha512-kRVH8BqMiaU2FTHYa68zNlAloS43jl4XtIEHkLKVH/7gUtwRmM4Gqj8P7RTrZdO1Lo7ksYnGj+AG05Z09CRbOw==",
|
||||
"version": "9.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.22.0.tgz",
|
||||
"integrity": "sha512-ixvtKmPF42Y6ckGUbFlB54OWI75H2gO5UYHojO6eXFpS7xO3ZGgV/QH6wb40mWK+0w5XZ0233FuU9VpsuE6mKA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/react": {
|
||||
"version": "9.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-9.18.0.tgz",
|
||||
"integrity": "sha512-1cCLYZrZ2gu6H8eE83DC47kLf+pzD1Rim3dDoOEvwt1F5cD3K/DBeIhoHZaXqBeQxuVyHXOOLXSAC/CIuas5Aw==",
|
||||
"version": "9.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-9.22.0.tgz",
|
||||
"integrity": "sha512-mI43NnioBYdG5TiXqRlhV1feZs9bnrrl+k5HOHBK7VQtymaXO0fkcsRLZTkdSgLRLMJGasZuvVhq2xK+18QyWQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/browser": "9.18.0",
|
||||
"@sentry/core": "9.18.0",
|
||||
"@sentry/browser": "9.22.0",
|
||||
"@sentry/core": "9.22.0",
|
||||
"hoist-non-react-statics": "^3.3.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4925,12 +4928,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/vite-plugin": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-3.4.0.tgz",
|
||||
"integrity": "sha512-pUFBGrKsHuc8K6A7B1wU2nx65n9aIzvTlcHX9yZ1qvjEO0cZFih0JCwu1Fcav/yrtT9RMN44L/ugu/kMBHQhjQ==",
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-3.5.0.tgz",
|
||||
"integrity": "sha512-jUnpTdpicG8wefamw7eNo2uO+Q3KCbOAiF76xH4gfNHSW6TN2hBfOtmLu7J+ive4c0Al3+NEHz19bIPR0lkwWg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/bundler-plugin-core": "3.4.0",
|
||||
"@sentry/bundler-plugin-core": "3.5.0",
|
||||
"unplugin": "1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4938,13 +4941,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/webpack-plugin": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-3.4.0.tgz",
|
||||
"integrity": "sha512-i+nAxxniJV5ovijojjTF5n+Yj08Xk8my+vm8+oo0C0I7xcnI2gOKft6B0sJOq01CNbo85X5m/3/edL0PKoWE9w==",
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-3.5.0.tgz",
|
||||
"integrity": "sha512-xvclj0QY2HyU7uJLzOlHSrZQBDwfnGKJxp8mmlU4L7CwmK+8xMCqlO7tYZoqE4K/wU3c2xpXql70x8qmvNMxzQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sentry/bundler-plugin-core": "3.4.0",
|
||||
"@sentry/bundler-plugin-core": "3.5.0",
|
||||
"unplugin": "1.0.1",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
@@ -5810,14 +5813,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.3.tgz",
|
||||
"integrity": "sha512-7FTQQuuLKmN1Ig/h+h/GO+44Q1IlglPlR2es4ab7Yvfx+Uk5xsv+Ykk+MEt/M2Yn/xGmzaLKxGw2lgy2bwuYqg==",
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.4.tgz",
|
||||
"integrity": "sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "3.1.3",
|
||||
"@vitest/utils": "3.1.3",
|
||||
"@vitest/spy": "3.1.4",
|
||||
"@vitest/utils": "3.1.4",
|
||||
"chai": "^5.2.0",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
},
|
||||
@@ -5826,13 +5829,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.3.tgz",
|
||||
"integrity": "sha512-PJbLjonJK82uCWHjzgBJZuR7zmAOrSvKk1QBxrennDIgtH4uK0TB1PvYmc0XBCigxxtiAVPfWtAdy4lpz8SQGQ==",
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.4.tgz",
|
||||
"integrity": "sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "3.1.3",
|
||||
"@vitest/spy": "3.1.4",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.17"
|
||||
},
|
||||
@@ -5873,9 +5876,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/pretty-format": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.3.tgz",
|
||||
"integrity": "sha512-i6FDiBeJUGLDKADw2Gb01UtUNb12yyXAqC/mmRWuYl+m/U9GS7s8us5ONmGkGpUUo7/iAYzI2ePVfOZTYvUifA==",
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.4.tgz",
|
||||
"integrity": "sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -5886,13 +5889,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.3.tgz",
|
||||
"integrity": "sha512-Tae+ogtlNfFei5DggOsSUvkIaSuVywujMj6HzR97AHK6XK8i3BuVyIifWAm/sE3a15lF5RH9yQIrbXYuo0IFyA==",
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.4.tgz",
|
||||
"integrity": "sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "3.1.3",
|
||||
"@vitest/utils": "3.1.4",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
"funding": {
|
||||
@@ -5907,13 +5910,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.3.tgz",
|
||||
"integrity": "sha512-XVa5OPNTYUsyqG9skuUkFzAeFnEzDp8hQu7kZ0N25B1+6KjGm4hWLtURyBbsIAOekfWQ7Wuz/N/XXzgYO3deWQ==",
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.4.tgz",
|
||||
"integrity": "sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "3.1.3",
|
||||
"@vitest/pretty-format": "3.1.4",
|
||||
"magic-string": "^0.30.17",
|
||||
"pathe": "^2.0.3"
|
||||
},
|
||||
@@ -5939,9 +5942,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.3.tgz",
|
||||
"integrity": "sha512-x6w+ctOEmEXdWaa6TO4ilb7l9DxPR5bwEb6hILKuxfU1NqWT2mpJD9NJN7t3OTfxmVlOMrvtoFJGdgyzZ605lQ==",
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.4.tgz",
|
||||
"integrity": "sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -5952,13 +5955,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.3.tgz",
|
||||
"integrity": "sha512-2Ltrpht4OmHO9+c/nmHtF09HWiyWdworqnHIwjfvDyWjuwKbdkcS9AnhsDn+8E2RM4x++foD1/tNuLPVvWG1Rg==",
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.4.tgz",
|
||||
"integrity": "sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "3.1.3",
|
||||
"@vitest/pretty-format": "3.1.4",
|
||||
"loupe": "^3.1.3",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
},
|
||||
@@ -6088,9 +6091,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/antd": {
|
||||
"version": "5.25.1",
|
||||
"resolved": "https://registry.npmjs.org/antd/-/antd-5.25.1.tgz",
|
||||
"integrity": "sha512-4KC7KuPCjr0z3Vuw9DsF+ceqJaPLbuUI3lOX1sY8ix25ceamp+P8yxOmk3Y2JHCD2ZAhq+5IQ/DTJRN2adWYKQ==",
|
||||
"version": "5.25.2",
|
||||
"resolved": "https://registry.npmjs.org/antd/-/antd-5.25.2.tgz",
|
||||
"integrity": "sha512-7R2nUvlHhey7Trx64+hCtGXOiy+DTUs1Lv5bwbV1LzEIZIhWb0at1AM6V3K108a5lyoR9n7DX3ptlLF7uYV/DQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/colors": "^7.2.0",
|
||||
@@ -6128,11 +6131,11 @@
|
||||
"rc-rate": "~2.13.1",
|
||||
"rc-resize-observer": "^1.4.3",
|
||||
"rc-segmented": "~2.7.0",
|
||||
"rc-select": "~14.16.7",
|
||||
"rc-select": "~14.16.8",
|
||||
"rc-slider": "~11.1.8",
|
||||
"rc-steps": "~6.0.1",
|
||||
"rc-switch": "~4.1.0",
|
||||
"rc-table": "~7.50.4",
|
||||
"rc-table": "~7.50.5",
|
||||
"rc-tabs": "~15.6.1",
|
||||
"rc-textarea": "~1.10.0",
|
||||
"rc-tooltip": "~6.4.0",
|
||||
@@ -11833,9 +11836,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/memfs": {
|
||||
"version": "4.17.1",
|
||||
"resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.1.tgz",
|
||||
"integrity": "sha512-thuTRd7F4m4dReCIy7vv4eNYnU6XI/tHMLSMMHLiortw/Y0QxqKtinG523U2aerzwYWGi606oBP4oMPy4+edag==",
|
||||
"version": "4.17.2",
|
||||
"resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.2.tgz",
|
||||
"integrity": "sha512-NgYhCOWgovOXSzvYgUW0LQ7Qy72rWQMGGFJDoWg4G30RHd3z77VbYdtJ4fembJXBy8pMIUA31XNAupobOQlwdg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -13569,9 +13572,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/query-string": {
|
||||
"version": "9.1.2",
|
||||
"resolved": "https://registry.npmjs.org/query-string/-/query-string-9.1.2.tgz",
|
||||
"integrity": "sha512-s3UlTyjxRux4KjwWaJsjh1Mp8zoCkSGKirbD9H89pEM9UOZsfpRZpdfzvsy2/mGlLfC3NnYVpy2gk7jXITHEtA==",
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/query-string/-/query-string-9.2.0.tgz",
|
||||
"integrity": "sha512-YIRhrHujoQxhexwRLxfy3VSjOXmvZRd2nyw1PwL1UUqZ/ys1dEZd1+NSgXkne2l/4X/7OXkigEAuhTX0g/ivJQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"decode-uri-component": "^0.4.1",
|
||||
@@ -14024,9 +14027,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rc-select": {
|
||||
"version": "14.16.7",
|
||||
"resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.7.tgz",
|
||||
"integrity": "sha512-lT9kO5gFHQdJzu9a0btcOtNaJHkhenSl8H5mcpgXN9VIMXP59rnkpbdHmPrteixWs1D5zFOTyoTYX3b7joADIQ==",
|
||||
"version": "14.16.8",
|
||||
"resolved": "https://registry.npmjs.org/rc-select/-/rc-select-14.16.8.tgz",
|
||||
"integrity": "sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.10.1",
|
||||
@@ -14097,9 +14100,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rc-table": {
|
||||
"version": "7.50.4",
|
||||
"resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.50.4.tgz",
|
||||
"integrity": "sha512-Y+YuncnQqoS5e7yHvfvlv8BmCvwDYDX/2VixTBEhkMDk9itS9aBINp4nhzXFKiBP/frG4w0pS9d9Rgisl0T1Bw==",
|
||||
"version": "7.50.5",
|
||||
"resolved": "https://registry.npmjs.org/rc-table/-/rc-table-7.50.5.tgz",
|
||||
"integrity": "sha512-FDZu8aolhSYd3v9KOc3lZOVAU77wmRRu44R0Wfb8Oj1dXRUsloFaXMSl6f7yuWZUxArJTli7k8TEOX2mvhDl4A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.10.1",
|
||||
@@ -15375,9 +15378,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.88.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.88.0.tgz",
|
||||
"integrity": "sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==",
|
||||
"version": "1.89.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.89.0.tgz",
|
||||
"integrity": "sha512-ld+kQU8YTdGNjOLfRWBzewJpU5cwEv/h5yyqlSeJcj6Yh8U4TDA9UA5FPicqDz/xgRPWRSYIQNiFks21TbA9KQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
@@ -17533,9 +17536,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite-node": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.3.tgz",
|
||||
"integrity": "sha512-uHV4plJ2IxCl4u1up1FQRrqclylKAogbtBfOTwcuJ28xFi+89PZ57BRh+naIRvH70HPwxy5QHYzg1OrEaC7AbA==",
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.4.tgz",
|
||||
"integrity": "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -17731,19 +17734,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.3.tgz",
|
||||
"integrity": "sha512-188iM4hAHQ0km23TN/adso1q5hhwKqUpv+Sd6p5sOuh6FhQnRNW3IsiIpvxqahtBabsJ2SLZgmGSpcYK4wQYJw==",
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.4.tgz",
|
||||
"integrity": "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/expect": "3.1.3",
|
||||
"@vitest/mocker": "3.1.3",
|
||||
"@vitest/pretty-format": "^3.1.3",
|
||||
"@vitest/runner": "3.1.3",
|
||||
"@vitest/snapshot": "3.1.3",
|
||||
"@vitest/spy": "3.1.3",
|
||||
"@vitest/utils": "3.1.3",
|
||||
"@vitest/expect": "3.1.4",
|
||||
"@vitest/mocker": "3.1.4",
|
||||
"@vitest/pretty-format": "^3.1.4",
|
||||
"@vitest/runner": "3.1.4",
|
||||
"@vitest/snapshot": "3.1.4",
|
||||
"@vitest/spy": "3.1.4",
|
||||
"@vitest/utils": "3.1.4",
|
||||
"chai": "^5.2.0",
|
||||
"debug": "^4.4.0",
|
||||
"expect-type": "^1.2.1",
|
||||
@@ -17756,7 +17759,7 @@
|
||||
"tinypool": "^1.0.2",
|
||||
"tinyrainbow": "^2.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"
|
||||
},
|
||||
"bin": {
|
||||
@@ -17772,8 +17775,8 @@
|
||||
"@edge-runtime/vm": "*",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||
"@vitest/browser": "3.1.3",
|
||||
"@vitest/ui": "3.1.3",
|
||||
"@vitest/browser": "3.1.4",
|
||||
"@vitest/ui": "3.1.4",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
|
||||
@@ -12,19 +12,19 @@
|
||||
"@apollo/client": "^3.13.6",
|
||||
"@emotion/is-prop-valid": "^1.3.1",
|
||||
"@fingerprintjs/fingerprintjs": "^4.6.1",
|
||||
"@firebase/analytics": "^0.10.13",
|
||||
"@firebase/app": "^0.12.1",
|
||||
"@firebase/auth": "^1.10.2",
|
||||
"@firebase/firestore": "^4.7.12",
|
||||
"@firebase/messaging": "^0.12.18",
|
||||
"@firebase/analytics": "^0.10.16",
|
||||
"@firebase/app": "^0.13.0",
|
||||
"@firebase/auth": "^1.10.5",
|
||||
"@firebase/firestore": "^4.7.15",
|
||||
"@firebase/messaging": "^0.12.21",
|
||||
"@jsreport/browser-client": "^3.1.0",
|
||||
"@reduxjs/toolkit": "^2.8.1",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"@sentry/cli": "^2.45.0",
|
||||
"@sentry/react": "^9.18.0",
|
||||
"@sentry/vite-plugin": "^3.4.0",
|
||||
"@sentry/react": "^9.22.0",
|
||||
"@sentry/vite-plugin": "^3.5.0",
|
||||
"@splitsoftware/splitio-react": "^2.1.1",
|
||||
"@tanem/react-nprogress": "^5.0.53",
|
||||
"antd": "^5.25.1",
|
||||
"antd": "^5.25.2",
|
||||
"apollo-link-logger": "^2.0.1",
|
||||
"apollo-link-sentry": "^4.3.0",
|
||||
"autosize": "^6.0.1",
|
||||
@@ -48,7 +48,7 @@
|
||||
"normalize-url": "^8.0.1",
|
||||
"object-hash": "^3.0.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^9.1.2",
|
||||
"query-string": "^9.2.0",
|
||||
"raf-schd": "^4.0.3",
|
||||
"react": "^18.3.1",
|
||||
"react-big-calendar": "^1.18.0",
|
||||
@@ -77,7 +77,7 @@
|
||||
"redux-saga": "^1.3.0",
|
||||
"redux-state-sync": "^3.1.4",
|
||||
"reselect": "^5.1.1",
|
||||
"sass": "^1.88.0",
|
||||
"sass": "^1.89.0",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"styled-components": "^6.1.18",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
@@ -130,12 +130,12 @@
|
||||
"@ant-design/icons": "^6.0.0",
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"@babel/preset-react": "^7.27.1",
|
||||
"@dotenvx/dotenvx": "^1.44.0",
|
||||
"@dotenvx/dotenvx": "^1.44.1",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@eslint/js": "^9.26.0",
|
||||
"@eslint/js": "^9.27.0",
|
||||
"@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/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
@@ -148,7 +148,7 @@
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^15.15.0",
|
||||
"jsdom": "^26.0.0",
|
||||
"memfs": "^4.17.1",
|
||||
"memfs": "^4.17.2",
|
||||
"os-browserify": "^0.3.0",
|
||||
"playwright": "^1.51.1",
|
||||
"react-error-overlay": "^6.1.0",
|
||||
@@ -160,7 +160,7 @@
|
||||
"vite-plugin-node-polyfills": "^0.23.0",
|
||||
"vite-plugin-pwa": "^1.0.0",
|
||||
"vite-plugin-style-import": "^2.0.0",
|
||||
"vitest": "^3.1.3",
|
||||
"vitest": "^3.1.4",
|
||||
"workbox-window": "^7.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,16 +34,14 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
||||
|
||||
SubscribeToTopicForFCMNotification();
|
||||
|
||||
//Register WS handlers
|
||||
// Register WebSocket handlers
|
||||
if (socket && socket.connected) {
|
||||
registerMessagingHandlers({ socket, client });
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (socket && socket.connected) {
|
||||
return () => {
|
||||
unregisterMessagingHandlers({ socket });
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
}, [bodyshop, socket, t, client]);
|
||||
|
||||
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { Virtuoso } from "react-virtuoso";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -10,35 +10,60 @@ import PhoneFormatter from "../../utils/PhoneFormatter";
|
||||
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
|
||||
import _ from "lodash";
|
||||
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({
|
||||
selectedConversation: selectSelectedConversation
|
||||
selectedConversation: selectSelectedConversation,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setSelectedConversation: (conversationId) => dispatch(setSelectedConversation(conversationId))
|
||||
});
|
||||
|
||||
function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation }) {
|
||||
// That comma is there for a reason, do not remove it
|
||||
function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation, bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
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(() => {
|
||||
const interval = setInterval(() => {
|
||||
forceUpdate((prev) => !prev); // Toggle state to trigger re-render
|
||||
}, 60000); // 1 minute in milliseconds
|
||||
|
||||
return () => clearInterval(interval); // Cleanup on unmount
|
||||
forceUpdate((prev) => !prev);
|
||||
}, 60000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// Memoize the sorted conversation list
|
||||
const sortedConversationList = React.useMemo(() => {
|
||||
const sortedConversationList = useMemo(() => {
|
||||
return _.orderBy(conversationList, ["updated_at"], ["desc"]);
|
||||
}, [conversationList]);
|
||||
|
||||
const renderConversation = (index) => {
|
||||
const renderConversation = (index, t) => {
|
||||
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 cardContentLeft =
|
||||
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 = () =>
|
||||
item.id === selectedConversation
|
||||
@@ -73,9 +103,25 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
|
||||
onClick={() => setSelectedConversation(item.id)}
|
||||
className={`chat-list-item ${item.id === selectedConversation ? "chat-list-selected-conversation" : ""}`}
|
||||
>
|
||||
<Card style={getCardStyle()} bordered={false} size="small" extra={cardExtra} title={cardTitle}>
|
||||
<div style={{ display: "inline-block", width: "70%", textAlign: "left" }}>{cardContentLeft}</div>
|
||||
<div style={{ display: "inline-block", width: "30%", textAlign: "right" }}>{cardContentRight}</div>
|
||||
<Card style={getCardStyle()} variant={true} size="small" extra={cardExtra} title={cardTitle}>
|
||||
<div
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width: "70%",
|
||||
textAlign: "left"
|
||||
}}
|
||||
>
|
||||
{cardContentLeft}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width: "30%",
|
||||
textAlign: "right"
|
||||
}}
|
||||
>
|
||||
{cardContentRight}
|
||||
</div>
|
||||
</Card>
|
||||
</List.Item>
|
||||
);
|
||||
@@ -85,7 +131,7 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
|
||||
<div className="chat-list-container">
|
||||
<Virtuoso
|
||||
data={sortedConversationList}
|
||||
itemContent={(index) => renderConversation(index)}
|
||||
itemContent={(index) => renderConversation(index, t)}
|
||||
style={{ height: "100%", width: "100%" }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
/* Add spacing and better alignment for items */
|
||||
.chat-list-item {
|
||||
padding: 0.5rem 0; /* Add spacing between list items */
|
||||
padding: 0.2rem 0; /* Add spacing between list items */
|
||||
|
||||
.ant-card {
|
||||
border-radius: 8px; /* Slight rounding for card edges */
|
||||
|
||||
@@ -37,7 +37,7 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
||||
fetchPolicy: "network-only",
|
||||
nextFetchPolicy: "network-only",
|
||||
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
|
||||
@@ -67,14 +67,14 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
|
||||
<>
|
||||
{!bodyshop.uselocalmediaserver && (
|
||||
<JobsDocumentImgproxyGalleryExternal
|
||||
jobId={conversation.job_conversations[0].jobid}
|
||||
jobId={conversation.job_conversations[0]?.jobid}
|
||||
externalMediaState={[selectedMedia, setSelectedMedia]}
|
||||
/>
|
||||
)}
|
||||
{bodyshop.uselocalmediaserver && open && (
|
||||
<JobDocumentsLocalGalleryExternal
|
||||
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 && (
|
||||
<JobDocumentsLocalGalleryExternal
|
||||
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 { Input, Spin } from "antd";
|
||||
import { Alert, Input, Spin } from "antd";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -10,6 +10,9 @@ import { selectIsSending, selectMessage } from "../../redux/messaging/messaging.
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import ChatMediaSelector from "../chat-media-selector/chat-media-selector.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({
|
||||
bodyshop: selectBodyshop,
|
||||
@@ -25,16 +28,24 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSending, message, setMessage }) {
|
||||
const inputArea = useRef(null);
|
||||
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(() => {
|
||||
inputArea.current.focus();
|
||||
}, [isSending, setMessage]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleEnter = () => {
|
||||
const selectedImages = selectedMedia.filter((i) => i.isSelected);
|
||||
if ((message === "" || !message) && selectedImages.length === 0) return;
|
||||
if (isOptedOut) return; // Prevent sending if phone number is opted out
|
||||
logImEXEvent("messaging_send_message");
|
||||
|
||||
if (selectedImages.length < 11) {
|
||||
@@ -44,7 +55,8 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
|
||||
messagingServiceSid: bodyshop.messagingservicesid,
|
||||
conversationid: conversation.id,
|
||||
selectedMedia: selectedImages,
|
||||
imexshopid: bodyshop.imexshopid
|
||||
imexshopid: bodyshop.imexshopid,
|
||||
bodyshopid: bodyshop.id
|
||||
};
|
||||
sendMessage(newMessage);
|
||||
setSelectedMedia(
|
||||
@@ -57,6 +69,7 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
|
||||
|
||||
return (
|
||||
<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" />
|
||||
<ChatMediaSelector
|
||||
conversation={conversation}
|
||||
@@ -71,18 +84,18 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
|
||||
ref={inputArea}
|
||||
autoSize={{ minRows: 1, maxRows: 4 }}
|
||||
value={message}
|
||||
disabled={isSending}
|
||||
disabled={isSending || isOptedOut}
|
||||
placeholder={t("messaging.labels.typeamessage")}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onPressEnter={(event) => {
|
||||
event.preventDefault();
|
||||
if (!!!event.shiftKey) handleEnter();
|
||||
if (!event.shiftKey && !isOptedOut) handleEnter();
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<SendOutlined
|
||||
className="chat-send-message-button"
|
||||
// disabled={message === "" || !message}
|
||||
disabled={isOptedOut || message === "" || !message}
|
||||
onClick={handleEnter}
|
||||
/>
|
||||
<Spin
|
||||
|
||||
@@ -81,8 +81,9 @@ export function HasFeatureAccess({ featureName, bodyshop, bypass, debug = false
|
||||
}
|
||||
return (
|
||||
bodyshop?.features?.allAccess ||
|
||||
bodyshop?.features?.[featureName] ||
|
||||
dayjs(bodyshop?.features[featureName]).isAfter(dayjs())
|
||||
(typeof bodyshop?.features?.[featureName] === "boolean"
|
||||
? bodyshop?.features?.[featureName]
|
||||
: dayjs(bodyshop?.features?.[featureName]).isAfter(dayjs()))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
HomeFilled,
|
||||
ImportOutlined,
|
||||
LineChartOutlined,
|
||||
OneToOneOutlined,
|
||||
PaperClipOutlined,
|
||||
PhoneOutlined,
|
||||
PlusCircleOutlined,
|
||||
@@ -24,6 +25,7 @@ import {
|
||||
TeamOutlined,
|
||||
ToolFilled,
|
||||
UnorderedListOutlined,
|
||||
UsergroupAddOutlined,
|
||||
UserOutlined
|
||||
} from "@ant-design/icons";
|
||||
import { useQuery } from "@apollo/client";
|
||||
@@ -40,6 +42,7 @@ import { RiSurveyLine } from "react-icons/ri";
|
||||
import { connect } from "react-redux";
|
||||
import { Link } from "react-router-dom";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
import { GET_UNREAD_COUNT } from "../../graphql/notifications.queries.js";
|
||||
import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors";
|
||||
import { setModalContext } from "../../redux/modals/modals.actions";
|
||||
@@ -47,11 +50,10 @@ import { signOutStart } from "../../redux/user/user.actions";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import day from "../../utils/day.js";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
||||
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
|
||||
import LockWrapper from "../lock-wrapper/lock-wrapper.component";
|
||||
import NotificationCenterContainer from "../notification-center/notification-center.container.jsx";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
import { useIsEmployee } from "../../utils/useIsEmployee.js";
|
||||
|
||||
// Redux mappings
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
@@ -642,17 +644,32 @@ function Header({
|
||||
label: t("menus.header.help"),
|
||||
onClick: () => window.open("https://help.imex.online/", "_blank")
|
||||
},
|
||||
...(InstanceRenderManager({ imex: true, rome: false })
|
||||
? [
|
||||
{
|
||||
key: "rescue",
|
||||
id: "header-rescue",
|
||||
icon: <CarFilled />,
|
||||
label: t("menus.header.rescueme"),
|
||||
onClick: () => window.open("https://imexrescue.com/", "_blank")
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: "remoteassist",
|
||||
id: "header-remote-assist",
|
||||
icon: <OneToOneOutlined />,
|
||||
label: t("menus.header.remoteassist"),
|
||||
children: [
|
||||
...(InstanceRenderManager({ imex: true, rome: true })
|
||||
? [
|
||||
{
|
||||
key: "rescue",
|
||||
id: "header-rescue",
|
||||
icon: <PlusCircleOutlined />,
|
||||
label: t("menus.header.rescueme"),
|
||||
onClick: () => window.open("https://imexrescue.com/", "_blank")
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: "rescue",
|
||||
id: "header-rescue-zoho",
|
||||
icon: <UsergroupAddOutlined />,
|
||||
label: t("menus.header.rescuemezoho"),
|
||||
onClick: () => window.open("https://join.zoho.com/", "_blank")
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: "shiftclock",
|
||||
id: "header-shiftclock",
|
||||
|
||||
@@ -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);
|
||||
@@ -20,7 +20,7 @@ const Board = ({ id, className, orientation, cardSettings, ...additionalProps })
|
||||
default:
|
||||
return cardSizesVertical.small;
|
||||
}
|
||||
}, [cardSettings]);
|
||||
}, [cardSettings?.cardSize]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -101,11 +101,33 @@ const BoardContainer = ({
|
||||
async ({ draggableId, type, source, reason, mode, destination, combine }) => {
|
||||
setIsDragging(false);
|
||||
|
||||
// Only update drag time if it's a valid drop with a different destination
|
||||
if (type === "lane" && source && destination && !isEqual(source, destination)) {
|
||||
setDragTime(source.droppableId);
|
||||
setIsProcessing(true);
|
||||
// Validate drag type and source
|
||||
if (type !== "lane" || !source) {
|
||||
// Invalid drag type or missing source, attempt to revert if possible
|
||||
if (source) {
|
||||
dispatch(
|
||||
actions.moveCardAcrossLanes({
|
||||
fromLaneId: source.droppableId,
|
||||
toLaneId: source.droppableId,
|
||||
cardId: draggableId,
|
||||
index: source.index
|
||||
})
|
||||
);
|
||||
}
|
||||
setIsProcessing(false);
|
||||
try {
|
||||
await onDragEnd({ draggableId, type, source, reason, mode, destination, combine });
|
||||
} catch (err) {
|
||||
console.error("Error in onLaneDrag for invalid drag type or source", err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setDragTime(source.droppableId);
|
||||
setIsProcessing(true);
|
||||
|
||||
// Handle valid drop to a different lane or position
|
||||
if (destination && !isEqual(source, destination)) {
|
||||
dispatch(
|
||||
actions.moveCardAcrossLanes({
|
||||
fromLaneId: source.droppableId,
|
||||
@@ -114,14 +136,33 @@ const BoardContainer = ({
|
||||
index: destination.index
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// Same-lane drop or no destination, revert to original position
|
||||
dispatch(
|
||||
actions.moveCardAcrossLanes({
|
||||
fromLaneId: source.droppableId,
|
||||
toLaneId: source.droppableId,
|
||||
cardId: draggableId,
|
||||
index: source.index
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await onDragEnd({ draggableId, type, source, reason, mode, destination, combine });
|
||||
} catch (err) {
|
||||
console.error("Error in onLaneDrag", err);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
try {
|
||||
await onDragEnd({ draggableId, type, source, reason, mode, destination, combine });
|
||||
} catch (err) {
|
||||
console.error("Error in onLaneDrag", err);
|
||||
// Ensure revert on error
|
||||
dispatch(
|
||||
actions.moveCardAcrossLanes({
|
||||
fromLaneId: source.droppableId,
|
||||
toLaneId: source.droppableId,
|
||||
cardId: draggableId,
|
||||
index: source.index
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
},
|
||||
[dispatch, onDragEnd, setDragTime]
|
||||
|
||||
@@ -133,7 +133,9 @@ const Lane = ({
|
||||
Item: ItemComponent
|
||||
},
|
||||
itemContent: (index, item) => <ItemWrapper>{renderDraggable(index, item)}</ItemWrapper>,
|
||||
overscan: { main: 10, reverse: 10 }
|
||||
overscan: { main: 10, reverse: 10 },
|
||||
// Ensure a minimum height for empty lanes to allow dropping
|
||||
style: renderedCards.length === 0 ? { minHeight: "5px" } : {}
|
||||
};
|
||||
|
||||
const horizontalProps = {
|
||||
@@ -149,8 +151,6 @@ const Lane = ({
|
||||
|
||||
const componentProps = orientation === "vertical" ? verticalProps : horizontalProps;
|
||||
|
||||
// If the lane is collapsed, we want to render a div instead of the virtualized list, and we want to set the height to the max height of the lane so that
|
||||
// the lane doesn't shrink when collapsed (in horizontal mode)
|
||||
const finalComponentProps = collapsed
|
||||
? orientation === "horizontal"
|
||||
? {
|
||||
@@ -161,9 +161,8 @@ const Lane = ({
|
||||
: {}
|
||||
: componentProps;
|
||||
|
||||
// If the lane is horizontal and collapsed, we want to render a placeholder so that the lane doesn't shrink to 0 height and grows when
|
||||
// a card is dragged over it
|
||||
const shouldRenderPlaceholder = orientation !== "horizontal" && (collapsed || renderedCards.length === 0);
|
||||
// Always render placeholder for empty lanes in vertical mode to ensure droppable area
|
||||
const shouldRenderPlaceholder = orientation === "vertical" ? collapsed || renderedCards.length === 0 : collapsed;
|
||||
|
||||
return (
|
||||
<HeightMemoryWrapper
|
||||
@@ -178,8 +177,8 @@ const Lane = ({
|
||||
override={orientation !== "horizontal" && (collapsed || !renderedCards.length)}
|
||||
>
|
||||
<div
|
||||
ref={laneRef} // Ensure laneRef is set here
|
||||
style={{ height: "100%", width: "100%" }} // Make it scrollable
|
||||
ref={laneRef}
|
||||
style={{ height: "100%", width: "100%" }}
|
||||
className={`react-trello-lane ${collapsed ? "lane-collapsed" : ""}`}
|
||||
>
|
||||
<div {...provided.droppableProps} ref={provided.innerRef} style={{ ...provided.droppableProps.style }}>
|
||||
|
||||
@@ -6,6 +6,7 @@ import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
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_ALL_VENDORS } from "../../graphql/vendors.queries";
|
||||
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 BlurWrapperComponent from "../feature-wrapper/blur-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 ReportCenterModalFiltersSortersComponent from "./report-center-modal-filters-sorters-component";
|
||||
import "./report-center-modal.styles.scss";
|
||||
import LockWrapperComponent from "../lock-wrapper/lock-wrapper.component";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
reportCenterModal: selectReportCenter,
|
||||
@@ -389,5 +389,7 @@ const restrictedReports = [
|
||||
{ key: "job_costing_ro_date_detail", days: 183 },
|
||||
{ key: "job_costing_ro_estimator", 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 }
|
||||
];
|
||||
|
||||
@@ -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);
|
||||
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 ShopInfoContainer from "../../components/shop-info/shop-info.container";
|
||||
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 { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import InstanceRenderManager from "../../utils/instanceRenderMgr";
|
||||
|
||||
import { HasFeatureAccess } from "../../components/feature-wrapper/feature-wrapper.component";
|
||||
import ShopTeamsContainer from "../../components/shop-teams/shop-teams.container";
|
||||
|
||||
@@ -91,6 +91,14 @@ export function ShopPage({ bodyshop, setSelectedHeader, setBreadcrumbs }) {
|
||||
children: <ShopCsiConfig />
|
||||
});
|
||||
}
|
||||
|
||||
// Add Consent Settings tab
|
||||
items.push({
|
||||
key: "consent",
|
||||
label: t("bodyshop.labels.consent_settings"),
|
||||
children: <ShopInfoConsentComponent bodyshop={bodyshop} />
|
||||
});
|
||||
|
||||
return (
|
||||
<RbacWrapper action="shop:config">
|
||||
<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.
|
||||
}
|
||||
};
|
||||
|
||||
case UserActionTypes.SET_SHOP_DETAILS:
|
||||
return {
|
||||
...state,
|
||||
@@ -126,6 +125,7 @@ const userReducer = (state = INITIAL_STATE, action) => {
|
||||
...state,
|
||||
imexshopid: action.payload
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -335,20 +335,12 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
||||
}
|
||||
|
||||
try {
|
||||
InstanceRenderManager({
|
||||
executeFunction: true,
|
||||
args: [],
|
||||
imex: () => {
|
||||
window.$crisp.push(["set", "user:company", [payload.shopname]]);
|
||||
window.$crisp.push(["set", "session:segments", [[`region:${payload.region_config}`]]]);
|
||||
if (authRecord[0] && authRecord[0].user.validemail) {
|
||||
window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]);
|
||||
}
|
||||
},
|
||||
rome: () => {
|
||||
window.$zoho.salesiq.visitor.info({ "Shop Name": payload.shopname });
|
||||
}
|
||||
});
|
||||
window.$crisp.push(["set", "user:company", [payload.shopname]]);
|
||||
window.$crisp.push(["set", "session:segments", [[`region:${payload.region_config}`]]]);
|
||||
if (authRecord[0] && authRecord[0].user.validemail) {
|
||||
window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]);
|
||||
}
|
||||
|
||||
payload.features?.allAccess === true
|
||||
? window.$crisp.push(["set", "session:segments", [["allAccess"]]])
|
||||
: (() => {
|
||||
@@ -359,6 +351,14 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
|
||||
);
|
||||
window.$crisp.push(["set", "session:segments", [["basic", ...featureKeys]]]);
|
||||
})();
|
||||
|
||||
InstanceRenderManager({
|
||||
executeFunction: true,
|
||||
args: [],
|
||||
rome: () => {
|
||||
window.$zoho.salesiq.visitor.info({ "Shop Name": payload.shopname });
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("Couldnt find $crisp.", error.message);
|
||||
}
|
||||
|
||||
@@ -656,6 +656,7 @@
|
||||
}
|
||||
},
|
||||
"labels": {
|
||||
"consent_settings": "Phone Number Opt-Out List",
|
||||
"2tiername": "Name => RO",
|
||||
"2tiersetup": "2 Tier Setup",
|
||||
"2tiersource": "Source => RO",
|
||||
@@ -2301,8 +2302,10 @@
|
||||
"productionlist": "Production Board - List",
|
||||
"readyjobs": "Ready Jobs",
|
||||
"recent": "Recent Items",
|
||||
"remoteassist": "Remote Assist",
|
||||
"reportcenter": "Report Center",
|
||||
"rescueme": "Rescue me!",
|
||||
"rescueme": "Rescue Me!",
|
||||
"rescuemezoho": "Remote Me In!",
|
||||
"schedule": "Schedule",
|
||||
"scoreboard": "Scoreboard",
|
||||
"search": {
|
||||
@@ -2377,7 +2380,8 @@
|
||||
"errors": {
|
||||
"invalidphone": "The phone number is invalid. Unable to open 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": {
|
||||
"addlabel": "Add a label to this conversation.",
|
||||
@@ -2393,7 +2397,8 @@
|
||||
"selectmedia": "Select Media",
|
||||
"sentby": "Sent by {{by}} at {{time}}",
|
||||
"typeamessage": "Send a message...",
|
||||
"unarchive": "Unarchive"
|
||||
"unarchive": "Unarchive",
|
||||
"no_consent": "No Consent"
|
||||
},
|
||||
"render": {
|
||||
"conversation_list": "Conversation List"
|
||||
@@ -3099,6 +3104,7 @@
|
||||
"credits_not_received_date_vendorid": "Credits not Received by Vendor",
|
||||
"csi": "CSI Responses",
|
||||
"customer_list": "Customer List",
|
||||
"customer_list_excel": "Customer List - Excel",
|
||||
"cycle_time_analysis": "Cycle Time Analysis",
|
||||
"estimates_written_converted": "Estimates Written/Converted",
|
||||
"estimator_detail": "Jobs by Estimator (Detail)",
|
||||
@@ -3862,6 +3868,14 @@
|
||||
"validation": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2301,8 +2301,10 @@
|
||||
"productionlist": "",
|
||||
"readyjobs": "",
|
||||
"recent": "",
|
||||
"remoteassist": "",
|
||||
"reportcenter": "",
|
||||
"rescueme": "",
|
||||
"rescuemezoho": "",
|
||||
"schedule": "Programar",
|
||||
"scoreboard": "",
|
||||
"search": {
|
||||
@@ -2498,7 +2500,8 @@
|
||||
},
|
||||
"tooltips": {
|
||||
"job-watchers": "",
|
||||
"not-employee": ""
|
||||
"not-employee": "",
|
||||
"not-employee-notifications": ""
|
||||
}
|
||||
},
|
||||
"owner": {
|
||||
@@ -3100,6 +3103,7 @@
|
||||
"credits_not_received_date_vendorid": "",
|
||||
"csi": "",
|
||||
"customer_list": "",
|
||||
"customer_list_excel": "",
|
||||
"cycle_time_analysis": "",
|
||||
"estimates_written_converted": "",
|
||||
"estimator_detail": "",
|
||||
|
||||
@@ -2301,8 +2301,10 @@
|
||||
"productionlist": "",
|
||||
"readyjobs": "",
|
||||
"recent": "",
|
||||
"remoteassist": "",
|
||||
"reportcenter": "",
|
||||
"rescueme": "",
|
||||
"rescuemezoho": "",
|
||||
"schedule": "Programme",
|
||||
"scoreboard": "",
|
||||
"search": {
|
||||
@@ -2498,7 +2500,8 @@
|
||||
},
|
||||
"tooltips": {
|
||||
"job-watchers": "",
|
||||
"not-employee": ""
|
||||
"not-employee": "",
|
||||
"not-employee-notifications": ""
|
||||
}
|
||||
},
|
||||
"owner": {
|
||||
@@ -3100,6 +3103,7 @@
|
||||
"credits_not_received_date_vendorid": "",
|
||||
"csi": "",
|
||||
"customer_list": "",
|
||||
"customer_list_excel": "",
|
||||
"cycle_time_analysis": "",
|
||||
"estimates_written_converted": "",
|
||||
"estimator_detail": "",
|
||||
|
||||
@@ -2004,6 +2004,18 @@ export const TemplateList = (type, context) => {
|
||||
},
|
||||
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: {
|
||||
title: 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")
|
||||
},
|
||||
group: "purchases"
|
||||
},
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
...(!type || type === "courtesycarcontract"
|
||||
|
||||
@@ -2681,6 +2681,9 @@
|
||||
- active:
|
||||
_eq: true
|
||||
allow_aggregations: true
|
||||
- table:
|
||||
name: integration_log
|
||||
schema: public
|
||||
- table:
|
||||
name: inventory
|
||||
schema: public
|
||||
@@ -5861,6 +5864,32 @@
|
||||
template_engine: Kriti
|
||||
url: '{{$base_url}}/opensearch'
|
||||
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:
|
||||
name: phonebook
|
||||
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;
|
||||
1461
package-lock.json
generated
1461
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
28
package.json
28
package.json
@@ -16,14 +16,14 @@
|
||||
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-cloudwatch-logs": "^3.808.0",
|
||||
"@aws-sdk/client-elasticache": "^3.808.0",
|
||||
"@aws-sdk/client-s3": "^3.808.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.808.0",
|
||||
"@aws-sdk/client-ses": "^3.808.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.808.0",
|
||||
"@aws-sdk/lib-storage": "^3.808.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.808.0",
|
||||
"@aws-sdk/client-cloudwatch-logs": "^3.812.0",
|
||||
"@aws-sdk/client-elasticache": "^3.812.0",
|
||||
"@aws-sdk/client-s3": "^3.812.0",
|
||||
"@aws-sdk/client-secrets-manager": "^3.812.0",
|
||||
"@aws-sdk/client-ses": "^3.812.0",
|
||||
"@aws-sdk/credential-provider-node": "^3.812.0",
|
||||
"@aws-sdk/lib-storage": "^3.812.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.812.0",
|
||||
"@opensearch-project/opensearch": "^2.13.0",
|
||||
"@socket.io/admin-ui": "^0.5.1",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
@@ -31,14 +31,14 @@
|
||||
"aws4": "^1.13.2",
|
||||
"axios": "^1.8.4",
|
||||
"better-queue": "^3.8.12",
|
||||
"bullmq": "^5.52.2",
|
||||
"bullmq": "^5.53.0",
|
||||
"chart.js": "^4.4.8",
|
||||
"cloudinary": "^2.6.1",
|
||||
"compression": "^1.8.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "2.8.5",
|
||||
"cors": "^2.8.5",
|
||||
"crisp-status-reporter": "^1.2.2",
|
||||
"dd-trace": "^5.51.0",
|
||||
"dd-trace": "^5.52.0",
|
||||
"dinero.js": "^1.9.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.21.1",
|
||||
@@ -73,14 +73,14 @@
|
||||
"xmlbuilder2": "^3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.26.0",
|
||||
"eslint": "^9.26.0",
|
||||
"@eslint/js": "^9.27.0",
|
||||
"eslint": "^9.27.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^15.15.0",
|
||||
"mock-require": "^3.0.3",
|
||||
"p-limit": "^3.1.0",
|
||||
"prettier": "^3.5.3",
|
||||
"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,
|
||||
environment: process.env.NODE_ENV === "production" ? "production" : "sandbox",
|
||||
redirectUri: process.env.QBO_REDIRECT_URI,
|
||||
logging: true
|
||||
});
|
||||
|
||||
//TODO:AIO Add in QBO callbacks.
|
||||
const url = InstanceEndpoints();
|
||||
|
||||
exports.default = async (req, res) => {
|
||||
|
||||
@@ -20,7 +20,6 @@ exports.default = async (req, res) => {
|
||||
clientSecret: process.env.QBO_SECRET,
|
||||
environment: process.env.NODE_ENV === "production" ? "production" : "sandbox",
|
||||
redirectUri: process.env.QBO_REDIRECT_URI,
|
||||
logging: true
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -149,6 +148,15 @@ async function QueryVendorRecord(oauthClient, qbo_realmId, req, bill) {
|
||||
"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);
|
||||
return (
|
||||
result.json &&
|
||||
@@ -178,6 +186,15 @@ async function InsertVendorRecord(oauthClient, qbo_realmId, req, bill) {
|
||||
},
|
||||
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);
|
||||
return result && result.json && result.json.Vendor;
|
||||
} catch (error) {
|
||||
@@ -190,7 +207,7 @@ async function InsertVendorRecord(oauthClient, qbo_realmId, req, bill) {
|
||||
}
|
||||
|
||||
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) =>
|
||||
generateBillLine(
|
||||
@@ -246,11 +263,11 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop)
|
||||
.format("YYYY-MM-DD"),
|
||||
...(!bill.is_credit_memo &&
|
||||
bill.vendor.due_date && {
|
||||
DueDate: moment(bill.date)
|
||||
//.tz(bill.job.bodyshop.timezone)
|
||||
.add(bill.vendor.due_date, "days")
|
||||
.format("YYYY-MM-DD")
|
||||
}),
|
||||
DueDate: moment(bill.date)
|
||||
//.tz(bill.job.bodyshop.timezone)
|
||||
.add(bill.vendor.due_date, "days")
|
||||
.format("YYYY-MM-DD")
|
||||
}),
|
||||
DocNumber: bill.invoice_number,
|
||||
//...(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.trim() !== "" && {
|
||||
DepartmentRef: { value: bodyshop.accountingconfig.qbo_departmentid }
|
||||
}),
|
||||
DepartmentRef: { value: bodyshop.accountingconfig.qbo_departmentid }
|
||||
}),
|
||||
PrivateNote: `RO ${bill.job.ro_number || ""}`,
|
||||
Line: lines
|
||||
};
|
||||
@@ -280,6 +297,15 @@ async function InsertBill(oauthClient, qbo_realmId, req, bill, vendor, bodyshop)
|
||||
},
|
||||
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);
|
||||
return result && result.json && result.json.Bill;
|
||||
} catch (error) {
|
||||
@@ -327,8 +353,8 @@ const generateBillLine = (
|
||||
accountingconfig.qbo && accountingconfig.qbo_usa && region_config.includes("CA_")
|
||||
? {}
|
||||
: {
|
||||
value: taxCodes[findTaxCode(billLine.applicable_taxes, ioSalesTaxCodes)]
|
||||
},
|
||||
value: taxCodes[findTaxCode(billLine.applicable_taxes, ioSalesTaxCodes)]
|
||||
},
|
||||
AccountRef: {
|
||||
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({
|
||||
url: urlBuilder(
|
||||
qbo_realmId,
|
||||
@@ -354,6 +380,14 @@ async function QueryMetaData(oauthClient, qbo_realmId, req) {
|
||||
"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);
|
||||
const taxCodes = await oauthClient.makeApiCall({
|
||||
url: urlBuilder(qbo_realmId, "query", `select * From TaxCode`),
|
||||
@@ -362,7 +396,14 @@ async function QueryMetaData(oauthClient, qbo_realmId, req) {
|
||||
"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({
|
||||
url: urlBuilder(qbo_realmId, "query", `select * From Class`),
|
||||
method: "POST",
|
||||
@@ -370,7 +411,14 @@ async function QueryMetaData(oauthClient, qbo_realmId, req) {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
logger.LogIntegrationCall({
|
||||
platform: "QBO",
|
||||
method: "POST",
|
||||
name: "QueryClasses",
|
||||
status: classes.status,
|
||||
bodyshopid,
|
||||
email: req.user.email
|
||||
})
|
||||
const taxCodeMapping = {};
|
||||
|
||||
taxCodes.json &&
|
||||
|
||||
@@ -29,7 +29,6 @@ exports.default = async (req, res) => {
|
||||
clientSecret: process.env.QBO_SECRET,
|
||||
environment: process.env.NODE_ENV === "production" ? "production" : "sandbox",
|
||||
redirectUri: process.env.QBO_REDIRECT_URI,
|
||||
logging: true
|
||||
});
|
||||
try {
|
||||
//Fetch the API Access Tokens & Set them for the session.
|
||||
@@ -198,7 +197,8 @@ async function InsertPayment(oauthClient, qbo_realmId, req, payment, parentRef,
|
||||
req,
|
||||
payment.job.ro_number,
|
||||
false,
|
||||
parentRef
|
||||
parentRef,
|
||||
payment.job.shopid
|
||||
);
|
||||
|
||||
if (invoices && invoices.length !== 1) {
|
||||
@@ -227,20 +227,20 @@ async function InsertPayment(oauthClient, qbo_realmId, req, payment, parentRef,
|
||||
PaymentRefNum: payment.transactionid,
|
||||
...(invoices && invoices.length === 1 && invoices[0]
|
||||
? {
|
||||
Line: [
|
||||
{
|
||||
Amount: Dinero({
|
||||
amount: Math.round(payment.amount * 100)
|
||||
}).toFormat(DineroQbFormat),
|
||||
LinkedTxn: [
|
||||
{
|
||||
TxnId: invoices[0].Id,
|
||||
TxnType: "Invoice"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Line: [
|
||||
{
|
||||
Amount: Dinero({
|
||||
amount: Math.round(payment.amount * 100)
|
||||
}).toFormat(DineroQbFormat),
|
||||
LinkedTxn: [
|
||||
{
|
||||
TxnId: invoices[0].Id,
|
||||
TxnType: "Invoice"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
: {})
|
||||
};
|
||||
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)
|
||||
});
|
||||
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);
|
||||
return result && result.Bill;
|
||||
} 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({
|
||||
url: urlBuilder(qbo_realmId, "query", `select * From Invoice where DocNumber like '${ro_number}%'`),
|
||||
method: "POST",
|
||||
@@ -274,7 +283,15 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditM
|
||||
"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({
|
||||
url: urlBuilder(qbo_realmId, "query", `select * From PaymentMethod`),
|
||||
method: "POST",
|
||||
@@ -282,6 +299,14 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditM
|
||||
"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);
|
||||
|
||||
// const classes = await oauthClient.makeApiCall({
|
||||
@@ -325,6 +350,15 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditM
|
||||
"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({
|
||||
url: urlBuilder(qbo_realmId, "query", `select * From Item`),
|
||||
method: "POST",
|
||||
@@ -332,6 +366,14 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, ro_number, isCreditM
|
||||
"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);
|
||||
|
||||
const itemMapping = {};
|
||||
@@ -377,7 +419,8 @@ async function InsertCreditMemo(oauthClient, qbo_realmId, req, payment, parentRe
|
||||
req,
|
||||
payment.job.ro_number,
|
||||
true,
|
||||
parentRef
|
||||
parentRef,
|
||||
payment.job.shopid
|
||||
);
|
||||
|
||||
if (invoices && invoices.length !== 1) {
|
||||
@@ -406,14 +449,14 @@ async function InsertCreditMemo(oauthClient, qbo_realmId, req, payment, parentRe
|
||||
TaxCodeRef: {
|
||||
value:
|
||||
taxCodes[
|
||||
findTaxCode(
|
||||
{
|
||||
local: false,
|
||||
federal: false,
|
||||
state: false
|
||||
},
|
||||
payment.job.bodyshop.md_responsibility_centers.sales_tax_codes
|
||||
)
|
||||
findTaxCode(
|
||||
{
|
||||
local: false,
|
||||
federal: false,
|
||||
state: false
|
||||
},
|
||||
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)
|
||||
});
|
||||
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);
|
||||
return result && result.Bill;
|
||||
} catch (error) {
|
||||
|
||||
@@ -22,8 +22,8 @@ exports.default = async (req, res) => {
|
||||
clientSecret: process.env.QBO_SECRET,
|
||||
environment: process.env.NODE_ENV === "production" ? "production" : "sandbox",
|
||||
redirectUri: process.env.QBO_REDIRECT_URI,
|
||||
logging: true
|
||||
});
|
||||
|
||||
try {
|
||||
//Fetch the API Access Tokens & Set them for the session.
|
||||
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"
|
||||
}
|
||||
});
|
||||
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);
|
||||
return (
|
||||
result.json &&
|
||||
@@ -279,6 +288,15 @@ async function InsertInsuranceCo(oauthClient, qbo_realmId, req, job, bodyshop) {
|
||||
},
|
||||
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);
|
||||
return result && result.json.Customer;
|
||||
} catch (error) {
|
||||
@@ -305,6 +323,15 @@ async function QueryOwner(oauthClient, qbo_realmId, req, job, isThreeTier, paren
|
||||
"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);
|
||||
return (
|
||||
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() } } : {}),
|
||||
...(isThreeTier
|
||||
? {
|
||||
Job: true,
|
||||
ParentRef: {
|
||||
value: parentTierRef.Id
|
||||
}
|
||||
Job: true,
|
||||
ParentRef: {
|
||||
value: parentTierRef.Id
|
||||
}
|
||||
}
|
||||
: {})
|
||||
};
|
||||
try {
|
||||
@@ -347,6 +374,15 @@ async function InsertOwner(oauthClient, qbo_realmId, req, job, isThreeTier, pare
|
||||
},
|
||||
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);
|
||||
return result && result.json.Customer;
|
||||
} catch (error) {
|
||||
@@ -372,6 +408,15 @@ async function QueryJob(oauthClient, qbo_realmId, req, job, parentTierRef) {
|
||||
"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);
|
||||
return (
|
||||
result.json &&
|
||||
@@ -411,6 +456,15 @@ async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) {
|
||||
},
|
||||
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);
|
||||
return result && result.json.Customer;
|
||||
} catch (error) {
|
||||
@@ -424,7 +478,7 @@ async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) {
|
||||
|
||||
exports.InsertJob = InsertJob;
|
||||
|
||||
async function QueryMetaData(oauthClient, qbo_realmId, req) {
|
||||
async function QueryMetaData(oauthClient, qbo_realmId, req, bodyshopid) {
|
||||
const items = await oauthClient.makeApiCall({
|
||||
url: urlBuilder(qbo_realmId, "query", `select * From Item where active=true maxresults 1000`),
|
||||
method: "POST",
|
||||
@@ -432,6 +486,14 @@ async function QueryMetaData(oauthClient, qbo_realmId, req) {
|
||||
"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);
|
||||
const taxCodes = await oauthClient.makeApiCall({
|
||||
url: urlBuilder(qbo_realmId, "query", `select * From TaxCode where active=true`),
|
||||
@@ -440,7 +502,14 @@ async function QueryMetaData(oauthClient, qbo_realmId, req) {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
logger.LogIntegrationCall({
|
||||
platform: "QBO",
|
||||
method: "POST",
|
||||
name: "QueryTaxCodes",
|
||||
status: taxCodes.response?.status,
|
||||
bodyshopid,
|
||||
email: req.user.email
|
||||
})
|
||||
const classes = await oauthClient.makeApiCall({
|
||||
url: urlBuilder(qbo_realmId, "query", `select * From Class`),
|
||||
method: "POST",
|
||||
@@ -448,7 +517,14 @@ async function QueryMetaData(oauthClient, qbo_realmId, req) {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
});
|
||||
|
||||
logger.LogIntegrationCall({
|
||||
platform: "QBO",
|
||||
method: "POST",
|
||||
name: "QueryClasses",
|
||||
status: classes.response?.status,
|
||||
bodyshopid,
|
||||
email: req.user.email
|
||||
})
|
||||
const taxCodeMapping = {};
|
||||
|
||||
taxCodes.json &&
|
||||
@@ -483,7 +559,7 @@ async function QueryMetaData(oauthClient, qbo_realmId, req) {
|
||||
}
|
||||
|
||||
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);
|
||||
const InvoiceLineAdd = CreateInvoiceLines({
|
||||
bodyshop,
|
||||
jobs_by_pk: job,
|
||||
@@ -499,57 +575,55 @@ async function InsertInvoice(oauthClient, qbo_realmId, req, job, bodyshop, paren
|
||||
DocNumber: job.ro_number,
|
||||
...(job.class ? { ClassRef: { value: classes[job.class] } } : {}),
|
||||
CustomerMemo: {
|
||||
value: `${job.clm_no ? `Claim No: ${job.clm_no}` : ``}${
|
||||
job.po_number ? `PO No: ${job.po_number}` : ``
|
||||
} Vehicle:${job.v_model_yr || ""} ${job.v_make_desc || ""} ${
|
||||
job.v_model_desc || ""
|
||||
} ${job.v_vin || ""} ${job.plate_no || ""} `.trim()
|
||||
value: `${job.clm_no ? `Claim No: ${job.clm_no}` : ``}${job.po_number ? `PO No: ${job.po_number}` : ``
|
||||
} Vehicle:${job.v_model_yr || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""
|
||||
} ${job.v_vin || ""} ${job.plate_no || ""} `.trim()
|
||||
},
|
||||
CustomerRef: {
|
||||
value: parentTierRef.Id
|
||||
},
|
||||
...(bodyshop.accountingconfig.qbo_departmentid &&
|
||||
bodyshop.accountingconfig.qbo_departmentid.trim() !== "" && {
|
||||
DepartmentRef: { value: bodyshop.accountingconfig.qbo_departmentid }
|
||||
}),
|
||||
DepartmentRef: { value: bodyshop.accountingconfig.qbo_departmentid }
|
||||
}),
|
||||
CustomField: [
|
||||
...(bodyshop.accountingconfig.ReceivableCustomField1
|
||||
? [
|
||||
{
|
||||
DefinitionId: "1",
|
||||
StringValue: job[bodyshop.accountingconfig.ReceivableCustomField1],
|
||||
Type: "StringType"
|
||||
}
|
||||
]
|
||||
{
|
||||
DefinitionId: "1",
|
||||
StringValue: job[bodyshop.accountingconfig.ReceivableCustomField1],
|
||||
Type: "StringType"
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(bodyshop.accountingconfig.ReceivableCustomField2
|
||||
? [
|
||||
{
|
||||
DefinitionId: "2",
|
||||
StringValue: job[bodyshop.accountingconfig.ReceivableCustomField2],
|
||||
Type: "StringType"
|
||||
}
|
||||
]
|
||||
{
|
||||
DefinitionId: "2",
|
||||
StringValue: job[bodyshop.accountingconfig.ReceivableCustomField2],
|
||||
Type: "StringType"
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(bodyshop.accountingconfig.ReceivableCustomField3
|
||||
? [
|
||||
{
|
||||
DefinitionId: "3",
|
||||
StringValue: job[bodyshop.accountingconfig.ReceivableCustomField3],
|
||||
Type: "StringType"
|
||||
}
|
||||
]
|
||||
{
|
||||
DefinitionId: "3",
|
||||
StringValue: job[bodyshop.accountingconfig.ReceivableCustomField3],
|
||||
Type: "StringType"
|
||||
}
|
||||
]
|
||||
: [])
|
||||
],
|
||||
...(bodyshop.accountingconfig &&
|
||||
bodyshop.accountingconfig.qbo &&
|
||||
bodyshop.accountingconfig.qbo_usa && {
|
||||
TxnTaxDetail: {
|
||||
TxnTaxCodeRef: {
|
||||
value: taxCodes[bodyshop.md_responsibility_centers.taxes.state.accountitem]
|
||||
}
|
||||
TxnTaxDetail: {
|
||||
TxnTaxCodeRef: {
|
||||
value: taxCodes[bodyshop.md_responsibility_centers.taxes.state.accountitem]
|
||||
}
|
||||
}),
|
||||
}
|
||||
}),
|
||||
|
||||
...(bodyshop.accountingconfig.printlater ? { PrintStatus: "NeedToPrint" } : {}),
|
||||
...(bodyshop.accountingconfig.emaillater && job.ownr_ea ? { EmailStatus: "NeedToSend" } : {}),
|
||||
@@ -575,6 +649,15 @@ async function InsertInvoice(oauthClient, qbo_realmId, req, job, bodyshop, paren
|
||||
},
|
||||
body: JSON.stringify(invoiceObj)
|
||||
});
|
||||
logger.LogIntegrationCall({
|
||||
platform: "QBO",
|
||||
method: "POST",
|
||||
name: "InsertInvoice",
|
||||
status: result.status,
|
||||
bodyshopid: job.shopid,
|
||||
jobid: job.id,
|
||||
email: req.user.email
|
||||
})
|
||||
setNewRefreshToken(req.user.email, result);
|
||||
return result && result.json && result.json.Invoice;
|
||||
} catch (error) {
|
||||
@@ -598,7 +681,7 @@ async function InsertInvoiceMultiPayerInvoice(
|
||||
payer,
|
||||
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({
|
||||
bodyshop,
|
||||
jobs_by_pk: job,
|
||||
@@ -616,58 +699,56 @@ async function InsertInvoiceMultiPayerInvoice(
|
||||
DocNumber: job.ro_number + suffix,
|
||||
...(job.class ? { ClassRef: { value: classes[job.class] } } : {}),
|
||||
CustomerMemo: {
|
||||
value: `${job.clm_no ? `Claim No: ${job.clm_no}` : ``}${
|
||||
job.po_number ? `PO No: ${job.po_number}` : ``
|
||||
} Vehicle:${job.v_model_yr || ""} ${job.v_make_desc || ""} ${
|
||||
job.v_model_desc || ""
|
||||
} ${job.v_vin || ""} ${job.plate_no || ""} `.trim()
|
||||
value: `${job.clm_no ? `Claim No: ${job.clm_no}` : ``}${job.po_number ? `PO No: ${job.po_number}` : ``
|
||||
} Vehicle:${job.v_model_yr || ""} ${job.v_make_desc || ""} ${job.v_model_desc || ""
|
||||
} ${job.v_vin || ""} ${job.plate_no || ""} `.trim()
|
||||
},
|
||||
CustomerRef: {
|
||||
value: parentTierRef.Id
|
||||
},
|
||||
...(bodyshop.accountingconfig.qbo_departmentid &&
|
||||
bodyshop.accountingconfig.qbo_departmentid.trim() !== "" && {
|
||||
DepartmentRef: { value: bodyshop.accountingconfig.qbo_departmentid }
|
||||
}),
|
||||
DepartmentRef: { value: bodyshop.accountingconfig.qbo_departmentid }
|
||||
}),
|
||||
CustomField: [
|
||||
...(bodyshop.accountingconfig.ReceivableCustomField1
|
||||
? [
|
||||
{
|
||||
DefinitionId: "1",
|
||||
StringValue: job[bodyshop.accountingconfig.ReceivableCustomField1],
|
||||
Type: "StringType"
|
||||
}
|
||||
]
|
||||
{
|
||||
DefinitionId: "1",
|
||||
StringValue: job[bodyshop.accountingconfig.ReceivableCustomField1],
|
||||
Type: "StringType"
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(bodyshop.accountingconfig.ReceivableCustomField2
|
||||
? [
|
||||
{
|
||||
DefinitionId: "2",
|
||||
StringValue: job[bodyshop.accountingconfig.ReceivableCustomField2],
|
||||
Type: "StringType"
|
||||
}
|
||||
]
|
||||
{
|
||||
DefinitionId: "2",
|
||||
StringValue: job[bodyshop.accountingconfig.ReceivableCustomField2],
|
||||
Type: "StringType"
|
||||
}
|
||||
]
|
||||
: []),
|
||||
...(bodyshop.accountingconfig.ReceivableCustomField3
|
||||
? [
|
||||
{
|
||||
DefinitionId: "3",
|
||||
StringValue: job[bodyshop.accountingconfig.ReceivableCustomField3],
|
||||
Type: "StringType"
|
||||
}
|
||||
]
|
||||
{
|
||||
DefinitionId: "3",
|
||||
StringValue: job[bodyshop.accountingconfig.ReceivableCustomField3],
|
||||
Type: "StringType"
|
||||
}
|
||||
]
|
||||
: [])
|
||||
],
|
||||
...(bodyshop.accountingconfig &&
|
||||
bodyshop.accountingconfig.qbo &&
|
||||
bodyshop.accountingconfig.qbo_usa &&
|
||||
bodyshop.region_config.includes("CA_") && {
|
||||
TxnTaxDetail: {
|
||||
TxnTaxCodeRef: {
|
||||
value: taxCodes[bodyshop.md_responsibility_centers.taxes.state.accountitem]
|
||||
}
|
||||
TxnTaxDetail: {
|
||||
TxnTaxCodeRef: {
|
||||
value: taxCodes[bodyshop.md_responsibility_centers.taxes.state.accountitem]
|
||||
}
|
||||
}),
|
||||
}
|
||||
}),
|
||||
|
||||
...(bodyshop.accountingconfig.printlater ? { PrintStatus: "NeedToPrint" } : {}),
|
||||
...(bodyshop.accountingconfig.emaillater && job.ownr_ea ? { EmailStatus: "NeedToSend" } : {}),
|
||||
@@ -693,6 +774,15 @@ async function InsertInvoiceMultiPayerInvoice(
|
||||
},
|
||||
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);
|
||||
return result && result.json && result.json.Invoice;
|
||||
} catch (error) {
|
||||
|
||||
@@ -6,7 +6,7 @@ require("dotenv").config({
|
||||
function urlBuilder(realmId, object, query = null) {
|
||||
return `https://${
|
||||
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) {
|
||||
|
||||
@@ -222,6 +222,7 @@ query QUERY_JOBS_FOR_RECEIVABLES_EXPORT($ids: [uuid!]!) {
|
||||
rate_mash
|
||||
rate_matd
|
||||
class
|
||||
shopid
|
||||
ca_bc_pvrt
|
||||
ca_customer_gst
|
||||
towing_payable
|
||||
@@ -480,6 +481,7 @@ query QUERY_BILLS_FOR_PAYABLES_EXPORT($bills: [uuid!]!) {
|
||||
ownr_ln
|
||||
ownr_co_nm
|
||||
class
|
||||
shopid
|
||||
}
|
||||
billlines{
|
||||
id
|
||||
@@ -530,6 +532,7 @@ exports.QUERY_PAYMENTS_FOR_EXPORT = `
|
||||
ownr_fn
|
||||
ownr_ln
|
||||
ownr_co_nm
|
||||
shopid
|
||||
bodyshop {
|
||||
accountingconfig
|
||||
md_responsibility_centers
|
||||
@@ -1596,6 +1599,7 @@ query QUERY_JOB_COSTING_DETAILS($id: uuid!) {
|
||||
ca_customer_gst
|
||||
dms_allocation
|
||||
cieca_pfl
|
||||
cieca_stl
|
||||
materials
|
||||
joblines(where: { removed: { _eq: false } }) {
|
||||
id
|
||||
@@ -1712,6 +1716,7 @@ exports.QUERY_JOB_COSTING_DETAILS_MULTI = ` query QUERY_JOB_COSTING_DETAILS_MULT
|
||||
ca_customer_gst
|
||||
dms_allocation
|
||||
cieca_pfl
|
||||
cieca_stl
|
||||
materials
|
||||
joblines(where: {removed: {_eq: false}}) {
|
||||
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?
|
||||
const selectedDmsAllocationConfig =
|
||||
(job.bodyshop.md_responsibility_centers.dms_defaults &&
|
||||
|
||||
@@ -7,7 +7,7 @@ const { status, markConversationRead } = require("../sms/status");
|
||||
const validateFirebaseIdTokenMiddleware = require("../middleware/validateFirebaseIdTokenMiddleware");
|
||||
|
||||
// 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" });
|
||||
|
||||
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 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 { admin } = require("../firebase/firebase-handler");
|
||||
const logger = require("../utils/logger");
|
||||
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 {
|
||||
logger,
|
||||
ioRedis,
|
||||
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
|
||||
} = req;
|
||||
@@ -20,7 +26,7 @@ exports.receive = async (req, res) => {
|
||||
msid: req.body.SmsMessageSid,
|
||||
text: req.body.Body,
|
||||
image: !!req.body.MediaUrl0,
|
||||
image_path: generateMediaArray(req.body)
|
||||
image_path: generateMediaArray(req.body, logger)
|
||||
};
|
||||
|
||||
logger.log("sms-inbound", "DEBUG", "api", null, loggerData);
|
||||
@@ -35,7 +41,7 @@ exports.receive = async (req, res) => {
|
||||
|
||||
try {
|
||||
// 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,
|
||||
phone: phone(req.body.From).phoneNumber
|
||||
});
|
||||
@@ -46,7 +52,7 @@ exports.receive = async (req, res) => {
|
||||
|
||||
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 existingConversation = sortedConversations.length
|
||||
? sortedConversations[sortedConversations.length - 1]
|
||||
@@ -57,25 +63,21 @@ exports.receive = async (req, res) => {
|
||||
msid: req.body.SmsMessageSid,
|
||||
text: req.body.Body,
|
||||
image: !!req.body.MediaUrl0,
|
||||
image_path: generateMediaArray(req.body),
|
||||
image_path: generateMediaArray(req.body, logger),
|
||||
isoutbound: false,
|
||||
userid: null // Add additional fields as necessary
|
||||
userid: null
|
||||
};
|
||||
|
||||
if (existingConversation) {
|
||||
// Use the existing conversation
|
||||
conversationid = existingConversation.id;
|
||||
|
||||
// Unarchive the conversation if necessary
|
||||
if (existingConversation.archived) {
|
||||
await client.request(queries.UNARCHIVE_CONVERSATION, {
|
||||
await client.request(UNARCHIVE_CONVERSATION, {
|
||||
id: conversationid,
|
||||
archived: false
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Create a new conversation
|
||||
const newConversationResponse = await client.request(queries.CREATE_CONVERSATION, {
|
||||
const newConversationResponse = await client.request(CREATE_CONVERSATION, {
|
||||
conversation: {
|
||||
bodyshopid: bodyshop.id,
|
||||
phone_num: phone(req.body.From).phoneNumber,
|
||||
@@ -86,13 +88,12 @@ exports.receive = async (req, res) => {
|
||||
conversationid = createdConversation.id;
|
||||
}
|
||||
|
||||
// Ensure `conversationid` is added to the message
|
||||
newMessage.conversationid = conversationid;
|
||||
|
||||
// Step 3: Insert the message into the conversation
|
||||
const insertresp = await client.request(queries.INSERT_MESSAGE, {
|
||||
// Step 5: Insert the message
|
||||
const insertresp = await client.request(INSERT_MESSAGE, {
|
||||
msg: newMessage,
|
||||
conversationid: conversationid
|
||||
conversationid
|
||||
});
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
// Step 4: Notify clients through Redis
|
||||
const broadcastRoom = getBodyshopRoom(conversation.bodyshop.id);
|
||||
// Step 6: Notify clients
|
||||
const conversationRoom = getBodyshopConversationRoom({
|
||||
bodyshopId: conversation.bodyshop.id,
|
||||
conversationId: conversation.id
|
||||
@@ -116,6 +116,8 @@ exports.receive = async (req, res) => {
|
||||
msid: message.sid
|
||||
};
|
||||
|
||||
const broadcastRoom = getBodyshopRoom(conversation.bodyshop.id);
|
||||
|
||||
ioRedis.to(broadcastRoom).emit("new-message-summary", {
|
||||
...commonPayload,
|
||||
existingConversation: !!existingConversation,
|
||||
@@ -131,13 +133,13 @@ exports.receive = async (req, res) => {
|
||||
summary: false
|
||||
});
|
||||
|
||||
// Step 5: Send FCM notification
|
||||
// Step 7: Send FCM notification
|
||||
const fcmresp = await admin.messaging().send({
|
||||
topic: `${message.conversation.bodyshop.imexshopid}-messaging`,
|
||||
notification: {
|
||||
title: InstanceManager({
|
||||
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
|
||||
},
|
||||
@@ -157,11 +159,17 @@ exports.receive = async (req, res) => {
|
||||
|
||||
res.status(200).send("");
|
||||
} 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;
|
||||
if (parseInt(NumMedia) > 0) {
|
||||
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, {
|
||||
msid: req.body.SmsMessageSid,
|
||||
text: req.body.Body,
|
||||
image: !!req.body.MediaUrl0,
|
||||
image_path: generateMediaArray(req.body),
|
||||
image_path: generateMediaArray(req.body, logger),
|
||||
messagingServiceSid: req.body.MessagingServiceSid,
|
||||
context,
|
||||
error
|
||||
@@ -187,3 +203,7 @@ const handleError = (req, error, res, context) => {
|
||||
|
||||
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 { phone } = require("phone");
|
||||
const queries = require("../graphql-client/queries");
|
||||
const logger = require("../utils/logger");
|
||||
const { INSERT_MESSAGE } = require("../graphql-client/queries");
|
||||
const client = twilio(process.env.TWILIO_AUTH_TOKEN, process.env.TWILIO_AUTH_KEY);
|
||||
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 {
|
||||
ioRedis,
|
||||
logger,
|
||||
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
|
||||
} = req;
|
||||
|
||||
@@ -25,8 +26,8 @@ exports.send = async (req, res) => {
|
||||
conversationid,
|
||||
isoutbound: true,
|
||||
userid: req.user.email,
|
||||
image: req.body.selectedMedia.length > 0,
|
||||
image_path: req.body.selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : []
|
||||
image: selectedMedia.length > 0,
|
||||
image_path: selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : []
|
||||
});
|
||||
|
||||
if (!to || !messagingServiceSid || (!body && selectedMedia.length === 0) || !conversationid) {
|
||||
@@ -38,8 +39,8 @@ exports.send = async (req, res) => {
|
||||
conversationid,
|
||||
isoutbound: true,
|
||||
userid: req.user.email,
|
||||
image: req.body.selectedMedia.length > 0,
|
||||
image_path: req.body.selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : []
|
||||
image: selectedMedia.length > 0,
|
||||
image_path: selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : []
|
||||
});
|
||||
res.status(400).json({ success: false, message: "Missing required parameter(s)." });
|
||||
return;
|
||||
@@ -59,12 +60,15 @@ exports.send = async (req, res) => {
|
||||
conversationid,
|
||||
isoutbound: true,
|
||||
userid: req.user.email,
|
||||
image: req.body.selectedMedia.length > 0,
|
||||
image_path: req.body.selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : []
|
||||
image: selectedMedia.length > 0,
|
||||
image_path: selectedMedia.length > 0 ? selectedMedia.map((i) => i.src) : []
|
||||
};
|
||||
|
||||
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, {
|
||||
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." });
|
||||
}
|
||||
};
|
||||
|
||||
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 queries = require("../graphql-client/queries");
|
||||
const { UPDATE_MESSAGE_STATUS, MARK_MESSAGES_AS_READ } = require("../graphql-client/queries");
|
||||
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 {
|
||||
ioRedis,
|
||||
@@ -21,7 +22,7 @@ exports.status = async (req, res) => {
|
||||
}
|
||||
|
||||
// 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,
|
||||
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 {
|
||||
ioRedis,
|
||||
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
|
||||
@@ -80,7 +87,7 @@ exports.markConversationRead = async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await client.request(queries.MARK_MESSAGES_AS_READ, {
|
||||
const response = await client.request(MARK_MESSAGES_AS_READ, {
|
||||
conversationId
|
||||
});
|
||||
|
||||
@@ -104,3 +111,8 @@ exports.markConversationRead = async (req, res) => {
|
||||
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 }) => {
|
||||
// Global Bodyshop Room
|
||||
const getBodyshopRoom = (bodyshopId) => `bodyshop-broadcast-room:${bodyshopId}`;
|
||||
|
||||
@@ -12,6 +12,9 @@ const { uploadFileToS3 } = require("./s3");
|
||||
const { v4 } = require("uuid");
|
||||
const { InstanceRegion } = require("./instanceMgr");
|
||||
const getHostNameOrIP = require("./getHostNameOrIP");
|
||||
const client = require("../graphql-client/graphql-client").client;
|
||||
const queries = require("../graphql-client/queries");
|
||||
|
||||
|
||||
const LOG_LEVELS = {
|
||||
error: { level: 0, name: "error" },
|
||||
@@ -99,13 +102,11 @@ const createLogger = () => {
|
||||
const labelColor = "\x1b[33m"; // Yellow
|
||||
const separatorColor = "\x1b[35m|\x1b[0m"; // Magenta for separators
|
||||
|
||||
return `${timestampColor} [${hostnameColor}] [${level}]: ${message} ${
|
||||
user ? `${separatorColor} ${labelColor}user:\x1b[0m ${JSON.stringify(user)}` : ""
|
||||
} ${record ? `${separatorColor} ${labelColor}record:\x1b[0m ${JSON.stringify(record)}` : ""}${
|
||||
meta
|
||||
return `${timestampColor} [${hostnameColor}] [${level}]: ${message} ${user ? `${separatorColor} ${labelColor}user:\x1b[0m ${JSON.stringify(user)}` : ""
|
||||
} ${record ? `${separatorColor} ${labelColor}record:\x1b[0m ${JSON.stringify(record)}` : ""}${meta
|
||||
? `\n${separatorColor} ${labelColor}meta:\x1b[0m ${JSON.stringify(meta, null, 2)} ${separatorColor}`
|
||||
: ""
|
||||
}`;
|
||||
}`;
|
||||
})
|
||||
)
|
||||
})
|
||||
@@ -194,9 +195,45 @@ const createLogger = () => {
|
||||
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 {
|
||||
log,
|
||||
logger: winstonLogger
|
||||
logger: winstonLogger,
|
||||
LogIntegrationCall
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Error setting up enhanced Logger, defaulting to console.: " + e?.message || "");
|
||||
|
||||
Reference in New Issue
Block a user