/* © 2026 K2B NETWORK SOLUTIONS LLC. All rights reserved. Proprietary and confidential. Unauthorized copying, distribution, or use of this file, via any medium, is strictly prohibited. Original work product. */ /* eslint-disable */ // K2B DMS — Messaging Center (texting + email, consent-gated). // window.MessagingCenter({ onClose, go, embedded }) route suggestion: "messaging" // window.jbCanMessage() who may send (Admin, managers, Service, F&I) // // • Consent registry — per phone/email, channel, transactional vs marketing, // status, SOURCE and TIMESTAMP of consent, who recorded it. Stored in // DMS msgConsents (DMS._mutate) AND on the server (authoritative for sending). // Opt-outs go on the server suppression list. // • STOP / STOPALL / UNSUBSCRIBE / CANCEL / END / QUIT texts are handled by the // server's Twilio webhook → suppression list; START / UNSTOP re-allows // transactional texts only. // • Compose — the server refuses suppressed addresses, marketing without // recorded consent or without an approved marketing template, and never // reports "sent" unless a CONNECTED provider accepted the message. // • History — server message log (incl. inbound keywords) + DMS messageLog. // • Templates — approved transactional / marketing templates; admin edits. const MC_SOURCES = ["Signed paper form", "Website form", "Credit application / buyer's order", "Text keyword (customer texted in)", "Verbal — recorded call", "Customer email request", "Other"]; const mcCanMessage = () => { const D = window.DMS; if (!D) return false; if (D.isAdmin && D.isAdmin()) return true; const r = String((D.userRole && D.userRole()) || ""); return /manager|service|f\s*&\s*i/i.test(r); }; const mcIsAdmin = () => !!(window.DMS && window.DMS.isAdmin && window.DMS.isAdmin()); const mcNorm = (channel, a) => { const s = String(a == null ? "" : a).trim(); if (channel === "email") return s.toLowerCase(); const digits = s.replace(/\D/g, ""); if (/^\+[1-9][0-9]{7,14}$/.test(s.replace(/[^0-9+]/g, ""))) return s.replace(/[^0-9+]/g, ""); if (digits.length === 10) return "+1" + digits; if (digits.length === 11 && digits[0] === "1") return "+" + digits; return digits ? "+" + digits : ""; }; const mcFmt = (d) => d ? new Date(d).toLocaleString([], { month: "short", day: "numeric", year: "2-digit", hour: "numeric", minute: "2-digit" }) : "—"; const mcTypeTag = (t) => t === "marketing" ? MARKETING : TRANSACTIONAL; const mcStatusTag = (s) => { const m = { "opted-in": ["#E6F7EC", "#0E7A37", "Opted in"], "opted-out": ["#FDECEA", "#B23A2E", "Opted out"], suppressed: ["#FDECEA", "#B23A2E", "Suppressed"] }[s] || ["#EEF0F3", "#666", "Unknown"]; return {m[2]}; }; const mcInp = { boxSizing: "border-box", padding: "6px 8px", border: "1px solid #C8CCD2", borderRadius: 4, fontSize: 12.5 }; const mcTh = { padding: "5px 8px", textAlign: "left", color: "#888", fontWeight: 600, fontSize: 11 }; const mcTd = { padding: "5px 8px", borderTop: "1px solid #F0F1F4", fontSize: 12, verticalAlign: "top" }; // mirror a consent into DMS msgConsents (shared, synced collection) const mcMirrorConsent = (rec) => { const D = window.DMS; if (!D || !D._mutate) return; D._mutate((state) => { state.msgConsents = state.msgConsents || []; const id = rec.channel + ":" + rec.address + ":" + rec.type; const i = state.msgConsents.findIndex(x => x.id === id); const row = { ...(i >= 0 ? state.msgConsents[i] : {}), ...rec, id }; if (i >= 0) state.msgConsents[i] = row; else state.msgConsents.unshift(row); }); }; const mcLogLocal = (entry) => { const D = window.DMS; if (!D || !D._mutate) return; D._mutate((state) => { state.messageLog = state.messageLog || []; state.messageLog.unshift({ id: (D._uid ? D._uid("MSG") : "MSG-" + Date.now()), ts: new Date().toISOString(), by: D.userId ? D.userId() : "", ...entry }); if (state.messageLog.length > 2000) state.messageLog.length = 2000; }); }; const MessagingCenter = ({ onClose, go, embedded }) => { const api = window.k2bIntApi; const D = window.DMS; const canSend = mcCanMessage(); const admin = mcIsAdmin(); const [tab, setTab] = React.useState("compose"); const [, force] = React.useReducer(n => n + 1, 0); React.useEffect(() => (D && D.subscribe ? D.subscribe(force) : undefined), []); const [srv, setSrv] = React.useState({ consent: [], suppression: [], templates: [], log: [], providers: [], err: "" }); const reload = React.useCallback(async () => { if (!api) return; const [c, s, t, l, st] = await Promise.all([api("/messages/consent"), api("/messages/suppression"), api("/messages/templates"), api("/messages/log?limit=300"), api("/status-summary")]); const err = [c, s, t, l].filter(r => !r.ok).map(r => r.error)[0] || ""; setSrv({ consent: c.ok ? c.data.consent : [], suppression: s.ok ? s.data.suppression : [], templates: t.ok ? t.data.templates : [], log: l.ok ? l.data.log : [], providers: st.ok ? st.data.providers : [], err }); }, []); React.useEffect(() => { reload(); }, [reload]); if (!canSend) { return
Messaging is limited to managers, service and F&I. Ask a manager to text or email a customer.
; } if (!api) return
ConnectedApps.jsx.txt (integration client) is not loaded.
; const supKey = (ch, a) => ch + "|" + a; const supSet = new Set((srv.suppression || []).map(x => supKey(x.channel, x.address))); const consentOf = (ch, addr, type) => { if (supSet.has(supKey(ch, addr))) return "suppressed"; const r = (srv.consent || []).find(x => x.channel === ch && x.address === addr && x.msg_type === type); return r ? r.status : "unknown"; }; const provFor = (ch) => (srv.providers || []).find(p => p.id === (ch === "sms" ? "twilio" : "email")) || { status: "not_configured" }; const tabs = [["compose", "Compose"], ["consent", "Consent registry"], ["history", "Message history"], ["templates", "Approved templates"], ["suppression", "Suppression list"]]; const body = (
This tool helps you follow your consent policy — it records consent (source, time, who), honors STOP-style opt-outs and the suppression list, and keeps transactional and marketing messages separate. It does not by itself make the dealership legally compliant (TCPA, CAN-SPAM, carrier/A2P rules, state law) — have counsel review your policy, templates and consent language.
{tabs.map(([k, l]) => ( ))}
{srv.err &&
Server: {srv.err}
} {tab === "compose" && } {tab === "consent" && } {tab === "history" && } {tab === "templates" && } {tab === "suppression" && }
); if (embedded) return (
Messaging & Consent
{body}
); return (
{body}
ESC-Close
); }; /* ─── customer picker (DMS customers) ─────────────────────────────────── */ const mcCustName = (c) => [c.lastName, c.firstName].filter(Boolean).join(", ") || c.name || c.custNum || ""; const McCustomerPick = ({ onPick }) => { const [q, setQ] = React.useState(""); const list = React.useMemo(() => { const t = q.trim().toLowerCase(); if (t.length < 2) return []; const all = Object.values((window.DMS && window.DMS.customers && window.DMS.customers()) || {}); return all.filter(c => (window.DMS && window.DMS.custQueryHit ? window.DMS.custQueryHit(q, c.custNum, [c.phone, c.cell]) : false) || [c.lastName, c.firstName, c.name, c.custNum, c.phone, c.cell, c.email].concat(Array.isArray(c.aliases) ? c.aliases : [], Array.isArray(c.names) ? c.names.map((n) => n && [n.first, n.last].filter(Boolean).join(" ")) : []).filter(Boolean).join(" ").toLowerCase().includes(t)).slice(0, 8); // RC30 (cn): + old numbers, household names }, [q]); return (
setQ(e.target.value)} placeholder="Find customer (name / phone / email)…" style={{ ...mcInp, width: 260 }} /> {list.length > 0 && (
{list.map(c => (
{ onPick(c); setQ(""); }} style={{ padding: "6px 9px", cursor: "pointer", fontSize: 12, borderTop: "1px solid #F0F1F4" }}> {mcCustName(c)} · #{c.custNum}{c.cell || c.phone ? " · " + (c.cell || c.phone) : ""} {c.email ? "· " + c.email : ""}
))}
)}
); }; /* ─── Compose ─────────────────────────────────────────────────────────── */ const McCompose = ({ srv, consentOf, provFor, reload }) => { const [channel, setChannel] = React.useState("sms"); const [to, setTo] = React.useState(""); const [cust, setCust] = React.useState(null); const [type, setType] = React.useState("transactional"); const [tplId, setTplId] = React.useState(""); const [subject, setSubject] = React.useState(""); const [text, setText] = React.useState(""); const [busy, setBusy] = React.useState(false); const [res, setRes] = React.useState(null); const addr = mcNorm(channel, to); const tpls = (srv.templates || []).filter(t => t.approved && t.channel === channel && t.msg_type === type); const consent = addr ? consentOf(channel, addr, type) : "unknown"; const prov = provFor(channel); const blocks = []; if (!addr) blocks.push("Enter a " + (channel === "sms" ? "mobile number" : "email address") + "."); if (consent === "suppressed") blocks.push("This address is on the suppression list (opted out) — nothing can be sent."); if (consent === "opted-out") blocks.push("Customer opted out of " + type + " " + (channel === "sms" ? "texts" : "email") + "."); if (type === "marketing" && consent === "unknown") blocks.push("No recorded marketing consent — record it in the Consent registry first."); if (type === "marketing" && !tplId) blocks.push("Marketing messages must use an approved marketing template."); if (!text.trim()) blocks.push("Message is empty."); const pickTpl = (id) => { setTplId(id); const t = (srv.templates || []).find(x => x.id === id); if (t) { const dealer = (window.K2BStore && window.K2BStore.branding().dba) || ""; // RC35 (brd, D52): the store's everyday name setText(String(t.body || "").split("{dealer}").join(dealer)); setSubject(String(t.subject || "").split("{dealer}").join(dealer)); } }; const send = async () => { setBusy(true); setRes(null); const r = await window.k2bIntApi("/messages/send", "POST", { channel, to, msg_type: type, templateId: tplId || undefined, body: text, subject: subject || undefined, customer: cust ? cust.custNum : undefined }); setBusy(false); const d = r.data || {}; const status = d.sent ? "sent" : d.reason === "provider-not-connected" ? "not-sent" : r.status === 409 ? "refused" : "failed"; setRes({ sent: !!d.sent, t: d.sent ? "Sent" + (d.providerRef ? " (ref " + d.providerRef + ")" : "") + "." : (d.error || r.error || "Not sent.") }); mcLogLocal({ channel, to: addr, custNum: cust ? cust.custNum : "", type, templateId: tplId || "", body: text.slice(0, 500), status, reason: d.reason || "", serverLogId: d.logId || null }); window.k2bToast && window.k2bToast(d.sent ? "Message sent." : "Not sent — " + String(d.reason || "see details").replace(/-/g, " "), d.sent ? "ok" : "warn", 3600); reload(); }; const provChip = window.K2BIntChip ? : prov.status; return (
{ setCust(c); setTo(channel === "sms" ? (c.cell || c.phone || "") : (c.email || "")); }} /> {/* RC30 (cn): a customer # is never a phone to text */} {cust && {mcCustName(cust)}}
Message type
{channel === "email" && setSubject(e.target.value)} placeholder="Subject" style={mcInp} />}