feature/IO-3182-Phone-Number-Consent - Checkpoint
This commit is contained in:
@@ -8,13 +8,15 @@ import ChatPopupComponent from "../chat-popup/chat-popup.component";
|
||||
import "./chat-affix.styles.scss";
|
||||
import { registerMessagingHandlers, unregisterMessagingHandlers } from "./registerMessagingSocketHandlers";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
import { GET_PHONE_NUMBER_CONSENT } from "../../graphql/consent.queries.js";
|
||||
import { GET_PHONE_NUMBER_CONSENT } from "../../graphql/consent.queries";
|
||||
|
||||
export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
||||
const { t } = useTranslation();
|
||||
const client = useApolloClient();
|
||||
const { socket } = useSocket();
|
||||
|
||||
const enforceConsent = bodyshop?.enforce_sms_consent ?? false;
|
||||
|
||||
useEffect(() => {
|
||||
if (!bodyshop || !bodyshop.messagingservicesid) return;
|
||||
|
||||
@@ -39,45 +41,52 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
||||
if (socket && socket.connected) {
|
||||
registerMessagingHandlers({ socket, client });
|
||||
|
||||
// Handle consent-changed events
|
||||
const handleConsentChanged = ({ bodyshopId, phone_number, consent_status }) => {
|
||||
// Handle consent-changed events only if enforce_sms_consent is true
|
||||
const handleConsentChanged = ({ bodyshopId, phone_number, consent_status, reason }) => {
|
||||
if (!enforceConsent || bodyshopId !== bodyshop.id) return;
|
||||
|
||||
try {
|
||||
client.cache.writeQuery(
|
||||
const cacheData = client.readQuery({
|
||||
query: GET_PHONE_NUMBER_CONSENT,
|
||||
variables: { bodyshopid: bodyshopId, phone_number }
|
||||
});
|
||||
|
||||
if (!cacheData?.phone_number_consent?.[0]) {
|
||||
console.warn("No cached data for GET_PHONE_NUMBER_CONSENT:", { bodyshopId, phone_number });
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedConsent = {
|
||||
...cacheData.phone_number_consent[0],
|
||||
consent_status,
|
||||
consent_updated_at: new Date().toISOString(),
|
||||
phone_number_consent_history: [
|
||||
{
|
||||
__typename: "phone_number_consent_history",
|
||||
id: `temp-${Date.now()}`,
|
||||
reason,
|
||||
changed_at: new Date().toISOString(),
|
||||
old_value: cacheData.phone_number_consent[0].consent_status,
|
||||
new_value: consent_status,
|
||||
changed_by: "system"
|
||||
},
|
||||
...(cacheData.phone_number_consent[0].phone_number_consent_history || [])
|
||||
]
|
||||
};
|
||||
|
||||
client.writeQuery(
|
||||
{
|
||||
query: GET_PHONE_NUMBER_CONSENT,
|
||||
variables: { bodyshopid: bodyshopId, phone_number }
|
||||
},
|
||||
(data) => {
|
||||
if (!data?.phone_number_consent?.[0]) {
|
||||
return {
|
||||
phone_number_consent: [
|
||||
{
|
||||
__typename: "phone_number_consent",
|
||||
id: null,
|
||||
bodyshopid: bodyshopId,
|
||||
phone_number,
|
||||
consent_status,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
consent_updated_at: new Date().toISOString(),
|
||||
history: []
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
return {
|
||||
phone_number_consent: [
|
||||
{
|
||||
...data.phone_number_consent[0],
|
||||
consent_status,
|
||||
consent_updated_at: new Date().toISOString()
|
||||
}
|
||||
]
|
||||
};
|
||||
{
|
||||
phone_number_consent: [updatedConsent]
|
||||
}
|
||||
);
|
||||
|
||||
console.log("Cache update in handleConsentChanged:", { phone_number, consent_status, updatedConsent });
|
||||
} catch (error) {
|
||||
console.error("Error updating consent cache:", error);
|
||||
console.error("Error updating consent cache in handleConsentChanged:", error.message, error.stack);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -88,7 +97,7 @@ export function ChatAffixContainer({ bodyshop, chatVisible }) {
|
||||
unregisterMessagingHandlers({ socket });
|
||||
};
|
||||
}
|
||||
}, [bodyshop, socket, t, client]);
|
||||
}, [bodyshop, socket, t, client, enforceConsent]);
|
||||
|
||||
if (!bodyshop || !bodyshop.messagingservicesid) return <></>;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Badge, Card, List, Space, Tag } from "antd";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
@@ -11,35 +11,37 @@ import { OwnerNameDisplayFunction } from "../owner-name-display/owner-name-displ
|
||||
import _ from "lodash";
|
||||
import "./chat-conversation-list.styles.scss";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { GET_PHONE_NUMBER_CONSENTS } from "../../graphql/consent.queries.js";
|
||||
import { GET_PHONE_NUMBER_CONSENTS } from "../../graphql/consent.queries";
|
||||
import { phone } from "phone";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
selectedConversation: selectSelectedConversation
|
||||
selectedConversation: selectSelectedConversation,
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
setSelectedConversation: (conversationId) => dispatch(setSelectedConversation(conversationId))
|
||||
});
|
||||
|
||||
function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation }) {
|
||||
function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation, bodyshop }) {
|
||||
const { t } = useTranslation();
|
||||
const [, forceUpdate] = useState(false);
|
||||
|
||||
// Normalize phone numbers and fetch consent statuses
|
||||
const enforceConsent = bodyshop?.enforce_sms_consent ?? false;
|
||||
|
||||
const phoneNumbers = conversationList.map((item) => phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, ""));
|
||||
const { data: consentData, loading: consentLoading } = useQuery(GET_PHONE_NUMBER_CONSENTS, {
|
||||
variables: {
|
||||
bodyshopid: conversationList[0]?.bodyshopid,
|
||||
phone_numbers: phoneNumbers
|
||||
},
|
||||
skip: !conversationList.length || !conversationList[0]?.bodyshopid,
|
||||
skip: !enforceConsent || !conversationList.length || !conversationList[0]?.bodyshopid,
|
||||
fetchPolicy: "cache-and-network"
|
||||
});
|
||||
|
||||
// Create a map of phone number to consent status
|
||||
const consentMap = React.useMemo(() => {
|
||||
const consentMap = useMemo(() => {
|
||||
const map = new Map();
|
||||
consentData?.phone_number_consent?.forEach((consent) => {
|
||||
map.set(consent.phone_number, consent.consent_status);
|
||||
@@ -54,14 +56,14 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const sortedConversationList = React.useMemo(() => {
|
||||
const sortedConversationList = useMemo(() => {
|
||||
return _.orderBy(conversationList, ["updated_at"], ["desc"]);
|
||||
}, [conversationList]);
|
||||
|
||||
const renderConversation = (index, t) => {
|
||||
const item = sortedConversationList[index];
|
||||
const normalizedPhone = phone(item.phone_num, "CA").phoneNumber.replace(/^\+1/, "");
|
||||
const isConsented = consentMap.get(normalizedPhone) ?? false;
|
||||
const isConsented = enforceConsent ? (consentMap.get(normalizedPhone) ?? false) : true;
|
||||
|
||||
const cardContentRight = <TimeAgoFormatter>{item.updated_at}</TimeAgoFormatter>;
|
||||
const cardContentLeft =
|
||||
@@ -87,7 +89,7 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
|
||||
const cardExtra = (
|
||||
<>
|
||||
<Badge count={item.messages_aggregate.aggregate.count} />
|
||||
{!isConsented && <Tag color="red">{t("messaging.labels.no_consent")}</Tag>}
|
||||
{enforceConsent && !isConsented && <Tag color="red">{t("messaging.labels.no_consent")}</Tag>}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -103,8 +105,24 @@ function ChatConversationListComponent({ conversationList, selectedConversation,
|
||||
className={`chat-list-item ${item.id === selectedConversation ? "chat-list-selected-conversation" : ""}`}
|
||||
>
|
||||
<Card style={getCardStyle()} variant={true} size="small" extra={cardExtra} title={cardTitle}>
|
||||
<div style={{ display: "inline-block", width: "70%", textAlign: "left" }}>{cardContentLeft}</div>
|
||||
<div style={{ display: "inline-block", width: "30%", textAlign: "right" }}>{cardContentRight}</div>
|
||||
<div
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width: "70%",
|
||||
textAlign: "left"
|
||||
}}
|
||||
>
|
||||
{cardContentLeft}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width: "30%",
|
||||
textAlign: "right"
|
||||
}}
|
||||
>
|
||||
{cardContentRight}
|
||||
</div>
|
||||
</Card>
|
||||
</List.Item>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LoadingOutlined, SendOutlined } from "@ant-design/icons";
|
||||
import { Input, Spin } from "antd";
|
||||
import { Input, Spin, Alert } from "antd";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
@@ -31,12 +31,15 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
|
||||
const [selectedMedia, setSelectedMedia] = useState([]);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const enforceConsent = bodyshop?.enforce_sms_consent ?? false;
|
||||
|
||||
const normalizedPhone = phone(conversation.phone_num, "CA").phoneNumber.replace(/^\+1/, "");
|
||||
const { data: consentData } = useQuery(GET_PHONE_NUMBER_CONSENT, {
|
||||
variables: { bodyshopid: bodyshop.id, phone_number: normalizedPhone },
|
||||
fetchPolicy: "cache-and-network"
|
||||
fetchPolicy: "cache-and-network",
|
||||
skip: !enforceConsent
|
||||
});
|
||||
const isConsented = consentData?.phone_number_consent?.[0]?.consent_status ?? false;
|
||||
const isConsented = enforceConsent ? (consentData?.phone_number_consent?.[0]?.consent_status ?? false) : true;
|
||||
|
||||
useEffect(() => {
|
||||
inputArea.current.focus();
|
||||
@@ -69,8 +72,8 @@ function ChatSendMessageComponent({ conversation, bodyshop, sendMessage, isSendi
|
||||
|
||||
return (
|
||||
<div className="imex-flex-row" style={{ width: "100%" }}>
|
||||
{!isConsented && (
|
||||
<AlertComponent message={t("messaging.errors.no_consent")} type="warning" style={{ marginBottom: 8 }} />
|
||||
{enforceConsent && !isConsented && (
|
||||
<Alert message={t("messaging.errors.no_consent")} type="warning" style={{ marginBottom: 8 }} />
|
||||
)}
|
||||
<ChatPresetsComponent className="imex-flex-row__margin" />
|
||||
<ChatMediaSelector
|
||||
|
||||
@@ -1,73 +1,260 @@
|
||||
import { useMutation, useQuery } from "@apollo/client";
|
||||
import { useQuery, useApolloClient } from "@apollo/client";
|
||||
import { Table, Switch, Input, Tooltip, Upload, Button } from "antd";
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import {
|
||||
GET_PHONE_NUMBER_CONSENTS,
|
||||
SET_PHONE_NUMBER_CONSENT,
|
||||
BULK_SET_PHONE_NUMBER_CONSENT
|
||||
} from "../../graphql/consent.queries.js";
|
||||
import { selectBodyshop, selectCurrentUser } from "../../redux/user/user.selectors";
|
||||
import { GET_PHONE_NUMBER_CONSENTS } from "../../graphql/consent.queries";
|
||||
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
|
||||
import { TimeAgoFormatter } from "../../utils/DateFormatter";
|
||||
import { UploadOutlined } from "@ant-design/icons";
|
||||
import { phone } from "phone";
|
||||
import axios from "axios";
|
||||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||||
import { useNotification } from "../../contexts/Notifications/notificationContext.jsx";
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
bodyshop: selectBodyshop,
|
||||
currentUser: selectCurrentUser
|
||||
});
|
||||
|
||||
const mapDispatchToProps = () => ({});
|
||||
|
||||
function PhoneNumberConsentList({ bodyshop }) {
|
||||
function PhoneNumberConsentList({ bodyshop, currentUser }) {
|
||||
const { t } = useTranslation();
|
||||
const [search, setSearch] = useState("");
|
||||
const { loading, data } = useQuery(GET_PHONE_NUMBER_CONSENTS, {
|
||||
variables: { bodyshopid: bodyshop.id, search },
|
||||
const notification = useNotification();
|
||||
const { loading, data, refetch } = useQuery(GET_PHONE_NUMBER_CONSENTS, {
|
||||
variables: { bodyshopid: bodyshop.id, search: search ? `%${search}%` : undefined },
|
||||
fetchPolicy: "network-only"
|
||||
});
|
||||
const [setConsent] = useMutation(SET_PHONE_NUMBER_CONSENT);
|
||||
const [bulkSetConsent] = useMutation(BULK_SET_PHONE_NUMBER_CONSENT);
|
||||
const client = useApolloClient();
|
||||
const { socket } = useSocket();
|
||||
|
||||
useEffect(() => {
|
||||
if (!socket || !socket.connected) return;
|
||||
|
||||
const handleConsentChanged = ({ bodyshopId, phone_number, consent_status, reason }) => {
|
||||
if (bodyshopId !== bodyshop.id) return;
|
||||
|
||||
try {
|
||||
const cacheData = client.readQuery({
|
||||
query: GET_PHONE_NUMBER_CONSENTS,
|
||||
variables: { bodyshopid: bodyshop.id, search: search ? `%${search}%` : undefined }
|
||||
});
|
||||
|
||||
if (!cacheData?.phone_number_consent) {
|
||||
console.warn("No cached data for GET_PHONE_NUMBER_CONSENTS in WebSocket handler");
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedConsents = cacheData.phone_number_consent.map((consent) =>
|
||||
consent.phone_number === phone_number
|
||||
? {
|
||||
...consent,
|
||||
consent_status,
|
||||
consent_updated_at: new Date().toISOString(),
|
||||
phone_number_consent_history: [
|
||||
{
|
||||
__typename: "phone_number_consent_history",
|
||||
id: `temp-${Date.now()}`,
|
||||
reason,
|
||||
changed_at: new Date().toISOString(),
|
||||
old_value: consent.consent_status,
|
||||
new_value: consent_status,
|
||||
changed_by: currentUser.email
|
||||
},
|
||||
...(consent.phone_number_consent_history || [])
|
||||
]
|
||||
}
|
||||
: consent
|
||||
);
|
||||
|
||||
client.writeQuery(
|
||||
{
|
||||
query: GET_PHONE_NUMBER_CONSENTS,
|
||||
variables: { bodyshopid: bodyshop.id, search: search ? `%${search}%` : undefined }
|
||||
},
|
||||
{
|
||||
phone_number_consent: updatedConsents
|
||||
}
|
||||
);
|
||||
|
||||
console.log("WebSocket cache update:", { phone_number, consent_status, updatedConsents });
|
||||
} catch (error) {
|
||||
console.error("Error updating consent cache (WebSocket):", error.message, error.stack);
|
||||
}
|
||||
};
|
||||
|
||||
socket.on("consent-changed", handleConsentChanged);
|
||||
|
||||
return () => {
|
||||
socket.off("consent-changed", handleConsentChanged);
|
||||
};
|
||||
}, [socket, client, bodyshop.id, search, currentUser.email]);
|
||||
|
||||
const handleSetConsent = async (phone_number, consent_status) => {
|
||||
try {
|
||||
const response = await axios.post("/sms/setConsent", {
|
||||
bodyshopid: bodyshop.id,
|
||||
phone_number,
|
||||
consent_status,
|
||||
reason: "Manual override in app",
|
||||
changed_by: currentUser.email
|
||||
});
|
||||
|
||||
const updatedConsent = {
|
||||
...response.data.consent,
|
||||
phone_number_consent_history: response.data.consent.phone_number_consent_history.map((history) => ({
|
||||
...history,
|
||||
__typename: "phone_number_consent_history"
|
||||
}))
|
||||
};
|
||||
|
||||
// Update Apollo cache
|
||||
const cacheData = client.readQuery({
|
||||
query: GET_PHONE_NUMBER_CONSENTS,
|
||||
variables: { bodyshopid: bodyshop.id, search: search ? `%${search}%` : undefined }
|
||||
});
|
||||
|
||||
let cacheUpdated = false;
|
||||
if (cacheData?.phone_number_consent) {
|
||||
const isPhoneNumberInCache = cacheData.phone_number_consent.some(
|
||||
(consent) => consent.phone_number === phone_number
|
||||
);
|
||||
|
||||
const updatedConsents = isPhoneNumberInCache
|
||||
? cacheData.phone_number_consent.map((consent) =>
|
||||
consent.phone_number === phone_number ? updatedConsent : consent
|
||||
)
|
||||
: [...cacheData.phone_number_consent, updatedConsent];
|
||||
|
||||
cacheUpdated = client.writeQuery(
|
||||
{
|
||||
query: GET_PHONE_NUMBER_CONSENTS,
|
||||
variables: { bodyshopid: bodyshop.id, search: search ? `%${search}%` : undefined }
|
||||
},
|
||||
{
|
||||
phone_number_consent: updatedConsents
|
||||
}
|
||||
);
|
||||
|
||||
console.log("Cache update in handleSetConsent:", {
|
||||
phone_number,
|
||||
consent_status,
|
||||
updatedConsents,
|
||||
search
|
||||
});
|
||||
} else {
|
||||
console.warn("No cached data for GET_PHONE_NUMBER_CONSENTS in handleSetConsent");
|
||||
}
|
||||
|
||||
// Always refetch to ensure UI updates
|
||||
await refetch();
|
||||
|
||||
notification.success({
|
||||
message: t("consent.update_success")
|
||||
});
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
message: t("consent.update_failed")
|
||||
});
|
||||
console.error("Error updating consent:", error.message, error.stack);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBulkUpload = async (file) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
const text = e.target.result;
|
||||
const lines = text.split("\n").slice(1); // Skip header
|
||||
const consents = lines
|
||||
.filter((line) => line.trim())
|
||||
.map((line) => {
|
||||
const [phone_number, consent_status] = line.split(",");
|
||||
return {
|
||||
phone_number: phone(phone_number, "CA").phoneNumber.replace(/^\+1/, ""),
|
||||
consent_status: consent_status.trim().toLowerCase() === "true"
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await axios.post("/sms/bulkSetConsent", {
|
||||
bodyshopid: bodyshop.id,
|
||||
consents
|
||||
});
|
||||
|
||||
const updatedConsents = response.data.consents.map((consent) => ({
|
||||
...consent,
|
||||
phone_number_consent_history: consent.phone_number_consent_history.map((history) => ({
|
||||
...history,
|
||||
__typename: "phone_number_consent_history"
|
||||
}))
|
||||
}));
|
||||
|
||||
// Update Apollo cache
|
||||
const cacheData = client.readQuery({
|
||||
query: GET_PHONE_NUMBER_CONSENTS,
|
||||
variables: { bodyshopid: bodyshop.id, search: search ? `%${search}%` : undefined }
|
||||
});
|
||||
|
||||
if (cacheData?.phone_number_consent) {
|
||||
const updatedConsentsMap = new Map(updatedConsents.map((consent) => [consent.phone_number, consent]));
|
||||
|
||||
const mergedConsents = cacheData.phone_number_consent.map((consent) =>
|
||||
updatedConsentsMap.has(consent.phone_number) ? updatedConsentsMap.get(consent.phone_number) : consent
|
||||
);
|
||||
|
||||
updatedConsents.forEach((consent) => {
|
||||
if (!mergedConsents.some((c) => c.phone_number === consent.phone_number)) {
|
||||
mergedConsents.push(consent);
|
||||
}
|
||||
});
|
||||
|
||||
client.writeQuery(
|
||||
{
|
||||
query: GET_PHONE_NUMBER_CONSENTS,
|
||||
variables: { bodyshopid: bodyshop.id, search: search ? `%${search}%` : undefined }
|
||||
},
|
||||
{
|
||||
phone_number_consent: mergedConsents
|
||||
}
|
||||
);
|
||||
|
||||
console.log("Cache update in handleBulkUpload:", { updatedConsents, mergedConsents });
|
||||
} else {
|
||||
console.warn("No cached data for GET_PHONE_NUMBER_CONSENTS in handleBulkUpload");
|
||||
}
|
||||
|
||||
// Refetch to ensure UI updates
|
||||
await refetch();
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
message: t("consent.bulk_update_failed")
|
||||
});
|
||||
console.error("Bulk upload failed:", error.message, error.stack);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
return false;
|
||||
};
|
||||
|
||||
if (!bodyshop?.enforce_sms_consent) return null;
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t("consent.phone_number"),
|
||||
dataIndex: "phone_number",
|
||||
render: (text) => <PhoneNumberFormatter>{text}</PhoneNumberFormatter>
|
||||
render: (text) => <PhoneNumberFormatter>{text}</PhoneNumberFormatter>,
|
||||
sorter: (a, b) => a.phone_number.localeCompare(b.phone_number)
|
||||
},
|
||||
{
|
||||
title: t("consent.status"),
|
||||
dataIndex: "consent_status",
|
||||
render: (status, record) => (
|
||||
<Tooltip title={record.history?.[0]?.reason || "No audit history"}>
|
||||
<Switch
|
||||
checked={status}
|
||||
onChange={(checked) =>
|
||||
setConsent({
|
||||
variables: {
|
||||
bodyshopid: bodyshop.id,
|
||||
phone_number: record.phone_number,
|
||||
consent_status: checked,
|
||||
reason: "Manual override in app",
|
||||
changed_by: "user" // Replace with actual user email from context
|
||||
},
|
||||
optimisticResponse: {
|
||||
insert_phone_number_consent_one: {
|
||||
__typename: "phone_number_consent",
|
||||
id: record.id,
|
||||
bodyshopid: bodyshop.id,
|
||||
phone_number: record.phone_number,
|
||||
consent_status: checked,
|
||||
created_at: record.created_at,
|
||||
updated_at: new Date().toISOString(),
|
||||
consent_updated_at: new Date().toISOString()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Tooltip title={record.phone_number_consent_history?.[0]?.reason || "No audit history"}>
|
||||
<Switch checked={status} onChange={(checked) => handleSetConsent(record.phone_number, checked)} />
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
@@ -78,35 +265,6 @@ function PhoneNumberConsentList({ bodyshop }) {
|
||||
}
|
||||
];
|
||||
|
||||
const handleBulkUpload = async (file) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (e) => {
|
||||
const text = e.target.result;
|
||||
const lines = text.split("\n").slice(1); // Skip header
|
||||
const objects = lines
|
||||
.filter((line) => line.trim())
|
||||
.map((line) => {
|
||||
const [phone_number, consent_status] = line.split(",");
|
||||
return {
|
||||
bodyshopid: bodyshop.id,
|
||||
phone_number: phone(phone_number, "CA").phoneNumber.replace(/^\+1/, ""),
|
||||
consent_status: consent_status.trim().toLowerCase() === "true"
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
await bulkSetConsent({
|
||||
variables: { objects },
|
||||
context: { headers: { "x-reason": "System update via bulk upload", "x-changed-by": "system" } }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Bulk upload failed:", error);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
return false;
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Input.Search
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useMutation } from "@apollo/client";
|
||||
import { Switch, Typography } from "antd";
|
||||
import { Switch, Typography, Tooltip, message } from "antd";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { connect } from "react-redux";
|
||||
import { createStructuredSelector } from "reselect";
|
||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||
import { updateBodyshopEnforceConsent } from "../../redux/user/user.actions";
|
||||
import { UPDATE_BODYSHOP_ENFORCE_CONSENT } from "../../graphql/bodyshop.queries";
|
||||
import PhoneNumberConsentList from "../phone-number-consent/phone-number-consent.component";
|
||||
|
||||
@@ -11,14 +12,23 @@ const mapStateToProps = createStructuredSelector({
|
||||
bodyshop: selectBodyshop
|
||||
});
|
||||
|
||||
const mapDispatchToProps = () => ({});
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
updateBodyshopEnforceConsent: (enforce_sms_consent) => dispatch(updateBodyshopEnforceConsent(enforce_sms_consent))
|
||||
});
|
||||
|
||||
function ShopInfoConsentComponent({ bodyshop }) {
|
||||
function ShopInfoConsentComponent({ bodyshop, updateBodyshopEnforceConsent }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [updateEnforceConsent] = useMutation(UPDATE_BODYSHOP_ENFORCE_CONSENT);
|
||||
|
||||
console.dir(bodyshop);
|
||||
const [updateEnforceConsent] = useMutation(UPDATE_BODYSHOP_ENFORCE_CONSENT, {
|
||||
onError: (error) => {
|
||||
message.error(t("settings.enforce_sms_consent_error"));
|
||||
console.error("Error updating enforce_sms_consent:", error);
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
message.success(t("settings.enforce_sms_consent_success"));
|
||||
updateBodyshopEnforceConsent(data.update_bodyshops_by_pk.enforce_sms_consent);
|
||||
}
|
||||
});
|
||||
|
||||
const enforceConsent = bodyshop?.enforce_sms_consent ?? false;
|
||||
|
||||
@@ -27,23 +37,29 @@ function ShopInfoConsentComponent({ bodyshop }) {
|
||||
<Typography.Title level={4}>{t("settings.title")}</Typography.Title>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Typography.Text>{t("settings.enforce_sms_consent")}</Typography.Text>
|
||||
<Switch
|
||||
checked={enforceConsent}
|
||||
onChange={(checked) =>
|
||||
updateEnforceConsent({
|
||||
variables: { id: bodyshop.id, enforce_sms_consent: checked },
|
||||
optimisticResponse: {
|
||||
update_bodyshops_by_pk: {
|
||||
__typename: "bodyshops",
|
||||
id: bodyshop.id,
|
||||
enforce_sms_consent: checked
|
||||
<Tooltip
|
||||
title={enforceConsent ? t("settings.enforce_sms_consent_warning") : t("settings.enforce_sms_consent_enable")}
|
||||
>
|
||||
<Switch
|
||||
checked={enforceConsent}
|
||||
onChange={(checked) => {
|
||||
if (!checked && enforceConsent) return; // Prevent disabling
|
||||
updateEnforceConsent({
|
||||
variables: { id: bodyshop.id, enforce_sms_consent: checked },
|
||||
optimisticResponse: {
|
||||
update_bodyshops_by_pk: {
|
||||
__typename: "bodyshops",
|
||||
id: bodyshop.id,
|
||||
enforce_sms_consent: checked
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
/>
|
||||
});
|
||||
}}
|
||||
disabled={enforceConsent}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<PhoneNumberConsentList bodyshop={bodyshop} />
|
||||
{enforceConsent && <PhoneNumberConsentList bodyshop={bodyshop} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,18 +10,23 @@ export const GET_PHONE_NUMBER_CONSENT = gql`
|
||||
created_at
|
||||
updated_at
|
||||
consent_updated_at
|
||||
history(order_by: { changed_at: desc }, limit: 1) {
|
||||
phone_number_consent_history(order_by: { changed_at: desc }, limit: 1) {
|
||||
id
|
||||
reason
|
||||
changed_at
|
||||
old_value
|
||||
new_value
|
||||
changed_by
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_PHONE_NUMBER_CONSENTS = gql`
|
||||
query GET_PHONE_NUMBER_CONSENTS($bodyshopid: uuid!, $phone_numbers: [String!]) {
|
||||
query GET_PHONE_NUMBER_CONSENTS($bodyshopid: uuid!, $search: String) {
|
||||
phone_number_consent(
|
||||
where: { bodyshopid: { _eq: $bodyshopid }, phone_number: { _in: $phone_numbers } }
|
||||
order_by: { consent_updated_at: desc }
|
||||
where: { bodyshopid: { _eq: $bodyshopid }, phone_number: { _ilike: $search } }
|
||||
order_by: [{ phone_number: asc }, { consent_updated_at: desc }]
|
||||
) {
|
||||
id
|
||||
bodyshopid
|
||||
@@ -30,60 +35,13 @@ export const GET_PHONE_NUMBER_CONSENTS = gql`
|
||||
created_at
|
||||
updated_at
|
||||
consent_updated_at
|
||||
history(order_by: { changed_at: desc }, limit: 1) {
|
||||
reason
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const SET_PHONE_NUMBER_CONSENT = gql`
|
||||
mutation SET_PHONE_NUMBER_CONSENT(
|
||||
$bodyshopid: uuid!
|
||||
$phone_number: String!
|
||||
$consent_status: Boolean!
|
||||
$reason: String!
|
||||
$changed_by: String!
|
||||
) {
|
||||
insert_phone_number_consent_one(
|
||||
object: {
|
||||
bodyshopid: $bodyshopid
|
||||
phone_number: $phone_number
|
||||
consent_status: $consent_status
|
||||
consent_updated_at: "now()"
|
||||
}
|
||||
on_conflict: {
|
||||
constraint: phone_number_consent_bodyshopid_phone_number_key
|
||||
update_columns: [consent_status, consent_updated_at]
|
||||
}
|
||||
) {
|
||||
id
|
||||
bodyshopid
|
||||
phone_number
|
||||
consent_status
|
||||
created_at
|
||||
updated_at
|
||||
consent_updated_at
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const BULK_SET_PHONE_NUMBER_CONSENT = gql`
|
||||
mutation BULK_SET_PHONE_NUMBER_CONSENT($objects: [phone_number_consent_insert_input!]!) {
|
||||
insert_phone_number_consent(
|
||||
objects: $objects
|
||||
on_conflict: {
|
||||
constraint: phone_number_consent_bodyshopid_phone_number_key
|
||||
update_columns: [consent_status, consent_updated_at]
|
||||
}
|
||||
) {
|
||||
affected_rows
|
||||
returning {
|
||||
phone_number_consent_history(order_by: { changed_at: desc }, limit: 1) {
|
||||
id
|
||||
bodyshopid
|
||||
phone_number
|
||||
consent_status
|
||||
consent_updated_at
|
||||
reason
|
||||
changed_at
|
||||
old_value
|
||||
new_value
|
||||
changed_by
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,3 +123,8 @@ export const setImexShopId = (imexshopid) => ({
|
||||
type: UserActionTypes.SET_IMEX_SHOP_ID,
|
||||
payload: imexshopid
|
||||
});
|
||||
|
||||
export const updateBodyshopEnforceConsent = (enforce_sms_consent) => ({
|
||||
type: UserActionTypes.UPDATE_BODYSHOP_ENFORCE_CONSENT,
|
||||
payload: enforce_sms_consent
|
||||
});
|
||||
|
||||
@@ -105,7 +105,6 @@ const userReducer = (state = INITIAL_STATE, action) => {
|
||||
...action.payload //Spread current user details in.
|
||||
}
|
||||
};
|
||||
|
||||
case UserActionTypes.SET_SHOP_DETAILS:
|
||||
return {
|
||||
...state,
|
||||
@@ -126,6 +125,14 @@ const userReducer = (state = INITIAL_STATE, action) => {
|
||||
...state,
|
||||
imexshopid: action.payload
|
||||
};
|
||||
case UserActionTypes.UPDATE_BODYSHOP_ENFORCE_CONSENT:
|
||||
return {
|
||||
...state,
|
||||
bodyshop: {
|
||||
...state.bodyshop,
|
||||
enforce_sms_consent: action.payload
|
||||
}
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ const UserActionTypes = {
|
||||
CHECK_ACTION_CODE_FAILURE: "CHECK_ACTION_CODE_FAILURE",
|
||||
SET_CURRENT_EULA: "SET_CURRENT_EULA",
|
||||
EULA_ACCEPTED: "EULA_ACCEPTED",
|
||||
SET_IMEX_SHOP_ID: "SET_IMEX_SHOP_ID"
|
||||
SET_IMEX_SHOP_ID: "SET_IMEX_SHOP_ID",
|
||||
UPDATE_BODYSHOP_ENFORCE_CONSENT: "UPDATE_BODYSHOP_ENFORCE_CONSENT"
|
||||
};
|
||||
export default UserActionTypes;
|
||||
|
||||
Reference in New Issue
Block a user