Compare commits

...

36 Commits

Author SHA1 Message Date
Dave Richer
e2c5a4cba4 feature/IO-3182-Phone-Number-Consent - Checkpoint 2025-05-30 14:14:05 -04:00
Dave Richer
a0566e76ab feature/IO-3182-Phone-Number-Consent - Checkpoint 2025-05-29 13:49:56 -04:00
Dave Richer
909a21023a feature/IO-3182-Phone-Number-Consent - Checkpoint 2025-05-28 14:01:24 -04:00
Dave Richer
94bdc6c43f feature/IO-3182-Phone-Number-Consent - Checkpoint 2025-05-28 13:45:12 -04:00
Dave Richer
9466d36e69 feature/IO-3182-Phone-Number-Consent - Checkpoint 2025-05-28 13:17:21 -04:00
Dave Richer
412efb06e5 feature/IO-3182-Phone-Number-Consent - Checkpoint 2025-05-28 13:07:11 -04:00
Dave Richer
da7e637183 feature/IO-3182-Phone-Number-Consent - Checkpoint 2025-05-28 12:26:48 -04:00
Dave Richer
2e95fa25af feature/IO-3182-Phone-Number-Consent - Checkpoint 2025-05-28 12:17:43 -04:00
Dave Richer
f6c63bbd74 Merged in feature/IO-3182-Phone-Number-Consent (pull request #2351)
feature/IO-3182-Phone-Number-Consent - Checkpoint
2025-05-27 15:43:03 +00:00
Dave Richer
0a654082c2 feature/IO-3182-Phone-Number-Consent - Checkpoint 2025-05-27 11:41:31 -04:00
Dave Richer
2c20b731d2 Merged in feature/IO-3182-Phone-Number-Consent (pull request #2350)
Feature/IO-3182 Phone Number Consent
2025-05-27 15:38:56 +00:00
Dave Richer
8a22897cdd feature/IO-3182-Phone-Number-Consent - Checkpoint 2025-05-27 11:35:16 -04:00
Dave Richer
677da61b52 Merge remote-tracking branch 'origin/release/2025-06-02' into feature/IO-3182-Phone-Number-Consent 2025-05-27 11:34:44 -04:00
Dave Richer
6513434bd7 feature/IO-3182-Phone-Number-Consent - Checkpoint 2025-05-27 11:34:16 -04:00
Dave Richer
fe2600029f feature/IO-3182-Phone-Number-Consent - Checkpoint 2025-05-27 11:03:09 -04:00
Allan Carr
c5b4efedfb Merged in feature/IO-3249-Delete-Job-Watchers (pull request #2348)
IO-3249 Delete Job Watchers

Approved-by: Dave Richer
2025-05-26 20:28:56 +00:00
Allan Carr
310321d0ab IO-3249 Delete Job Watchers
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2025-05-26 12:49:58 -07:00
Allan Carr
7e884c42ea Merged in feature/IO-3214-Extend-New-Fields-to-Audit-Log (pull request #2346)
IO-3214 Extend New Fields to Audit Log

Approved-by: Dave Richer
2025-05-26 19:10:16 +00:00
Allan Carr
e279bf41a4 IO-3214 Extend New Fields to Audit Log
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2025-05-26 12:11:26 -07:00
Dave Richer
4a060ab51c Merged in feature/IO-3182-Phone-Number-Consent (pull request #2345)
Feature/IO-3182 Phone Number Consent
2025-05-26 19:08:47 +00:00
Dave Richer
62c1c77a18 feature/IO-3182-Phone-Number-Consent - Finish core functionality 2025-05-26 15:07:57 -04:00
Dave Richer
db19ecb28c feature/IO-3182-Phone-Number-Consent - Front/Back Start/stop logic complete 2025-05-26 14:49:02 -04:00
Dave Richer
51748ce28d feature/IO-3182-Phone-Number-Consent - Front/Back Start/stop logic complete 2025-05-26 14:44:27 -04:00
Allan Carr
4bbfd8a9da Merged in feature/IO-3247-Quick-Deliver-Require-Delivery (pull request #2343)
IO-3247 Quick Deliver Require Delivery

Approved-by: Dave Richer
2025-05-26 17:37:36 +00:00
Allan Carr
d4d2db2cac IO-3247 Quick Deliver Require Delivery
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2025-05-26 10:29:03 -07:00
Dave Richer
23483144e1 Merged in feature/IO-3182-Phone-Number-Consent (pull request #2341)
feature/IO-3182-Phone-Number-Consent - Up Deps
2025-05-26 17:09:48 +00:00
Dave Richer
67d5dcb062 feature/IO-3182-Phone-Number-Consent - Up Deps 2025-05-26 13:09:07 -04:00
Allan Carr
901a49e571 Merged in feature/IO-3246-Remote-Assist (pull request #2337)
IO-3246 Remote Assist

Approved-by: Dave Richer
2025-05-24 17:29:38 +00:00
Dave Richer
49ae107fde release/2025-06-02 - add phone 2025-05-24 13:23:38 -04:00
Dave Richer
0135281bcd release/2025-06-02 - test push 2025-05-24 13:15:53 -04:00
Allan Carr
99cf95daf0 IO-3246 Remote Assist
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2025-05-23 16:45:24 -07:00
Allan Carr
8c1758ae49 Merged in feature/IO-3075-Crisp-Basic-Info (pull request #2335)
IO-3075 Crisp Basic Info

Approved-by: Dave Richer
2025-05-23 18:03:15 +00:00
Allan Carr
2d764921ff IO-3075 Crisp Basic Info
Signed-off-by: Allan Carr <allan@imexsystems.ca>
2025-05-23 10:43:15 -07:00
Dave Richer
4859239f55 Merged in feature/IO-3242-Visual-Production-Board-Vertical-Drag-Bug (pull request #2332)
feature/IO-3242-Visual-Production-Board-Vertical-Drag-Bug - Fix bug
2025-05-23 15:01:44 +00:00
Dave Richer
5c64d7185e feature/IO-3242-Visual-Production-Board-Vertical-Drag-Bug - Fix bug 2025-05-23 11:00:21 -04:00
Patrick Fic
152479bc08 Merged in feature/IO-3239-integration-logging (pull request #2331)
Feature/IO-3239 integration logging
2025-05-22 18:56:05 +00:00
53 changed files with 1446 additions and 659 deletions

View File

@@ -12791,27 +12791,6 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>allow_text_message</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>checklist</name> <name>checklist</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>
@@ -42614,27 +42593,6 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>allow_text_message</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>name</name> <name>name</name>
<definition_loaded>false</definition_loaded> <definition_loaded>false</definition_loaded>

220
client/package-lock.json generated
View File

@@ -15,17 +15,17 @@
"@fingerprintjs/fingerprintjs": "^4.6.1", "@fingerprintjs/fingerprintjs": "^4.6.1",
"@firebase/analytics": "^0.10.16", "@firebase/analytics": "^0.10.16",
"@firebase/app": "^0.13.0", "@firebase/app": "^0.13.0",
"@firebase/auth": "^1.10.5", "@firebase/auth": "^1.10.6",
"@firebase/firestore": "^4.7.15", "@firebase/firestore": "^4.7.16",
"@firebase/messaging": "^0.12.21", "@firebase/messaging": "^0.12.21",
"@jsreport/browser-client": "^3.1.0", "@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.8.2", "@reduxjs/toolkit": "^2.8.2",
"@sentry/cli": "^2.45.0", "@sentry/cli": "^2.46.0",
"@sentry/react": "^9.22.0", "@sentry/react": "^9.23.0",
"@sentry/vite-plugin": "^3.5.0", "@sentry/vite-plugin": "^3.5.0",
"@splitsoftware/splitio-react": "^2.1.1", "@splitsoftware/splitio-react": "^2.1.1",
"@tanem/react-nprogress": "^5.0.53", "@tanem/react-nprogress": "^5.0.53",
"antd": "^5.25.2", "antd": "^5.25.3",
"apollo-link-logger": "^2.0.1", "apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^4.3.0", "apollo-link-sentry": "^4.3.0",
"autosize": "^6.0.1", "autosize": "^6.0.1",
@@ -48,6 +48,7 @@
"memoize-one": "^6.0.0", "memoize-one": "^6.0.0",
"normalize-url": "^8.0.1", "normalize-url": "^8.0.1",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"phone": "^3.1.59",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"query-string": "^9.2.0", "query-string": "^9.2.0",
"raf-schd": "^4.0.3", "raf-schd": "^4.0.3",
@@ -59,7 +60,7 @@
"react-drag-listview": "^2.0.0", "react-drag-listview": "^2.0.0",
"react-grid-gallery": "^1.0.1", "react-grid-gallery": "^1.0.1",
"react-grid-layout": "1.3.4", "react-grid-layout": "1.3.4",
"react-i18next": "^15.4.1", "react-i18next": "^15.5.2",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-image-lightbox": "^5.1.4", "react-image-lightbox": "^5.1.4",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
@@ -99,7 +100,7 @@
"@testing-library/dom": "^10.4.0", "@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.5.0",
"browserslist": "^4.24.5", "browserslist": "^4.24.5",
"browserslist-to-esbuild": "^2.1.1", "browserslist-to-esbuild": "^2.1.1",
"chalk": "^5.4.1", "chalk": "^5.4.1",
@@ -2966,9 +2967,9 @@
} }
}, },
"node_modules/@firebase/auth": { "node_modules/@firebase/auth": {
"version": "1.10.5", "version": "1.10.6",
"resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.10.5.tgz", "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.10.6.tgz",
"integrity": "sha512-6wF/NdMTwObL4RNQePunuzMr9O3gyftisvFZFFKf57D2HONXo87YymogRV8d+Z7SLA0rcNBN1gLJVk2D0y97gA==", "integrity": "sha512-cFbo2FymQltog4atI9cKTO6CxKxS0dOMXslTQrlNZRH7qhDG44/d7QeI6GXLweFZtrnlecf52ESnNz1DU6ek8w==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@firebase/component": "0.6.17", "@firebase/component": "0.6.17",
@@ -3003,9 +3004,9 @@
} }
}, },
"node_modules/@firebase/firestore": { "node_modules/@firebase/firestore": {
"version": "4.7.15", "version": "4.7.16",
"resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.7.15.tgz", "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.7.16.tgz",
"integrity": "sha512-FgWTmkNBEXdKCoN2ngBNjrMaXuBx6QwjiZZVnOGg+VjUmiBq5gAqlDIW5bZY6i/NYvLUrWugdqIs7y9GHEqwww==", "integrity": "sha512-5OpvlwYVUTLEnqewOlXmtIpH8t2ISlZHDW0NDbKROM2D0ATMqFkMHdvl+/wz9zOAcb8GMQYlhCihOnVAliUbpQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@firebase/component": "0.6.17", "@firebase/component": "0.6.17",
@@ -3885,6 +3886,13 @@
"react": ">=16.8.0" "react": ">=16.8.0"
} }
}, },
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.9",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz",
"integrity": "sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==",
"dev": true,
"license": "MIT"
},
"node_modules/@rollup/plugin-babel": { "node_modules/@rollup/plugin-babel": {
"version": "5.3.1", "version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@@ -4461,50 +4469,50 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@sentry-internal/browser-utils": { "node_modules/@sentry-internal/browser-utils": {
"version": "9.22.0", "version": "9.23.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.22.0.tgz", "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.23.0.tgz",
"integrity": "sha512-Ou1tBnVxFAIn8i9gvrWzRotNJQYiu3awNXpsFCw6qFwmiKAVPa6b13vCdolhXnrIiuR77jY1LQnKh9hXpoRzsg==", "integrity": "sha512-hyN2Q6mh7ggw8sDVHeRyWz5LR6gjvf8zHSzQnMaF7QkeSyaeGM/SVSL4ODwqR9TRH7U2ku6nZFMbKhaBPV+Hfg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sentry/core": "9.22.0" "@sentry/core": "9.23.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@sentry-internal/feedback": { "node_modules/@sentry-internal/feedback": {
"version": "9.22.0", "version": "9.23.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.22.0.tgz", "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.23.0.tgz",
"integrity": "sha512-zgMVkoC61fgi41zLcSZA59vOtKxcLrKBo1ECYhPD1hxEaneNqY5fhXDwlQBw96P5l2yqkgfX6YZtSdU4ejI9yA==", "integrity": "sha512-Xf+KqV69TBiPo1gk2EsU6O/dumuTMxWOF52uVWJddQYI3pQTU5DqSeoZ5AY76bIIhV9n6AEFDGqNPXmuj4Acrw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sentry/core": "9.22.0" "@sentry/core": "9.23.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@sentry-internal/replay": { "node_modules/@sentry-internal/replay": {
"version": "9.22.0", "version": "9.23.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.22.0.tgz", "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.23.0.tgz",
"integrity": "sha512-9GOycoKbrclcRXfcbNV8svbmAsOS5R4wXBQmKF4pFLkmFA/lJv9kdZSNYkRvkrxdNfbMIJXP+DV9EqTZcryXig==", "integrity": "sha512-0/q15tvSboaK7/05BFQhs71bqgHKejJoDJgXmH0lySqgsRh/S18867ZxQNiuYhuVt337h07u1QaCyjnNJKHfuA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sentry-internal/browser-utils": "9.22.0", "@sentry-internal/browser-utils": "9.23.0",
"@sentry/core": "9.22.0" "@sentry/core": "9.23.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@sentry-internal/replay-canvas": { "node_modules/@sentry-internal/replay-canvas": {
"version": "9.22.0", "version": "9.23.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.22.0.tgz", "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.23.0.tgz",
"integrity": "sha512-EcG9IMSEalFe49kowBTJObWjof/iHteDwpyuAszsFDdQUYATrVUtwpwN7o52vDYWJud4arhjrQnMamIGxa79eQ==", "integrity": "sha512-cYlw5svJjyPequm0PJjFGLpee86L1NieONEHlujOXkIG6IEriiorMm+8bNpGsHRuyvg41B+4P/YmcQAGtEGxXg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sentry-internal/replay": "9.22.0", "@sentry-internal/replay": "9.23.0",
"@sentry/core": "9.22.0" "@sentry/core": "9.23.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@@ -4520,16 +4528,16 @@
} }
}, },
"node_modules/@sentry/browser": { "node_modules/@sentry/browser": {
"version": "9.22.0", "version": "9.23.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.22.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.23.0.tgz",
"integrity": "sha512-3TeRm74dvX0JdjX0AgkQa+22iUHwHnY+Q6M05NZ+tDeCNHGK/mEBTeqquS1oQX67jWyuvYmG3VV6RJUxtG9Paw==", "integrity": "sha512-QRkNxWys8e088260vByztoTsEVZ0W6v/XnZfKT6wkPPGn0aFeOrg/xjgxfI8D5huqZCxT28Cf23akOOly4FXjg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sentry-internal/browser-utils": "9.22.0", "@sentry-internal/browser-utils": "9.23.0",
"@sentry-internal/feedback": "9.22.0", "@sentry-internal/feedback": "9.23.0",
"@sentry-internal/replay": "9.22.0", "@sentry-internal/replay": "9.23.0",
"@sentry-internal/replay-canvas": "9.22.0", "@sentry-internal/replay-canvas": "9.23.0",
"@sentry/core": "9.22.0" "@sentry/core": "9.23.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@@ -4720,9 +4728,9 @@
} }
}, },
"node_modules/@sentry/cli": { "node_modules/@sentry/cli": {
"version": "2.45.0", "version": "2.46.0",
"resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.45.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.46.0.tgz",
"integrity": "sha512-4sWu7zgzgHAjIxIjXUA/66qgeEf5ZOlloO+/JaGD5qXNSW0G7KMTR6iYjReNKMgdBCTH6bUUt9qiuA+Ex9Masw==", "integrity": "sha512-nqoPl7UCr446QFkylrsRrUXF51x8Z9dGquyf4jaQU+OzbOJMqclnYEvU6iwbwvaw3tu/2DnoZE/Og+Nq1h63sA==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
@@ -4739,20 +4747,20 @@
"node": ">= 10" "node": ">= 10"
}, },
"optionalDependencies": { "optionalDependencies": {
"@sentry/cli-darwin": "2.45.0", "@sentry/cli-darwin": "2.46.0",
"@sentry/cli-linux-arm": "2.45.0", "@sentry/cli-linux-arm": "2.46.0",
"@sentry/cli-linux-arm64": "2.45.0", "@sentry/cli-linux-arm64": "2.46.0",
"@sentry/cli-linux-i686": "2.45.0", "@sentry/cli-linux-i686": "2.46.0",
"@sentry/cli-linux-x64": "2.45.0", "@sentry/cli-linux-x64": "2.46.0",
"@sentry/cli-win32-arm64": "2.45.0", "@sentry/cli-win32-arm64": "2.46.0",
"@sentry/cli-win32-i686": "2.45.0", "@sentry/cli-win32-i686": "2.46.0",
"@sentry/cli-win32-x64": "2.45.0" "@sentry/cli-win32-x64": "2.46.0"
} }
}, },
"node_modules/@sentry/cli-darwin": { "node_modules/@sentry/cli-darwin": {
"version": "2.45.0", "version": "2.46.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.45.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.46.0.tgz",
"integrity": "sha512-p4Uxfv/L2fQdP3/wYnKVVz9gzZJf/1Xp9D+6raax/3Bu5y87yHYUqcdt98y/VAXQD4ofp2QgmhGUVPofvQNZmg==", "integrity": "sha512-5Ll+e5KAdIk9OYiZO8aifMBRNWmNyPjSqdjaHlBC1Qfh7pE3b1zyzoHlsUazG0bv0sNrSGea8e7kF5wIO1hvyg==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"optional": true, "optional": true,
"os": [ "os": [
@@ -4763,9 +4771,9 @@
} }
}, },
"node_modules/@sentry/cli-linux-arm": { "node_modules/@sentry/cli-linux-arm": {
"version": "2.45.0", "version": "2.46.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.45.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.46.0.tgz",
"integrity": "sha512-6sEskFLlFKJ+e0MOYgIclBTUX5jYMyYhHIxXahEkI/4vx6JO0uvpyRAkUJRpJkRh/lPog0FM+tbP3so+VxB2qQ==", "integrity": "sha512-WRrLNq/TEX/TNJkGqq6Ad0tGyapd5dwlxtsPbVBrIdryuL1mA7VCBoaHBr3kcwJLsgBHFH0lmkMee2ubNZZdkg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -4773,16 +4781,17 @@
"optional": true, "optional": true,
"os": [ "os": [
"linux", "linux",
"freebsd" "freebsd",
"android"
], ],
"engines": { "engines": {
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@sentry/cli-linux-arm64": { "node_modules/@sentry/cli-linux-arm64": {
"version": "2.45.0", "version": "2.46.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.45.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.46.0.tgz",
"integrity": "sha512-gUcLoEjzg7AIc4QQGEZwRHri+EHf3Gcms9zAR1VHiNF3/C/jL4WeDPJF2YiWAQt6EtH84tHiyhw1Ab/R8XFClg==", "integrity": "sha512-OEJN8yAjI9y5B4telyqzu27Hi3+S4T8VxZCqJz1+z2Mp0Q/MZ622AahVPpcrVq/5bxrnlZR16+lKh8L1QwNFPg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -4790,16 +4799,17 @@
"optional": true, "optional": true,
"os": [ "os": [
"linux", "linux",
"freebsd" "freebsd",
"android"
], ],
"engines": { "engines": {
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@sentry/cli-linux-i686": { "node_modules/@sentry/cli-linux-i686": {
"version": "2.45.0", "version": "2.46.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.45.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.46.0.tgz",
"integrity": "sha512-VmmOaEAzSW23YdGNdy/+oQjCNAMY+HmOGA77A25/ep/9AV7PQB6FI7xO5Y1PVvlkxZFJ23e373njSsEeg4uDZw==", "integrity": "sha512-xko3/BVa4LX8EmRxVOCipV+PwfcK5Xs8lP6lgF+7NeuAHMNL4DqF6iV9rrN8gkGUHCUI9RXSve37uuZnFy55+Q==",
"cpu": [ "cpu": [
"x86", "x86",
"ia32" "ia32"
@@ -4808,16 +4818,17 @@
"optional": true, "optional": true,
"os": [ "os": [
"linux", "linux",
"freebsd" "freebsd",
"android"
], ],
"engines": { "engines": {
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@sentry/cli-linux-x64": { "node_modules/@sentry/cli-linux-x64": {
"version": "2.45.0", "version": "2.46.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.45.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.46.0.tgz",
"integrity": "sha512-a0Oj68mrb25a0WjX/ShZ6AAd4PPiuLcgyzQr7bl2+DvYxIOajwkGbR+CZFEhOVZcfhTnixKy/qIXEzApEPHPQg==", "integrity": "sha512-hJ1g5UEboYcOuRia96LxjJ0jhnmk8EWLDvlGnXLnYHkwy3ree/L7sNgdp/QsY8Z4j2PGO5f22Va+UDhSjhzlfQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -4825,16 +4836,17 @@
"optional": true, "optional": true,
"os": [ "os": [
"linux", "linux",
"freebsd" "freebsd",
"android"
], ],
"engines": { "engines": {
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@sentry/cli-win32-arm64": { "node_modules/@sentry/cli-win32-arm64": {
"version": "2.45.0", "version": "2.46.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.45.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.46.0.tgz",
"integrity": "sha512-vn+CwS4p+52pQSLNPoi20ZOrQmv01ZgAmuMnjkh1oUZfTyBAwWLrAh6Cy4cztcN8DfL5dOWKQBo8DBKURE4ttg==", "integrity": "sha512-mN7cpPoCv2VExFRGHt+IoK11yx4pM4ADZQGEso5BAUZ5duViXB2WrAXCLd8DrwMnP0OE978a7N8OtzsFqjkbNA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -4848,9 +4860,9 @@
} }
}, },
"node_modules/@sentry/cli-win32-i686": { "node_modules/@sentry/cli-win32-i686": {
"version": "2.45.0", "version": "2.46.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.45.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.46.0.tgz",
"integrity": "sha512-8mMoDdlwxtcdNIMtteMK7dbi7054jak8wKSHJ5yzMw8UmWxC5thc/gXBc1uPduiaI56VjoJV+phWHBKCD+6I4w==", "integrity": "sha512-6F73AUE3lm71BISUO19OmlnkFD5WVe4/wA1YivtLZTc1RU3eUYJLYxhDfaH3P77+ycDppQ2yCgemLRaA4A8mNQ==",
"cpu": [ "cpu": [
"x86", "x86",
"ia32" "ia32"
@@ -4865,9 +4877,9 @@
} }
}, },
"node_modules/@sentry/cli-win32-x64": { "node_modules/@sentry/cli-win32-x64": {
"version": "2.45.0", "version": "2.46.0",
"resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.45.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.46.0.tgz",
"integrity": "sha512-ZvK9cIqFaq7vZ0jkHJ/xh5au6902Dr+AUxSk6L6vCL7JCe2p93KGL/4d8VFB5PD/P7Y9b+105G/e0QIFKzpeOw==", "integrity": "sha512-yuGVcfepnNL84LGA0GjHzdMIcOzMe0bjPhq/rwPsPN+zu11N+nPR2wV2Bum4U0eQdqYH3iAlMdL5/BEQfuLJww==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -4902,22 +4914,22 @@
} }
}, },
"node_modules/@sentry/core": { "node_modules/@sentry/core": {
"version": "9.22.0", "version": "9.23.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.22.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.23.0.tgz",
"integrity": "sha512-ixvtKmPF42Y6ckGUbFlB54OWI75H2gO5UYHojO6eXFpS7xO3ZGgV/QH6wb40mWK+0w5XZ0233FuU9VpsuE6mKA==", "integrity": "sha512-9846pn/BvASGgl7WsnKY4xry98WreP9ToeLfCQTQOf+pNfD/qNPn1/0xPInGni3LVMAXRtfHHMPm2Ghz255N7A==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@sentry/react": { "node_modules/@sentry/react": {
"version": "9.22.0", "version": "9.23.0",
"resolved": "https://registry.npmjs.org/@sentry/react/-/react-9.22.0.tgz", "resolved": "https://registry.npmjs.org/@sentry/react/-/react-9.23.0.tgz",
"integrity": "sha512-mI43NnioBYdG5TiXqRlhV1feZs9bnrrl+k5HOHBK7VQtymaXO0fkcsRLZTkdSgLRLMJGasZuvVhq2xK+18QyWQ==", "integrity": "sha512-2J/oOx8jd7Jr2koYIe5IcJyStHBXpjkQnxawo54Zyyvzc96MftyM2Dv5TeYdz7fChU1NIXw7BVbEpkQ9XEQlqg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sentry/browser": "9.22.0", "@sentry/browser": "9.23.0",
"@sentry/core": "9.22.0", "@sentry/core": "9.23.0",
"hoist-non-react-statics": "^3.3.2" "hoist-non-react-statics": "^3.3.2"
}, },
"engines": { "engines": {
@@ -5793,15 +5805,16 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/@vitejs/plugin-react": { "node_modules/@vitejs/plugin-react": {
"version": "4.4.1", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.0.tgz",
"integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", "integrity": "sha512-JuLWaEqypaJmOJPLWwO335Ig6jSgC1FTONCWAxnqcQthLTK/Yc9aH6hr9z/87xciejbQcnP3GnA1FWUSWeXaeg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/core": "^7.26.10", "@babel/core": "^7.26.10",
"@babel/plugin-transform-react-jsx-self": "^7.25.9", "@babel/plugin-transform-react-jsx-self": "^7.25.9",
"@babel/plugin-transform-react-jsx-source": "^7.25.9", "@babel/plugin-transform-react-jsx-source": "^7.25.9",
"@rolldown/pluginutils": "1.0.0-beta.9",
"@types/babel__core": "^7.20.5", "@types/babel__core": "^7.20.5",
"react-refresh": "^0.17.0" "react-refresh": "^0.17.0"
}, },
@@ -6091,12 +6104,12 @@
} }
}, },
"node_modules/antd": { "node_modules/antd": {
"version": "5.25.2", "version": "5.25.3",
"resolved": "https://registry.npmjs.org/antd/-/antd-5.25.2.tgz", "resolved": "https://registry.npmjs.org/antd/-/antd-5.25.3.tgz",
"integrity": "sha512-7R2nUvlHhey7Trx64+hCtGXOiy+DTUs1Lv5bwbV1LzEIZIhWb0at1AM6V3K108a5lyoR9n7DX3ptlLF7uYV/DQ==", "integrity": "sha512-tBBcAFRjmWM3sitxrL/FEbQL+MTQntYY5bGa5c1ZZZHXWCynkhS3Ch/gy25mGMUY1M/9Uw3pH029v/RGht1x3w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ant-design/colors": "^7.2.0", "@ant-design/colors": "^7.2.1",
"@ant-design/cssinjs": "^1.23.0", "@ant-design/cssinjs": "^1.23.0",
"@ant-design/cssinjs-utils": "^1.1.3", "@ant-design/cssinjs-utils": "^1.1.3",
"@ant-design/fast-color": "^2.0.6", "@ant-design/fast-color": "^2.0.6",
@@ -6156,9 +6169,9 @@
} }
}, },
"node_modules/antd/node_modules/@ant-design/colors": { "node_modules/antd/node_modules/@ant-design/colors": {
"version": "7.2.0", "version": "7.2.1",
"resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.0.tgz", "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz",
"integrity": "sha512-bjTObSnZ9C/O8MB/B4OUtd/q9COomuJAR2SYfhxLyHvCKn4EKwCN3e+fWGMo7H5InAyV0wL17jdE9ALrdOW/6A==", "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ant-design/fast-color": "^2.0.6" "@ant-design/fast-color": "^2.0.6"
@@ -13266,6 +13279,15 @@
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/phone": {
"version": "3.1.59",
"resolved": "https://registry.npmjs.org/phone/-/phone-3.1.59.tgz",
"integrity": "sha512-CUv22jw0Zgrb/h7v3sEd262zJXS/66h7zyCCRIynx+2FswAJuuFsXsJkIxMUT4UcosKxDx1bJwdZeGnDELLsCw==",
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -14403,9 +14425,9 @@
} }
}, },
"node_modules/react-i18next": { "node_modules/react-i18next": {
"version": "15.5.1", "version": "15.5.2",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.1.tgz", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.2.tgz",
"integrity": "sha512-C8RZ7N7H0L+flitiX6ASjq9p5puVJU1Z8VyL3OgM/QOMRf40BMZX+5TkpxzZVcTmOLPX5zlti4InEX5pFyiVeA==", "integrity": "sha512-ePODyXgmZQAOYTbZXQn5rRsSBu3Gszo69jxW6aKmlSgxKAI1fOhDwSu6bT4EKHciWPKQ7v7lPrjeiadR6Gi+1A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.25.0", "@babel/runtime": "^7.25.0",

View File

@@ -14,17 +14,17 @@
"@fingerprintjs/fingerprintjs": "^4.6.1", "@fingerprintjs/fingerprintjs": "^4.6.1",
"@firebase/analytics": "^0.10.16", "@firebase/analytics": "^0.10.16",
"@firebase/app": "^0.13.0", "@firebase/app": "^0.13.0",
"@firebase/auth": "^1.10.5", "@firebase/auth": "^1.10.6",
"@firebase/firestore": "^4.7.15", "@firebase/firestore": "^4.7.16",
"@firebase/messaging": "^0.12.21", "@firebase/messaging": "^0.12.21",
"@jsreport/browser-client": "^3.1.0", "@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.8.2", "@reduxjs/toolkit": "^2.8.2",
"@sentry/cli": "^2.45.0", "@sentry/cli": "^2.46.0",
"@sentry/react": "^9.22.0", "@sentry/react": "^9.23.0",
"@sentry/vite-plugin": "^3.5.0", "@sentry/vite-plugin": "^3.5.0",
"@splitsoftware/splitio-react": "^2.1.1", "@splitsoftware/splitio-react": "^2.1.1",
"@tanem/react-nprogress": "^5.0.53", "@tanem/react-nprogress": "^5.0.53",
"antd": "^5.25.2", "antd": "^5.25.3",
"apollo-link-logger": "^2.0.1", "apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^4.3.0", "apollo-link-sentry": "^4.3.0",
"autosize": "^6.0.1", "autosize": "^6.0.1",
@@ -47,6 +47,7 @@
"memoize-one": "^6.0.0", "memoize-one": "^6.0.0",
"normalize-url": "^8.0.1", "normalize-url": "^8.0.1",
"object-hash": "^3.0.0", "object-hash": "^3.0.0",
"phone": "^3.1.59",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"query-string": "^9.2.0", "query-string": "^9.2.0",
"raf-schd": "^4.0.3", "raf-schd": "^4.0.3",
@@ -58,7 +59,7 @@
"react-drag-listview": "^2.0.0", "react-drag-listview": "^2.0.0",
"react-grid-gallery": "^1.0.1", "react-grid-gallery": "^1.0.1",
"react-grid-layout": "1.3.4", "react-grid-layout": "1.3.4",
"react-i18next": "^15.4.1", "react-i18next": "^15.5.2",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-image-lightbox": "^5.1.4", "react-image-lightbox": "^5.1.4",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
@@ -139,7 +140,7 @@
"@testing-library/dom": "^10.4.0", "@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@vitejs/plugin-react": "^4.3.4", "@vitejs/plugin-react": "^4.5.0",
"browserslist": "^4.24.5", "browserslist": "^4.24.5",
"browserslist-to-esbuild": "^2.1.1", "browserslist-to-esbuild": "^2.1.1",
"chalk": "^5.4.1", "chalk": "^5.4.1",

View File

@@ -3,6 +3,7 @@ import { Button, Form, InputNumber, Popover, Space } from "antd";
import React, { useState } from "react"; import React, { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { logImEXEvent } from "../../firebase/firebase.utils"; import { logImEXEvent } from "../../firebase/firebase.utils";
export default function CABCpvrtCalculator({ disabled, form }) { export default function CABCpvrtCalculator({ disabled, form }) {
const [visibility, setVisibility] = useState(false); const [visibility, setVisibility] = useState(false);
@@ -39,7 +40,7 @@ export default function CABCpvrtCalculator({ disabled, form }) {
); );
return ( return (
<Popover destroyTooltipOnHide content={popContent} open={visibility} disabled={disabled}> <Popover destroyOnHidden content={popContent} open={visibility} disabled={disabled}>
<Button disabled={disabled} onClick={() => setVisibility(true)}> <Button disabled={disabled} onClick={() => setVisibility(true)}>
<CalculatorFilled /> <CalculatorFilled />
</Button> </Button>

View File

@@ -202,8 +202,6 @@ export const registerMessagingHandlers = ({ socket, client }) => {
text: message.text text: message.text
}; };
// Add cases for other known message types as needed
default: default:
// Log a warning for unhandled message types // Log a warning for unhandled message types
logLocal("handleMessageChanged - Unhandled message type", { type: message.type }); logLocal("handleMessageChanged - Unhandled message type", { type: message.type });
@@ -211,7 +209,7 @@ export const registerMessagingHandlers = ({ socket, client }) => {
} }
} }
return messageRef; // Keep other messages unchanged return messageRef;
}); });
} }
} }
@@ -245,11 +243,8 @@ export const registerMessagingHandlers = ({ socket, client }) => {
}); });
const updatedList = existingList?.conversations const updatedList = existingList?.conversations
? [ ? [newConversation, ...existingList.conversations.filter((conv) => conv.id !== newConversation.id)]
newConversation, : [newConversation]; // Prevent duplicates
...existingList.conversations.filter((conv) => conv.id !== newConversation.id) // Prevent duplicates
]
: [newConversation];
client.cache.writeQuery({ client.cache.writeQuery({
query: CONVERSATION_LIST_QUERY, query: CONVERSATION_LIST_QUERY,
@@ -403,6 +398,7 @@ export const registerMessagingHandlers = ({ socket, client }) => {
} }
break; break;
default: default:
logLocal("handleConversationChanged - Unhandled type", { type }); logLocal("handleConversationChanged - Unhandled type", { type });
client.cache.modify({ client.cache.modify({
@@ -419,10 +415,95 @@ export const registerMessagingHandlers = ({ socket, client }) => {
} }
}; };
// Existing handler for phone number opt-out
const handlePhoneNumberOptedOut = async (data) => {
const { bodyshopid, phone_number } = data;
logLocal("handlePhoneNumberOptedOut - Start", data);
try {
client.cache.modify({
id: "ROOT_QUERY",
fields: {
phone_number_opt_out(existing = [], { readField }) {
const phoneNumberExists = existing.some(
(ref) => readField("phone_number", ref) === phone_number && readField("bodyshopid", ref) === bodyshopid
);
if (phoneNumberExists) {
logLocal("handlePhoneNumberOptedOut - Phone number already in cache", { phone_number, bodyshopid });
return existing;
}
const newOptOut = {
__typename: "phone_number_opt_out",
id: `temporary-${phone_number}-${Date.now()}`,
bodyshopid,
phone_number,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
return [...existing, newOptOut];
}
},
broadcast: true
});
client.cache.evict({
id: "ROOT_QUERY",
fieldName: "phone_number_opt_out",
args: { bodyshopid, search: phone_number }
});
client.cache.gc();
logLocal("handlePhoneNumberOptedOut - Cache updated successfully", data);
} catch (error) {
console.error("Error updating cache for phone number opt-out:", error);
logLocal("handlePhoneNumberOptedOut - Error", { error: error.message });
}
};
// New handler for phone number opt-in
const handlePhoneNumberOptedIn = async (data) => {
const { bodyshopid, phone_number } = data;
logLocal("handlePhoneNumberOptedIn - Start", data);
try {
// Update the Apollo cache for GET_PHONE_NUMBER_OPT_OUTS by removing the phone number
client.cache.modify({
id: "ROOT_QUERY",
fields: {
phone_number_opt_out(existing = [], { readField }) {
// Filter out the phone number from the opt-out list
return existing.filter(
(ref) => !(readField("phone_number", ref) === phone_number && readField("bodyshopid", ref) === bodyshopid)
);
}
},
broadcast: true // Trigger UI updates
});
// Evict the cache entry to force a refetch on next query
client.cache.evict({
id: "ROOT_QUERY",
fieldName: "phone_number_opt_out",
args: { bodyshopid, search: phone_number }
});
client.cache.gc();
logLocal("handlePhoneNumberOptedIn - Cache updated successfully", data);
} catch (error) {
console.error("Error updating cache for phone number opt-in:", error);
logLocal("handlePhoneNumberOptedIn - Error", { error: error.message });
}
};
socket.on("new-message-summary", handleNewMessageSummary); socket.on("new-message-summary", handleNewMessageSummary);
socket.on("new-message-detailed", handleNewMessageDetailed); socket.on("new-message-detailed", handleNewMessageDetailed);
socket.on("message-changed", handleMessageChanged); socket.on("message-changed", handleMessageChanged);
socket.on("conversation-changed", handleConversationChanged); socket.on("conversation-changed", handleConversationChanged);
socket.on("phone-number-opted-out", handlePhoneNumberOptedOut);
socket.on("phone-number-opted-in", handlePhoneNumberOptedIn);
}; };
export const unregisterMessagingHandlers = ({ socket }) => { export const unregisterMessagingHandlers = ({ socket }) => {
@@ -431,4 +512,6 @@ export const unregisterMessagingHandlers = ({ socket }) => {
socket.off("new-message-detailed"); socket.off("new-message-detailed");
socket.off("message-changed"); socket.off("message-changed");
socket.off("conversation-changed"); socket.off("conversation-changed");
socket.off("phone-number-opted-out");
socket.off("phone-number-opted-in");
}; };

View File

@@ -1,4 +1,4 @@
import { Badge, Card, List, Space, Tag } from "antd"; import { Badge, Card, List, Space, Tag, Tooltip } from "antd";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Virtuoso } from "react-virtuoso"; import { Virtuoso } from "react-virtuoso";
@@ -9,6 +9,7 @@ import { TimeAgoFormatter } from "../../utils/DateFormatter";
import PhoneFormatter from "../../utils/PhoneFormatter"; import PhoneFormatter from "../../utils/PhoneFormatter";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component"; import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import _ from "lodash"; import _ from "lodash";
import { ExclamationCircleOutlined } from "@ant-design/icons";
import "./chat-conversation-list.styles.scss"; import "./chat-conversation-list.styles.scss";
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import { GET_PHONE_NUMBER_OPT_OUTS } from "../../graphql/phone-number-opt-out.queries.js"; import { GET_PHONE_NUMBER_OPT_OUTS } from "../../graphql/phone-number-opt-out.queries.js";
@@ -88,7 +89,13 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
const cardExtra = ( const cardExtra = (
<> <>
<Badge count={item.messages_aggregate.aggregate.count} /> <Badge count={item.messages_aggregate.aggregate.count} />
{hasOptOutEntry && <Tag color="red">{t("messaging.labels.no_consent")}</Tag>} {hasOptOutEntry && (
<Tooltip title={t("consent.text_body")}>
<Tag color="red" icon={<ExclamationCircleOutlined />}>
{t("messaging.labels.no_consent")}
</Tag>
</Tooltip>
)}
</> </>
); );

View File

@@ -58,6 +58,7 @@ function ChatConversationContainer({ bodyshop, selectedConversation }) {
userid userid
created_at created_at
read read
is_system
} }
`, `,
data: message data: message

View File

@@ -13,13 +13,14 @@ import JobDocumentsGalleryExternal from "../jobs-documents-gallery/jobs-document
import JobsDocumentImgproxyGalleryExternal from "../jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.external.component"; import JobsDocumentImgproxyGalleryExternal from "../jobs-documents-imgproxy-gallery/jobs-documents-imgproxy-gallery.external.component";
import JobDocumentsLocalGalleryExternal from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component"; import JobDocumentsLocalGalleryExternal from "../jobs-documents-local-gallery/jobs-documents-local-gallery.external.component";
import LoadingSpinner from "../loading-spinner/loading-spinner.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component";
import "./chat-media-selector.styles.scss";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop bodyshop: selectBodyshop
}); });
const mapDispatchToProps = (dispatch) => ({
//setUserLanguage: language => dispatch(setUserLanguage(language)) const mapDispatchToProps = (dispatch) => ({});
});
export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector); export default connect(mapStateToProps, mapDispatchToProps)(ChatMediaSelector);
export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, conversation }) { export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, conversation }) {
@@ -37,9 +38,8 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
fetchPolicy: "network-only", fetchPolicy: "network-only",
nextFetchPolicy: "network-only", nextFetchPolicy: "network-only",
variables: { variables: {
jobId: conversation.job_conversations[0] && conversation.job_conversations[0]?.jobid jobId: conversation.job_conversations[0]?.jobid
}, },
skip: !open || !conversation.job_conversations || conversation.job_conversations.length === 0 skip: !open || !conversation.job_conversations || conversation.job_conversations.length === 0
}); });
@@ -56,11 +56,11 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
//If Imageproxy is on, rely only on the LMS selector //If Imageproxy is on, rely only on the LMS selector
//If not on, use the old methods. //If not on, use the old methods.
const content = ( const content = (
<div> <div className="media-selector-content">
{loading && <LoadingSpinner />} {loading && <LoadingSpinner />}
{error && <AlertComponent message={error.message} type="error" />} {error && <AlertComponent message={error.message} type="error" />}
{selectedMedia.filter((s) => s.isSelected).length >= 10 ? ( {selectedMedia.filter((s) => s.isSelected).length >= 10 ? (
<div style={{ color: "red" }}>{t("messaging.labels.maxtenimages")}</div> <div className="error-message">{t("messaging.labels.maxtenimages")}</div>
) : null} ) : null}
{Imgproxy.treatment === "on" ? ( {Imgproxy.treatment === "on" ? (
@@ -74,7 +74,7 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
{bodyshop.uselocalmediaserver && open && ( {bodyshop.uselocalmediaserver && open && (
<JobDocumentsLocalGalleryExternal <JobDocumentsLocalGalleryExternal
externalMediaState={[selectedMedia, setSelectedMedia]} externalMediaState={[selectedMedia, setSelectedMedia]}
jobId={conversation.job_conversations[0] && conversation.job_conversations[0]?.jobid} jobId={conversation.job_conversations[0]?.jobid}
/> />
)} )}
</> </>
@@ -89,7 +89,7 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
{bodyshop.uselocalmediaserver && open && ( {bodyshop.uselocalmediaserver && open && (
<JobDocumentsLocalGalleryExternal <JobDocumentsLocalGalleryExternal
externalMediaState={[selectedMedia, setSelectedMedia]} externalMediaState={[selectedMedia, setSelectedMedia]}
jobId={conversation.job_conversations[0] && conversation.job_conversations[0]?.jobid} jobId={conversation.job_conversations[0]?.jobid}
/> />
)} )}
</> </>
@@ -100,12 +100,17 @@ export function ChatMediaSelector({ bodyshop, selectedMedia, setSelectedMedia, c
return ( return (
<Popover <Popover
content={ content={
conversation.job_conversations.length === 0 ? <div>{t("messaging.errors.noattachedjobs")}</div> : content conversation.job_conversations.length === 0 ? (
<div className="no-jobs-message">{t("messaging.errors.noattachedjobs")}</div>
) : (
content
)
} }
title={t("messaging.labels.selectmedia")} title={t("messaging.labels.selectmedia")}
trigger="click" trigger="click"
open={open} open={open}
onOpenChange={handleVisibleChange} onOpenChange={handleVisibleChange}
overlayClassName="media-selector-popover"
> >
<Badge count={selectedMedia.filter((s) => s.isSelected).length}> <Badge count={selectedMedia.filter((s) => s.isSelected).length}>
<PictureFilled style={{ margin: "0 .5rem" }} /> <PictureFilled style={{ margin: "0 .5rem" }} />

View File

@@ -0,0 +1,52 @@
.media-selector-popover {
.ant-popover-inner-content {
max-width: 640px;
max-height: 480px;
overflow-y: auto;
padding: 8px;
background-color: #fff;
border-radius: 8px;
}
}
.media-selector-content {
display: flex;
flex-direction: column;
gap: 4px;
}
.error-message {
color: red;
font-size: 12px;
text-align: center;
margin-bottom: 8px;
}
.no-jobs-message {
font-size: 14px;
color: #888;
text-align: center;
padding: 8px;
}
/* Style images within gallery components */
.media-selector-content img {
object-fit: cover;
border-radius: 4px;
margin: 4px;
cursor: pointer;
transition: transform 0.2s;
&:hover {
transform: scale(1.05);
}
}
/* Grid layout for gallery components */
.media-selector-content .ant-image, /* Assuming gallery components use Ant Design's Image */
.media-selector-content .gallery-container { /* Fallback for custom gallery classes */
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 4px;
}

View File

@@ -4,13 +4,16 @@
height: 100%; height: 100%;
width: 100%; width: 100%;
} }
.archive-button { .archive-button {
height: 20px; height: 20px;
border-radius: 4px; border-radius: 4px;
} }
.chat-title { .chat-title {
margin-bottom: 5px; margin-bottom: 5px;
} }
.messages { .messages {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -37,11 +40,13 @@
gap: 8px; gap: 8px;
} }
} }
.chat-send-message-button{
.chat-send-message-button {
margin: 0.3rem; margin: 0.3rem;
padding-left: 0.5rem; padding-left: 0.5rem;
} }
.message-icon { .message-icon {
position: absolute; position: absolute;
bottom: 0.1rem; bottom: 0.1rem;
@@ -125,6 +130,37 @@
} }
} }
.system {
align-items: center;
margin: 0.5rem 10%;
.message {
background-color: #f5f5f5;
border-radius: 10px;
padding: 0.5rem 1rem;
text-align: center;
font-style: italic;
color: #555;
width: fit-content;
max-width: 80%;
}
.system-label {
font-size: 0.75rem;
color: #888;
margin-bottom: 0.2rem;
display: block;
}
.system-date {
font-size: 0.75rem;
color: #888;
margin-top: 0.2rem;
text-align: center;
}
}
.virtuoso-container { .virtuoso-container {
flex: 1; flex: 1;
overflow: auto; overflow: auto;

View File

@@ -2,17 +2,29 @@ import Icon from "@ant-design/icons";
import { Tooltip } from "antd"; import { Tooltip } from "antd";
import i18n from "i18next"; import i18n from "i18next";
import dayjs from "../../utils/day"; import dayjs from "../../utils/day";
import { MdDone, MdDoneAll } from "react-icons/md"; import { MdClose, MdDone, MdDoneAll } from "react-icons/md";
import { DateTimeFormatter } from "../../utils/DateFormatter"; import { DateTimeFormatter } from "../../utils/DateFormatter";
export const renderMessage = (messages, index) => { export const renderMessage = (messages, index) => {
const message = messages[index]; const message = messages[index];
const isSystem = message.is_system;
// Determine message class
const messageClass = isSystem ? "system messages" : message.isoutbound ? "mine messages" : "yours messages";
// Tooltip content based on message type
const tooltipTitle = isSystem ? (
i18n.t("consent.text_body")
) : (
<DateTimeFormatter>{message.created_at}</DateTimeFormatter>
);
return ( return (
<div key={index} className={`${message.isoutbound ? "mine messages" : "yours messages"}`}> <div key={index} className={messageClass}>
<div className="message msgmargin"> <div className="message msgmargin">
<Tooltip title={DateTimeFormatter({ children: message.created_at })}> <Tooltip title={tooltipTitle}>
<div> <div>
{isSystem && <span className="system-label">System</span>}
{/* Render images if available */} {/* Render images if available */}
{message.image && message.image_path?.length > 0 && ( {message.image && message.image_path?.length > 0 && (
<div className="message-images"> <div className="message-images">
@@ -26,20 +38,31 @@ export const renderMessage = (messages, index) => {
</div> </div>
)} )}
{/* Render text if available */} {/* Render text if available */}
{message.text && <div>{message.text}</div>} {message.text && <div className="message-text">{message.text}</div>}
{/* Render date for system messages */}
{isSystem && (
<div className="system-date">
<DateTimeFormatter>{message.created_at}</DateTimeFormatter>
</div>
)}
</div> </div>
</Tooltip> </Tooltip>
{/* Message status icons */} {/* Message status icons for non-system messages */}
{message.status && (message.status === "sent" || message.status === "delivered") && ( {!isSystem &&
<div className="message-status"> message.status &&
<Icon component={message.status === "sent" ? MdDone : MdDoneAll} className="message-icon" /> (message.status === "sent" || message.status === "delivered" || message.status === "failed") && (
</div> <div className="message-status">
)} <Icon
component={message.status === "sent" ? MdDone : message.status === "delivered" ? MdDoneAll : MdClose}
className="message-icon"
style={message.status === "failed" ? { color: "#ff0000" } : undefined}
/>
</div>
)}
</div> </div>
{/* Outbound message metadata for non-system messages */}
{/* Outbound message metadata */} {!isSystem && message.isoutbound && (
{message.isoutbound && (
<div style={{ fontSize: 10 }}> <div style={{ fontSize: 10 }}>
{i18n.t("messaging.labels.sentby", { {i18n.t("messaging.labels.sentby", {
by: message.userid, by: message.userid,

View File

@@ -1,6 +1,6 @@
import { LoadingOutlined, SendOutlined } from "@ant-design/icons"; import { ExclamationCircleOutlined, LoadingOutlined, SendOutlined } from "@ant-design/icons";
import { Alert, Input, Spin } from "antd"; import { Alert, Input, Space, Spin, Tooltip } from "antd";
import React, { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
@@ -68,48 +68,67 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
}; };
return ( return (
<div className="imex-flex-row" style={{ width: "100%" }}> <Space direction="vertical" style={{ width: "100%" }} size="middle">
{isOptedOut && <Alert message={t("messaging.errors.no_consent")} type="warning" style={{ marginBottom: 8 }} />} {isOptedOut && (
<ChatPresetsComponent className="imex-flex-row__margin" /> <Tooltip title={t("consent.text_body")}>
<ChatMediaSelector <Alert
conversation={conversation} showIcon={true}
selectedMedia={selectedMedia} icon={<ExclamationCircleOutlined />}
setSelectedMedia={setSelectedMedia} message={t("messaging.errors.no_consent")}
/> type="error"
<span style={{ flex: 1 }}>
<Input.TextArea
className="imex-flex-row__margin imex-flex-row__grow"
allowClear
autoFocus
ref={inputArea}
autoSize={{ minRows: 1, maxRows: 4 }}
value={message}
disabled={isSending || isOptedOut}
placeholder={t("messaging.labels.typeamessage")}
onChange={(e) => setMessage(e.target.value)}
onPressEnter={(event) => {
event.preventDefault();
if (!event.shiftKey && !isOptedOut) handleEnter();
}}
/>
</span>
<SendOutlined
className="chat-send-message-button"
disabled={isOptedOut || message === "" || !message}
onClick={handleEnter}
/>
<Spin
style={{ display: `${isSending ? "" : "none"}` }}
indicator={
<LoadingOutlined
style={{
fontSize: 24
}}
spin
/> />
} </Tooltip>
/> )}
</div> <div className="imex-flex-row" style={{ width: "100%" }}>
{!isOptedOut && (
<>
<ChatPresetsComponent disabled={isSending} className="imex-flex-row__margin" />
<ChatMediaSelector
disabled={isSending}
conversation={conversation}
selectedMedia={selectedMedia}
setSelectedMedia={setSelectedMedia}
/>
</>
)}
<span style={{ flex: 1 }}>
<Input.TextArea
className="imex-flex-row__margin imex-flex-row__grow"
allowClear
autoFocus
ref={inputArea}
autoSize={{ minRows: 1, maxRows: 4 }}
value={message}
disabled={isSending || isOptedOut}
placeholder={t("messaging.labels.typeamessage")}
onChange={(e) => setMessage(e.target.value)}
onPressEnter={(event) => {
event.preventDefault();
if (!event.shiftKey && !isOptedOut) handleEnter();
}}
/>
</span>
{!isOptedOut && (
<SendOutlined
className="chat-send-message-button"
disabled={isSending || message === "" || !message}
onClick={handleEnter}
/>
)}
<Spin
style={{ display: `${isSending ? "" : "none"}` }}
indicator={
<LoadingOutlined
style={{
fontSize: 24
}}
spin
/>
}
/>
</div>
</Space>
); );
} }

View File

@@ -15,6 +15,7 @@ import {
HomeFilled, HomeFilled,
ImportOutlined, ImportOutlined,
LineChartOutlined, LineChartOutlined,
OneToOneOutlined,
PaperClipOutlined, PaperClipOutlined,
PhoneOutlined, PhoneOutlined,
PlusCircleOutlined, PlusCircleOutlined,
@@ -24,6 +25,7 @@ import {
TeamOutlined, TeamOutlined,
ToolFilled, ToolFilled,
UnorderedListOutlined, UnorderedListOutlined,
UsergroupAddOutlined,
UserOutlined UserOutlined
} from "@ant-design/icons"; } from "@ant-design/icons";
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
@@ -40,6 +42,7 @@ import { RiSurveyLine } from "react-icons/ri";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
import { GET_UNREAD_COUNT } from "../../graphql/notifications.queries.js"; import { GET_UNREAD_COUNT } from "../../graphql/notifications.queries.js";
import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors"; import { selectRecentItems, selectSelectedHeader } from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions"; 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 { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import day from "../../utils/day.js"; import day from "../../utils/day.js";
import InstanceRenderManager from "../../utils/instanceRenderMgr"; import InstanceRenderManager from "../../utils/instanceRenderMgr";
import { useIsEmployee } from "../../utils/useIsEmployee.js";
import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component"; import { HasFeatureAccess } from "../feature-wrapper/feature-wrapper.component";
import LockWrapper from "../lock-wrapper/lock-wrapper.component"; import LockWrapper from "../lock-wrapper/lock-wrapper.component";
import NotificationCenterContainer from "../notification-center/notification-center.container.jsx"; import NotificationCenterContainer from "../notification-center/notification-center.container.jsx";
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
import { useIsEmployee } from "../../utils/useIsEmployee.js";
// Redux mappings // Redux mappings
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
@@ -642,17 +644,32 @@ function Header({
label: t("menus.header.help"), label: t("menus.header.help"),
onClick: () => window.open("https://help.imex.online/", "_blank") onClick: () => window.open("https://help.imex.online/", "_blank")
}, },
...(InstanceRenderManager({ imex: true, rome: false }) {
? [ key: "remoteassist",
{ id: "header-remote-assist",
key: "rescue", icon: <OneToOneOutlined />,
id: "header-rescue", label: t("menus.header.remoteassist"),
icon: <CarFilled />, children: [
label: t("menus.header.rescueme"), ...(InstanceRenderManager({ imex: true, rome: false })
onClick: () => window.open("https://imexrescue.com/", "_blank") ? [
} {
] key: "rescue",
: []), id: "header-rescue",
icon: <PlusCircleOutlined />,
label: t("menus.header.rescueme"),
onClick: () => window.open("https://imexrescue.com/", "_blank")
}
]
: []),
{
key: "rescue-zoho",
id: "header-rescue-zoho",
icon: <UsergroupAddOutlined />,
label: t("menus.header.rescuemezoho"),
onClick: () => window.open("https://join.zoho.com/", "_blank")
}
]
},
{ {
key: "shiftclock", key: "shiftclock",
id: "header-shiftclock", id: "header-shiftclock",

View File

@@ -1,7 +1,7 @@
import { useMutation } from "@apollo/client"; import { useMutation } from "@apollo/client";
import { Button, Card, Form, Input, Switch } from "antd"; import { Button, Card, Form, Input, Switch } from "antd";
import queryString from "query-string"; import queryString from "query-string";
import React, { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { useLocation, useNavigate, useParams } from "react-router-dom"; import { useLocation, useNavigate, useParams } from "react-router-dom";
@@ -9,7 +9,6 @@ import { createStructuredSelector } from "reselect";
import { logImEXEvent } from "../../../../firebase/firebase.utils"; import { logImEXEvent } from "../../../../firebase/firebase.utils";
import { MARK_APPOINTMENT_ARRIVED, MARK_LATEST_APPOINTMENT_ARRIVED } from "../../../../graphql/appointments.queries"; import { MARK_APPOINTMENT_ARRIVED, MARK_LATEST_APPOINTMENT_ARRIVED } from "../../../../graphql/appointments.queries";
import { UPDATE_JOB } from "../../../../graphql/jobs.queries"; import { UPDATE_JOB } from "../../../../graphql/jobs.queries";
import { UPDATE_OWNER } from "../../../../graphql/owners.queries";
import { insertAuditTrail } from "../../../../redux/application/application.actions"; import { insertAuditTrail } from "../../../../redux/application/application.actions";
import { selectBodyshop, selectCurrentUser } from "../../../../redux/user/user.selectors"; import { selectBodyshop, selectCurrentUser } from "../../../../redux/user/user.selectors";
import AuditTrailMapping from "../../../../utils/AuditTrailMappings"; import AuditTrailMapping from "../../../../utils/AuditTrailMappings";
@@ -32,7 +31,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [markAptArrived] = useMutation(MARK_APPOINTMENT_ARRIVED); const [markAptArrived] = useMutation(MARK_APPOINTMENT_ARRIVED);
const [markLatestAptArrived] = useMutation(MARK_LATEST_APPOINTMENT_ARRIVED); const [markLatestAptArrived] = useMutation(MARK_LATEST_APPOINTMENT_ARRIVED);
const [updateOwner] = useMutation(UPDATE_OWNER);
const notification = useNotification(); const notification = useNotification();
const { jobId } = useParams(); const { jobId } = useParams();
@@ -129,24 +127,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
} }
} }
if (type === "intake" && job.owner && job.owner.id) {
//Updae Owner Allow to Text
const updateOwnerResult = await updateOwner({
variables: {
ownerId: job.owner.id,
owner: { allow_text_message: values.allow_text_message }
}
});
if (!!updateOwnerResult.errors) {
notification["error"]({
message: t("checklist.errors.complete", {
error: JSON.stringify(result.errors)
})
});
}
}
setLoading(false); setLoading(false);
if (!!!result.errors) { if (!!!result.errors) {
@@ -189,7 +169,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
initialValues={{ initialValues={{
...(type === "intake" && { ...(type === "intake" && {
addToProduction: true, addToProduction: true,
allow_text_message: job.owner && job.owner.allow_text_message,
scheduled_completion: scheduled_completion:
(job && job.scheduled_completion && dayjs(job.scheduled_completion)) || (job && job.scheduled_completion && dayjs(job.scheduled_completion)) ||
(job && (job &&
@@ -228,14 +207,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
> >
<Switch disabled={readOnly} /> <Switch disabled={readOnly} />
</Form.Item> </Form.Item>
<Form.Item
name="allow_text_message"
valuePropName="checked"
label={t("checklist.labels.allow_text_message")}
disabled={readOnly}
>
<Switch disabled={readOnly} />
</Form.Item>
<Form.Item <Form.Item
name="scheduled_completion" name="scheduled_completion"
label={t("jobs.fields.scheduled_completion")} label={t("jobs.fields.scheduled_completion")}

View File

@@ -80,7 +80,7 @@ export function JobEmployeeAssignments({
); );
return ( return (
<Popover destroyTooltipOnHide content={popContent} open={visibility}> <Popover destroyOnHidden content={popContent} open={visibility}>
<Spin spinning={loading}> <Spin spinning={loading}>
<DataLabel label={t("jobs.fields.employee_body")}> <DataLabel label={t("jobs.fields.employee_body")}>
{body ? ( {body ? (

View File

@@ -1,4 +1,4 @@
import { Form, Input, Switch } from "antd"; import { Form, Input } from "antd";
import React, { useContext } from "react"; import React, { useContext } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import JobCreateContext from "../../pages/jobs-create/jobs-create.context"; import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
@@ -129,13 +129,6 @@ export default function JobsCreateOwnerInfoNewComponent() {
<Form.Item label={t("owners.fields.preferred_contact")} name={["owner", "data", "preferred_contact"]}> <Form.Item label={t("owners.fields.preferred_contact")} name={["owner", "data", "preferred_contact"]}>
<Input disabled={!state.owner.new} /> <Input disabled={!state.owner.new} />
</Form.Item> </Form.Item>
<Form.Item
label={t("owners.fields.allow_text_message")}
valuePropName="checked"
name={["owner", "data", "allow_text_message"]}
>
<Switch disabled={!state.owner.new} />
</Form.Item>
</LayoutFormRow> </LayoutFormRow>
</div> </div>
); );

View File

@@ -72,7 +72,7 @@ export default function JobsCreateVehicleInfoPredefined({ disabled, form }) {
open={open} open={open}
placement="left" placement="left"
onOpenChange={handleOpenChange} onOpenChange={handleOpenChange}
destroyTooltipOnHide destroyOnHidden
> >
<SearchOutlined style={{ cursor: "pointer" }} /> <SearchOutlined style={{ cursor: "pointer" }} />
</Popover> </Popover>

View File

@@ -9,6 +9,7 @@ import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { useSocket } from "../../contexts/SocketIO/useSocket.js"; import { useSocket } from "../../contexts/SocketIO/useSocket.js";
import { auth, logImEXEvent } from "../../firebase/firebase.utils"; import { auth, logImEXEvent } from "../../firebase/firebase.utils";
import { CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT } from "../../graphql/appointments.queries"; import { CANCEL_APPOINTMENTS_BY_JOB_ID, INSERT_MANUAL_APPT } from "../../graphql/appointments.queries";
@@ -32,7 +33,6 @@ import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util"; import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
import DuplicateJob from "./jobs-detail-header-actions.duplicate.util"; import DuplicateJob from "./jobs-detail-header-actions.duplicate.util";
import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production"; import JobsDetailHeaderActionsToggleProduction from "./jobs-detail-header-actions.toggle-production";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -1078,17 +1078,26 @@ export function JobsDetailHeaderActions({
menuItems.push({ menuItems.push({
key: "deletejob", key: "deletejob",
id: "job-actions-deletejob", id: "job-actions-deletejob",
label: ( label:
<Popconfirm job.job_watchers.length === 0 ? (
title={t("jobs.labels.deleteconfirm")} <Popconfirm
okText={t("general.labels.yes")} title={t("jobs.labels.deleteconfirm")}
cancelText={t("general.labels.no")} okText={t("general.labels.yes")}
onClick={(e) => e.stopPropagation()} cancelText={t("general.labels.no")}
onConfirm={handleDeleteJob} onClick={(e) => e.stopPropagation()}
> onConfirm={handleDeleteJob}
{t("menus.jobsactions.deletejob")} >
</Popconfirm> {t("menus.jobsactions.deletejob")}
) </Popconfirm>
) : (
<Popconfirm
title={t("jobs.labels.deletewatchers")}
onClick={(e) => e.stopPropagation()}
showCancel={false}
>
{t("menus.jobsactions.deletejob")}
</Popconfirm>
)
}); });
} }
@@ -1109,8 +1118,8 @@ export function JobsDetailHeaderActions({
<RbacWrapper action="jobs:void" noauth> <RbacWrapper action="jobs:void" noauth>
<Popconfirm <Popconfirm
title={t("jobs.labels.voidjob")} title={t("jobs.labels.voidjob")}
okText="Yes" okText={t("general.labels.yes")}
cancelText="No" cancelText={t("general.labels.no")}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
onConfirm={handleVoidJob} onConfirm={handleVoidJob}
> >

View File

@@ -167,7 +167,18 @@ export function JobsDetailHeaderActionsToggleProduction({
<FormDateTimePickerComponent disabled={jobRO} /> <FormDateTimePickerComponent disabled={jobRO} />
</Form.Item> </Form.Item>
<Form.Item name={["actual_delivery"]} label={t("jobs.fields.actual_delivery")}> <Form.Item
name={["actual_delivery"]}
label={t("jobs.fields.actual_delivery")}
rules={[
{
required: bodyshop.deliverchecklist.actual_delivery
? bodyshop.deliverchecklist.actual_delivery
: false
//message: t("general.validation.required"),
}
]}
>
<FormDateTimePickerComponent disabled={jobRO} /> <FormDateTimePickerComponent disabled={jobRO} />
</Form.Item> </Form.Item>
</> </>

View File

@@ -1,17 +1,20 @@
import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined, WarningFilled } from "@ant-design/icons"; import { BranchesOutlined, ExclamationCircleFilled, PauseCircleOutlined, WarningFilled } from "@ant-design/icons";
import { useMutation } from "@apollo/client";
import { Card, Checkbox, Col, Divider, Row, Space, Tag, Tooltip } from "antd"; import { Card, Checkbox, Col, Divider, Row, Space, Tag, Tooltip } from "antd";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { useMutation } from "@apollo/client";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { UPDATE_JOB } from "../../graphql/jobs.queries";
import { insertAuditTrail } from "../../redux/application/application.actions.js";
import { selectJobReadOnly } from "../../redux/application/application.selectors"; import { selectJobReadOnly } from "../../redux/application/application.selectors";
import { setModalContext } from "../../redux/modals/modals.actions"; import { setModalContext } from "../../redux/modals/modals.actions";
import { selectBodyshop } from "../../redux/user/user.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors";
import { UPDATE_JOB } from "../../graphql/jobs.queries"; import AuditTrailMapping from "../../utils/AuditTrailMappings.js";
import CurrencyFormatter from "../../utils/CurrencyFormatter"; import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateTimeFormatter } from "../../utils/DateFormatter"; import { DateTimeFormatter, DateTimeFormatterFunction } from "../../utils/DateFormatter";
import dayjs from "../../utils/day"; import dayjs from "../../utils/day";
import PhoneNumberFormatter from "../../utils/PhoneFormatter"; import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import ChatOpenButton from "../chat-open-button/chat-open-button.component"; import ChatOpenButton from "../chat-open-button/chat-open-button.component";
@@ -24,7 +27,6 @@ import ProductionListColumnComment from "../production-list-columns/production-l
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component"; import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component"; import VehicleVinDisplay from "../vehicle-vin-display/vehicle-vin-display.component";
import "./jobs-detail-header.styles.scss"; import "./jobs-detail-header.styles.scss";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
jobRO: selectJobReadOnly, jobRO: selectJobReadOnly,
@@ -38,6 +40,14 @@ const mapDispatchToProps = (dispatch) => ({
context: context, context: context,
modal: "printCenter" modal: "printCenter"
}) })
),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(
insertAuditTrail({
jobid,
operation,
type
})
) )
}); });
@@ -49,7 +59,7 @@ const colSpan = {
xl: { span: 6 } xl: { span: 6 }
}; };
export function JobsDetailHeader({ job, bodyshop, disabled }) { export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { notification } = useNotification(); const { notification } = useNotification();
const [notesClamped, setNotesClamped] = useState(true); const [notesClamped, setNotesClamped] = useState(true);
@@ -66,7 +76,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
const handleCheckboxChange = async (field, checked) => { const handleCheckboxChange = async (field, checked) => {
const value = checked ? dayjs().toISOString() : null; const value = checked ? dayjs().toISOString() : null;
try { try {
await updateJob({ const ret = await updateJob({
variables: { variables: {
jobId: job.id, jobId: job.id,
job: { [field]: value } job: { [field]: value }
@@ -74,6 +84,16 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
refetchQueries: ["GET_JOB_BY_PK"], refetchQueries: ["GET_JOB_BY_PK"],
awaitRefetchQueries: true awaitRefetchQueries: true
}); });
insertAuditTrail({
jobid: job.id,
operation: AuditTrailMapping.jobfieldchange(
field,
ret.data.update_jobs.returning[0][field]
? DateTimeFormatterFunction(ret.data.update_jobs.returning[0][field])
: checked
),
type: "jobfieldchange"
});
} catch (error) { } catch (error) {
notification.error({ notification.error({
message: t("jobs.errors.saving", { error: error.message }) message: t("jobs.errors.saving", { error: error.message })

View File

@@ -1,14 +1,15 @@
import { Form, Input, Switch } from "antd"; import { Form, Input, Tooltip } from "antd";
import React from "react"; import { CloseCircleFilled } from "@ant-design/icons";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
import FormItemEmail from "../form-items-formatted/email-form-item.component"; import FormItemEmail from "../form-items-formatted/email-form-item.component";
import FormItemPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
import LayoutFormRow from "../layout-form-row/layout-form-row.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component";
import { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
export default function OwnerDetailFormComponent({ form, loading }) { export default function OwnerDetailFormComponent({ form, loading, isPhone1OptedOut, isPhone2OptedOut }) {
const { t } = useTranslation(); const { t } = useTranslation();
const { getFieldValue } = form; const { getFieldValue } = form;
return ( return (
<div> <div>
<FormFieldsChanged form={form} /> <FormFieldsChanged form={form} />
@@ -26,7 +27,7 @@ export default function OwnerDetailFormComponent({ form, loading }) {
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item label={t("owners.fields.accountingid")} name="accountingid"> <Form.Item label={t("owners.fields.accountingid")} name="accountingid">
<Input disabled/> <Input disabled />
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow header={t("owners.forms.address")}> <LayoutFormRow header={t("owners.forms.address")}>
@@ -50,9 +51,6 @@ export default function OwnerDetailFormComponent({ form, loading }) {
</Form.Item> </Form.Item>
</LayoutFormRow> </LayoutFormRow>
<LayoutFormRow header={t("owners.forms.contact")}> <LayoutFormRow header={t("owners.forms.contact")}>
<Form.Item label={t("owners.fields.allow_text_message")} name="allow_text_message" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item <Form.Item
label={t("owners.fields.ownr_ea")} label={t("owners.fields.ownr_ea")}
name="ownr_ea" name="ownr_ea"
@@ -65,19 +63,55 @@ export default function OwnerDetailFormComponent({ form, loading }) {
> >
<FormItemEmail email={getFieldValue("ownr_ea")} /> <FormItemEmail email={getFieldValue("ownr_ea")} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item label={t("owners.fields.ownr_ph1")} style={{ marginBottom: 0 }}>
label={t("owners.fields.ownr_ph1")} <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
name="ownr_ph1" <Form.Item
rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "ownr_ph1")]} name="ownr_ph1"
> noStyle
<FormItemPhone /> rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "ownr_ph1")]}
>
<Input style={{ flex: 1, minWidth: "150px" }} />
</Form.Item>
{isPhone1OptedOut && (
<Tooltip title={t("consent.text_body")}>
<CloseCircleFilled
style={{
color: "#ff4d4f",
fontSize: 16,
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "100%"
}}
/>
</Tooltip>
)}
</div>
</Form.Item> </Form.Item>
<Form.Item <Form.Item label={t("owners.fields.ownr_ph2")} style={{ marginBottom: 0 }}>
label={t("owners.fields.ownr_ph2")} <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
name="ownr_ph2" <Form.Item
rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "ownr_ph2")]} name="ownr_ph2"
> noStyle
<FormItemPhone /> rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "ownr_ph2")]}
>
<Input style={{ flex: 1, minWidth: "150px" }} />
</Form.Item>
{isPhone2OptedOut && (
<Tooltip title={t("consent.text_body")}>
<CloseCircleFilled
style={{
color: "#ff4d4f",
fontSize: 16,
display: "flex",
alignItems: "center",
justifyContent: "center",
height: "100%"
}}
/>
</Tooltip>
)}
</div>
</Form.Item> </Form.Item>
<Form.Item label={t("owners.fields.preferred_contact")} name="preferred_contact"> <Form.Item label={t("owners.fields.preferred_contact")} name="preferred_contact">
<Input /> <Input />

View File

@@ -1,69 +1,115 @@
import { Button, Form, Popconfirm } from "antd"; import { Button, Form, Popconfirm } from "antd";
import { PageHeader } from "@ant-design/pro-layout"; import { PageHeader } from "@ant-design/pro-layout";
import { useEffect, useState } from "react";
import React, { useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useMutation } from "@apollo/client"; import { useApolloClient, useMutation } from "@apollo/client";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { DELETE_OWNER, UPDATE_OWNER } from "../../graphql/owners.queries"; import { DELETE_OWNER, UPDATE_OWNER } from "../../graphql/owners.queries";
import { selectBodyshop } from "../../redux/user/user.selectors"; // Adjust path
import { phoneNumberOptOutService } from "../../utils/phoneOptOutService.js"; // Adjust path
import OwnerDetailFormComponent from "./owner-detail-form.component"; import OwnerDetailFormComponent from "./owner-detail-form.component";
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { phone } from "phone"; // Import phone utility for formatting
function OwnerDetailFormContainer({ owner, refetch }) { // Connect to Redux to access bodyshop
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
function OwnerDetailFormContainer({ owner, refetch, bodyshop }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [form] = Form.useForm(); const [form] = Form.useForm();
const history = useNavigate(); const navigate = useNavigate();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [optedOutPhones, setOptedOutPhones] = useState(new Set());
const [updateOwner] = useMutation(UPDATE_OWNER); const [updateOwner] = useMutation(UPDATE_OWNER);
const [deleteOwner] = useMutation(DELETE_OWNER); const [deleteOwner] = useMutation(DELETE_OWNER);
const notification = useNotification(); const notification = useNotification();
const apolloClient = useApolloClient();
// Fetch opt-out status on mount
useEffect(() => {
const fetchOptOutStatus = async () => {
if (bodyshop?.id && bodyshop?.messagingservicesid && (owner?.ownr_ph1 || owner?.ownr_ph2)) {
const phoneNumbers = [owner.ownr_ph1, owner.ownr_ph2].filter(Boolean);
const optOutSet = await phoneNumberOptOutService(apolloClient, bodyshop.id, phoneNumbers);
setOptedOutPhones(optOutSet);
} else {
setOptedOutPhones(new Set());
}
};
fetchOptOutStatus();
}, [apolloClient, bodyshop?.id, bodyshop?.messagingservicesid, owner?.ownr_ph1, owner?.ownr_ph2]);
// Reset form fields when owner changes
useEffect(() => {
form.setFieldsValue({
ownr_ph1: owner?.ownr_ph1,
ownr_ph2: owner?.ownr_ph2,
...owner
});
}, [owner, form]);
const handleDelete = async () => { const handleDelete = async () => {
setLoading(true); setLoading(true);
const result = await deleteOwner({ try {
variables: { id: owner.id } const result = await deleteOwner({
}); variables: { id: owner.id }
console.log(result); });
if (result.errors) { if (result.errors) {
notification["error"]({ notification.error({
message: t("owners.errors.deleting", {
error: JSON.stringify(result.errors)
})
});
} else {
notification.success({
message: t("owners.successes.delete")
});
navigate(`/manage/owners`);
}
} catch (error) {
notification.error({
message: t("owners.errors.deleting", { message: t("owners.errors.deleting", {
error: JSON.stringify(result.errors) error: error.message
}) })
}); });
} finally {
setLoading(false); setLoading(false);
} else {
notification["success"]({
message: t("owners.successes.delete")
});
setLoading(false);
history(`/manage/owners`);
} }
}; };
const handleFinish = async (values) => { const handleFinish = async (values) => {
setLoading(true); setLoading(true);
const result = await updateOwner({ try {
variables: { ownerId: owner.id, owner: values } const result = await updateOwner({
}); variables: { ownerId: owner.id, owner: values }
});
if (!!result.errors) { if (result.errors) {
notification["error"]({ notification.error({
message: t("owners.errors.saving", {
error: JSON.stringify(result.errors)
})
});
} else {
notification.success({
message: t("owners.successes.save")
});
if (refetch) await refetch();
form.resetFields();
}
} catch (error) {
notification.error({
message: t("owners.errors.saving", { message: t("owners.errors.saving", {
error: JSON.stringify(result.errors) error: error.message
}) })
}); });
} finally {
setLoading(false); setLoading(false);
return;
} }
notification["success"]({
message: t("owners.successes.save")
});
if (refetch) await refetch();
form.resetFields();
form.resetFields();
setLoading(false);
}; };
return ( return (
@@ -72,6 +118,7 @@ function OwnerDetailFormContainer({ owner, refetch }) {
title={t("menus.header.owners")} title={t("menus.header.owners")}
extra={[ extra={[
<Popconfirm <Popconfirm
key="delete"
trigger="click" trigger="click"
onConfirm={handleDelete} onConfirm={handleDelete}
disabled={owner.jobs.length !== 0} disabled={owner.jobs.length !== 0}
@@ -81,16 +128,29 @@ function OwnerDetailFormContainer({ owner, refetch }) {
{t("general.actions.delete")} {t("general.actions.delete")}
</Button> </Button>
</Popconfirm>, </Popconfirm>,
<Button type="primary" loading={loading} onClick={() => form.submit()}> <Button key="save" type="primary" loading={loading} onClick={() => form.submit()}>
{t("general.actions.save")} {t("general.actions.save")}
</Button> </Button>
]} ]}
/> />
<Form form={form} onFinish={handleFinish} autoComplete="off" layout="vertical" initialValues={owner}> <Form form={form} onFinish={handleFinish} autoComplete="off" layout="vertical" initialValues={owner}>
<OwnerDetailFormComponent loading={loading} form={form} /> <OwnerDetailFormComponent
loading={loading}
form={form}
isPhone1OptedOut={
bodyshop?.messagingservicesid &&
owner?.ownr_ph1 &&
optedOutPhones.has(phone(owner.ownr_ph1, "CA").phoneNumber?.replace(/^\+1/, ""))
}
isPhone2OptedOut={
bodyshop?.messagingservicesid &&
owner?.ownr_ph2 &&
optedOutPhones.has(phone(owner.ownr_ph2, "CA").phoneNumber?.replace(/^\+1/, ""))
}
/>
</Form> </Form>
</> </>
); );
} }
export default OwnerDetailFormContainer; export default connect(mapStateToProps)(OwnerDetailFormContainer);

View File

@@ -75,7 +75,7 @@ export function PartsOrderBackorderEta({
); );
return ( return (
<Popover destroyTooltipOnHide content={popContent} open={visibility} disabled={disabled}> <Popover destroyOnHidden content={popContent} open={visibility} disabled={disabled}>
<DateFormatter>{backordered_eta}</DateFormatter> <DateFormatter>{backordered_eta}</DateFormatter>
{isAlreadyBackordered && <CalendarFilled style={{ cursor: "pointer" }} onClick={handlePopover} />} {isAlreadyBackordered && <CalendarFilled style={{ cursor: "pointer" }} onClick={handlePopover} />}
{loading && <Spin />} {loading && <Spin />}

View File

@@ -84,7 +84,7 @@ export function PartsOrderLineBackorderButton({ partsOrderStatus, partsLineId, j
); );
return ( return (
<Popover destroyTooltipOnHide content={popContent} open={visibility} disabled={disabled}> <Popover destroyOnHidden content={popContent} open={visibility} disabled={disabled}>
<Button loading={loading} onClick={handlePopover}> <Button loading={loading} onClick={handlePopover}>
{isAlreadyBackordered ? t("parts_orders.actions.receive") : t("parts_orders.actions.backordered")} {isAlreadyBackordered ? t("parts_orders.actions.receive") : t("parts_orders.actions.backordered")}
</Button> </Button>

View File

@@ -1,14 +1,20 @@
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import { Input, Table } from "antd"; import { Input, Table, Typography } from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { createStructuredSelector } from "reselect"; import { createStructuredSelector } from "reselect";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors"; import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import { GET_PHONE_NUMBER_OPT_OUTS } from "../../graphql/phone-number-opt-out.queries"; import { GET_PHONE_NUMBER_OPT_OUTS } from "../../graphql/phone-number-opt-out.queries";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import { TimeAgoFormatter } from "../../utils/DateFormatter"; import { TimeAgoFormatter } from "../../utils/DateFormatter";
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
import { useTranslation } from "react-i18next";
import { useState } from "react";
const { Paragraph } = Typography;
// Commented out Associated Owners section for now
//import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
//import { Link } from "react-router-dom";
//import { useMemo, useState } from "react";
const mapStateToProps = createStructuredSelector({ const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop, bodyshop: selectBodyshop,
@@ -20,18 +26,95 @@ const mapDispatchToProps = () => ({});
function PhoneNumberConsentList({ bodyshop, currentUser }) { function PhoneNumberConsentList({ bodyshop, currentUser }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const { loading, data } = useQuery(GET_PHONE_NUMBER_OPT_OUTS, {
// Fetch opt-out phone numbers
const { loading: optOutLoading, data: optOutData } = useQuery(GET_PHONE_NUMBER_OPT_OUTS, {
variables: { bodyshopid: bodyshop.id, search: search ? `%${search}%` : undefined }, variables: { bodyshopid: bodyshop.id, search: search ? `%${search}%` : undefined },
fetchPolicy: "network-only" fetchPolicy: "network-only"
}); });
// Commented out Associated Owners section for now
/*// Prepare phone numbers for owner query
const phoneNumbers = useMemo(() => {
return optOutData?.phone_number_opt_out?.map((item) => item.phone_number) || [];
}, [optOutData?.phone_number_opt_out]);
const allPhoneNumbers = useMemo(() => {
const normalized = phoneNumbers;
const withPlusOne = phoneNumbers.map((num) => `+1${num}`);
return [...normalized, ...withPlusOne].filter(Boolean);
}, [phoneNumbers]);
// Fetch owners for all phone numbers
const { loading: ownersLoading, data: ownersData } = useQuery(SEARCH_OWNERS_BY_PHONE_NUMBERS, {
variables: { bodyshopid: bodyshop.id, phone_numbers: allPhoneNumbers },
skip: allPhoneNumbers.length === 0 || !bodyshop.id,
fetchPolicy: "network-only"
});
// Map phone numbers to their associated owners and identify phone field
const getAssociatedOwners = (phoneNumber) => {
if (!ownersData?.owners) return [];
const normalizedPhone = phoneNumber.replace(/^\+1/, "");
return ownersData.owners
.filter(
(owner) =>
owner.ownr_ph1 === phoneNumber ||
owner.ownr_ph2 === phoneNumber ||
owner.ownr_ph1 === normalizedPhone ||
owner.ownr_ph2 === normalizedPhone ||
owner.ownr_ph1 === `+1${phoneNumber}` ||
owner.ownr_ph2 === `+1${phoneNumber}`
)
.map((owner) => ({
...owner,
phoneField:
[owner.ownr_ph1, owner.ownr_ph2].includes(phoneNumber) ||
[owner.ownr_ph1, owner.ownr_ph2].includes(normalizedPhone) ||
[owner.ownr_ph1, owner.ownr_ph2].includes(`+1${phoneNumber}`)
? owner.ownr_ph1 === phoneNumber ||
owner.ownr_ph1 === normalizedPhone ||
owner.ownr_ph1 === `+1${phoneNumber}`
? t("consent.phone_1")
: t("consent.phone_2")
: null
}));
};*/
const columns = [ const columns = [
{ {
title: t("consent.phone_number"), title: t("consent.phone_number"),
dataIndex: "phone_number", dataIndex: "phone_number",
render: (text) => <PhoneNumberFormatter>{text}</PhoneNumberFormatter>, render: (text) => <ChatOpenButton phone={text} />,
sorter: (a, b) => a.phone_number.localeCompare(b.phone_number) sorter: (a, b) => a.phone_number.localeCompare(b.phone_number)
}, },
// Commented out Associated Owners section for now
/*{
title: t("consent.associated_owners"),
dataIndex: "phone_number",
render: (phoneNumber) => {
const owners = getAssociatedOwners(phoneNumber);
if (!owners || owners.length === 0) {
return t("consent.no_owners");
}
return owners.map((owner) => (
<div key={owner.id}>
<Space direction="horizontal">
<Link to={"/manage/owners/" + owner.id}>
<OwnerNameDisplay ownerObject={owner} />
</Link>
({owner.phoneField})
</Space>
</div>
));
},
sorter: (a, b) => {
const aOwners = getAssociatedOwners(a.phone_number);
const bOwners = getAssociatedOwners(b.phone_number);
const aName = aOwners[0] ? `${aOwners[0].ownr_fn} ${aOwners[0].ownr_ln}` : "";
const bName = bOwners[0] ? `${bOwners[0].ownr_fn} ${bOwners[0].ownr_ln}` : "";
return aName.localeCompare(bName);
}
},*/
{ {
title: t("consent.created_at"), title: t("consent.created_at"),
dataIndex: "created_at", dataIndex: "created_at",
@@ -42,6 +125,7 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) {
return ( return (
<div> <div>
<Paragraph>{t("consent.text_body")}</Paragraph>
<Input.Search <Input.Search
placeholder={t("general.labels.search")} placeholder={t("general.labels.search")}
onSearch={(value) => setSearch(value)} onSearch={(value) => setSearch(value)}
@@ -50,8 +134,8 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) {
<Table <Table
columns={columns} columns={columns}
dataSource={data?.phone_number_opt_out} dataSource={optOutData?.phone_number_opt_out}
loading={loading} loading={optOutLoading /* || ownersLoading*/}
rowKey="id" rowKey="id"
style={{ marginTop: 16 }} style={{ marginTop: 16 }}
/> />

View File

@@ -20,7 +20,7 @@ const Board = ({ id, className, orientation, cardSettings, ...additionalProps })
default: default:
return cardSizesVertical.small; return cardSizesVertical.small;
} }
}, [cardSettings]); }, [cardSettings?.cardSize]);
return ( return (
<> <>

View File

@@ -101,11 +101,33 @@ const BoardContainer = ({
async ({ draggableId, type, source, reason, mode, destination, combine }) => { async ({ draggableId, type, source, reason, mode, destination, combine }) => {
setIsDragging(false); setIsDragging(false);
// Only update drag time if it's a valid drop with a different destination // Validate drag type and source
if (type === "lane" && source && destination && !isEqual(source, destination)) { if (type !== "lane" || !source) {
setDragTime(source.droppableId); // Invalid drag type or missing source, attempt to revert if possible
setIsProcessing(true); 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( dispatch(
actions.moveCardAcrossLanes({ actions.moveCardAcrossLanes({
fromLaneId: source.droppableId, fromLaneId: source.droppableId,
@@ -114,14 +136,33 @@ const BoardContainer = ({
index: destination.index 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 { try {
await onDragEnd({ draggableId, type, source, reason, mode, destination, combine }); await onDragEnd({ draggableId, type, source, reason, mode, destination, combine });
} catch (err) { } catch (err) {
console.error("Error in onLaneDrag", err); console.error("Error in onLaneDrag", err);
} finally { // Ensure revert on error
setIsProcessing(false); dispatch(
} actions.moveCardAcrossLanes({
fromLaneId: source.droppableId,
toLaneId: source.droppableId,
cardId: draggableId,
index: source.index
})
);
} finally {
setIsProcessing(false);
} }
}, },
[dispatch, onDragEnd, setDragTime] [dispatch, onDragEnd, setDragTime]

View File

@@ -133,7 +133,9 @@ const Lane = ({
Item: ItemComponent Item: ItemComponent
}, },
itemContent: (index, item) => <ItemWrapper>{renderDraggable(index, item)}</ItemWrapper>, 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 = { const horizontalProps = {
@@ -149,8 +151,6 @@ const Lane = ({
const componentProps = orientation === "vertical" ? verticalProps : horizontalProps; 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 const finalComponentProps = collapsed
? orientation === "horizontal" ? orientation === "horizontal"
? { ? {
@@ -161,9 +161,8 @@ const Lane = ({
: {} : {}
: componentProps; : 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 // Always render placeholder for empty lanes in vertical mode to ensure droppable area
// a card is dragged over it const shouldRenderPlaceholder = orientation === "vertical" ? collapsed || renderedCards.length === 0 : collapsed;
const shouldRenderPlaceholder = orientation !== "horizontal" && (collapsed || renderedCards.length === 0);
return ( return (
<HeightMemoryWrapper <HeightMemoryWrapper
@@ -178,8 +177,8 @@ const Lane = ({
override={orientation !== "horizontal" && (collapsed || !renderedCards.length)} override={orientation !== "horizontal" && (collapsed || !renderedCards.length)}
> >
<div <div
ref={laneRef} // Ensure laneRef is set here ref={laneRef}
style={{ height: "100%", width: "100%" }} // Make it scrollable style={{ height: "100%", width: "100%" }}
className={`react-trello-lane ${collapsed ? "lane-collapsed" : ""}`} className={`react-trello-lane ${collapsed ? "lane-collapsed" : ""}`}
> >
<div {...provided.droppableProps} ref={provided.innerRef} style={{ ...provided.droppableProps.style }}> <div {...provided.droppableProps} ref={provided.innerRef} style={{ ...provided.droppableProps.style }}>

View File

@@ -140,7 +140,7 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record
if (record[type]) theEmployee = bodyshop.employees.find((e) => e.id === record[type]); if (record[type]) theEmployee = bodyshop.employees.find((e) => e.id === record[type]);
return ( return (
<Popover destroyTooltipOnHide content={popContent} open={visibility}> <Popover destroyOnHidden content={popContent} open={visibility}>
<Spin spinning={loading}> <Spin spinning={loading}>
{record[type] ? ( {record[type] ? (
<div> <div>

View File

@@ -107,7 +107,7 @@ export default function TimeTicketCalculatorComponent({
open={visible} open={visible}
onOpenChange={handleOpenChange} onOpenChange={handleOpenChange}
placement="right" placement="right"
destroyTooltipOnHide destroyOnHidden
> >
<Button onClick={(e) => e.preventDefault()}> <Button onClick={(e) => e.preventDefault()}>
<Space> <Space>

View File

@@ -312,7 +312,6 @@ export const QUERY_INTAKE_CHECKLIST = gql`
intakechecklist intakechecklist
status status
owner { owner {
allow_text_message
id id
} }
labhrs: joblines_aggregate(where: { _and: [{ mod_lbr_ty: { _neq: "LAR" } }, { removed: { _eq: false } }] }) { labhrs: joblines_aggregate(where: { _and: [{ mod_lbr_ty: { _neq: "LAR" } }, { removed: { _eq: false } }] }) {

View File

@@ -43,6 +43,7 @@ export const CONVERSATION_SUBSCRIPTION_BY_PK = gql`
id id
status status
text text
is_system
isoutbound isoutbound
image image
image_path image_path
@@ -77,6 +78,7 @@ export const GET_CONVERSATION_DETAILS = gql`
id id
status status
text text
is_system
isoutbound isoutbound
image image
image_path image_path

View File

@@ -874,7 +874,6 @@ export const QUERY_JOB_CARD_DETAILS = gql`
} }
owner { owner {
id id
allow_text_message
preferred_contact preferred_contact
tax_number tax_number
} }
@@ -2071,7 +2070,6 @@ export const QUERY_JOB_CHECKLISTS = gql`
production_vars production_vars
owner { owner {
id id
allow_text_message
} }
bodyshop { bodyshop {
id id
@@ -2428,7 +2426,6 @@ export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql`
ownr_ph2 ownr_ph2
owner { owner {
id id
allow_text_message
preferred_contact preferred_contact
tax_number tax_number
} }

View File

@@ -49,7 +49,6 @@ export const QUERY_OWNER_BY_ID = gql`
owners_by_pk(id: $id) { owners_by_pk(id: $id) {
id id
accountingid accountingid
allow_text_message
ownr_addr1 ownr_addr1
ownr_addr2 ownr_addr2
ownr_co_nm ownr_co_nm
@@ -104,7 +103,6 @@ export const QUERY_ALL_OWNERS = gql`
query QUERY_ALL_OWNERS { query QUERY_ALL_OWNERS {
owners { owners {
id id
allow_text_message
created_at created_at
ownr_addr1 ownr_addr1
ownr_addr2 ownr_addr2
@@ -129,7 +127,6 @@ export const QUERY_ALL_OWNERS_PAGINATED = gql`
query QUERY_ALL_OWNERS_PAGINATED($search: String, $offset: Int, $limit: Int, $order: [owners_order_by!]!) { query QUERY_ALL_OWNERS_PAGINATED($search: String, $offset: Int, $limit: Int, $order: [owners_order_by!]!) {
search_owners(args: { search: $search }, offset: $offset, limit: $limit, order_by: $order) { search_owners(args: { search: $search }, offset: $offset, limit: $limit, order_by: $order) {
id id
allow_text_message
created_at created_at
ownr_addr1 ownr_addr1
ownr_addr2 ownr_addr2

View File

@@ -26,3 +26,39 @@ export const GET_PHONE_NUMBER_OPT_OUTS = gql`
} }
} }
`; `;
export const GET_PHONE_NUMBER_OPT_OUTS_BY_NUMBERS = gql`
query GET_PHONE_NUMBER_OPT_OUTS_BY_NUMBERS($bodyshopid: uuid!, $phone_numbers: [String!]) {
phone_number_opt_out(
where: { bodyshopid: { _eq: $bodyshopid }, phone_number: { _in: $phone_numbers } }
) {
id
bodyshopid
phone_number
created_at
updated_at
}
}
`;
export const SEARCH_OWNERS_BY_PHONE_NUMBERS = gql`
query SEARCH_OWNERS_BY_PHONE_NUMBERS($bodyshopid: uuid!, $phone_numbers: [String!]) {
owners(
where: {
shopid: { _eq: $bodyshopid },
_or: [
{ ownr_ph1: { _in: $phone_numbers } },
{ ownr_ph2: { _in: $phone_numbers } }
]
}
) {
id
ownr_fn
ownr_ln
ownr_co_nm
ownr_ph1
ownr_ph2
__typename
}
}
`;

View File

@@ -114,7 +114,6 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
if (!!!job.ownerid) { if (!!!job.ownerid) {
ownerData = job.owner.data; ownerData = job.owner.data;
ownerData.shopid = bodyshop.id; ownerData.shopid = bodyshop.id;
delete ownerData.allow_text_message;
delete ownerData.preferred_contact; delete ownerData.preferred_contact;
delete job.ownerid; delete job.ownerid;
} else { } else {

View File

@@ -92,13 +92,15 @@ export function ShopPage({ bodyshop, setSelectedHeader, setBreadcrumbs }) {
}); });
} }
// Add Consent Settings tab if (bodyshop.messagingservicesid) {
items.push({ // Add Consent Settings tab
key: "consent", items.push({
label: t("bodyshop.labels.consent_settings"), key: "consent",
children: <ShopInfoConsentComponent bodyshop={bodyshop} /> label: t("bodyshop.labels.consent_settings"),
}); children: <ShopInfoConsentComponent bodyshop={bodyshop} />
});
}
return ( return (
<RbacWrapper action="shop:config"> <RbacWrapper action="shop:config">
<Tabs activeKey={search.tab} onChange={(key) => history({ search: `?tab=${key}` })} items={items} /> <Tabs activeKey={search.tab} onChange={(key) => history({ search: `?tab=${key}` })} items={items} />

View File

@@ -335,20 +335,12 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
} }
try { try {
InstanceRenderManager({ window.$crisp.push(["set", "user:company", [payload.shopname]]);
executeFunction: true, window.$crisp.push(["set", "session:segments", [[`region:${payload.region_config}`]]]);
args: [], if (authRecord[0] && authRecord[0].user.validemail) {
imex: () => { window.$crisp.push(["set", "user:email", [authRecord[0].user.email]]);
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 });
}
});
payload.features?.allAccess === true payload.features?.allAccess === true
? window.$crisp.push(["set", "session:segments", [["allAccess"]]]) ? window.$crisp.push(["set", "session:segments", [["allAccess"]]])
: (() => { : (() => {
@@ -359,6 +351,14 @@ export function* SetAuthLevelFromShopDetails({ payload }) {
); );
window.$crisp.push(["set", "session:segments", [["basic", ...featureKeys]]]); window.$crisp.push(["set", "session:segments", [["basic", ...featureKeys]]]);
})(); })();
InstanceRenderManager({
executeFunction: true,
args: [],
rome: () => {
window.$zoho.salesiq.visitor.info({ "Shop Name": payload.shopname });
}
});
} catch (error) { } catch (error) {
console.warn("Couldnt find $crisp.", error.message); console.warn("Couldnt find $crisp.", error.message);
} }

View File

@@ -775,7 +775,6 @@
}, },
"labels": { "labels": {
"addtoproduction": "Add Job to Production?", "addtoproduction": "Add Job to Production?",
"allow_text_message": "Permission to Text?",
"checklist": "Checklist", "checklist": "Checklist",
"printpack": "Job Intake Print Pack", "printpack": "Job Intake Print Pack",
"removefromproduction": "Remove Job from Production?" "removefromproduction": "Remove Job from Production?"
@@ -2032,9 +2031,10 @@
"stands": "Stands", "stands": "Stands",
"waived": "Waived" "waived": "Waived"
}, },
"deleteconfirm": "Are you sure you want to delete this Job? This cannot be undone. ", "deleteconfirm": "Are you sure you want to delete this Job? This cannot be undone.",
"deletedelivery": "Delete Delivery Checklist", "deletedelivery": "Delete Delivery Checklist",
"deleteintake": "Delete Intake Checklist", "deleteintake": "Delete Intake Checklist",
"deletewatchers": "Remove Watchers before deleting this Job.",
"deliverchecklist": "Deliver Checklist", "deliverchecklist": "Deliver Checklist",
"difference": "Difference", "difference": "Difference",
"diskscan": "Scan Disk for Estimates", "diskscan": "Scan Disk for Estimates",
@@ -2302,8 +2302,10 @@
"productionlist": "Production Board - List", "productionlist": "Production Board - List",
"readyjobs": "Ready Jobs", "readyjobs": "Ready Jobs",
"recent": "Recent Items", "recent": "Recent Items",
"remoteassist": "Remote Assist",
"reportcenter": "Report Center", "reportcenter": "Report Center",
"rescueme": "Rescue me!", "rescueme": "Rescue Me!",
"rescuemezoho": "Remote Me In!",
"schedule": "Schedule", "schedule": "Schedule",
"scoreboard": "Scoreboard", "scoreboard": "Scoreboard",
"search": { "search": {
@@ -2379,7 +2381,7 @@
"invalidphone": "The phone number is invalid. Unable to open conversation. ", "invalidphone": "The phone number is invalid. Unable to open conversation. ",
"noattachedjobs": "No Jobs have been associated to this conversation. ", "noattachedjobs": "No Jobs have been associated to this conversation. ",
"updatinglabel": "Error updating label. {{error}}", "updatinglabel": "Error updating label. {{error}}",
"no_consent": "This phone number has not consented to receive messages." "no_consent": "This phone number has opted-out of Messaging."
}, },
"labels": { "labels": {
"addlabel": "Add a label to this conversation.", "addlabel": "Add a label to this conversation.",
@@ -2396,7 +2398,7 @@
"sentby": "Sent by {{by}} at {{time}}", "sentby": "Sent by {{by}} at {{time}}",
"typeamessage": "Send a message...", "typeamessage": "Send a message...",
"unarchive": "Unarchive", "unarchive": "Unarchive",
"no_consent": "No Consent" "no_consent": "Opted-out"
}, },
"render": { "render": {
"conversation_list": "Conversation List" "conversation_list": "Conversation List"
@@ -2521,7 +2523,6 @@
"fields": { "fields": {
"accountingid": "Accounting ID", "accountingid": "Accounting ID",
"address": "Address", "address": "Address",
"allow_text_message": "Permission to Text?",
"name": "Name", "name": "Name",
"note": "Owner Note", "note": "Owner Note",
"ownr_addr1": "Address", "ownr_addr1": "Address",
@@ -3869,8 +3870,12 @@
}, },
"consent": { "consent": {
"phone_number": "Phone Number", "phone_number": "Phone Number",
"status": "Consent Status", "associated_owners": "Associated Owners",
"created_at": "Created At" "created_at": "Opt-Out Date",
"no_owners": "No Associated Owners",
"phone_1": "Phone 1",
"phone_2": "Phone 2",
"text_body": "Users can opt out of receiving SMS messages by replying with keywords such as STOP, UNSUBSCRIBE, CANCEL, END, QUIT, STOPALL, REVOKE and OPTOUT. To opt back in, users can reply with START, YES, or UNSTOP. Even after opting out, users can still send messages to us, which will be received and processed as needed. Ensure customers are informed to reply with these keywords to manage their messaging preferences. After opting out, users receive a confirmation message and will not receive further messages until they opt back in."
}, },
"settings": { "settings": {
"title": "Phone Number Opt-Out List" "title": "Phone Number Opt-Out List"

View File

@@ -656,6 +656,7 @@
} }
}, },
"labels": { "labels": {
"consent_settings": "",
"2tiername": "", "2tiername": "",
"2tiersetup": "", "2tiersetup": "",
"2tiersource": "", "2tiersource": "",
@@ -774,7 +775,6 @@
}, },
"labels": { "labels": {
"addtoproduction": "", "addtoproduction": "",
"allow_text_message": "",
"checklist": "", "checklist": "",
"printpack": "", "printpack": "",
"removefromproduction": "" "removefromproduction": ""
@@ -2034,6 +2034,7 @@
"deleteconfirm": "", "deleteconfirm": "",
"deletedelivery": "", "deletedelivery": "",
"deleteintake": "", "deleteintake": "",
"deletewatchers": "",
"deliverchecklist": "", "deliverchecklist": "",
"difference": "", "difference": "",
"diskscan": "", "diskscan": "",
@@ -2301,8 +2302,10 @@
"productionlist": "", "productionlist": "",
"readyjobs": "", "readyjobs": "",
"recent": "", "recent": "",
"remoteassist": "",
"reportcenter": "", "reportcenter": "",
"rescueme": "", "rescueme": "",
"rescuemezoho": "",
"schedule": "Programar", "schedule": "Programar",
"scoreboard": "", "scoreboard": "",
"search": { "search": {
@@ -2377,7 +2380,8 @@
"errors": { "errors": {
"invalidphone": "", "invalidphone": "",
"noattachedjobs": "", "noattachedjobs": "",
"updatinglabel": "" "updatinglabel": "",
"no_consent": ""
}, },
"labels": { "labels": {
"addlabel": "", "addlabel": "",
@@ -2393,7 +2397,8 @@
"selectmedia": "", "selectmedia": "",
"sentby": "", "sentby": "",
"typeamessage": "Enviar un mensaje...", "typeamessage": "Enviar un mensaje...",
"unarchive": "" "unarchive": "",
"no_consent": ""
}, },
"render": { "render": {
"conversation_list": "" "conversation_list": ""
@@ -2498,7 +2503,8 @@
}, },
"tooltips": { "tooltips": {
"job-watchers": "", "job-watchers": "",
"not-employee": "" "not-employee": "",
"not-employee-notifications": ""
} }
}, },
"owner": { "owner": {
@@ -2519,7 +2525,6 @@
"fields": { "fields": {
"accountingid": "", "accountingid": "",
"address": "Dirección", "address": "Dirección",
"allow_text_message": "Permiso de texto?",
"name": "Nombre", "name": "Nombre",
"note": "", "note": "",
"ownr_addr1": "Dirección", "ownr_addr1": "Dirección",
@@ -3864,6 +3869,18 @@
"validation": { "validation": {
"unique_vendor_name": "" "unique_vendor_name": ""
} }
},
"consent": {
"phone_number": "",
"associated_owners": "",
"created_at": "",
"no_owners": "",
"phone_1": "",
"phone_2": "",
"text_body": ""
},
"settings": {
"title": ""
} }
} }
} }

View File

@@ -656,6 +656,7 @@
} }
}, },
"labels": { "labels": {
"consent_settings": "",
"2tiername": "", "2tiername": "",
"2tiersetup": "", "2tiersetup": "",
"2tiersource": "", "2tiersource": "",
@@ -774,7 +775,6 @@
}, },
"labels": { "labels": {
"addtoproduction": "", "addtoproduction": "",
"allow_text_message": "",
"checklist": "", "checklist": "",
"printpack": "", "printpack": "",
"removefromproduction": "" "removefromproduction": ""
@@ -2034,6 +2034,7 @@
"deleteconfirm": "", "deleteconfirm": "",
"deletedelivery": "", "deletedelivery": "",
"deleteintake": "", "deleteintake": "",
"deletewatchers": "",
"deliverchecklist": "", "deliverchecklist": "",
"difference": "", "difference": "",
"diskscan": "", "diskscan": "",
@@ -2301,8 +2302,10 @@
"productionlist": "", "productionlist": "",
"readyjobs": "", "readyjobs": "",
"recent": "", "recent": "",
"remoteassist": "",
"reportcenter": "", "reportcenter": "",
"rescueme": "", "rescueme": "",
"rescuemezoho": "",
"schedule": "Programme", "schedule": "Programme",
"scoreboard": "", "scoreboard": "",
"search": { "search": {
@@ -2377,7 +2380,8 @@
"errors": { "errors": {
"invalidphone": "", "invalidphone": "",
"noattachedjobs": "", "noattachedjobs": "",
"updatinglabel": "" "updatinglabel": "",
"no_consent": ""
}, },
"labels": { "labels": {
"addlabel": "", "addlabel": "",
@@ -2393,7 +2397,8 @@
"selectmedia": "", "selectmedia": "",
"sentby": "", "sentby": "",
"typeamessage": "Envoyer un message...", "typeamessage": "Envoyer un message...",
"unarchive": "" "unarchive": "",
"no_consent": ""
}, },
"render": { "render": {
"conversation_list": "" "conversation_list": ""
@@ -2498,7 +2503,8 @@
}, },
"tooltips": { "tooltips": {
"job-watchers": "", "job-watchers": "",
"not-employee": "" "not-employee": "",
"not-employee-notifications": ""
} }
}, },
"owner": { "owner": {
@@ -2519,7 +2525,6 @@
"fields": { "fields": {
"accountingid": "", "accountingid": "",
"address": "Adresse", "address": "Adresse",
"allow_text_message": "Autorisation de texte?",
"name": "Prénom", "name": "Prénom",
"note": "", "note": "",
"ownr_addr1": "Adresse", "ownr_addr1": "Adresse",
@@ -3864,6 +3869,18 @@
"validation": { "validation": {
"unique_vendor_name": "" "unique_vendor_name": ""
} }
},
"consent": {
"phone_number": "Phone Number",
"associated_owners": "Associated Owners",
"created_at": "Opt-Out Date",
"no_owners": "No Associated Owners",
"phone_1": "Phone 1",
"phone_2": "Phone 2",
"text_body": ""
},
"settings": {
"title": ""
} }
} }
} }

View File

@@ -1,6 +1,5 @@
import { Tooltip } from "antd"; import { Tooltip } from "antd";
import dayjs from "../utils/day"; import dayjs from "../utils/day";
import React from "react";
export function DateFormatter(props) { export function DateFormatter(props) {
return props.children ? dayjs(props.children).format(props.includeDay ? "ddd MM/DD/YYYY" : "MM/DD/YYYY") : null; return props.children ? dayjs(props.children).format(props.includeDay ? "ddd MM/DD/YYYY" : "MM/DD/YYYY") : null;

View File

@@ -0,0 +1,48 @@
import { phone } from "phone";
import { GET_PHONE_NUMBER_OPT_OUTS_BY_NUMBERS } from "../graphql/phone-number-opt-out.queries";
/**
* Check if phone numbers are opted out for a given bodyshop
* @param {Object} apolloClient - Apollo Client instance
* @param {string} bodyshopId - The ID of the bodyshop
* @param {string[]} phoneNumbers - Array of phone numbers to check
* @returns {Promise<Set<string>>} - Set of normalized opted-out phone numbers
*/
export const phoneNumberOptOutService = async (apolloClient, bodyshopId, phoneNumbers) => {
if (!apolloClient || !bodyshopId || !phoneNumbers?.length) {
return new Set();
}
// Normalize phone numbers (remove +1 for CA numbers)
const normalizedPhones = phoneNumbers
.filter(Boolean)
.map((num) => phone(num, "CA").phoneNumber?.replace(/^\+1/, ""))
.filter(Boolean);
if (!normalizedPhones.length) {
return new Set();
}
const optedOutPhones = new Set();
try {
const { data } = await apolloClient.query({
query: GET_PHONE_NUMBER_OPT_OUTS_BY_NUMBERS,
variables: {
bodyshopid: bodyshopId,
phone_numbers: normalizedPhones // Array of phone numbers
},
fetchPolicy: "network-only"
});
if (data?.phone_number_opt_out?.length) {
data.phone_number_opt_out.forEach((optOut) => {
optedOutPhones.add(optOut.phone_number);
});
}
} catch (error) {
console.error("Error checking opt-out statuses:", error);
}
return optedOutPhones;
};

View File

@@ -4742,6 +4742,7 @@
- id - id
- image - image
- image_path - image_path
- is_system
- isoutbound - isoutbound
- msid - msid
- read - read

View File

@@ -0,0 +1,4 @@
-- Could not auto-generate a down migration.
-- Please write an appropriate down migration for the SQL below:
-- alter table "public"."messages" add column "is_system" boolean
-- null default 'false';

View File

@@ -0,0 +1,2 @@
alter table "public"."messages" add column "is_system" boolean
null default 'false';

View File

@@ -10028,25 +10028,6 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>allow_text_message</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-ES</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>checklist</name> <name>checklist</name>
<description/> <description/>
@@ -33000,25 +32981,6 @@
</translation> </translation>
</translations> </translations>
</concept_node> </concept_node>
<concept_node>
<name>allow_text_message</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-ES</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node> <concept_node>
<name>name</name> <name>name</name>
<description/> <description/>

287
package-lock.json generated
View File

@@ -9,14 +9,14 @@
"version": "0.2.0", "version": "0.2.0",
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@aws-sdk/client-cloudwatch-logs": "^3.812.0", "@aws-sdk/client-cloudwatch-logs": "^3.817.0",
"@aws-sdk/client-elasticache": "^3.812.0", "@aws-sdk/client-elasticache": "^3.817.0",
"@aws-sdk/client-s3": "^3.812.0", "@aws-sdk/client-s3": "^3.817.0",
"@aws-sdk/client-secrets-manager": "^3.812.0", "@aws-sdk/client-secrets-manager": "^3.817.0",
"@aws-sdk/client-ses": "^3.812.0", "@aws-sdk/client-ses": "^3.817.0",
"@aws-sdk/credential-provider-node": "^3.812.0", "@aws-sdk/credential-provider-node": "^3.817.0",
"@aws-sdk/lib-storage": "^3.812.0", "@aws-sdk/lib-storage": "^3.817.0",
"@aws-sdk/s3-request-presigner": "^3.812.0", "@aws-sdk/s3-request-presigner": "^3.817.0",
"@opensearch-project/opensearch": "^2.13.0", "@opensearch-project/opensearch": "^2.13.0",
"@socket.io/admin-ui": "^0.5.1", "@socket.io/admin-ui": "^0.5.1",
"@socket.io/redis-adapter": "^8.3.0", "@socket.io/redis-adapter": "^8.3.0",
@@ -31,7 +31,7 @@
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"cors": "^2.8.5", "cors": "^2.8.5",
"crisp-status-reporter": "^1.2.2", "crisp-status-reporter": "^1.2.2",
"dd-trace": "^5.52.0", "dd-trace": "^5.53.0",
"dinero.js": "^1.9.1", "dinero.js": "^1.9.1",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.21.1", "express": "^4.21.1",
@@ -45,7 +45,7 @@
"juice": "^11.0.1", "juice": "^11.0.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.30.1", "moment": "^2.30.1",
"moment-timezone": "^0.5.48", "moment-timezone": "^0.6.0",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"node-persist": "^4.0.4", "node-persist": "^4.0.4",
"nodemailer": "^6.10.0", "nodemailer": "^6.10.0",
@@ -284,24 +284,24 @@
} }
}, },
"node_modules/@aws-sdk/client-cloudwatch-logs": { "node_modules/@aws-sdk/client-cloudwatch-logs": {
"version": "3.812.0", "version": "3.817.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.812.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.817.0.tgz",
"integrity": "sha512-SLvqaMwRviAwb+z4XAq2QmlbUjr7rXN6zAEr4/x2ltyrsxEV95gBo0KHeroAsWhd4eD19USjAgg64KJgvUtNGw==", "integrity": "sha512-dbR4YZZ2wulMzblgSSE43yd9jgbXDMSrZS7w7r0DqDNAbsXrp79qU2CvA+lb47wGpDxMNppgvoCMu5kcIP5gXw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0", "@aws-crypto/sha256-js": "5.2.0",
"@aws-sdk/core": "3.812.0", "@aws-sdk/core": "3.816.0",
"@aws-sdk/credential-provider-node": "3.812.0", "@aws-sdk/credential-provider-node": "3.817.0",
"@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-host-header": "3.804.0",
"@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0",
"@aws-sdk/middleware-recursion-detection": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0",
"@aws-sdk/middleware-user-agent": "3.812.0", "@aws-sdk/middleware-user-agent": "3.816.0",
"@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/region-config-resolver": "3.808.0",
"@aws-sdk/types": "3.804.0", "@aws-sdk/types": "3.804.0",
"@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-endpoints": "3.808.0",
"@aws-sdk/util-user-agent-browser": "3.804.0", "@aws-sdk/util-user-agent-browser": "3.804.0",
"@aws-sdk/util-user-agent-node": "3.812.0", "@aws-sdk/util-user-agent-node": "3.816.0",
"@smithy/config-resolver": "^4.1.2", "@smithy/config-resolver": "^4.1.2",
"@smithy/core": "^3.3.3", "@smithy/core": "^3.3.3",
"@smithy/eventstream-serde-browser": "^4.0.2", "@smithy/eventstream-serde-browser": "^4.0.2",
@@ -352,24 +352,24 @@
} }
}, },
"node_modules/@aws-sdk/client-elasticache": { "node_modules/@aws-sdk/client-elasticache": {
"version": "3.812.0", "version": "3.817.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-elasticache/-/client-elasticache-3.812.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/client-elasticache/-/client-elasticache-3.817.0.tgz",
"integrity": "sha512-o1KC5Glo3c0T/RN2XBanHu40k3M99MJyq+e/02tIMgEGKIPmnvB8A8muE2F3rQ2A0qLCxvjhm+kprlmDwzpryw==", "integrity": "sha512-TO1Zfv3racKQsRoll4owV2q4kNcw1x64D19KFrWd87rQ517ahbXRcPpaKOqe9CYG1Zo3SIzRySaJLoPftXDfRQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0", "@aws-crypto/sha256-js": "5.2.0",
"@aws-sdk/core": "3.812.0", "@aws-sdk/core": "3.816.0",
"@aws-sdk/credential-provider-node": "3.812.0", "@aws-sdk/credential-provider-node": "3.817.0",
"@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-host-header": "3.804.0",
"@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0",
"@aws-sdk/middleware-recursion-detection": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0",
"@aws-sdk/middleware-user-agent": "3.812.0", "@aws-sdk/middleware-user-agent": "3.816.0",
"@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/region-config-resolver": "3.808.0",
"@aws-sdk/types": "3.804.0", "@aws-sdk/types": "3.804.0",
"@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-endpoints": "3.808.0",
"@aws-sdk/util-user-agent-browser": "3.804.0", "@aws-sdk/util-user-agent-browser": "3.804.0",
"@aws-sdk/util-user-agent-node": "3.812.0", "@aws-sdk/util-user-agent-node": "3.816.0",
"@smithy/config-resolver": "^4.1.2", "@smithy/config-resolver": "^4.1.2",
"@smithy/core": "^3.3.3", "@smithy/core": "^3.3.3",
"@smithy/fetch-http-handler": "^5.0.2", "@smithy/fetch-http-handler": "^5.0.2",
@@ -403,32 +403,32 @@
} }
}, },
"node_modules/@aws-sdk/client-s3": { "node_modules/@aws-sdk/client-s3": {
"version": "3.812.0", "version": "3.817.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.812.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.817.0.tgz",
"integrity": "sha512-kHgw9JDXNPLa/mHtWpOd5btBVXFSe+wwp1Ed9+bqz9uLkv0iV4joZrdQwnydkO8zlTs60Sc5ez+P2OiZ76i2Qg==", "integrity": "sha512-nZyjhlLMEXDs0ofWbpikI8tKoeKuuSgYcIb6eEZJk90Nt5HkkXn6nkWOs/kp2FdhpoGJyTILOVsDgdm7eutnLA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha1-browser": "5.2.0",
"@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0", "@aws-crypto/sha256-js": "5.2.0",
"@aws-sdk/core": "3.812.0", "@aws-sdk/core": "3.816.0",
"@aws-sdk/credential-provider-node": "3.812.0", "@aws-sdk/credential-provider-node": "3.817.0",
"@aws-sdk/middleware-bucket-endpoint": "3.808.0", "@aws-sdk/middleware-bucket-endpoint": "3.808.0",
"@aws-sdk/middleware-expect-continue": "3.804.0", "@aws-sdk/middleware-expect-continue": "3.804.0",
"@aws-sdk/middleware-flexible-checksums": "3.812.0", "@aws-sdk/middleware-flexible-checksums": "3.816.0",
"@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-host-header": "3.804.0",
"@aws-sdk/middleware-location-constraint": "3.804.0", "@aws-sdk/middleware-location-constraint": "3.804.0",
"@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0",
"@aws-sdk/middleware-recursion-detection": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0",
"@aws-sdk/middleware-sdk-s3": "3.812.0", "@aws-sdk/middleware-sdk-s3": "3.816.0",
"@aws-sdk/middleware-ssec": "3.804.0", "@aws-sdk/middleware-ssec": "3.804.0",
"@aws-sdk/middleware-user-agent": "3.812.0", "@aws-sdk/middleware-user-agent": "3.816.0",
"@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/region-config-resolver": "3.808.0",
"@aws-sdk/signature-v4-multi-region": "3.812.0", "@aws-sdk/signature-v4-multi-region": "3.816.0",
"@aws-sdk/types": "3.804.0", "@aws-sdk/types": "3.804.0",
"@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-endpoints": "3.808.0",
"@aws-sdk/util-user-agent-browser": "3.804.0", "@aws-sdk/util-user-agent-browser": "3.804.0",
"@aws-sdk/util-user-agent-node": "3.812.0", "@aws-sdk/util-user-agent-node": "3.816.0",
"@aws-sdk/xml-builder": "3.804.0", "@aws-sdk/xml-builder": "3.804.0",
"@smithy/config-resolver": "^4.1.2", "@smithy/config-resolver": "^4.1.2",
"@smithy/core": "^3.3.3", "@smithy/core": "^3.3.3",
@@ -470,24 +470,24 @@
} }
}, },
"node_modules/@aws-sdk/client-secrets-manager": { "node_modules/@aws-sdk/client-secrets-manager": {
"version": "3.812.0", "version": "3.817.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.812.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.817.0.tgz",
"integrity": "sha512-RyGzi7kkacjPd0QgVjw6OYvZVvuqtd1wRwG0Aek32dPUYu8eOs9FDaqBsDnNIqdw+lAqC/pKIOPYWtLu2OxE0Q==", "integrity": "sha512-Hx74xmJo9xPeHRFtFGdsT5qFx6p9V13ptQ3HICnkmcbtA+CX8soTuc5mglkp9vTdTjvRwKVAmQhx6NPf9ELcjQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0", "@aws-crypto/sha256-js": "5.2.0",
"@aws-sdk/core": "3.812.0", "@aws-sdk/core": "3.816.0",
"@aws-sdk/credential-provider-node": "3.812.0", "@aws-sdk/credential-provider-node": "3.817.0",
"@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-host-header": "3.804.0",
"@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0",
"@aws-sdk/middleware-recursion-detection": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0",
"@aws-sdk/middleware-user-agent": "3.812.0", "@aws-sdk/middleware-user-agent": "3.816.0",
"@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/region-config-resolver": "3.808.0",
"@aws-sdk/types": "3.804.0", "@aws-sdk/types": "3.804.0",
"@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-endpoints": "3.808.0",
"@aws-sdk/util-user-agent-browser": "3.804.0", "@aws-sdk/util-user-agent-browser": "3.804.0",
"@aws-sdk/util-user-agent-node": "3.812.0", "@aws-sdk/util-user-agent-node": "3.816.0",
"@smithy/config-resolver": "^4.1.2", "@smithy/config-resolver": "^4.1.2",
"@smithy/core": "^3.3.3", "@smithy/core": "^3.3.3",
"@smithy/fetch-http-handler": "^5.0.2", "@smithy/fetch-http-handler": "^5.0.2",
@@ -535,24 +535,24 @@
} }
}, },
"node_modules/@aws-sdk/client-ses": { "node_modules/@aws-sdk/client-ses": {
"version": "3.812.0", "version": "3.817.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.812.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.817.0.tgz",
"integrity": "sha512-7JUS2u0AKMYiEmRrxAYQj8ifFwVUgMAHt5H/KjMhh+1El0NqAQDt3JLD4Asmzy7/TvTAWZfk5np2LQPNB2wZpw==", "integrity": "sha512-cf2FsdcTT5HiOFOnWk3tzRc84iXcrUNXe4O4KaH75tRToBuQkTaidPI/K9wHnOybNDEkkCcgJo9skv4ftz8qYA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0", "@aws-crypto/sha256-js": "5.2.0",
"@aws-sdk/core": "3.812.0", "@aws-sdk/core": "3.816.0",
"@aws-sdk/credential-provider-node": "3.812.0", "@aws-sdk/credential-provider-node": "3.817.0",
"@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-host-header": "3.804.0",
"@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0",
"@aws-sdk/middleware-recursion-detection": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0",
"@aws-sdk/middleware-user-agent": "3.812.0", "@aws-sdk/middleware-user-agent": "3.816.0",
"@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/region-config-resolver": "3.808.0",
"@aws-sdk/types": "3.804.0", "@aws-sdk/types": "3.804.0",
"@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-endpoints": "3.808.0",
"@aws-sdk/util-user-agent-browser": "3.804.0", "@aws-sdk/util-user-agent-browser": "3.804.0",
"@aws-sdk/util-user-agent-node": "3.812.0", "@aws-sdk/util-user-agent-node": "3.816.0",
"@smithy/config-resolver": "^4.1.2", "@smithy/config-resolver": "^4.1.2",
"@smithy/core": "^3.3.3", "@smithy/core": "^3.3.3",
"@smithy/fetch-http-handler": "^5.0.2", "@smithy/fetch-http-handler": "^5.0.2",
@@ -586,23 +586,23 @@
} }
}, },
"node_modules/@aws-sdk/client-sso": { "node_modules/@aws-sdk/client-sso": {
"version": "3.812.0", "version": "3.817.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.812.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.817.0.tgz",
"integrity": "sha512-O//smQRj1+RXELB7xX54s5pZB0V69KHXpUZmz8V+8GAYO1FKTHfbpUgK+zyMNb+lFZxG9B69yl8pWPZ/K8bvxA==", "integrity": "sha512-fCh5rUHmWmWDvw70NNoWpE5+BRdtNi45kDnIoeoszqVg7UKF79SlG+qYooUT52HKCgDNHqgbWaXxMOSqd2I/OQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0", "@aws-crypto/sha256-js": "5.2.0",
"@aws-sdk/core": "3.812.0", "@aws-sdk/core": "3.816.0",
"@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-host-header": "3.804.0",
"@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0",
"@aws-sdk/middleware-recursion-detection": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0",
"@aws-sdk/middleware-user-agent": "3.812.0", "@aws-sdk/middleware-user-agent": "3.816.0",
"@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/region-config-resolver": "3.808.0",
"@aws-sdk/types": "3.804.0", "@aws-sdk/types": "3.804.0",
"@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-endpoints": "3.808.0",
"@aws-sdk/util-user-agent-browser": "3.804.0", "@aws-sdk/util-user-agent-browser": "3.804.0",
"@aws-sdk/util-user-agent-node": "3.812.0", "@aws-sdk/util-user-agent-node": "3.816.0",
"@smithy/config-resolver": "^4.1.2", "@smithy/config-resolver": "^4.1.2",
"@smithy/core": "^3.3.3", "@smithy/core": "^3.3.3",
"@smithy/fetch-http-handler": "^5.0.2", "@smithy/fetch-http-handler": "^5.0.2",
@@ -635,9 +635,9 @@
} }
}, },
"node_modules/@aws-sdk/core": { "node_modules/@aws-sdk/core": {
"version": "3.812.0", "version": "3.816.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.812.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.816.0.tgz",
"integrity": "sha512-myWA9oHMBVDObKrxG+puAkIGs8igcWInQ1PWCRTS/zN4BkhUMFjjh/JPV/4Vzvtvj5E36iujq2WtlrDLl1PpOw==", "integrity": "sha512-Lx50wjtyarzKpMFV6V+gjbSZDgsA/71iyifbClGUSiNPoIQ4OCV0KVOmAAj7mQRVvGJqUMWKVM+WzK79CjbjWA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-sdk/types": "3.804.0", "@aws-sdk/types": "3.804.0",
@@ -657,12 +657,12 @@
} }
}, },
"node_modules/@aws-sdk/credential-provider-env": { "node_modules/@aws-sdk/credential-provider-env": {
"version": "3.812.0", "version": "3.816.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.812.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.816.0.tgz",
"integrity": "sha512-Ge7IEu06ANurGBZx39q9CNN/ncqb1K8lpKZCY969uNWO0/7YPhnplrRJGMZYIS35nD2mBm3ortEKjY/wMZZd5g==", "integrity": "sha512-wUJZwRLe+SxPxRV9AENYBLrJZRrNIo+fva7ZzejsC83iz7hdfq6Rv6B/aHEdPwG/nQC4+q7UUvcRPlomyrpsBA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-sdk/core": "3.812.0", "@aws-sdk/core": "3.816.0",
"@aws-sdk/types": "3.804.0", "@aws-sdk/types": "3.804.0",
"@smithy/property-provider": "^4.0.2", "@smithy/property-provider": "^4.0.2",
"@smithy/types": "^4.2.0", "@smithy/types": "^4.2.0",
@@ -673,12 +673,12 @@
} }
}, },
"node_modules/@aws-sdk/credential-provider-http": { "node_modules/@aws-sdk/credential-provider-http": {
"version": "3.812.0", "version": "3.816.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.812.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.816.0.tgz",
"integrity": "sha512-Vux2U42vPGXeE407Lp6v3yVA65J7hBO9rB67LXshyGVi7VZLAYWc4mrZxNJNqabEkjcDEmMQQakLPT6zc5SvFw==", "integrity": "sha512-gcWGzMQ7yRIF+ljTkR8Vzp7727UY6cmeaPrFQrvcFB8PhOqWpf7g0JsgOf5BSaP8CkkSQcTQHc0C5ZYAzUFwPg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-sdk/core": "3.812.0", "@aws-sdk/core": "3.816.0",
"@aws-sdk/types": "3.804.0", "@aws-sdk/types": "3.804.0",
"@smithy/fetch-http-handler": "^5.0.2", "@smithy/fetch-http-handler": "^5.0.2",
"@smithy/node-http-handler": "^4.0.4", "@smithy/node-http-handler": "^4.0.4",
@@ -694,18 +694,18 @@
} }
}, },
"node_modules/@aws-sdk/credential-provider-ini": { "node_modules/@aws-sdk/credential-provider-ini": {
"version": "3.812.0", "version": "3.817.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.812.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.817.0.tgz",
"integrity": "sha512-oltqGvQ488xtPY5wrNjbD+qQYYkuCjn30IDE1qKMxJ58EM6UVTQl3XV44Xq07xfF5gKwVJQkfIyOkRAguOVybg==", "integrity": "sha512-kyEwbQyuXE+phWVzloMdkFv6qM6NOon+asMXY5W0fhDKwBz9zQLObDRWBrvQX9lmqq8BbDL1sCfZjOh82Y+RFw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-sdk/core": "3.812.0", "@aws-sdk/core": "3.816.0",
"@aws-sdk/credential-provider-env": "3.812.0", "@aws-sdk/credential-provider-env": "3.816.0",
"@aws-sdk/credential-provider-http": "3.812.0", "@aws-sdk/credential-provider-http": "3.816.0",
"@aws-sdk/credential-provider-process": "3.812.0", "@aws-sdk/credential-provider-process": "3.816.0",
"@aws-sdk/credential-provider-sso": "3.812.0", "@aws-sdk/credential-provider-sso": "3.817.0",
"@aws-sdk/credential-provider-web-identity": "3.812.0", "@aws-sdk/credential-provider-web-identity": "3.817.0",
"@aws-sdk/nested-clients": "3.812.0", "@aws-sdk/nested-clients": "3.817.0",
"@aws-sdk/types": "3.804.0", "@aws-sdk/types": "3.804.0",
"@smithy/credential-provider-imds": "^4.0.4", "@smithy/credential-provider-imds": "^4.0.4",
"@smithy/property-provider": "^4.0.2", "@smithy/property-provider": "^4.0.2",
@@ -718,17 +718,17 @@
} }
}, },
"node_modules/@aws-sdk/credential-provider-node": { "node_modules/@aws-sdk/credential-provider-node": {
"version": "3.812.0", "version": "3.817.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.812.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.817.0.tgz",
"integrity": "sha512-SnvSWBP6cr9nqx784eETnL2Zl7ZnMB/oJgFVEG1aejAGbT1H9gTpMwuUsBXk4u/mEYe3f1lh1Wqo+HwDgNkfrg==", "integrity": "sha512-b5mz7av0Lhavs1Bz3Zb+jrs0Pki93+8XNctnVO0drBW98x1fM4AR38cWvGbM/w9F9Q0/WEH3TinkmrMPrP4T/w==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-sdk/credential-provider-env": "3.812.0", "@aws-sdk/credential-provider-env": "3.816.0",
"@aws-sdk/credential-provider-http": "3.812.0", "@aws-sdk/credential-provider-http": "3.816.0",
"@aws-sdk/credential-provider-ini": "3.812.0", "@aws-sdk/credential-provider-ini": "3.817.0",
"@aws-sdk/credential-provider-process": "3.812.0", "@aws-sdk/credential-provider-process": "3.816.0",
"@aws-sdk/credential-provider-sso": "3.812.0", "@aws-sdk/credential-provider-sso": "3.817.0",
"@aws-sdk/credential-provider-web-identity": "3.812.0", "@aws-sdk/credential-provider-web-identity": "3.817.0",
"@aws-sdk/types": "3.804.0", "@aws-sdk/types": "3.804.0",
"@smithy/credential-provider-imds": "^4.0.4", "@smithy/credential-provider-imds": "^4.0.4",
"@smithy/property-provider": "^4.0.2", "@smithy/property-provider": "^4.0.2",
@@ -741,12 +741,12 @@
} }
}, },
"node_modules/@aws-sdk/credential-provider-process": { "node_modules/@aws-sdk/credential-provider-process": {
"version": "3.812.0", "version": "3.816.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.812.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.816.0.tgz",
"integrity": "sha512-YI8bb153XeEOb59F9KtTZEwDAc14s2YHZz58+OFiJ2udnKsPV87mNiFhJPW6ba9nmOLXVat5XDcwtVT1b664wg==", "integrity": "sha512-9Tm+AxMoV2Izvl5b9tyMQRbBwaex8JP06HN7ZeCXgC5sAsSN+o8dsThnEhf8jKN+uBpT6CLWKN1TXuUMrAmW1A==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-sdk/core": "3.812.0", "@aws-sdk/core": "3.816.0",
"@aws-sdk/types": "3.804.0", "@aws-sdk/types": "3.804.0",
"@smithy/property-provider": "^4.0.2", "@smithy/property-provider": "^4.0.2",
"@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2",
@@ -758,14 +758,14 @@
} }
}, },
"node_modules/@aws-sdk/credential-provider-sso": { "node_modules/@aws-sdk/credential-provider-sso": {
"version": "3.812.0", "version": "3.817.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.812.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.817.0.tgz",
"integrity": "sha512-ODsPcNhgiO6GOa82TVNskM97mml9rioe9Cbhemz48lkfDQPv1u06NaCR0o3FsvprX1sEhMvJTR3sE1fyEOzvJQ==", "integrity": "sha512-gFUAW3VmGvdnueK1bh6TOcRX+j99Xm0men1+gz3cA4RE+rZGNy1Qjj8YHlv0hPwI9OnTPZquvPzA5fkviGREWg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-sdk/client-sso": "3.812.0", "@aws-sdk/client-sso": "3.817.0",
"@aws-sdk/core": "3.812.0", "@aws-sdk/core": "3.816.0",
"@aws-sdk/token-providers": "3.812.0", "@aws-sdk/token-providers": "3.817.0",
"@aws-sdk/types": "3.804.0", "@aws-sdk/types": "3.804.0",
"@smithy/property-provider": "^4.0.2", "@smithy/property-provider": "^4.0.2",
"@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2",
@@ -777,13 +777,13 @@
} }
}, },
"node_modules/@aws-sdk/credential-provider-web-identity": { "node_modules/@aws-sdk/credential-provider-web-identity": {
"version": "3.812.0", "version": "3.817.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.812.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.817.0.tgz",
"integrity": "sha512-E9Bmiujvm/Hp9DM/Vc1S+D0pQbx8/x4dR/zyAEZU9EoRq0duQOQ1reWYWbebYmL1OklcVpTfKV0a/VCwuAtGSg==", "integrity": "sha512-A2kgkS9g6NY0OMT2f2EdXHpL17Ym81NhbGnQ8bRXPqESIi7TFypFD2U6osB2VnsFv+MhwM+Ke4PKXSmLun22/A==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-sdk/core": "3.812.0", "@aws-sdk/core": "3.816.0",
"@aws-sdk/nested-clients": "3.812.0", "@aws-sdk/nested-clients": "3.817.0",
"@aws-sdk/types": "3.804.0", "@aws-sdk/types": "3.804.0",
"@smithy/property-provider": "^4.0.2", "@smithy/property-provider": "^4.0.2",
"@smithy/types": "^4.2.0", "@smithy/types": "^4.2.0",
@@ -794,9 +794,9 @@
} }
}, },
"node_modules/@aws-sdk/lib-storage": { "node_modules/@aws-sdk/lib-storage": {
"version": "3.812.0", "version": "3.817.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.812.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.817.0.tgz",
"integrity": "sha512-z37ykuXQXfGO7dqQFbEnj1Wu9UwUUXpZhr4iWXsehbIzSqyl5FiCMp0cI5XK8jLVACCfSCssZCz6QD4oDYdKlQ==", "integrity": "sha512-2zOO8+2EmiS049PjLSNdqmmZMQj7fzE1hZJ70A94vO+KNaVhVZYuMOOiOmwMw6ePkTCcFwK40vZIIXwEQQ1v1g==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@smithy/abort-controller": "^4.0.2", "@smithy/abort-controller": "^4.0.2",
@@ -811,7 +811,7 @@
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
"peerDependencies": { "peerDependencies": {
"@aws-sdk/client-s3": "^3.812.0" "@aws-sdk/client-s3": "^3.817.0"
} }
}, },
"node_modules/@aws-sdk/middleware-bucket-endpoint": { "node_modules/@aws-sdk/middleware-bucket-endpoint": {
@@ -848,15 +848,15 @@
} }
}, },
"node_modules/@aws-sdk/middleware-flexible-checksums": { "node_modules/@aws-sdk/middleware-flexible-checksums": {
"version": "3.812.0", "version": "3.816.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.812.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.816.0.tgz",
"integrity": "sha512-/ayAooUZvV1GTomNMrfbhjUHAEaz0Wmio3lKyaTJsW4WdLJXBuzdo57YADRmYYUqx6awzJ6VJ6HGc1Uc6tOlbw==", "integrity": "sha512-kftcwDxB/VoCBsUiRgkm5CIuKbTfCN1WLPbis9LRwX3kQhKgGVxG2gG78SHk4TBB0qviWVAd/t+i/KaUgwiAcA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32": "5.2.0",
"@aws-crypto/crc32c": "5.2.0", "@aws-crypto/crc32c": "5.2.0",
"@aws-crypto/util": "5.2.0", "@aws-crypto/util": "5.2.0",
"@aws-sdk/core": "3.812.0", "@aws-sdk/core": "3.816.0",
"@aws-sdk/types": "3.804.0", "@aws-sdk/types": "3.804.0",
"@smithy/is-array-buffer": "^4.0.0", "@smithy/is-array-buffer": "^4.0.0",
"@smithy/node-config-provider": "^4.1.1", "@smithy/node-config-provider": "^4.1.1",
@@ -930,12 +930,12 @@
} }
}, },
"node_modules/@aws-sdk/middleware-sdk-s3": { "node_modules/@aws-sdk/middleware-sdk-s3": {
"version": "3.812.0", "version": "3.816.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.812.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.816.0.tgz",
"integrity": "sha512-e8AqRRIaTsunL1hqtO1hksa9oTYdsIbfezHUyVpPGugUIB1lMqPt/DlBsanI85OzUD711UfNSEcZ1mqAxpDOoA==", "integrity": "sha512-jJ+EAXM7gnOwiCM6rrl4AUNY5urmtIsX7roTkxtb4DevJxcS+wFYRRg3/j33fQbuxQZrvk21HqxyZYx5UH70PA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-sdk/core": "3.812.0", "@aws-sdk/core": "3.816.0",
"@aws-sdk/types": "3.804.0", "@aws-sdk/types": "3.804.0",
"@aws-sdk/util-arn-parser": "3.804.0", "@aws-sdk/util-arn-parser": "3.804.0",
"@smithy/core": "^3.3.3", "@smithy/core": "^3.3.3",
@@ -969,12 +969,12 @@
} }
}, },
"node_modules/@aws-sdk/middleware-user-agent": { "node_modules/@aws-sdk/middleware-user-agent": {
"version": "3.812.0", "version": "3.816.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.812.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.816.0.tgz",
"integrity": "sha512-r+HFwtSvnAs6Fydp4mijylrTX0og9p/xfxOcKsqhMuk3HpZAIcf9sSjRQI6MBusYklg7pnM4sGEnPAZIrdRotA==", "integrity": "sha512-bHRSlWZ0xDsFR8E2FwDb//0Ff6wMkVx4O+UKsfyNlAbtqCiiHRt5ANNfKPafr95cN2CCxLxiPvFTFVblQM5TsQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-sdk/core": "3.812.0", "@aws-sdk/core": "3.816.0",
"@aws-sdk/types": "3.804.0", "@aws-sdk/types": "3.804.0",
"@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-endpoints": "3.808.0",
"@smithy/core": "^3.3.3", "@smithy/core": "^3.3.3",
@@ -987,23 +987,23 @@
} }
}, },
"node_modules/@aws-sdk/nested-clients": { "node_modules/@aws-sdk/nested-clients": {
"version": "3.812.0", "version": "3.817.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.812.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.817.0.tgz",
"integrity": "sha512-FS/fImbEpJU3cXtBGR9fyVd+CP51eNKlvTMi3f4/6lSk3RmHjudNC9yEF/og3jtpT3O+7vsNOUW9mHco5IjdQQ==", "integrity": "sha512-vQ2E06A48STJFssueJQgxYD8lh1iGJoLJnHdshRDWOQb8gy1wVQR+a7MkPGhGR6lGoS0SCnF/Qp6CZhnwLsqsQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0", "@aws-crypto/sha256-js": "5.2.0",
"@aws-sdk/core": "3.812.0", "@aws-sdk/core": "3.816.0",
"@aws-sdk/middleware-host-header": "3.804.0", "@aws-sdk/middleware-host-header": "3.804.0",
"@aws-sdk/middleware-logger": "3.804.0", "@aws-sdk/middleware-logger": "3.804.0",
"@aws-sdk/middleware-recursion-detection": "3.804.0", "@aws-sdk/middleware-recursion-detection": "3.804.0",
"@aws-sdk/middleware-user-agent": "3.812.0", "@aws-sdk/middleware-user-agent": "3.816.0",
"@aws-sdk/region-config-resolver": "3.808.0", "@aws-sdk/region-config-resolver": "3.808.0",
"@aws-sdk/types": "3.804.0", "@aws-sdk/types": "3.804.0",
"@aws-sdk/util-endpoints": "3.808.0", "@aws-sdk/util-endpoints": "3.808.0",
"@aws-sdk/util-user-agent-browser": "3.804.0", "@aws-sdk/util-user-agent-browser": "3.804.0",
"@aws-sdk/util-user-agent-node": "3.812.0", "@aws-sdk/util-user-agent-node": "3.816.0",
"@smithy/config-resolver": "^4.1.2", "@smithy/config-resolver": "^4.1.2",
"@smithy/core": "^3.3.3", "@smithy/core": "^3.3.3",
"@smithy/fetch-http-handler": "^5.0.2", "@smithy/fetch-http-handler": "^5.0.2",
@@ -1053,12 +1053,12 @@
} }
}, },
"node_modules/@aws-sdk/s3-request-presigner": { "node_modules/@aws-sdk/s3-request-presigner": {
"version": "3.812.0", "version": "3.817.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.812.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.817.0.tgz",
"integrity": "sha512-OpyANELjcD2oknkd3/qWanaRaZDx4SSV6NwYuWIk+fuxDZ+KxZZrrfue1X7OAdaP2TdSapbs7xLisxtTuptWYg==", "integrity": "sha512-FMV0YefefGwPqIbGcHdkkHaiVWKIZoI0wOhYhYDZI129aUD5+CEOtTi7KFp1iJjAK+Cx9bW5tAYc+e9shaWEyQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-sdk/signature-v4-multi-region": "3.812.0", "@aws-sdk/signature-v4-multi-region": "3.816.0",
"@aws-sdk/types": "3.804.0", "@aws-sdk/types": "3.804.0",
"@aws-sdk/util-format-url": "3.804.0", "@aws-sdk/util-format-url": "3.804.0",
"@smithy/middleware-endpoint": "^4.1.6", "@smithy/middleware-endpoint": "^4.1.6",
@@ -1072,12 +1072,12 @@
} }
}, },
"node_modules/@aws-sdk/signature-v4-multi-region": { "node_modules/@aws-sdk/signature-v4-multi-region": {
"version": "3.812.0", "version": "3.816.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.812.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.816.0.tgz",
"integrity": "sha512-JTpk3ZHf7TXYbicKfOKi+VrsBTqcAszg9QR9fQmT9aCxPp39gsF3WsXq7NjepwZ5So11ixGIsPE/jtMym399QQ==", "integrity": "sha512-idcr9NW86sSIXASSej3423Selu6fxlhhJJtMgpAqoCH/HJh1eQrONJwNKuI9huiruPE8+02pwxuePvLW46X2mw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-sdk/middleware-sdk-s3": "3.812.0", "@aws-sdk/middleware-sdk-s3": "3.816.0",
"@aws-sdk/types": "3.804.0", "@aws-sdk/types": "3.804.0",
"@smithy/protocol-http": "^5.1.0", "@smithy/protocol-http": "^5.1.0",
"@smithy/signature-v4": "^5.1.0", "@smithy/signature-v4": "^5.1.0",
@@ -1089,12 +1089,13 @@
} }
}, },
"node_modules/@aws-sdk/token-providers": { "node_modules/@aws-sdk/token-providers": {
"version": "3.812.0", "version": "3.817.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.812.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.817.0.tgz",
"integrity": "sha512-dbVBaKxrxE708ub5uH3w+cmKIeRQas+2Xf6rpckhohYY+IiflGOdK6aLrp3T6dOQgr/FJ37iQtcYNonAG+yVBQ==", "integrity": "sha512-CYN4/UO0VaqyHf46ogZzNrVX7jI3/CfiuktwKlwtpKA6hjf2+ivfgHSKzPpgPBcSEfiibA/26EeLuMnB6cpSrQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-sdk/nested-clients": "3.812.0", "@aws-sdk/core": "3.816.0",
"@aws-sdk/nested-clients": "3.817.0",
"@aws-sdk/types": "3.804.0", "@aws-sdk/types": "3.804.0",
"@smithy/property-provider": "^4.0.2", "@smithy/property-provider": "^4.0.2",
"@smithy/shared-ini-file-loader": "^4.0.2", "@smithy/shared-ini-file-loader": "^4.0.2",
@@ -1185,12 +1186,12 @@
} }
}, },
"node_modules/@aws-sdk/util-user-agent-node": { "node_modules/@aws-sdk/util-user-agent-node": {
"version": "3.812.0", "version": "3.816.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.812.0.tgz", "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.816.0.tgz",
"integrity": "sha512-8pt+OkHhS2U0LDwnzwRnFxyKn8sjSe752OIZQCNv263odud8jQu9pYO2pKqb2kRBk9h9szynjZBDLXfnvSQ7Bg==", "integrity": "sha512-Q6dxmuj4hL7pudhrneWEQ7yVHIQRBFr0wqKLF1opwOi1cIePuoEbPyJ2jkel6PDEv1YMfvsAKaRshp6eNA8VHg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@aws-sdk/middleware-user-agent": "3.812.0", "@aws-sdk/middleware-user-agent": "3.816.0",
"@aws-sdk/types": "3.804.0", "@aws-sdk/types": "3.804.0",
"@smithy/node-config-provider": "^4.1.1", "@smithy/node-config-provider": "^4.1.1",
"@smithy/types": "^4.2.0", "@smithy/types": "^4.2.0",
@@ -5473,9 +5474,9 @@
} }
}, },
"node_modules/dd-trace": { "node_modules/dd-trace": {
"version": "5.52.0", "version": "5.53.0",
"resolved": "https://registry.npmjs.org/dd-trace/-/dd-trace-5.52.0.tgz", "resolved": "https://registry.npmjs.org/dd-trace/-/dd-trace-5.53.0.tgz",
"integrity": "sha512-ZF+OWLMcgVUWJEAIYIl76LocgnbbkPJ6WgJCG1fhLk4UCsUvoHRvBx9qlexbytL0jkktk1pvzODcjL0wyxLAOQ==", "integrity": "sha512-ayraB+H05yAag5Ia70YwNkkAS4q0O/Bx1suijTUaYBXirTVlfK9CDSpZRf0Rcjk2uRqf8ANNNsws1fesP4cRmQ==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "(Apache-2.0 OR BSD-3-Clause)", "license": "(Apache-2.0 OR BSD-3-Clause)",
"dependencies": { "dependencies": {
@@ -9034,9 +9035,9 @@
} }
}, },
"node_modules/moment-timezone": { "node_modules/moment-timezone": {
"version": "0.5.48", "version": "0.6.0",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.6.0.tgz",
"integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", "integrity": "sha512-ldA5lRNm3iJCWZcBCab4pnNL3HSZYXVb/3TYr75/1WCTWYuTqYUb5f/S384pncYjJ88lbO8Z4uPDvmoluHJc8Q==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"moment": "^2.29.4" "moment": "^2.29.4"

View File

@@ -16,14 +16,14 @@
"job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js" "job-totals-fixtures:local": "docker exec node-app /usr/bin/node /app/download-job-totals-fixtures.js"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-cloudwatch-logs": "^3.812.0", "@aws-sdk/client-cloudwatch-logs": "^3.817.0",
"@aws-sdk/client-elasticache": "^3.812.0", "@aws-sdk/client-elasticache": "^3.817.0",
"@aws-sdk/client-s3": "^3.812.0", "@aws-sdk/client-s3": "^3.817.0",
"@aws-sdk/client-secrets-manager": "^3.812.0", "@aws-sdk/client-secrets-manager": "^3.817.0",
"@aws-sdk/client-ses": "^3.812.0", "@aws-sdk/client-ses": "^3.817.0",
"@aws-sdk/credential-provider-node": "^3.812.0", "@aws-sdk/credential-provider-node": "^3.817.0",
"@aws-sdk/lib-storage": "^3.812.0", "@aws-sdk/lib-storage": "^3.817.0",
"@aws-sdk/s3-request-presigner": "^3.812.0", "@aws-sdk/s3-request-presigner": "^3.817.0",
"@opensearch-project/opensearch": "^2.13.0", "@opensearch-project/opensearch": "^2.13.0",
"@socket.io/admin-ui": "^0.5.1", "@socket.io/admin-ui": "^0.5.1",
"@socket.io/redis-adapter": "^8.3.0", "@socket.io/redis-adapter": "^8.3.0",
@@ -38,7 +38,7 @@
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"cors": "^2.8.5", "cors": "^2.8.5",
"crisp-status-reporter": "^1.2.2", "crisp-status-reporter": "^1.2.2",
"dd-trace": "^5.52.0", "dd-trace": "^5.53.0",
"dinero.js": "^1.9.1", "dinero.js": "^1.9.1",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.21.1", "express": "^4.21.1",
@@ -52,7 +52,7 @@
"juice": "^11.0.1", "juice": "^11.0.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"moment": "^2.30.1", "moment": "^2.30.1",
"moment-timezone": "^0.5.48", "moment-timezone": "^0.6.0",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"node-persist": "^4.0.4", "node-persist": "^4.0.4",
"nodemailer": "^6.10.0", "nodemailer": "^6.10.0",

View File

@@ -282,6 +282,7 @@ const applySocketIO = async ({ server, app }) => {
logger.log("Redis connections closed.", "INFO", "redis", "api"); logger.log("Redis connections closed.", "INFO", "redis", "api");
}); });
// IO Redis
const ioRedis = new Server(server, { const ioRedis = new Server(server, {
path: "/wss", path: "/wss",
adapter: createAdapter(pubClient, subClient), adapter: createAdapter(pubClient, subClient),

View File

@@ -2980,4 +2980,59 @@ exports.INSERT_INTEGRATION_LOG = `
id id
} }
} }
`; `;
exports.INSERT_PHONE_NUMBER_OPT_OUT = `
mutation INSERT_PHONE_NUMBER_OPT_OUT($optOutInput: [phone_number_opt_out_insert_input!]!) {
insert_phone_number_opt_out(objects: $optOutInput, on_conflict: { constraint: phone_number_consent_bodyshopid_phone_number_key, update_columns: [updated_at] }) {
affected_rows
returning {
id
bodyshopid
phone_number
created_at
updated_at
}
}
}
`;
// Query to check if a phone number is opted out
exports.CHECK_PHONE_NUMBER_OPT_OUT = `
query CHECK_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
}
}
`;
// Query to check if a phone number is opted out
exports.CHECK_PHONE_NUMBER_OPT_OUT = `
query CHECK_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
}
}
`;
// Mutation to delete a phone number opt-out record
exports.DELETE_PHONE_NUMBER_OPT_OUT = `
mutation DELETE_PHONE_NUMBER_OPT_OUT($bodyshopid: uuid!, $phone_number: String!) {
delete_phone_number_opt_out(where: { bodyshopid: { _eq: $bodyshopid }, phone_number: { _eq: $phone_number } }) {
affected_rows
returning {
id
bodyshopid
phone_number
}
}
}
`;

View File

@@ -3,12 +3,25 @@ const {
FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID, FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID,
UNARCHIVE_CONVERSATION, UNARCHIVE_CONVERSATION,
CREATE_CONVERSATION, CREATE_CONVERSATION,
INSERT_MESSAGE INSERT_MESSAGE,
CHECK_PHONE_NUMBER_OPT_OUT,
DELETE_PHONE_NUMBER_OPT_OUT,
INSERT_PHONE_NUMBER_OPT_OUT
} = require("../graphql-client/queries"); } = require("../graphql-client/queries");
const { phone } = require("phone"); const { phone } = require("phone");
const { admin } = require("../firebase/firebase-handler"); const { admin } = require("../firebase/firebase-handler");
const InstanceManager = require("../utils/instanceMgr").default; const InstanceManager = require("../utils/instanceMgr").default;
// Note: When we handle different languages, we might need to adjust these keywords accordingly.
const optInKeywords = ["START", "YES", "UNSTOP"];
const optOutKeywords = ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT", "REVOKE", "OPTOUT"];
// System Message text, will also need to be localized if we support multiple languages
const systemMessageOptions = {
optIn: "Customer has opted-in",
optOut: "Customer has opted-out"
};
/** /**
* Receive SMS messages from Twilio and process them * Receive SMS messages from Twilio and process them
* @param req * @param req
@@ -51,22 +64,16 @@ const receive = async (req, res) => {
} }
const bodyshop = response.bodyshops[0]; const bodyshop = response.bodyshops[0];
const normalizedPhone = phone(req.body.From).phoneNumber.replace(/^\+1/, ""); // Normalize phone number (remove +1 for CA numbers)
const messageText = (req.body.Body || "").trim().toUpperCase();
// Step 4: Process conversation // Step 2: Process conversation
const sortedConversations = bodyshop.conversations.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); const sortedConversations = bodyshop.conversations.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
const existingConversation = sortedConversations.length const existingConversation = sortedConversations.length
? sortedConversations[sortedConversations.length - 1] ? sortedConversations[sortedConversations.length - 1]
: null; : null;
let conversationid; let conversationid;
let newMessage = {
msid: req.body.SmsMessageSid,
text: req.body.Body,
image: !!req.body.MediaUrl0,
image_path: generateMediaArray(req.body, logger),
isoutbound: false,
userid: null
};
if (existingConversation) { if (existingConversation) {
conversationid = existingConversation.id; conversationid = existingConversation.id;
@@ -88,9 +95,134 @@ const receive = async (req, res) => {
conversationid = createdConversation.id; conversationid = createdConversation.id;
} }
newMessage.conversationid = conversationid; // Step 3: Handle opt-in or opt-out keywords
let systemMessageText = "";
let socketEventType = "";
if (optInKeywords.includes(messageText) || optOutKeywords.includes(messageText)) {
// Check if the phone number is in phone_number_opt_out
const optOutCheck = await client.request(CHECK_PHONE_NUMBER_OPT_OUT, {
bodyshopid: bodyshop.id,
phone_number: normalizedPhone
});
// Opt In
if (optInKeywords.includes(messageText)) {
// Handle opt-in
if (optOutCheck.phone_number_opt_out.length > 0) {
// Phone number is opted out; delete the record
const deleteResponse = await client.request(DELETE_PHONE_NUMBER_OPT_OUT, {
bodyshopid: bodyshop.id,
phone_number: normalizedPhone
});
logger.log("sms-opt-in-success", "INFO", "api", null, {
msid: req.body.SmsMessageSid,
bodyshopid: bodyshop.id,
phone_number: normalizedPhone,
affected_rows: deleteResponse.delete_phone_number_opt_out.affected_rows
});
systemMessageText = systemMessageOptions.optIn;
socketEventType = "phone-number-opted-in";
}
}
// Opt Out
else if (optOutKeywords.includes(messageText)) {
// Handle opt-out
if (optOutCheck.phone_number_opt_out.length === 0) {
// Phone number is not opted out; insert a new record
const now = new Date().toISOString();
const optOutInput = {
bodyshopid: bodyshop.id,
phone_number: normalizedPhone,
created_at: now,
updated_at: now
};
const insertResponse = await client.request(INSERT_PHONE_NUMBER_OPT_OUT, {
optOutInput: [optOutInput]
});
logger.log("sms-opt-out-success", "INFO", "api", null, {
msid: req.body.SmsMessageSid,
bodyshopid: bodyshop.id,
phone_number: normalizedPhone,
affected_rows: insertResponse.insert_phone_number_opt_out.affected_rows
});
systemMessageText = systemMessageOptions.optOut;
socketEventType = "phone-number-opted-out";
}
}
// Insert system message if an opt-in or opt-out action was taken
if (systemMessageText) {
const systemMessage = {
msid: `SYS_${req.body.SmsMessageSid}_${Date.now()}`, // Unique ID for system message
text: systemMessageText,
conversationid,
isoutbound: false,
userid: null,
image: false,
image_path: null,
is_system: true
};
const systemMessageResponse = await client.request(INSERT_MESSAGE, {
msg: systemMessage,
conversationid
});
const insertedSystemMessage = systemMessageResponse.insert_messages.returning[0];
// Emit WebSocket events for system message
const broadcastRoom = getBodyshopRoom(bodyshop.id);
const conversationRoom = getBodyshopConversationRoom({
bodyshopId: bodyshop.id,
conversationId: conversationid
});
const systemPayload = {
isoutbound: false,
conversationId: conversationid,
updated_at: insertedSystemMessage.updated_at,
msid: insertedSystemMessage.msid,
existingConversation: !!existingConversation,
newConversation: !existingConversation ? insertedSystemMessage.conversation : null
};
ioRedis.to(broadcastRoom).emit("new-message-summary", {
...systemPayload,
summary: true
});
ioRedis.to(conversationRoom).emit("new-message-detailed", {
newMessage: insertedSystemMessage,
...systemPayload,
summary: false
});
// Emit opt-in or opt-out event
ioRedis.to(broadcastRoom).emit(socketEventType, {
bodyshopid: bodyshop.id,
phone_number: normalizedPhone
});
}
}
// Step 4: Insert the original message
const newMessage = {
msid: req.body.SmsMessageSid,
text: req.body.Body,
image: !!req.body.MediaUrl0,
image_path: generateMediaArray(req.body, logger),
isoutbound: false,
userid: null,
conversationid,
is_system: false
};
// Step 5: Insert the message
const insertresp = await client.request(INSERT_MESSAGE, { const insertresp = await client.request(INSERT_MESSAGE, {
msg: newMessage, msg: newMessage,
conversationid conversationid
@@ -103,7 +235,7 @@ const receive = async (req, res) => {
throw new Error("Conversation data is missing from the response."); throw new Error("Conversation data is missing from the response.");
} }
// Step 6: Notify clients // Step 5: Notify clients for original message
const conversationRoom = getBodyshopConversationRoom({ const conversationRoom = getBodyshopConversationRoom({
bodyshopId: conversation.bodyshop.id, bodyshopId: conversation.bodyshop.id,
conversationId: conversation.id conversationId: conversation.id
@@ -113,7 +245,7 @@ const receive = async (req, res) => {
isoutbound: false, isoutbound: false,
conversationId: conversation.id, conversationId: conversation.id,
updated_at: message.updated_at, updated_at: message.updated_at,
msid: message.sid msid: message.msid
}; };
const broadcastRoom = getBodyshopRoom(conversation.bodyshop.id); const broadcastRoom = getBodyshopRoom(conversation.bodyshop.id);
@@ -133,7 +265,7 @@ const receive = async (req, res) => {
summary: false summary: false
}); });
// Step 7: Send FCM notification // Step 6: Send FCM notification
const fcmresp = await admin.messaging().send({ const fcmresp = await admin.messaging().send({
topic: `${message.conversation.bodyshop.imexshopid}-messaging`, topic: `${message.conversation.bodyshop.imexshopid}-messaging`,
notification: { notification: {

View File

@@ -1,6 +1,12 @@
const client = require("../graphql-client/graphql-client").client; const client = require("../graphql-client/graphql-client").client;
const { UPDATE_MESSAGE_STATUS, MARK_MESSAGES_AS_READ } = require("../graphql-client/queries"); const {
UPDATE_MESSAGE_STATUS,
MARK_MESSAGES_AS_READ,
INSERT_PHONE_NUMBER_OPT_OUT,
FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID
} = require("../graphql-client/queries");
const logger = require("../utils/logger"); const logger = require("../utils/logger");
const { phone } = require("phone");
/** /**
* Handle the status of an SMS message * Handle the status of an SMS message
@@ -9,7 +15,7 @@ const logger = require("../utils/logger");
* @returns {Promise<*>} * @returns {Promise<*>}
*/ */
const status = async (req, res) => { const status = async (req, res) => {
const { SmsSid, SmsStatus } = req.body; const { SmsSid, SmsStatus, ErrorCode, To, MessagingServiceSid } = req.body;
const { const {
ioRedis, ioRedis,
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom } ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
@@ -21,18 +27,76 @@ const status = async (req, res) => {
return res.status(200).json({ message: "Status 'queued' disregarded." }); return res.status(200).json({ message: "Status 'queued' disregarded." });
} }
// Handle ErrorCode 21610 (Attempt to send to unsubscribed recipient) first
if (ErrorCode === "21610" && To && MessagingServiceSid) {
try {
// Step 1: Find the bodyshop by MessagingServiceSid
const bodyshopResponse = await client.request(FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID, {
mssid: MessagingServiceSid,
phone: phone(To).phoneNumber // Pass the normalized phone number as required
});
const bodyshop = bodyshopResponse.bodyshops[0];
if (!bodyshop) {
logger.log("sms-opt-out-error", "ERROR", "api", null, {
msid: SmsSid,
messagingServiceSid: MessagingServiceSid,
to: To,
error: "No matching bodyshop found"
});
} else {
// Step 2: Insert into phone_number_opt_out table
const now = new Date().toISOString();
const optOutInput = {
bodyshopid: bodyshop.id,
phone_number: phone(To).phoneNumber.replace(/^\+1/, ""), // Normalize phone number (remove +1 for CA numbers)
created_at: now,
updated_at: now
};
const optOutResponse = await client.request(INSERT_PHONE_NUMBER_OPT_OUT, {
optOutInput: [optOutInput]
});
logger.log("sms-opt-out-success", "INFO", null, null, {
msid: SmsSid,
bodyshopid: bodyshop.id,
phone_number: optOutInput.phone_number,
affected_rows: optOutResponse.insert_phone_number_opt_out.affected_rows
});
// Store bodyshopid for potential use in WebSocket notification
const broadcastRoom = getBodyshopRoom(bodyshop.id);
ioRedis.to(broadcastRoom).emit("phone-number-opted-out", {
bodyshopid: bodyshop.id,
phone_number: optOutInput.phone_number
// Note: conversationId is not included yet; will be set after message lookup
});
}
} catch (error) {
logger.log("sms-opt-out-error", "ERROR", "api", null, {
msid: SmsSid,
messagingServiceSid: MessagingServiceSid,
to: To,
error: error.message,
stack: error.stack
});
// Continue processing to update message status
}
}
// Update message status in the database // Update message status in the database
const response = await client.request(UPDATE_MESSAGE_STATUS, { const response = await client.request(UPDATE_MESSAGE_STATUS, {
msid: SmsSid, msid: SmsSid,
fields: { status: SmsStatus } fields: { status: SmsStatus }
}); });
const message = response.update_messages.returning[0]; const message = response.update_messages?.returning?.[0];
if (message) { if (message) {
logger.log("sms-status-update", "DEBUG", "api", null, { logger.log("sms-status-update", "DEBUG", "api", null, {
msid: SmsSid, msid: SmsSid,
fields: { status: SmsStatus } status: SmsStatus
}); });
// Emit WebSocket event to notify the change in message status // Emit WebSocket event to notify the change in message status
@@ -47,20 +111,20 @@ const status = async (req, res) => {
type: "status-changed" type: "status-changed"
}); });
} else { } else {
logger.log("sms-status-update-warning", "WARN", "api", null, { logger.log("sms-status-update-warning", "WARN", null, null, {
msid: SmsSid, msid: SmsSid,
fields: { status: SmsStatus }, status: SmsStatus,
warning: "No message returned from the database update." warning: "No message found in database for update"
}); });
} }
res.sendStatus(200); res.sendStatus(200);
} catch (error) { } catch (err) {
logger.log("sms-status-update-error", "ERROR", "api", null, { logger.log("sms-status-update-error", "ERROR", "api", null, {
msid: SmsSid, msid: SmsSid,
fields: { status: SmsStatus }, status: SmsStatus,
stack: error.stack, error: err.message,
message: error.message stack: err.stack
}); });
res.status(500).json({ error: "Failed to update message status." }); res.status(500).json({ error: "Failed to update message status." });
} }