feature/IO-3182-Phone-Number-Consent - Front/Back Start/stop logic complete
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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");
|
||||
};
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -650,7 +650,7 @@ function Header({
|
||||
icon: <OneToOneOutlined />,
|
||||
label: t("menus.header.remoteassist"),
|
||||
children: [
|
||||
...(InstanceRenderManager({ imex: true, rome: true })
|
||||
...(InstanceRenderManager({ imex: true, rome: false })
|
||||
? [
|
||||
{
|
||||
key: "rescue",
|
||||
@@ -662,7 +662,7 @@ function Header({
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: "rescue",
|
||||
key: "rescue-zoho",
|
||||
id: "header-rescue-zoho",
|
||||
icon: <UsergroupAddOutlined />,
|
||||
label: t("menus.header.rescuemezoho"),
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -72,7 +72,7 @@ export default function JobsCreateVehicleInfoPredefined({ disabled, form }) {
|
||||
open={open}
|
||||
placement="left"
|
||||
onOpenChange={handleOpenChange}
|
||||
destroyTooltipOnHide
|
||||
destroyOnHidden
|
||||
>
|
||||
<SearchOutlined style={{ cursor: "pointer" }} />
|
||||
</Popover>
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -107,7 +107,7 @@ export default function TimeTicketCalculatorComponent({
|
||||
open={visible}
|
||||
onOpenChange={handleOpenChange}
|
||||
placement="right"
|
||||
destroyTooltipOnHide
|
||||
destroyOnHidden
|
||||
>
|
||||
<Button onClick={(e) => e.preventDefault()}>
|
||||
<Space>
|
||||
|
||||
Reference in New Issue
Block a user