Compare commits

..

1 Commits

Author SHA1 Message Date
Patrick Fic
d52426f5f5 IO-3239 Additional logging fixes. 2025-05-28 15:21:42 -07:00
55 changed files with 667 additions and 1451 deletions

View File

@@ -12791,6 +12791,27 @@
</translation>
</translations>
</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>
<name>checklist</name>
<definition_loaded>false</definition_loaded>
@@ -42593,6 +42614,27 @@
</translation>
</translations>
</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>
<name>name</name>
<definition_loaded>false</definition_loaded>

220
client/package-lock.json generated
View File

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

View File

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

View File

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

View File

@@ -202,6 +202,8 @@ export const registerMessagingHandlers = ({ socket, client }) => {
text: message.text
};
// Add cases for other known message types as needed
default:
// Log a warning for unhandled message types
logLocal("handleMessageChanged - Unhandled message type", { type: message.type });
@@ -209,7 +211,7 @@ export const registerMessagingHandlers = ({ socket, client }) => {
}
}
return messageRef;
return messageRef; // Keep other messages unchanged
});
}
}
@@ -243,8 +245,11 @@ export const registerMessagingHandlers = ({ socket, client }) => {
});
const updatedList = existingList?.conversations
? [newConversation, ...existingList.conversations.filter((conv) => conv.id !== newConversation.id)]
: [newConversation]; // Prevent duplicates
? [
newConversation,
...existingList.conversations.filter((conv) => conv.id !== newConversation.id) // Prevent duplicates
]
: [newConversation];
client.cache.writeQuery({
query: CONVERSATION_LIST_QUERY,
@@ -398,7 +403,6 @@ export const registerMessagingHandlers = ({ socket, client }) => {
}
break;
default:
logLocal("handleConversationChanged - Unhandled type", { type });
client.cache.modify({
@@ -415,95 +419,10 @@ 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-detailed", handleNewMessageDetailed);
socket.on("message-changed", handleMessageChanged);
socket.on("conversation-changed", handleConversationChanged);
socket.on("phone-number-opted-out", handlePhoneNumberOptedOut);
socket.on("phone-number-opted-in", handlePhoneNumberOptedIn);
};
export const unregisterMessagingHandlers = ({ socket }) => {
@@ -512,6 +431,4 @@ export const unregisterMessagingHandlers = ({ socket }) => {
socket.off("new-message-detailed");
socket.off("message-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, Tooltip } from "antd";
import { Badge, Card, List, Space, Tag } from "antd";
import { useEffect, useMemo, useState } from "react";
import { connect } from "react-redux";
import { Virtuoso } from "react-virtuoso";
@@ -9,7 +9,6 @@ import { TimeAgoFormatter } from "../../utils/DateFormatter";
import PhoneFormatter from "../../utils/PhoneFormatter";
import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-display.component";
import _ from "lodash";
import { ExclamationCircleOutlined } from "@ant-design/icons";
import "./chat-conversation-list.styles.scss";
import { useQuery } from "@apollo/client";
import { GET_PHONE_NUMBER_OPT_OUTS } from "../../graphql/phone-number-opt-out.queries.js";
@@ -89,13 +88,7 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
const cardExtra = (
<>
<Badge count={item.messages_aggregate.aggregate.count} />
{hasOptOutEntry && (
<Tooltip title={t("consent.text_body")}>
<Tag color="red" icon={<ExclamationCircleOutlined />}>
{t("messaging.labels.no_consent")}
</Tag>
</Tooltip>
)}
{hasOptOutEntry && <Tag color="red">{t("messaging.labels.no_consent")}</Tag>}
</>
);

View File

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

View File

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

View File

@@ -1,52 +0,0 @@
.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,16 +4,13 @@
height: 100%;
width: 100%;
}
.archive-button {
height: 20px;
border-radius: 4px;
}
.chat-title {
margin-bottom: 5px;
}
.messages {
display: flex;
flex-direction: column;
@@ -40,13 +37,11 @@
gap: 8px;
}
}
.chat-send-message-button {
.chat-send-message-button{
margin: 0.3rem;
padding-left: 0.5rem;
}
.message-icon {
position: absolute;
bottom: 0.1rem;
@@ -130,37 +125,6 @@
}
}
.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 {
flex: 1;
overflow: auto;

View File

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

View File

@@ -1,6 +1,6 @@
import { ExclamationCircleOutlined, LoadingOutlined, SendOutlined } from "@ant-design/icons";
import { Alert, Input, Space, Spin, Tooltip } from "antd";
import { useEffect, useRef, useState } from "react";
import { LoadingOutlined, SendOutlined } from "@ant-design/icons";
import { Alert, Input, Spin } from "antd";
import React, { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
@@ -68,67 +68,48 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
};
return (
<Space direction="vertical" style={{ width: "100%" }} size="middle">
{isOptedOut && (
<Tooltip title={t("consent.text_body")}>
<Alert
showIcon={true}
icon={<ExclamationCircleOutlined />}
message={t("messaging.errors.no_consent")}
type="error"
/>
</Tooltip>
)}
<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 className="imex-flex-row" style={{ width: "100%" }}>
{isOptedOut && <Alert message={t("messaging.errors.no_consent")} type="warning" style={{ marginBottom: 8 }} />}
<ChatPresetsComponent className="imex-flex-row__margin" />
<ChatMediaSelector
conversation={conversation}
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();
}}
/>
</div>
</Space>
</span>
<SendOutlined
className="chat-send-message-button"
disabled={isOptedOut || message === "" || !message}
onClick={handleEnter}
/>
<Spin
style={{ display: `${isSending ? "" : "none"}` }}
indicator={
<LoadingOutlined
style={{
fontSize: 24
}}
spin
/>
}
/>
</div>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -167,18 +167,7 @@ export function JobsDetailHeaderActionsToggleProduction({
<FormDateTimePickerComponent disabled={jobRO} />
</Form.Item>
<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"),
}
]}
>
<Form.Item name={["actual_delivery"]} label={t("jobs.fields.actual_delivery")}>
<FormDateTimePickerComponent disabled={jobRO} />
</Form.Item>
</>

View File

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

View File

@@ -1,15 +1,14 @@
import { Form, Input, Tooltip } from "antd";
import { CloseCircleFilled } from "@ant-design/icons";
import { Form, Input, Switch } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.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 { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component";
export default function OwnerDetailFormComponent({ form, loading, isPhone1OptedOut, isPhone2OptedOut }) {
export default function OwnerDetailFormComponent({ form, loading }) {
const { t } = useTranslation();
const { getFieldValue } = form;
return (
<div>
<FormFieldsChanged form={form} />
@@ -27,7 +26,7 @@ export default function OwnerDetailFormComponent({ form, loading, isPhone1OptedO
<Input />
</Form.Item>
<Form.Item label={t("owners.fields.accountingid")} name="accountingid">
<Input disabled />
<Input disabled/>
</Form.Item>
</LayoutFormRow>
<LayoutFormRow header={t("owners.forms.address")}>
@@ -51,6 +50,9 @@ export default function OwnerDetailFormComponent({ form, loading, isPhone1OptedO
</Form.Item>
</LayoutFormRow>
<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
label={t("owners.fields.ownr_ea")}
name="ownr_ea"
@@ -63,55 +65,19 @@ export default function OwnerDetailFormComponent({ form, loading, isPhone1OptedO
>
<FormItemEmail email={getFieldValue("ownr_ea")} />
</Form.Item>
<Form.Item label={t("owners.fields.ownr_ph1")} style={{ marginBottom: 0 }}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<Form.Item
name="ownr_ph1"
noStyle
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
label={t("owners.fields.ownr_ph1")}
name="ownr_ph1"
rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "ownr_ph1")]}
>
<FormItemPhone />
</Form.Item>
<Form.Item label={t("owners.fields.ownr_ph2")} style={{ marginBottom: 0 }}>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<Form.Item
name="ownr_ph2"
noStyle
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
label={t("owners.fields.ownr_ph2")}
name="ownr_ph2"
rules={[({ getFieldValue }) => PhoneItemFormatterValidation(getFieldValue, "ownr_ph2")]}
>
<FormItemPhone />
</Form.Item>
<Form.Item label={t("owners.fields.preferred_contact")} name="preferred_contact">
<Input />

View File

@@ -1,115 +1,69 @@
import { Button, Form, Popconfirm } from "antd";
import { PageHeader } from "@ant-design/pro-layout";
import { useEffect, useState } from "react";
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useApolloClient, useMutation } from "@apollo/client";
import { useMutation } from "@apollo/client";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
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 { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
import { phone } from "phone"; // Import phone utility for formatting
// Connect to Redux to access bodyshop
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop
});
function OwnerDetailFormContainer({ owner, refetch, bodyshop }) {
function OwnerDetailFormContainer({ owner, refetch }) {
const { t } = useTranslation();
const [form] = Form.useForm();
const navigate = useNavigate();
const history = useNavigate();
const [loading, setLoading] = useState(false);
const [optedOutPhones, setOptedOutPhones] = useState(new Set());
const [updateOwner] = useMutation(UPDATE_OWNER);
const [deleteOwner] = useMutation(DELETE_OWNER);
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 () => {
setLoading(true);
try {
const result = await deleteOwner({
variables: { id: owner.id }
});
if (result.errors) {
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({
const result = await deleteOwner({
variables: { id: owner.id }
});
console.log(result);
if (result.errors) {
notification["error"]({
message: t("owners.errors.deleting", {
error: error.message
error: JSON.stringify(result.errors)
})
});
} finally {
setLoading(false);
} else {
notification["success"]({
message: t("owners.successes.delete")
});
setLoading(false);
history(`/manage/owners`);
}
};
const handleFinish = async (values) => {
setLoading(true);
try {
const result = await updateOwner({
variables: { ownerId: owner.id, owner: values }
});
if (result.errors) {
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({
const result = await updateOwner({
variables: { ownerId: owner.id, owner: values }
});
if (!!result.errors) {
notification["error"]({
message: t("owners.errors.saving", {
error: error.message
error: JSON.stringify(result.errors)
})
});
} finally {
setLoading(false);
return;
}
notification["success"]({
message: t("owners.successes.save")
});
if (refetch) await refetch();
form.resetFields();
form.resetFields();
setLoading(false);
};
return (
@@ -118,7 +72,6 @@ function OwnerDetailFormContainer({ owner, refetch, bodyshop }) {
title={t("menus.header.owners")}
extra={[
<Popconfirm
key="delete"
trigger="click"
onConfirm={handleDelete}
disabled={owner.jobs.length !== 0}
@@ -128,29 +81,16 @@ function OwnerDetailFormContainer({ owner, refetch, bodyshop }) {
{t("general.actions.delete")}
</Button>
</Popconfirm>,
<Button key="save" type="primary" loading={loading} onClick={() => form.submit()}>
<Button type="primary" loading={loading} onClick={() => form.submit()}>
{t("general.actions.save")}
</Button>
]}
/>
<Form form={form} onFinish={handleFinish} autoComplete="off" layout="vertical" initialValues={owner}>
<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/, ""))
}
/>
<OwnerDetailFormComponent loading={loading} form={form} />
</Form>
</>
);
}
export default connect(mapStateToProps)(OwnerDetailFormContainer);
export default OwnerDetailFormContainer;

View File

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

View File

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

View File

@@ -1,20 +1,14 @@
import { useQuery } from "@apollo/client";
import { Input, Table, Typography } from "antd";
import { Input, Table } from "antd";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
import { GET_PHONE_NUMBER_OPT_OUTS } from "../../graphql/phone-number-opt-out.queries";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import { TimeAgoFormatter } from "../../utils/DateFormatter";
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({
bodyshop: selectBodyshop,
@@ -26,95 +20,18 @@ const mapDispatchToProps = () => ({});
function PhoneNumberConsentList({ bodyshop, currentUser }) {
const { t } = useTranslation();
const [search, setSearch] = useState("");
// Fetch opt-out phone numbers
const { loading: optOutLoading, data: optOutData } = useQuery(GET_PHONE_NUMBER_OPT_OUTS, {
const { loading, data } = useQuery(GET_PHONE_NUMBER_OPT_OUTS, {
variables: { bodyshopid: bodyshop.id, search: search ? `%${search}%` : undefined },
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 = [
{
title: t("consent.phone_number"),
dataIndex: "phone_number",
render: (text) => <ChatOpenButton phone={text} />,
render: (text) => <PhoneNumberFormatter>{text}</PhoneNumberFormatter>,
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"),
dataIndex: "created_at",
@@ -125,7 +42,6 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) {
return (
<div>
<Paragraph>{t("consent.text_body")}</Paragraph>
<Input.Search
placeholder={t("general.labels.search")}
onSearch={(value) => setSearch(value)}
@@ -134,8 +50,8 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) {
<Table
columns={columns}
dataSource={optOutData?.phone_number_opt_out}
loading={optOutLoading /* || ownersLoading*/}
dataSource={data?.phone_number_opt_out}
loading={loading}
rowKey="id"
style={{ marginTop: 16 }}
/>

View File

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

View File

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

View File

@@ -133,9 +133,7 @@ const Lane = ({
Item: ItemComponent
},
itemContent: (index, item) => <ItemWrapper>{renderDraggable(index, item)}</ItemWrapper>,
overscan: { main: 10, reverse: 10 },
// Ensure a minimum height for empty lanes to allow dropping
style: renderedCards.length === 0 ? { minHeight: "5px" } : {}
overscan: { main: 10, reverse: 10 }
};
const horizontalProps = {
@@ -151,6 +149,8 @@ const Lane = ({
const componentProps = orientation === "vertical" ? verticalProps : horizontalProps;
// If the lane is collapsed, we want to render a div instead of the virtualized list, and we want to set the height to the max height of the lane so that
// the lane doesn't shrink when collapsed (in horizontal mode)
const finalComponentProps = collapsed
? orientation === "horizontal"
? {
@@ -161,8 +161,9 @@ const Lane = ({
: {}
: componentProps;
// Always render placeholder for empty lanes in vertical mode to ensure droppable area
const shouldRenderPlaceholder = orientation === "vertical" ? collapsed || renderedCards.length === 0 : collapsed;
// If the lane is horizontal and collapsed, we want to render a placeholder so that the lane doesn't shrink to 0 height and grows when
// a card is dragged over it
const shouldRenderPlaceholder = orientation !== "horizontal" && (collapsed || renderedCards.length === 0);
return (
<HeightMemoryWrapper
@@ -177,8 +178,8 @@ const Lane = ({
override={orientation !== "horizontal" && (collapsed || !renderedCards.length)}
>
<div
ref={laneRef}
style={{ height: "100%", width: "100%" }}
ref={laneRef} // Ensure laneRef is set here
style={{ height: "100%", width: "100%" }} // Make it scrollable
className={`react-trello-lane ${collapsed ? "lane-collapsed" : ""}`}
>
<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]);
return (
<Popover destroyOnHidden content={popContent} open={visibility}>
<Popover destroyTooltipOnHide content={popContent} open={visibility}>
<Spin spinning={loading}>
{record[type] ? (
<div>

View File

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

View File

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

View File

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

View File

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

View File

@@ -49,6 +49,7 @@ export const QUERY_OWNER_BY_ID = gql`
owners_by_pk(id: $id) {
id
accountingid
allow_text_message
ownr_addr1
ownr_addr2
ownr_co_nm
@@ -103,6 +104,7 @@ export const QUERY_ALL_OWNERS = gql`
query QUERY_ALL_OWNERS {
owners {
id
allow_text_message
created_at
ownr_addr1
ownr_addr2
@@ -127,6 +129,7 @@ export const QUERY_ALL_OWNERS_PAGINATED = gql`
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) {
id
allow_text_message
created_at
ownr_addr1
ownr_addr2

View File

@@ -26,39 +26,3 @@ 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,6 +114,7 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
if (!!!job.ownerid) {
ownerData = job.owner.data;
ownerData.shopid = bodyshop.id;
delete ownerData.allow_text_message;
delete ownerData.preferred_contact;
delete job.ownerid;
} else {

View File

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

View File

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

View File

@@ -775,6 +775,7 @@
},
"labels": {
"addtoproduction": "Add Job to Production?",
"allow_text_message": "Permission to Text?",
"checklist": "Checklist",
"printpack": "Job Intake Print Pack",
"removefromproduction": "Remove Job from Production?"
@@ -2031,10 +2032,9 @@
"stands": "Stands",
"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",
"deleteintake": "Delete Intake Checklist",
"deletewatchers": "Remove Watchers before deleting this Job.",
"deliverchecklist": "Deliver Checklist",
"difference": "Difference",
"diskscan": "Scan Disk for Estimates",
@@ -2302,10 +2302,8 @@
"productionlist": "Production Board - List",
"readyjobs": "Ready Jobs",
"recent": "Recent Items",
"remoteassist": "Remote Assist",
"reportcenter": "Report Center",
"rescueme": "Rescue Me!",
"rescuemezoho": "Remote Me In!",
"rescueme": "Rescue me!",
"schedule": "Schedule",
"scoreboard": "Scoreboard",
"search": {
@@ -2381,7 +2379,7 @@
"invalidphone": "The phone number is invalid. Unable to open conversation. ",
"noattachedjobs": "No Jobs have been associated to this conversation. ",
"updatinglabel": "Error updating label. {{error}}",
"no_consent": "This phone number has opted-out of Messaging."
"no_consent": "This phone number has not consented to receive messages."
},
"labels": {
"addlabel": "Add a label to this conversation.",
@@ -2398,7 +2396,7 @@
"sentby": "Sent by {{by}} at {{time}}",
"typeamessage": "Send a message...",
"unarchive": "Unarchive",
"no_consent": "Opted-out"
"no_consent": "No Consent"
},
"render": {
"conversation_list": "Conversation List"
@@ -2523,6 +2521,7 @@
"fields": {
"accountingid": "Accounting ID",
"address": "Address",
"allow_text_message": "Permission to Text?",
"name": "Name",
"note": "Owner Note",
"ownr_addr1": "Address",
@@ -3870,12 +3869,8 @@
},
"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": "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."
"status": "Consent Status",
"created_at": "Created At"
},
"settings": {
"title": "Phone Number Opt-Out List"

View File

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

View File

@@ -656,7 +656,6 @@
}
},
"labels": {
"consent_settings": "",
"2tiername": "",
"2tiersetup": "",
"2tiersource": "",
@@ -775,6 +774,7 @@
},
"labels": {
"addtoproduction": "",
"allow_text_message": "",
"checklist": "",
"printpack": "",
"removefromproduction": ""
@@ -2034,7 +2034,6 @@
"deleteconfirm": "",
"deletedelivery": "",
"deleteintake": "",
"deletewatchers": "",
"deliverchecklist": "",
"difference": "",
"diskscan": "",
@@ -2302,10 +2301,8 @@
"productionlist": "",
"readyjobs": "",
"recent": "",
"remoteassist": "",
"reportcenter": "",
"rescueme": "",
"rescuemezoho": "",
"schedule": "Programme",
"scoreboard": "",
"search": {
@@ -2380,8 +2377,7 @@
"errors": {
"invalidphone": "",
"noattachedjobs": "",
"updatinglabel": "",
"no_consent": ""
"updatinglabel": ""
},
"labels": {
"addlabel": "",
@@ -2397,8 +2393,7 @@
"selectmedia": "",
"sentby": "",
"typeamessage": "Envoyer un message...",
"unarchive": "",
"no_consent": ""
"unarchive": ""
},
"render": {
"conversation_list": ""
@@ -2503,8 +2498,7 @@
},
"tooltips": {
"job-watchers": "",
"not-employee": "",
"not-employee-notifications": ""
"not-employee": ""
}
},
"owner": {
@@ -2525,6 +2519,7 @@
"fields": {
"accountingid": "",
"address": "Adresse",
"allow_text_message": "Autorisation de texte?",
"name": "Prénom",
"note": "",
"ownr_addr1": "Adresse",
@@ -3869,18 +3864,6 @@
"validation": {
"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,5 +1,6 @@
import { Tooltip } from "antd";
import dayjs from "../utils/day";
import React from "react";
export function DateFormatter(props) {
return props.children ? dayjs(props.children).format(props.includeDay ? "ddd MM/DD/YYYY" : "MM/DD/YYYY") : null;

View File

@@ -1,48 +0,0 @@
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,7 +4742,6 @@
- id
- image
- image_path
- is_system
- isoutbound
- msid
- read

View File

@@ -1,4 +0,0 @@
-- 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

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

View File

@@ -10028,6 +10028,25 @@
</translation>
</translations>
</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>
<name>checklist</name>
<description/>
@@ -32981,6 +33000,25 @@
</translation>
</translations>
</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>
<name>name</name>
<description/>

287
package-lock.json generated
View File

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

View File

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

View File

@@ -478,7 +478,7 @@ async function InsertJob(oauthClient, qbo_realmId, req, job, parentTierRef) {
exports.InsertJob = InsertJob;
async function QueryMetaData(oauthClient, qbo_realmId, req, bodyshopid) {
async function QueryMetaData(oauthClient, qbo_realmId, req, bodyshopid, jobid) {
const items = await oauthClient.makeApiCall({
url: urlBuilder(qbo_realmId, "query", `select * From Item where active=true maxresults 1000`),
method: "POST",
@@ -492,6 +492,7 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, bodyshopid) {
name: "QueryItems",
status: items.response?.status,
bodyshopid,
jobid: jobid,
email: req.user.email
})
setNewRefreshToken(req.user.email, items);
@@ -508,6 +509,7 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, bodyshopid) {
name: "QueryTaxCodes",
status: taxCodes.response?.status,
bodyshopid,
jobid: jobid,
email: req.user.email
})
const classes = await oauthClient.makeApiCall({
@@ -523,6 +525,7 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, bodyshopid) {
name: "QueryClasses",
status: classes.response?.status,
bodyshopid,
jobid: jobid,
email: req.user.email
})
const taxCodeMapping = {};
@@ -559,7 +562,7 @@ async function QueryMetaData(oauthClient, qbo_realmId, req, bodyshopid) {
}
async function InsertInvoice(oauthClient, qbo_realmId, req, job, bodyshop, parentTierRef) {
const { items, taxCodes, classes } = await QueryMetaData(oauthClient, qbo_realmId, req, job.shopid);
const { items, taxCodes, classes } = await QueryMetaData(oauthClient, qbo_realmId, req, job.shopid, job.id);
const InvoiceLineAdd = CreateInvoiceLines({
bodyshop,
jobs_by_pk: job,
@@ -653,7 +656,7 @@ async function InsertInvoice(oauthClient, qbo_realmId, req, job, bodyshop, paren
platform: "QBO",
method: "POST",
name: "InsertInvoice",
status: result.status,
status: result.response?.status,
bodyshopid: job.shopid,
jobid: job.id,
email: req.user.email
@@ -778,7 +781,7 @@ async function InsertInvoiceMultiPayerInvoice(
platform: "QBO",
method: "POST",
name: "InsertInvoice",
status: result.response.status,
status: result.response?.status,
bodyshopid: job.shopid,
jobid: job.id,
email: req.user.email

View File

@@ -2980,59 +2980,4 @@ exports.INSERT_INTEGRATION_LOG = `
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,25 +3,12 @@ const {
FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID,
UNARCHIVE_CONVERSATION,
CREATE_CONVERSATION,
INSERT_MESSAGE,
CHECK_PHONE_NUMBER_OPT_OUT,
DELETE_PHONE_NUMBER_OPT_OUT,
INSERT_PHONE_NUMBER_OPT_OUT
INSERT_MESSAGE
} = require("../graphql-client/queries");
const { phone } = require("phone");
const { admin } = require("../firebase/firebase-handler");
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
* @param req
@@ -64,16 +51,22 @@ const receive = async (req, res) => {
}
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 2: Process conversation
// Step 4: Process conversation
const sortedConversations = bodyshop.conversations.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
const existingConversation = sortedConversations.length
? sortedConversations[sortedConversations.length - 1]
: null;
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) {
conversationid = existingConversation.id;
@@ -95,134 +88,9 @@ const receive = async (req, res) => {
conversationid = createdConversation.id;
}
// 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
};
newMessage.conversationid = conversationid;
// Step 5: Insert the message
const insertresp = await client.request(INSERT_MESSAGE, {
msg: newMessage,
conversationid
@@ -235,7 +103,7 @@ const receive = async (req, res) => {
throw new Error("Conversation data is missing from the response.");
}
// Step 5: Notify clients for original message
// Step 6: Notify clients
const conversationRoom = getBodyshopConversationRoom({
bodyshopId: conversation.bodyshop.id,
conversationId: conversation.id
@@ -245,7 +113,7 @@ const receive = async (req, res) => {
isoutbound: false,
conversationId: conversation.id,
updated_at: message.updated_at,
msid: message.msid
msid: message.sid
};
const broadcastRoom = getBodyshopRoom(conversation.bodyshop.id);
@@ -265,7 +133,7 @@ const receive = async (req, res) => {
summary: false
});
// Step 6: Send FCM notification
// Step 7: Send FCM notification
const fcmresp = await admin.messaging().send({
topic: `${message.conversation.bodyshop.imexshopid}-messaging`,
notification: {

View File

@@ -1,12 +1,6 @@
const client = require("../graphql-client/graphql-client").client;
const {
UPDATE_MESSAGE_STATUS,
MARK_MESSAGES_AS_READ,
INSERT_PHONE_NUMBER_OPT_OUT,
FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID
} = require("../graphql-client/queries");
const { UPDATE_MESSAGE_STATUS, MARK_MESSAGES_AS_READ } = require("../graphql-client/queries");
const logger = require("../utils/logger");
const { phone } = require("phone");
/**
* Handle the status of an SMS message
@@ -15,7 +9,7 @@ const { phone } = require("phone");
* @returns {Promise<*>}
*/
const status = async (req, res) => {
const { SmsSid, SmsStatus, ErrorCode, To, MessagingServiceSid } = req.body;
const { SmsSid, SmsStatus } = req.body;
const {
ioRedis,
ioHelpers: { getBodyshopRoom, getBodyshopConversationRoom }
@@ -27,76 +21,18 @@ const status = async (req, res) => {
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
const response = await client.request(UPDATE_MESSAGE_STATUS, {
msid: SmsSid,
fields: { status: SmsStatus }
});
const message = response.update_messages?.returning?.[0];
const message = response.update_messages.returning[0];
if (message) {
logger.log("sms-status-update", "DEBUG", "api", null, {
msid: SmsSid,
status: SmsStatus
fields: { status: SmsStatus }
});
// Emit WebSocket event to notify the change in message status
@@ -111,20 +47,20 @@ const status = async (req, res) => {
type: "status-changed"
});
} else {
logger.log("sms-status-update-warning", "WARN", null, null, {
logger.log("sms-status-update-warning", "WARN", "api", null, {
msid: SmsSid,
status: SmsStatus,
warning: "No message found in database for update"
fields: { status: SmsStatus },
warning: "No message returned from the database update."
});
}
res.sendStatus(200);
} catch (err) {
} catch (error) {
logger.log("sms-status-update-error", "ERROR", "api", null, {
msid: SmsSid,
status: SmsStatus,
error: err.message,
stack: err.stack
fields: { status: SmsStatus },
stack: error.stack,
message: error.message
});
res.status(500).json({ error: "Failed to update message status." });
}

View File

@@ -206,7 +206,7 @@ const createLogger = () => {
jobid,
paymentid,
billid,
status: status.toString() ?? "0",
status: status?.toString() ?? "0",
bodyshopid,
email
}