diff --git a/client/src/components/ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component.jsx b/client/src/components/ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component.jsx index 6d296edd1..c48875f4c 100644 --- a/client/src/components/ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component.jsx +++ b/client/src/components/ca-bc-pvrt-calculator/ca-bc-pvrt-calculator.component.jsx @@ -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 ( - + diff --git a/client/src/components/chat-affix/registerMessagingSocketHandlers.js b/client/src/components/chat-affix/registerMessagingSocketHandlers.js index f2d9d1ef9..88e0ac9df 100644 --- a/client/src/components/chat-affix/registerMessagingSocketHandlers.js +++ b/client/src/components/chat-affix/registerMessagingSocketHandlers.js @@ -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"); }; diff --git a/client/src/components/chat-messages-list/renderMessage.jsx b/client/src/components/chat-messages-list/renderMessage.jsx index b94e69ee0..c572d77e3 100644 --- a/client/src/components/chat-messages-list/renderMessage.jsx +++ b/client/src/components/chat-messages-list/renderMessage.jsx @@ -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) => { {/* Message status icons */} - {message.status && (message.status === "sent" || message.status === "delivered") && ( -
- -
- )} + {message.status && + (message.status === "sent" || message.status === "delivered" || message.status === "failed") && ( +
+ +
+ )} - {/* Outbound message metadata */} {message.isoutbound && (
diff --git a/client/src/components/chat-send-message/chat-send-message.component.jsx b/client/src/components/chat-send-message/chat-send-message.component.jsx index 798532a3e..c3e91b2b1 100644 --- a/client/src/components/chat-send-message/chat-send-message.component.jsx +++ b/client/src/components/chat-send-message/chat-send-message.component.jsx @@ -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 ( -
- {isOptedOut && } - - - - setMessage(e.target.value)} - onPressEnter={(event) => { - event.preventDefault(); - if (!event.shiftKey && !isOptedOut) handleEnter(); - }} - /> - - - + {isOptedOut && } +
+ {!isOptedOut && ( + <> + + + + )} + + setMessage(e.target.value)} + onPressEnter={(event) => { + event.preventDefault(); + if (!event.shiftKey && !isOptedOut) handleEnter(); }} - spin /> - } - /> -
+ + {!isOptedOut && ( + + )} + + + } + /> +
+ ); } diff --git a/client/src/components/header/header.component.jsx b/client/src/components/header/header.component.jsx index d65f78496..826edea54 100644 --- a/client/src/components/header/header.component.jsx +++ b/client/src/components/header/header.component.jsx @@ -650,7 +650,7 @@ function Header({ icon: , 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: , label: t("menus.header.rescuemezoho"), diff --git a/client/src/components/job-employee-assignments/job-employee-assignments.component.jsx b/client/src/components/job-employee-assignments/job-employee-assignments.component.jsx index 85e037e86..680428a57 100644 --- a/client/src/components/job-employee-assignments/job-employee-assignments.component.jsx +++ b/client/src/components/job-employee-assignments/job-employee-assignments.component.jsx @@ -80,7 +80,7 @@ export function JobEmployeeAssignments({ ); return ( - + {body ? ( diff --git a/client/src/components/jobs-create-vehicle-info/jobs-create-vehicle-info.predefined.component.jsx b/client/src/components/jobs-create-vehicle-info/jobs-create-vehicle-info.predefined.component.jsx index 338e873dd..5f0c3a2dc 100644 --- a/client/src/components/jobs-create-vehicle-info/jobs-create-vehicle-info.predefined.component.jsx +++ b/client/src/components/jobs-create-vehicle-info/jobs-create-vehicle-info.predefined.component.jsx @@ -72,7 +72,7 @@ export default function JobsCreateVehicleInfoPredefined({ disabled, form }) { open={open} placement="left" onOpenChange={handleOpenChange} - destroyTooltipOnHide + destroyOnHidden > diff --git a/client/src/components/parts-order-backorder-eta/parts-order-backorder-eta.component.jsx b/client/src/components/parts-order-backorder-eta/parts-order-backorder-eta.component.jsx index d8895803b..7fd2cf2e4 100644 --- a/client/src/components/parts-order-backorder-eta/parts-order-backorder-eta.component.jsx +++ b/client/src/components/parts-order-backorder-eta/parts-order-backorder-eta.component.jsx @@ -75,7 +75,7 @@ export function PartsOrderBackorderEta({ ); return ( - + {backordered_eta} {isAlreadyBackordered && } {loading && } diff --git a/client/src/components/parts-order-line-backorder-button/parts-order-line-backorder-button.component.jsx b/client/src/components/parts-order-line-backorder-button/parts-order-line-backorder-button.component.jsx index 26910643c..e38b30ecb 100644 --- a/client/src/components/parts-order-line-backorder-button/parts-order-line-backorder-button.component.jsx +++ b/client/src/components/parts-order-line-backorder-button/parts-order-line-backorder-button.component.jsx @@ -84,7 +84,7 @@ export function PartsOrderLineBackorderButton({ partsOrderStatus, partsLineId, j ); return ( - + diff --git a/client/src/components/phone-number-consent/phone-number-consent.component.jsx b/client/src/components/phone-number-consent/phone-number-consent.component.jsx index 7981a3db2..0df53ca30 100644 --- a/client/src/components/phone-number-consent/phone-number-consent.component.jsx +++ b/client/src/components/phone-number-consent/phone-number-consent.component.jsx @@ -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) => {text}, 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) => ( +
+ {formatOwnerName(owner)} ({owner.phoneField}) +
+ )); + }, + 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 }) { diff --git a/client/src/components/production-list-columns/production-list-columns.empassignment.component.jsx b/client/src/components/production-list-columns/production-list-columns.empassignment.component.jsx index 3e7cdbdcd..e326a455f 100644 --- a/client/src/components/production-list-columns/production-list-columns.empassignment.component.jsx +++ b/client/src/components/production-list-columns/production-list-columns.empassignment.component.jsx @@ -140,7 +140,7 @@ export function ProductionListEmpAssignment({ insertAuditTrail, bodyshop, record if (record[type]) theEmployee = bodyshop.employees.find((e) => e.id === record[type]); return ( - + {record[type] ? (
diff --git a/client/src/components/time-ticket-calculator/time-ticket-calculator.component.jsx b/client/src/components/time-ticket-calculator/time-ticket-calculator.component.jsx index c5b174f26..be74b1f99 100644 --- a/client/src/components/time-ticket-calculator/time-ticket-calculator.component.jsx +++ b/client/src/components/time-ticket-calculator/time-ticket-calculator.component.jsx @@ -107,7 +107,7 @@ export default function TimeTicketCalculatorComponent({ open={visible} onOpenChange={handleOpenChange} placement="right" - destroyTooltipOnHide + destroyOnHidden >