From 412efb06e5da986a6a2ade84b64c03689144eaa9 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 28 May 2025 13:07:11 -0400 Subject: [PATCH] feature/IO-3182-Phone-Number-Consent - Checkpoint --- .../owner-detail-form.component.jsx | 69 +++++++--- .../owner-detail-form.container.jsx | 126 +++++++++++++----- client/src/utils/phoneOptOutService.js | 44 ++++++ 3 files changed, 187 insertions(+), 52 deletions(-) create mode 100644 client/src/utils/phoneOptOutService.js diff --git a/client/src/components/owner-detail-form/owner-detail-form.component.jsx b/client/src/components/owner-detail-form/owner-detail-form.component.jsx index a71cbc836..99bacd566 100644 --- a/client/src/components/owner-detail-form/owner-detail-form.component.jsx +++ b/client/src/components/owner-detail-form/owner-detail-form.component.jsx @@ -1,14 +1,15 @@ -import { Form, Input } from "antd"; -import React from "react"; +import { Form, Input, Tooltip } from "antd"; +import { CloseCircleFilled } from "@ant-design/icons"; import { useTranslation } from "react-i18next"; import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component"; import FormItemEmail from "../form-items-formatted/email-form-item.component"; -import FormItemPhone, { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component"; import LayoutFormRow from "../layout-form-row/layout-form-row.component"; +import { PhoneItemFormatterValidation } from "../form-items-formatted/phone-form-item.component"; -export default function OwnerDetailFormComponent({ form, loading }) { +export default function OwnerDetailFormComponent({ form, loading, isPhone1OptedOut, isPhone2OptedOut }) { const { t } = useTranslation(); const { getFieldValue } = form; + return (
@@ -62,19 +63,55 @@ export default function OwnerDetailFormComponent({ form, loading }) { > - PhoneItemFormatterValidation(getFieldValue, "ownr_ph1")]} - > - + +
+ PhoneItemFormatterValidation(getFieldValue, "ownr_ph1")]} + > + + + {isPhone1OptedOut && ( + + + + )} +
- PhoneItemFormatterValidation(getFieldValue, "ownr_ph2")]} - > - + +
+ PhoneItemFormatterValidation(getFieldValue, "ownr_ph2")]} + > + + + {isPhone2OptedOut && ( + + + + )} +
diff --git a/client/src/components/owner-detail-form/owner-detail-form.container.jsx b/client/src/components/owner-detail-form/owner-detail-form.container.jsx index d1f34b82b..8bb0bab85 100644 --- a/client/src/components/owner-detail-form/owner-detail-form.container.jsx +++ b/client/src/components/owner-detail-form/owner-detail-form.container.jsx @@ -1,69 +1,113 @@ import { Button, Form, Popconfirm } from "antd"; import { PageHeader } from "@ant-design/pro-layout"; - -import React, { useState } from "react"; +import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { useMutation } from "@apollo/client"; +import { useApolloClient, useMutation } from "@apollo/client"; import { useTranslation } from "react-i18next"; +import { connect } from "react-redux"; +import { createStructuredSelector } from "reselect"; import { DELETE_OWNER, UPDATE_OWNER } from "../../graphql/owners.queries"; +import { selectBodyshop } from "../../redux/user/user.selectors"; // Adjust path +import { checkPhoneOptOutStatus } from "../../utils/phoneOptOutService.js"; // Adjust path import OwnerDetailFormComponent from "./owner-detail-form.component"; import { useNotification } from "../../contexts/Notifications/notificationContext.jsx"; +import { phone } from "phone"; // Import phone utility for formatting -function OwnerDetailFormContainer({ owner, refetch }) { +// Connect to Redux to access bodyshop +const mapStateToProps = createStructuredSelector({ + bodyshop: selectBodyshop +}); + +function OwnerDetailFormContainer({ owner, refetch, bodyshop }) { const { t } = useTranslation(); const [form] = Form.useForm(); - const history = useNavigate(); + const navigate = useNavigate(); const [loading, setLoading] = useState(false); + const [optedOutPhones, setOptedOutPhones] = useState(new Set()); const [updateOwner] = useMutation(UPDATE_OWNER); const [deleteOwner] = useMutation(DELETE_OWNER); const notification = useNotification(); + const apolloClient = useApolloClient(); + + // Fetch opt-out status on mount + useEffect(() => { + const fetchOptOutStatus = async () => { + if (bodyshop?.id && (owner?.ownr_ph1 || owner?.ownr_ph2)) { + const phoneNumbers = [owner.ownr_ph1, owner.ownr_ph2].filter(Boolean); + const optOutSet = await checkPhoneOptOutStatus(apolloClient, bodyshop.id, phoneNumbers); + setOptedOutPhones(optOutSet); + } + }; + + fetchOptOutStatus(); + }, [apolloClient, bodyshop?.id, owner?.ownr_ph1, owner?.ownr_ph2]); + + // Reset form fields when owner changes + useEffect(() => { + form.setFieldsValue({ + ownr_ph1: owner?.ownr_ph1, + ownr_ph2: owner?.ownr_ph2, + ...owner + }); + }, [owner, form]); const handleDelete = async () => { setLoading(true); - const result = await deleteOwner({ - variables: { id: owner.id } - }); - console.log(result); - if (result.errors) { + try { + const result = await deleteOwner({ + variables: { id: owner.id } + }); + if (result.errors) { + notification.error({ + message: t("owners.errors.deleting", { + error: JSON.stringify(result.errors) + }) + }); + } else { + notification.success({ + message: t("owners.successes.delete") + }); + navigate(`/manage/owners`); + } + } catch (error) { notification.error({ message: t("owners.errors.deleting", { - error: JSON.stringify(result.errors) + error: error.message }) }); + } finally { setLoading(false); - } else { - notification.success({ - message: t("owners.successes.delete") - }); - setLoading(false); - history(`/manage/owners`); } }; const handleFinish = async (values) => { setLoading(true); - const result = await updateOwner({ - variables: { ownerId: owner.id, owner: values } - }); - - if (!!result.errors) { + try { + const result = await updateOwner({ + variables: { ownerId: owner.id, owner: values } + }); + if (result.errors) { + notification.error({ + message: t("owners.errors.saving", { + error: JSON.stringify(result.errors) + }) + }); + } else { + notification.success({ + message: t("owners.successes.save") + }); + if (refetch) await refetch(); + form.resetFields(); + } + } catch (error) { notification.error({ message: t("owners.errors.saving", { - error: JSON.stringify(result.errors) + error: error.message }) }); + } finally { setLoading(false); - return; } - - notification.success({ - message: t("owners.successes.save") - }); - - if (refetch) await refetch(); - form.resetFields(); - form.resetFields(); - setLoading(false); }; return ( @@ -72,6 +116,7 @@ function OwnerDetailFormContainer({ owner, refetch }) { title={t("menus.header.owners")} extra={[ , - ]} />
- + ); } -export default OwnerDetailFormContainer; +export default connect(mapStateToProps)(OwnerDetailFormContainer); diff --git a/client/src/utils/phoneOptOutService.js b/client/src/utils/phoneOptOutService.js new file mode 100644 index 000000000..037e21c11 --- /dev/null +++ b/client/src/utils/phoneOptOutService.js @@ -0,0 +1,44 @@ +import { phone } from "phone"; +import { GET_PHONE_NUMBER_OPT_OUT } from "../graphql/phone-number-opt-out.queries"; + +/** + * Check if phone numbers are opted out for a given bodyshop + * @param {Object} apolloClient - Apollo Client instance + * @param {string} bodyshopId - The ID of the bodyshop + * @param {string[]} phoneNumbers - Array of phone numbers to check + * @returns {Promise>} - Set of normalized opted-out phone numbers + */ +export const checkPhoneOptOutStatus = async (apolloClient, bodyshopId, phoneNumbers) => { + if (!apolloClient || !bodyshopId || !phoneNumbers?.length) { + return new Set(); + } + + // Normalize phone numbers (remove +1 for CA numbers) + const normalizedPhones = phoneNumbers + .filter(Boolean) + .map((num) => phone(num, "CA").phoneNumber?.replace(/^\+1/, "")) + .filter(Boolean); + + const optedOutPhones = new Set(); + + for (const phoneNum of normalizedPhones) { + try { + const { data } = await apolloClient.query({ + query: GET_PHONE_NUMBER_OPT_OUT, + variables: { + bodyshopid: bodyshopId, + phone_number: phoneNum // Single string + }, + fetchPolicy: "network-only" + }); + + if (data?.phone_number_opt_out?.length) { + optedOutPhones.add(phoneNum); + } + } catch (error) { + console.error(`Error checking opt-out for ${phoneNum}:`, error); + } + } + + return optedOutPhones; +};