Compare commits

...

20 Commits

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

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

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

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

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

View File

@@ -15,8 +15,8 @@
"@fingerprintjs/fingerprintjs": "^4.6.1",
"@firebase/analytics": "^0.10.16",
"@firebase/app": "^0.13.0",
"@firebase/auth": "^1.10.5",
"@firebase/firestore": "^4.7.15",
"@firebase/auth": "^1.10.6",
"@firebase/firestore": "^4.7.16",
"@firebase/messaging": "^0.12.21",
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.8.2",
@@ -25,7 +25,7 @@
"@sentry/vite-plugin": "^3.5.0",
"@splitsoftware/splitio-react": "^2.1.1",
"@tanem/react-nprogress": "^5.0.53",
"antd": "^5.25.2",
"antd": "^5.25.3",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^4.3.0",
"autosize": "^6.0.1",
@@ -48,6 +48,7 @@
"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 +60,7 @@
"react-drag-listview": "^2.0.0",
"react-grid-gallery": "^1.0.1",
"react-grid-layout": "1.3.4",
"react-i18next": "^15.4.1",
"react-i18next": "^15.5.2",
"react-icons": "^5.5.0",
"react-image-lightbox": "^5.1.4",
"react-markdown": "^10.1.0",
@@ -99,7 +100,7 @@
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@vitejs/plugin-react": "^4.3.4",
"@vitejs/plugin-react": "^4.5.0",
"browserslist": "^4.24.5",
"browserslist-to-esbuild": "^2.1.1",
"chalk": "^5.4.1",
@@ -2966,9 +2967,9 @@
}
},
"node_modules/@firebase/auth": {
"version": "1.10.5",
"resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.10.5.tgz",
"integrity": "sha512-6wF/NdMTwObL4RNQePunuzMr9O3gyftisvFZFFKf57D2HONXo87YymogRV8d+Z7SLA0rcNBN1gLJVk2D0y97gA==",
"version": "1.10.6",
"resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.10.6.tgz",
"integrity": "sha512-cFbo2FymQltog4atI9cKTO6CxKxS0dOMXslTQrlNZRH7qhDG44/d7QeI6GXLweFZtrnlecf52ESnNz1DU6ek8w==",
"license": "Apache-2.0",
"dependencies": {
"@firebase/component": "0.6.17",
@@ -3003,9 +3004,9 @@
}
},
"node_modules/@firebase/firestore": {
"version": "4.7.15",
"resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.7.15.tgz",
"integrity": "sha512-FgWTmkNBEXdKCoN2ngBNjrMaXuBx6QwjiZZVnOGg+VjUmiBq5gAqlDIW5bZY6i/NYvLUrWugdqIs7y9GHEqwww==",
"version": "4.7.16",
"resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.7.16.tgz",
"integrity": "sha512-5OpvlwYVUTLEnqewOlXmtIpH8t2ISlZHDW0NDbKROM2D0ATMqFkMHdvl+/wz9zOAcb8GMQYlhCihOnVAliUbpQ==",
"license": "Apache-2.0",
"dependencies": {
"@firebase/component": "0.6.17",
@@ -3885,6 +3886,13 @@
"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",
@@ -5793,15 +5801,16 @@
"license": "ISC"
},
"node_modules/@vitejs/plugin-react": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz",
"integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==",
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.0.tgz",
"integrity": "sha512-JuLWaEqypaJmOJPLWwO335Ig6jSgC1FTONCWAxnqcQthLTK/Yc9aH6hr9z/87xciejbQcnP3GnA1FWUSWeXaeg==",
"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"
},
@@ -6091,12 +6100,12 @@
}
},
"node_modules/antd": {
"version": "5.25.2",
"resolved": "https://registry.npmjs.org/antd/-/antd-5.25.2.tgz",
"integrity": "sha512-7R2nUvlHhey7Trx64+hCtGXOiy+DTUs1Lv5bwbV1LzEIZIhWb0at1AM6V3K108a5lyoR9n7DX3ptlLF7uYV/DQ==",
"version": "5.25.3",
"resolved": "https://registry.npmjs.org/antd/-/antd-5.25.3.tgz",
"integrity": "sha512-tBBcAFRjmWM3sitxrL/FEbQL+MTQntYY5bGa5c1ZZZHXWCynkhS3Ch/gy25mGMUY1M/9Uw3pH029v/RGht1x3w==",
"license": "MIT",
"dependencies": {
"@ant-design/colors": "^7.2.0",
"@ant-design/colors": "^7.2.1",
"@ant-design/cssinjs": "^1.23.0",
"@ant-design/cssinjs-utils": "^1.1.3",
"@ant-design/fast-color": "^2.0.6",
@@ -6156,9 +6165,9 @@
}
},
"node_modules/antd/node_modules/@ant-design/colors": {
"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==",
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-7.2.1.tgz",
"integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==",
"license": "MIT",
"dependencies": {
"@ant-design/fast-color": "^2.0.6"
@@ -13266,6 +13275,15 @@
"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",
@@ -14403,9 +14421,9 @@
}
},
"node_modules/react-i18next": {
"version": "15.5.1",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.1.tgz",
"integrity": "sha512-C8RZ7N7H0L+flitiX6ASjq9p5puVJU1Z8VyL3OgM/QOMRf40BMZX+5TkpxzZVcTmOLPX5zlti4InEX5pFyiVeA==",
"version": "15.5.2",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.2.tgz",
"integrity": "sha512-ePODyXgmZQAOYTbZXQn5rRsSBu3Gszo69jxW6aKmlSgxKAI1fOhDwSu6bT4EKHciWPKQ7v7lPrjeiadR6Gi+1A==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.0",

View File

@@ -14,8 +14,8 @@
"@fingerprintjs/fingerprintjs": "^4.6.1",
"@firebase/analytics": "^0.10.16",
"@firebase/app": "^0.13.0",
"@firebase/auth": "^1.10.5",
"@firebase/firestore": "^4.7.15",
"@firebase/auth": "^1.10.6",
"@firebase/firestore": "^4.7.16",
"@firebase/messaging": "^0.12.21",
"@jsreport/browser-client": "^3.1.0",
"@reduxjs/toolkit": "^2.8.2",
@@ -24,7 +24,7 @@
"@sentry/vite-plugin": "^3.5.0",
"@splitsoftware/splitio-react": "^2.1.1",
"@tanem/react-nprogress": "^5.0.53",
"antd": "^5.25.2",
"antd": "^5.25.3",
"apollo-link-logger": "^2.0.1",
"apollo-link-sentry": "^4.3.0",
"autosize": "^6.0.1",
@@ -47,6 +47,7 @@
"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",
@@ -58,7 +59,7 @@
"react-drag-listview": "^2.0.0",
"react-grid-gallery": "^1.0.1",
"react-grid-layout": "1.3.4",
"react-i18next": "^15.4.1",
"react-i18next": "^15.5.2",
"react-icons": "^5.5.0",
"react-image-lightbox": "^5.1.4",
"react-markdown": "^10.1.0",
@@ -139,7 +140,7 @@
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@vitejs/plugin-react": "^4.3.4",
"@vitejs/plugin-react": "^4.5.0",
"browserslist": "^4.24.5",
"browserslist-to-esbuild": "^2.1.1",
"chalk": "^5.4.1",

View File

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

View File

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

@@ -2,7 +2,7 @@ import Icon from "@ant-design/icons";
import { Tooltip } from "antd";
import i18n from "i18next";
import dayjs from "../../utils/day";
import { MdDone, MdDoneAll } from "react-icons/md";
import { MdClose, MdDone, MdDoneAll } from "react-icons/md";
import { DateTimeFormatter } from "../../utils/DateFormatter";
export const renderMessage = (messages, index) => {
@@ -31,13 +31,16 @@ export const renderMessage = (messages, index) => {
</Tooltip>
{/* 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>
)}
{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"
/>
</div>
)}
</div>
{/* Outbound message metadata */}
{message.isoutbound && (
<div style={{ fontSize: 10 }}>

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ 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";
@@ -32,7 +33,6 @@ import ShareToTeamsButton from "../share-to-teams/share-to-teams.component.jsx";
import AddToProduction from "./jobs-detail-header-actions.addtoproduction.util";
import 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,17 +1078,26 @@ export function JobsDetailHeaderActions({
menuItems.push({
key: "deletejob",
id: "job-actions-deletejob",
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>
)
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>
)
});
}
@@ -1109,8 +1118,8 @@ export function JobsDetailHeaderActions({
<RbacWrapper action="jobs:void" noauth>
<Popconfirm
title={t("jobs.labels.voidjob")}
okText="Yes"
cancelText="No"
okText={t("general.labels.yes")}
cancelText={t("general.labels.no")}
onClick={(e) => e.stopPropagation()}
onConfirm={handleVoidJob}
>

View File

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

View File

@@ -1,17 +1,20 @@
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 { UPDATE_JOB } from "../../graphql/jobs.queries";
import AuditTrailMapping from "../../utils/AuditTrailMappings.js";
import CurrencyFormatter from "../../utils/CurrencyFormatter";
import { DateTimeFormatter } from "../../utils/DateFormatter";
import { DateTimeFormatter, DateTimeFormatterFunction } from "../../utils/DateFormatter";
import dayjs from "../../utils/day";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import ChatOpenButton from "../chat-open-button/chat-open-button.component";
@@ -24,7 +27,6 @@ import ProductionListColumnComment from "../production-list-columns/production-l
import ProductionListColumnProductionNote from "../production-list-columns/production-list-columns.productionnote.component";
import 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,
@@ -38,6 +40,14 @@ const mapDispatchToProps = (dispatch) => ({
context: context,
modal: "printCenter"
})
),
insertAuditTrail: ({ jobid, operation, type }) =>
dispatch(
insertAuditTrail({
jobid,
operation,
type
})
)
});
@@ -49,7 +59,7 @@ const colSpan = {
xl: { span: 6 }
};
export function JobsDetailHeader({ job, bodyshop, disabled }) {
export function JobsDetailHeader({ job, bodyshop, disabled, insertAuditTrail }) {
const { t } = useTranslation();
const { notification } = useNotification();
const [notesClamped, setNotesClamped] = useState(true);
@@ -66,7 +76,7 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
const handleCheckboxChange = async (field, checked) => {
const value = checked ? dayjs().toISOString() : null;
try {
await updateJob({
const ret = await updateJob({
variables: {
jobId: job.id,
job: { [field]: value }
@@ -74,6 +84,16 @@ export function JobsDetailHeader({ job, bodyshop, disabled }) {
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

@@ -75,7 +75,7 @@ export function PartsOrderBackorderEta({
);
return (
<Popover destroyTooltipOnHide content={popContent} open={visibility} disabled={disabled}>
<Popover destroyOnHidden 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 destroyTooltipOnHide content={popContent} open={visibility} disabled={disabled}>
<Popover destroyOnHidden 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,11 +1,11 @@
import { useQuery } from "@apollo/client";
import { Input, Table } from "antd";
import { useState } from "react";
import { useMemo, 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 { GET_PHONE_NUMBER_OPT_OUTS, SEARCH_OWNERS_BY_PHONE_NUMBERS } from "../../graphql/phone-number-opt-out.queries";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import { TimeAgoFormatter } from "../../utils/DateFormatter";
@@ -20,11 +20,71 @@ const mapDispatchToProps = () => ({});
function PhoneNumberConsentList({ bodyshop, currentUser }) {
const { t } = useTranslation();
const [search, setSearch] = useState("");
const { loading, data } = useQuery(GET_PHONE_NUMBER_OPT_OUTS, {
// Fetch opt-out phone numbers
const { loading: optOutLoading, data: optOutData } = useQuery(GET_PHONE_NUMBER_OPT_OUTS, {
variables: { bodyshopid: bodyshop.id, search: search ? `%${search}%` : undefined },
fetchPolicy: "network-only"
});
// 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"
});
// Format owner names for display
const formatOwnerName = (owner) => {
const parts = [];
if (owner.ownr_fn || owner.ownr_ln) {
parts.push([owner.ownr_fn, owner.ownr_ln].filter(Boolean).join(" "));
}
if (owner.ownr_co_nm) {
parts.push(owner.ownr_co_nm);
}
return parts.join(", ") || "-";
};
// 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"),
@@ -32,6 +92,28 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) {
render: (text) => <PhoneNumberFormatter>{text}</PhoneNumberFormatter>,
sorter: (a, b) => a.phone_number.localeCompare(b.phone_number)
},
{
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}>
{formatOwnerName(owner)} ({owner.phoneField})
</div>
));
},
sorter: (a, b) => {
const aOwners = getAssociatedOwners(a.phone_number);
const bOwners = getAssociatedOwners(b.phone_number);
const aName = aOwners[0] ? formatOwnerName(aOwners[0]) : "";
const bName = bOwners[0] ? formatOwnerName(bOwners[0]) : "";
return aName.localeCompare(bName);
}
},
{
title: t("consent.created_at"),
dataIndex: "created_at",
@@ -50,8 +132,8 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) {
<Table
columns={columns}
dataSource={data?.phone_number_opt_out}
loading={loading}
dataSource={optOutData?.phone_number_opt_out}
loading={optOutLoading || ownersLoading}
rowKey="id"
style={{ marginTop: 16 }}
/>

View File

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

View File

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

View File

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

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 destroyTooltipOnHide content={popContent} open={visibility}>
<Popover destroyOnHidden 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"
destroyTooltipOnHide
destroyOnHidden
>
<Button onClick={(e) => e.preventDefault()}>
<Space>

View File

@@ -26,3 +26,25 @@ export const GET_PHONE_NUMBER_OPT_OUTS = gql`
}
}
`;
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

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

View File

@@ -2032,9 +2032,10 @@
"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,8 +2303,10 @@
"productionlist": "Production Board - List",
"readyjobs": "Ready Jobs",
"recent": "Recent Items",
"remoteassist": "Remote Assist",
"reportcenter": "Report Center",
"rescueme": "Rescue me!",
"rescueme": "Rescue Me!",
"rescuemezoho": "Remote Me In!",
"schedule": "Schedule",
"scoreboard": "Scoreboard",
"search": {
@@ -2379,7 +2382,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 not consented to receive messages."
"no_consent": "This phone number has Opted-out of Messaging."
},
"labels": {
"addlabel": "Add a label to this conversation.",
@@ -2396,7 +2399,7 @@
"sentby": "Sent by {{by}} at {{time}}",
"typeamessage": "Send a message...",
"unarchive": "Unarchive",
"no_consent": "No Consent"
"no_consent": "Opt-out"
},
"render": {
"conversation_list": "Conversation List"
@@ -3869,8 +3872,11 @@
},
"consent": {
"phone_number": "Phone Number",
"status": "Consent Status",
"created_at": "Created At"
"associated_owners": "Associated Owners",
"created_at": "Opt-Out Date",
"no_owners": "No Associated Owners",
"phone_1": "Phone 1",
"phone_2": "Phone 2"
},
"settings": {
"title": "Phone Number Opt-Out List"

View File

@@ -656,6 +656,7 @@
}
},
"labels": {
"consent_settings": "",
"2tiername": "",
"2tiersetup": "",
"2tiersource": "",
@@ -2034,6 +2035,7 @@
"deleteconfirm": "",
"deletedelivery": "",
"deleteintake": "",
"deletewatchers": "",
"deliverchecklist": "",
"difference": "",
"diskscan": "",
@@ -2301,8 +2303,10 @@
"productionlist": "",
"readyjobs": "",
"recent": "",
"remoteassist": "",
"reportcenter": "",
"rescueme": "",
"rescuemezoho": "",
"schedule": "Programar",
"scoreboard": "",
"search": {
@@ -2377,7 +2381,8 @@
"errors": {
"invalidphone": "",
"noattachedjobs": "",
"updatinglabel": ""
"updatinglabel": "",
"no_consent": ""
},
"labels": {
"addlabel": "",
@@ -2393,7 +2398,8 @@
"selectmedia": "",
"sentby": "",
"typeamessage": "Enviar un mensaje...",
"unarchive": ""
"unarchive": "",
"no_consent": ""
},
"render": {
"conversation_list": ""
@@ -2498,7 +2504,8 @@
},
"tooltips": {
"job-watchers": "",
"not-employee": ""
"not-employee": "",
"not-employee-notifications": ""
}
},
"owner": {
@@ -3864,6 +3871,17 @@
"validation": {
"unique_vendor_name": ""
}
},
"consent": {
"phone_number": "",
"associated_owners": "",
"created_at": "",
"no_owners": "",
"phone_1": "",
"phone_2": ""
},
"settings": {
"title": ""
}
}
}

View File

@@ -656,6 +656,7 @@
}
},
"labels": {
"consent_settings": "",
"2tiername": "",
"2tiersetup": "",
"2tiersource": "",
@@ -2034,6 +2035,7 @@
"deleteconfirm": "",
"deletedelivery": "",
"deleteintake": "",
"deletewatchers": "",
"deliverchecklist": "",
"difference": "",
"diskscan": "",
@@ -2301,8 +2303,10 @@
"productionlist": "",
"readyjobs": "",
"recent": "",
"remoteassist": "",
"reportcenter": "",
"rescueme": "",
"rescuemezoho": "",
"schedule": "Programme",
"scoreboard": "",
"search": {
@@ -2377,7 +2381,8 @@
"errors": {
"invalidphone": "",
"noattachedjobs": "",
"updatinglabel": ""
"updatinglabel": "",
"no_consent": ""
},
"labels": {
"addlabel": "",
@@ -2393,7 +2398,8 @@
"selectmedia": "",
"sentby": "",
"typeamessage": "Envoyer un message...",
"unarchive": ""
"unarchive": "",
"no_consent": ""
},
"render": {
"conversation_list": ""
@@ -2498,7 +2504,8 @@
},
"tooltips": {
"job-watchers": "",
"not-employee": ""
"not-employee": "",
"not-employee-notifications": ""
}
},
"owner": {
@@ -3864,6 +3871,17 @@
"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"
},
"settings": {
"title": ""
}
}
}

View File

@@ -1,6 +1,5 @@
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;

287
package-lock.json generated
View File

@@ -9,14 +9,14 @@
"version": "0.2.0",
"license": "UNLICENSED",
"dependencies": {
"@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",
"@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",
"@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.52.0",
"dd-trace": "^5.53.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.5.48",
"moment-timezone": "^0.6.0",
"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.812.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.812.0.tgz",
"integrity": "sha512-SLvqaMwRviAwb+z4XAq2QmlbUjr7rXN6zAEr4/x2ltyrsxEV95gBo0KHeroAsWhd4eD19USjAgg64KJgvUtNGw==",
"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==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
"@aws-sdk/core": "3.812.0",
"@aws-sdk/credential-provider-node": "3.812.0",
"@aws-sdk/core": "3.816.0",
"@aws-sdk/credential-provider-node": "3.817.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.812.0",
"@aws-sdk/middleware-user-agent": "3.816.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.812.0",
"@aws-sdk/util-user-agent-node": "3.816.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.812.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-elasticache/-/client-elasticache-3.812.0.tgz",
"integrity": "sha512-o1KC5Glo3c0T/RN2XBanHu40k3M99MJyq+e/02tIMgEGKIPmnvB8A8muE2F3rQ2A0qLCxvjhm+kprlmDwzpryw==",
"version": "3.817.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-elasticache/-/client-elasticache-3.817.0.tgz",
"integrity": "sha512-TO1Zfv3racKQsRoll4owV2q4kNcw1x64D19KFrWd87rQ517ahbXRcPpaKOqe9CYG1Zo3SIzRySaJLoPftXDfRQ==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
"@aws-sdk/core": "3.812.0",
"@aws-sdk/credential-provider-node": "3.812.0",
"@aws-sdk/core": "3.816.0",
"@aws-sdk/credential-provider-node": "3.817.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.812.0",
"@aws-sdk/middleware-user-agent": "3.816.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.812.0",
"@aws-sdk/util-user-agent-node": "3.816.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.812.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.812.0.tgz",
"integrity": "sha512-kHgw9JDXNPLa/mHtWpOd5btBVXFSe+wwp1Ed9+bqz9uLkv0iV4joZrdQwnydkO8zlTs60Sc5ez+P2OiZ76i2Qg==",
"version": "3.817.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.817.0.tgz",
"integrity": "sha512-nZyjhlLMEXDs0ofWbpikI8tKoeKuuSgYcIb6eEZJk90Nt5HkkXn6nkWOs/kp2FdhpoGJyTILOVsDgdm7eutnLA==",
"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.812.0",
"@aws-sdk/credential-provider-node": "3.812.0",
"@aws-sdk/core": "3.816.0",
"@aws-sdk/credential-provider-node": "3.817.0",
"@aws-sdk/middleware-bucket-endpoint": "3.808.0",
"@aws-sdk/middleware-expect-continue": "3.804.0",
"@aws-sdk/middleware-flexible-checksums": "3.812.0",
"@aws-sdk/middleware-flexible-checksums": "3.816.0",
"@aws-sdk/middleware-host-header": "3.804.0",
"@aws-sdk/middleware-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.812.0",
"@aws-sdk/middleware-sdk-s3": "3.816.0",
"@aws-sdk/middleware-ssec": "3.804.0",
"@aws-sdk/middleware-user-agent": "3.812.0",
"@aws-sdk/middleware-user-agent": "3.816.0",
"@aws-sdk/region-config-resolver": "3.808.0",
"@aws-sdk/signature-v4-multi-region": "3.812.0",
"@aws-sdk/signature-v4-multi-region": "3.816.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.812.0",
"@aws-sdk/util-user-agent-node": "3.816.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.812.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.812.0.tgz",
"integrity": "sha512-RyGzi7kkacjPd0QgVjw6OYvZVvuqtd1wRwG0Aek32dPUYu8eOs9FDaqBsDnNIqdw+lAqC/pKIOPYWtLu2OxE0Q==",
"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==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
"@aws-sdk/core": "3.812.0",
"@aws-sdk/credential-provider-node": "3.812.0",
"@aws-sdk/core": "3.816.0",
"@aws-sdk/credential-provider-node": "3.817.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.812.0",
"@aws-sdk/middleware-user-agent": "3.816.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.812.0",
"@aws-sdk/util-user-agent-node": "3.816.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.812.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.812.0.tgz",
"integrity": "sha512-7JUS2u0AKMYiEmRrxAYQj8ifFwVUgMAHt5H/KjMhh+1El0NqAQDt3JLD4Asmzy7/TvTAWZfk5np2LQPNB2wZpw==",
"version": "3.817.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.817.0.tgz",
"integrity": "sha512-cf2FsdcTT5HiOFOnWk3tzRc84iXcrUNXe4O4KaH75tRToBuQkTaidPI/K9wHnOybNDEkkCcgJo9skv4ftz8qYA==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
"@aws-sdk/core": "3.812.0",
"@aws-sdk/credential-provider-node": "3.812.0",
"@aws-sdk/core": "3.816.0",
"@aws-sdk/credential-provider-node": "3.817.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.812.0",
"@aws-sdk/middleware-user-agent": "3.816.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.812.0",
"@aws-sdk/util-user-agent-node": "3.816.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.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==",
"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==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
"@aws-sdk/core": "3.812.0",
"@aws-sdk/core": "3.816.0",
"@aws-sdk/middleware-host-header": "3.804.0",
"@aws-sdk/middleware-logger": "3.804.0",
"@aws-sdk/middleware-recursion-detection": "3.804.0",
"@aws-sdk/middleware-user-agent": "3.812.0",
"@aws-sdk/middleware-user-agent": "3.816.0",
"@aws-sdk/region-config-resolver": "3.808.0",
"@aws-sdk/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.812.0",
"@aws-sdk/util-user-agent-node": "3.816.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.812.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.812.0.tgz",
"integrity": "sha512-myWA9oHMBVDObKrxG+puAkIGs8igcWInQ1PWCRTS/zN4BkhUMFjjh/JPV/4Vzvtvj5E36iujq2WtlrDLl1PpOw==",
"version": "3.816.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.816.0.tgz",
"integrity": "sha512-Lx50wjtyarzKpMFV6V+gjbSZDgsA/71iyifbClGUSiNPoIQ4OCV0KVOmAAj7mQRVvGJqUMWKVM+WzK79CjbjWA==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/types": "3.804.0",
@@ -657,12 +657,12 @@
}
},
"node_modules/@aws-sdk/credential-provider-env": {
"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==",
"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==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "3.812.0",
"@aws-sdk/core": "3.816.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.812.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.812.0.tgz",
"integrity": "sha512-Vux2U42vPGXeE407Lp6v3yVA65J7hBO9rB67LXshyGVi7VZLAYWc4mrZxNJNqabEkjcDEmMQQakLPT6zc5SvFw==",
"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==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "3.812.0",
"@aws-sdk/core": "3.816.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.812.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.812.0.tgz",
"integrity": "sha512-oltqGvQ488xtPY5wrNjbD+qQYYkuCjn30IDE1qKMxJ58EM6UVTQl3XV44Xq07xfF5gKwVJQkfIyOkRAguOVybg==",
"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==",
"license": "Apache-2.0",
"dependencies": {
"@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/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/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.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==",
"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==",
"license": "Apache-2.0",
"dependencies": {
"@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/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/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.812.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.812.0.tgz",
"integrity": "sha512-YI8bb153XeEOb59F9KtTZEwDAc14s2YHZz58+OFiJ2udnKsPV87mNiFhJPW6ba9nmOLXVat5XDcwtVT1b664wg==",
"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==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "3.812.0",
"@aws-sdk/core": "3.816.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.812.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.812.0.tgz",
"integrity": "sha512-ODsPcNhgiO6GOa82TVNskM97mml9rioe9Cbhemz48lkfDQPv1u06NaCR0o3FsvprX1sEhMvJTR3sE1fyEOzvJQ==",
"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==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/client-sso": "3.812.0",
"@aws-sdk/core": "3.812.0",
"@aws-sdk/token-providers": "3.812.0",
"@aws-sdk/client-sso": "3.817.0",
"@aws-sdk/core": "3.816.0",
"@aws-sdk/token-providers": "3.817.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.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==",
"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==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "3.812.0",
"@aws-sdk/nested-clients": "3.812.0",
"@aws-sdk/core": "3.816.0",
"@aws-sdk/nested-clients": "3.817.0",
"@aws-sdk/types": "3.804.0",
"@smithy/property-provider": "^4.0.2",
"@smithy/types": "^4.2.0",
@@ -794,9 +794,9 @@
}
},
"node_modules/@aws-sdk/lib-storage": {
"version": "3.812.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.812.0.tgz",
"integrity": "sha512-z37ykuXQXfGO7dqQFbEnj1Wu9UwUUXpZhr4iWXsehbIzSqyl5FiCMp0cI5XK8jLVACCfSCssZCz6QD4oDYdKlQ==",
"version": "3.817.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.817.0.tgz",
"integrity": "sha512-2zOO8+2EmiS049PjLSNdqmmZMQj7fzE1hZJ70A94vO+KNaVhVZYuMOOiOmwMw6ePkTCcFwK40vZIIXwEQQ1v1g==",
"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.812.0"
"@aws-sdk/client-s3": "^3.817.0"
}
},
"node_modules/@aws-sdk/middleware-bucket-endpoint": {
@@ -848,15 +848,15 @@
}
},
"node_modules/@aws-sdk/middleware-flexible-checksums": {
"version": "3.812.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.812.0.tgz",
"integrity": "sha512-/ayAooUZvV1GTomNMrfbhjUHAEaz0Wmio3lKyaTJsW4WdLJXBuzdo57YADRmYYUqx6awzJ6VJ6HGc1Uc6tOlbw==",
"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==",
"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.812.0",
"@aws-sdk/core": "3.816.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.812.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.812.0.tgz",
"integrity": "sha512-e8AqRRIaTsunL1hqtO1hksa9oTYdsIbfezHUyVpPGugUIB1lMqPt/DlBsanI85OzUD711UfNSEcZ1mqAxpDOoA==",
"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==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "3.812.0",
"@aws-sdk/core": "3.816.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.812.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.812.0.tgz",
"integrity": "sha512-r+HFwtSvnAs6Fydp4mijylrTX0og9p/xfxOcKsqhMuk3HpZAIcf9sSjRQI6MBusYklg7pnM4sGEnPAZIrdRotA==",
"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==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/core": "3.812.0",
"@aws-sdk/core": "3.816.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.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==",
"version": "3.817.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.817.0.tgz",
"integrity": "sha512-vQ2E06A48STJFssueJQgxYD8lh1iGJoLJnHdshRDWOQb8gy1wVQR+a7MkPGhGR6lGoS0SCnF/Qp6CZhnwLsqsQ==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
"@aws-sdk/core": "3.812.0",
"@aws-sdk/core": "3.816.0",
"@aws-sdk/middleware-host-header": "3.804.0",
"@aws-sdk/middleware-logger": "3.804.0",
"@aws-sdk/middleware-recursion-detection": "3.804.0",
"@aws-sdk/middleware-user-agent": "3.812.0",
"@aws-sdk/middleware-user-agent": "3.816.0",
"@aws-sdk/region-config-resolver": "3.808.0",
"@aws-sdk/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.812.0",
"@aws-sdk/util-user-agent-node": "3.816.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.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==",
"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==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/signature-v4-multi-region": "3.812.0",
"@aws-sdk/signature-v4-multi-region": "3.816.0",
"@aws-sdk/types": "3.804.0",
"@aws-sdk/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.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==",
"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==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/middleware-sdk-s3": "3.812.0",
"@aws-sdk/middleware-sdk-s3": "3.816.0",
"@aws-sdk/types": "3.804.0",
"@smithy/protocol-http": "^5.1.0",
"@smithy/signature-v4": "^5.1.0",
@@ -1089,12 +1089,13 @@
}
},
"node_modules/@aws-sdk/token-providers": {
"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==",
"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==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/nested-clients": "3.812.0",
"@aws-sdk/core": "3.816.0",
"@aws-sdk/nested-clients": "3.817.0",
"@aws-sdk/types": "3.804.0",
"@smithy/property-provider": "^4.0.2",
"@smithy/shared-ini-file-loader": "^4.0.2",
@@ -1185,12 +1186,12 @@
}
},
"node_modules/@aws-sdk/util-user-agent-node": {
"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==",
"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==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/middleware-user-agent": "3.812.0",
"@aws-sdk/middleware-user-agent": "3.816.0",
"@aws-sdk/types": "3.804.0",
"@smithy/node-config-provider": "^4.1.1",
"@smithy/types": "^4.2.0",
@@ -5473,9 +5474,9 @@
}
},
"node_modules/dd-trace": {
"version": "5.52.0",
"resolved": "https://registry.npmjs.org/dd-trace/-/dd-trace-5.52.0.tgz",
"integrity": "sha512-ZF+OWLMcgVUWJEAIYIl76LocgnbbkPJ6WgJCG1fhLk4UCsUvoHRvBx9qlexbytL0jkktk1pvzODcjL0wyxLAOQ==",
"version": "5.53.0",
"resolved": "https://registry.npmjs.org/dd-trace/-/dd-trace-5.53.0.tgz",
"integrity": "sha512-ayraB+H05yAag5Ia70YwNkkAS4q0O/Bx1suijTUaYBXirTVlfK9CDSpZRf0Rcjk2uRqf8ANNNsws1fesP4cRmQ==",
"hasInstallScript": true,
"license": "(Apache-2.0 OR BSD-3-Clause)",
"dependencies": {
@@ -9034,9 +9035,9 @@
}
},
"node_modules/moment-timezone": {
"version": "0.5.48",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
"integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.6.0.tgz",
"integrity": "sha512-ldA5lRNm3iJCWZcBCab4pnNL3HSZYXVb/3TYr75/1WCTWYuTqYUb5f/S384pncYjJ88lbO8Z4uPDvmoluHJc8Q==",
"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.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",
"@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",
"@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.52.0",
"dd-trace": "^5.53.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.5.48",
"moment-timezone": "^0.6.0",
"multer": "^1.4.5-lts.1",
"node-persist": "^4.0.4",
"nodemailer": "^6.10.0",

View File

@@ -282,6 +282,7 @@ 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

@@ -2980,4 +2980,59 @@ 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,7 +3,10 @@ const {
FIND_BODYSHOP_BY_MESSAGING_SERVICE_SID,
UNARCHIVE_CONVERSATION,
CREATE_CONVERSATION,
INSERT_MESSAGE
INSERT_MESSAGE,
CHECK_PHONE_NUMBER_OPT_OUT,
DELETE_PHONE_NUMBER_OPT_OUT,
INSERT_PHONE_NUMBER_OPT_OUT
} = require("../graphql-client/queries");
const { phone } = require("phone");
const { admin } = require("../firebase/firebase-handler");
@@ -51,8 +54,81 @@ 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 4: Process conversation
// Step 2: Check for opt-in or opt-out keywords
const optInKeywords = ["START", "YES", "UNSTOP"];
const optOutKeywords = ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT"];
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
});
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
});
// Emit WebSocket event to notify clients
const broadcastRoom = getBodyshopRoom(bodyshop.id);
ioRedis.to(broadcastRoom).emit("phone-number-opted-in", {
bodyshopid: bodyshop.id,
phone_number: normalizedPhone
});
}
} 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
});
// Emit WebSocket event to notify clients
const broadcastRoom = getBodyshopRoom(bodyshop.id);
ioRedis.to(broadcastRoom).emit("phone-number-opted-out", {
bodyshopid: bodyshop.id,
phone_number: normalizedPhone
});
}
}
// Respond immediately without processing as a regular message
res.status(200).send("");
return;
}
// Step 3: 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]
@@ -90,7 +166,7 @@ const receive = async (req, res) => {
newMessage.conversationid = conversationid;
// Step 5: Insert the message
// Step 4: Insert the message
const insertresp = await client.request(INSERT_MESSAGE, {
msg: newMessage,
conversationid
@@ -103,7 +179,7 @@ const receive = async (req, res) => {
throw new Error("Conversation data is missing from the response.");
}
// Step 6: Notify clients
// Step 5: Notify clients
const conversationRoom = getBodyshopConversationRoom({
bodyshopId: conversation.bodyshop.id,
conversationId: conversation.id
@@ -133,7 +209,7 @@ const receive = async (req, res) => {
summary: false
});
// Step 7: Send FCM notification
// Step 6: Send FCM notification
const fcmresp = await admin.messaging().send({
topic: `${message.conversation.bodyshop.imexshopid}-messaging`,
notification: {

View File

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