Merged in feature/IO-3182-Phone-Number-Consent (pull request #2350)

Feature/IO-3182 Phone Number Consent
This commit is contained in:
Dave Richer
2025-05-27 15:38:56 +00:00
15 changed files with 36 additions and 164 deletions

View File

@@ -12791,27 +12791,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>allow_text_message</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>checklist</name>
<definition_loaded>false</definition_loaded>
@@ -42614,27 +42593,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>allow_text_message</name>
<definition_loaded>false</definition_loaded>
<description></description>
<comment></comment>
<default_text></default_text>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-MX</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>name</name>
<definition_loaded>false</definition_loaded>

View File

@@ -129,24 +129,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
}
}
if (type === "intake" && job.owner && job.owner.id) {
//Updae Owner Allow to Text
const updateOwnerResult = await updateOwner({
variables: {
ownerId: job.owner.id,
owner: { allow_text_message: values.allow_text_message }
}
});
if (!!updateOwnerResult.errors) {
notification["error"]({
message: t("checklist.errors.complete", {
error: JSON.stringify(result.errors)
})
});
}
}
setLoading(false);
if (!!!result.errors) {
@@ -189,7 +171,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
initialValues={{
...(type === "intake" && {
addToProduction: true,
allow_text_message: job.owner && job.owner.allow_text_message,
scheduled_completion:
(job && job.scheduled_completion && dayjs(job.scheduled_completion)) ||
(job &&
@@ -228,14 +209,6 @@ export function JobChecklistForm({ insertAuditTrail, formItems, bodyshop, curren
>
<Switch disabled={readOnly} />
</Form.Item>
<Form.Item
name="allow_text_message"
valuePropName="checked"
label={t("checklist.labels.allow_text_message")}
disabled={readOnly}
>
<Switch disabled={readOnly} />
</Form.Item>
<Form.Item
name="scheduled_completion"
label={t("jobs.fields.scheduled_completion")}

View File

@@ -1,4 +1,4 @@
import { Form, Input, Switch } from "antd";
import { Form, Input } from "antd";
import React, { useContext } from "react";
import { useTranslation } from "react-i18next";
import JobCreateContext from "../../pages/jobs-create/jobs-create.context";
@@ -129,13 +129,6 @@ export default function JobsCreateOwnerInfoNewComponent() {
<Form.Item label={t("owners.fields.preferred_contact")} name={["owner", "data", "preferred_contact"]}>
<Input disabled={!state.owner.new} />
</Form.Item>
<Form.Item
label={t("owners.fields.allow_text_message")}
valuePropName="checked"
name={["owner", "data", "allow_text_message"]}
>
<Switch disabled={!state.owner.new} />
</Form.Item>
</LayoutFormRow>
</div>
);

View File

@@ -1,4 +1,4 @@
import { Form, Input, Switch } from "antd";
import { Form, Input } from "antd";
import React from "react";
import { useTranslation } from "react-i18next";
import FormFieldsChanged from "../form-fields-changed-alert/form-fields-changed-alert.component";
@@ -26,7 +26,7 @@ export default function OwnerDetailFormComponent({ form, loading }) {
<Input />
</Form.Item>
<Form.Item label={t("owners.fields.accountingid")} name="accountingid">
<Input disabled/>
<Input disabled />
</Form.Item>
</LayoutFormRow>
<LayoutFormRow header={t("owners.forms.address")}>
@@ -50,9 +50,6 @@ export default function OwnerDetailFormComponent({ form, loading }) {
</Form.Item>
</LayoutFormRow>
<LayoutFormRow header={t("owners.forms.contact")}>
<Form.Item label={t("owners.fields.allow_text_message")} name="allow_text_message" valuePropName="checked">
<Switch />
</Form.Item>
<Form.Item
label={t("owners.fields.ownr_ea")}
name="ownr_ea"

View File

@@ -1,14 +1,17 @@
import { useQuery } from "@apollo/client";
import { Input, Table } from "antd";
import { Input, Space, Table, Typography } from "antd";
import { Link } from "react-router-dom";
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, SEARCH_OWNERS_BY_PHONE_NUMBERS } from "../../graphql/phone-number-opt-out.queries";
import PhoneNumberFormatter from "../../utils/PhoneFormatter";
import { TimeAgoFormatter } from "../../utils/DateFormatter";
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
const { Paragraph } = Typography; // Destructure Paragraph from Typography
const mapStateToProps = createStructuredSelector({
bodyshop: selectBodyshop,
@@ -44,18 +47,6 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) {
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 [];
@@ -102,15 +93,20 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) {
}
return owners.map((owner) => (
<div key={owner.id}>
{formatOwnerName(owner)} ({owner.phoneField})
<Space direction="horizontal">
<Link to={"/manage/owners/" + owner.id}>
<OwnerNameDisplay ownerObject={owner} />
</Link>
({owner.phoneField})
</Space>
</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]) : "";
const aName = aOwners[0] ? `${aOwners[0].ownr_fn} ${aOwners[0].ownr_ln}` : "";
const bName = bOwners[0] ? `${bOwners[0].ownr_fn} ${bOwners[0].ownr_ln}` : "";
return aName.localeCompare(bName);
}
},
@@ -124,6 +120,7 @@ function PhoneNumberConsentList({ bodyshop, currentUser }) {
return (
<div>
<Paragraph>{t("consent.text_body")}</Paragraph>
<Input.Search
placeholder={t("general.labels.search")}
onSearch={(value) => setSearch(value)}

View File

@@ -312,7 +312,6 @@ export const QUERY_INTAKE_CHECKLIST = gql`
intakechecklist
status
owner {
allow_text_message
id
}
labhrs: joblines_aggregate(where: { _and: [{ mod_lbr_ty: { _neq: "LAR" } }, { removed: { _eq: false } }] }) {

View File

@@ -874,7 +874,6 @@ export const QUERY_JOB_CARD_DETAILS = gql`
}
owner {
id
allow_text_message
preferred_contact
tax_number
}
@@ -2071,7 +2070,6 @@ export const QUERY_JOB_CHECKLISTS = gql`
production_vars
owner {
id
allow_text_message
}
bodyshop {
id
@@ -2428,7 +2426,6 @@ export const QUERY_PARTS_QUEUE_CARD_DETAILS = gql`
ownr_ph2
owner {
id
allow_text_message
preferred_contact
tax_number
}

View File

@@ -49,7 +49,6 @@ export const QUERY_OWNER_BY_ID = gql`
owners_by_pk(id: $id) {
id
accountingid
allow_text_message
ownr_addr1
ownr_addr2
ownr_co_nm
@@ -104,7 +103,6 @@ export const QUERY_ALL_OWNERS = gql`
query QUERY_ALL_OWNERS {
owners {
id
allow_text_message
created_at
ownr_addr1
ownr_addr2
@@ -129,7 +127,6 @@ export const QUERY_ALL_OWNERS_PAGINATED = gql`
query QUERY_ALL_OWNERS_PAGINATED($search: String, $offset: Int, $limit: Int, $order: [owners_order_by!]!) {
search_owners(args: { search: $search }, offset: $offset, limit: $limit, order_by: $order) {
id
allow_text_message
created_at
ownr_addr1
ownr_addr2

View File

@@ -114,7 +114,6 @@ function JobsCreateContainer({ bodyshop, setBreadcrumbs, setSelectedHeader }) {
if (!!!job.ownerid) {
ownerData = job.owner.data;
ownerData.shopid = bodyshop.id;
delete ownerData.allow_text_message;
delete ownerData.preferred_contact;
delete job.ownerid;
} else {

View File

@@ -92,13 +92,15 @@ export function ShopPage({ bodyshop, setSelectedHeader, setBreadcrumbs }) {
});
}
// Add Consent Settings tab
items.push({
key: "consent",
label: t("bodyshop.labels.consent_settings"),
children: <ShopInfoConsentComponent bodyshop={bodyshop} />
});
if (bodyshop.messagingservicesid) {
// Add Consent Settings tab
items.push({
key: "consent",
label: t("bodyshop.labels.consent_settings"),
children: <ShopInfoConsentComponent bodyshop={bodyshop} />
});
}
return (
<RbacWrapper action="shop:config">
<Tabs activeKey={search.tab} onChange={(key) => history({ search: `?tab=${key}` })} items={items} />

View File

@@ -775,7 +775,6 @@
},
"labels": {
"addtoproduction": "Add Job to Production?",
"allow_text_message": "Permission to Text?",
"checklist": "Checklist",
"printpack": "Job Intake Print Pack",
"removefromproduction": "Remove Job from Production?"
@@ -2524,7 +2523,6 @@
"fields": {
"accountingid": "Accounting ID",
"address": "Address",
"allow_text_message": "Permission to Text?",
"name": "Name",
"note": "Owner Note",
"ownr_addr1": "Address",
@@ -3876,7 +3874,8 @@
"created_at": "Opt-Out Date",
"no_owners": "No Associated Owners",
"phone_1": "Phone 1",
"phone_2": "Phone 2"
"phone_2": "Phone 2",
"text_body": "Users can opt out of receiving SMS messages by replying with keywords such as STOP, UNSUBSCRIBE, CANCEL, END, QUIT, STOPALL, REVOKE and OPTOUT. To opt back in, users can reply with START, YES, or UNSTOP. Even after opting out, users can still send messages to us, which will be received and processed as needed. Ensure customers are informed to reply with these keywords to manage their messaging preferences. After opting out, users receive a confirmation message and will not receive further messages until they opt back in."
},
"settings": {
"title": "Phone Number Opt-Out List"

View File

@@ -775,7 +775,6 @@
},
"labels": {
"addtoproduction": "",
"allow_text_message": "",
"checklist": "",
"printpack": "",
"removefromproduction": ""
@@ -2526,7 +2525,6 @@
"fields": {
"accountingid": "",
"address": "Dirección",
"allow_text_message": "Permiso de texto?",
"name": "Nombre",
"note": "",
"ownr_addr1": "Dirección",
@@ -3878,7 +3876,8 @@
"created_at": "",
"no_owners": "",
"phone_1": "",
"phone_2": ""
"phone_2": "",
"text_body": ""
},
"settings": {
"title": ""

View File

@@ -775,7 +775,6 @@
},
"labels": {
"addtoproduction": "",
"allow_text_message": "",
"checklist": "",
"printpack": "",
"removefromproduction": ""
@@ -2526,7 +2525,6 @@
"fields": {
"accountingid": "",
"address": "Adresse",
"allow_text_message": "Autorisation de texte?",
"name": "Prénom",
"note": "",
"ownr_addr1": "Adresse",
@@ -3878,7 +3876,8 @@
"created_at": "Opt-Out Date",
"no_owners": "No Associated Owners",
"phone_1": "Phone 1",
"phone_2": "Phone 2"
"phone_2": "Phone 2",
"text_body": ""
},
"settings": {
"title": ""

View File

@@ -10028,25 +10028,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>allow_text_message</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-ES</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>checklist</name>
<description/>
@@ -33000,25 +32981,6 @@
</translation>
</translations>
</concept_node>
<concept_node>
<name>allow_text_message</name>
<description/>
<comment/>
<translations>
<translation>
<language>en-US</language>
<approved>false</approved>
</translation>
<translation>
<language>es-ES</language>
<approved>false</approved>
</translation>
<translation>
<language>fr-CA</language>
<approved>false</approved>
</translation>
</translations>
</concept_node>
<concept_node>
<name>name</name>
<description/>

View File

@@ -12,6 +12,10 @@ const { phone } = require("phone");
const { admin } = require("../firebase/firebase-handler");
const InstanceManager = require("../utils/instanceMgr").default;
// Note: When we handle different languages, we might need to adjust these keywords accordingly.
const optInKeywords = ["START", "YES", "UNSTOP"];
const optOutKeywords = ["STOP", "STOPALL", "UNSUBSCRIBE", "CANCEL", "END", "QUIT", "REVOKE", "OPTOUT"];
/**
* Receive SMS messages from Twilio and process them
* @param req
@@ -58,9 +62,6 @@ const receive = async (req, res) => {
const messageText = (req.body.Body || "").trim().toUpperCase();
// 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, {