/* eslint-disable */ /* © 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. */ // JBREWS DMS — Repair Order e-sign. Three pieces: // • RoSignaturePad — draw (mouse/finger) or type a signature. // • RoEsignModal — advisor "sends" a sign request by text/email; shows a // copyable link and a button to open the customer signing screen. // • RoSignScreen — full-screen, token-driven customer authorization page // (opened via ?rosign=TOKEN). Captures the signature and authorizes the RO. // Real SMS/email delivery needs a server; here the send is simulated and the // link is shown/copyable so the flow is fully demonstrable. const roMoney = (n) => "$" + (Number(n) || 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); // RC32 (lane ros): the store's settings for the customer's page — store name (X1), the authorization wording (X2), what the customer // approves (X4: "whole" = labor + parts + sublet + tax, owner Q1; "labor" = today), the e-sign link (X6). Missing = today's behaviour. const roSignCfg = () => { try { const s = window.DMS && DMS.svcSettings ? DMS.svcSettings() : null; return s && s.ro ? s : null; } catch (e) { return null; } }; const roSignStoreName = (cfg) => { const x = cfg && cfg.ro && cfg.ro.texts && String(cfg.ro.texts.storeName || "").trim(); if (x) return x; return String((window.K2BStore && window.K2BStore.branding().dba) || "").trim(); // RC35 (brd, D52): the store's everyday name }; // The wording the customer agrees to: the RO's own text (edited on this RO), else the store's (X2), else the standard text. const roSignAuthText = (ro, cfg) => { if (ro && ro.disclosure != null && String(ro.disclosure).trim()) return String(ro.disclosure); const st = cfg && cfg.ro && cfg.ro.texts && cfg.ro.texts.authText && String(cfg.ro.texts.authText.text || "").trim(); return st || RO_AUTH_DISCLAIMER; }; // What the customer approves (X4). "whole": each job with its own parts and sublet, then labor / parts / sublet / fees / tax and the // total — DMS.roMoney (the same figures the RO, the invoice and the close use; declined jobs are not on it). "labor": today's page. const roSignEstimate = (ro, cfg) => { const shows = (cfg && cfg.ro && cfg.ro.texts && cfg.ro.texts.approvalAmount) || "labor"; const ops = (ro.ops || []).filter(o => o.approval === "approved" || o.approval === "pending" || !o.approval); const m = window.DMS && DMS.roMoney ? DMS.roMoney(ro) : null; if (shows !== "whole" || !m) { const tot = DMS.roEstimateTotals(ro.id); return { shows: "labor", lines: ops.map(o => ({ id: o.id, desc: o.desc, note: o.complaint, amount: +o.lab || 0 })), total: tot.approved + tot.pending, parts: null }; } const lines = ops.map(o => { const p = (ro.partsLines || []).filter(l => l.jobId != null && String(l.jobId) === String(o.id)).reduce((a, l) => a + (+l.sale || 0) * (+l.qty || 0), 0); const sb = (ro.sublets || []).filter(l => l.jobId != null && String(l.jobId) === String(o.id)).reduce((a, l) => a + (+l.sale || 0) * (l.qty == null ? 1 : +l.qty || 0), 0); return { id: o.id, desc: o.desc, note: o.complaint, amount: Math.round(((+o.lab || 0) + p + sb) * 100) / 100 }; }); return { shows: "whole", lines, total: m.total, parts: { labor: m.labor, parts: m.parts, sublet: m.sublet, fees: m.fees, tax: m.tax } }; }; const RO_AUTH_DISCLAIMER = "I hereby authorize the repair work described above along with the necessary " + "materials, and grant you and/or your employees permission to operate the " + "vehicle for the purpose of testing and/or inspection. An express mechanic's " + "lien is acknowledged on this vehicle to secure the amount of repairs. You will " + "not be held responsible for loss or damage to the vehicle or articles left in " + "it in case of fire, theft, or any other cause beyond your control. I have read " + "and approve the estimated charges, including any approved supplements, and " + "agree the estimate may change only with my further authorization."; // ── Signature pad ────────────────────────────────────────────────────────── const RoSignaturePad = ({ onChange, height }) => { const [mode, setMode] = React.useState("draw"); // draw | type const [typed, setTyped] = React.useState(""); const canvasRef = React.useRef(null); const drawing = React.useRef(false); const dirty = React.useRef(false); const ctxOf = () => { const c = canvasRef.current; const ctx = c.getContext("2d"); ctx.lineWidth = 2.5; ctx.lineCap = "round"; ctx.strokeStyle = "#11264D"; return ctx; }; const pos = (e) => { const c = canvasRef.current; const r = c.getBoundingClientRect(); const t = e.touches ? e.touches[0] : e; return { x: (t.clientX - r.left) * (c.width / r.width), y: (t.clientY - r.top) * (c.height / r.height) }; }; const start = (e) => { e.preventDefault(); drawing.current = true; const ctx = ctxOf(); const p = pos(e); ctx.beginPath(); ctx.moveTo(p.x, p.y); }; const move = (e) => { if (!drawing.current) return; e.preventDefault(); const ctx = ctxOf(); const p = pos(e); ctx.lineTo(p.x, p.y); ctx.stroke(); dirty.current = true; }; const end = () => { if (!drawing.current) return; drawing.current = false; if (dirty.current && canvasRef.current) onChange(canvasRef.current.toDataURL("image/png")); }; const clear = () => { const c = canvasRef.current; if (c) c.getContext("2d").clearRect(0, 0, c.width, c.height); dirty.current = false; onChange(""); }; // Render a typed name to an image so it lands on the printed RO too. const commitTyped = (val) => { setTyped(val); const c = canvasRef.current; if (!c) return; const ctx = c.getContext("2d"); ctx.clearRect(0, 0, c.width, c.height); if (val.trim()) { ctx.fillStyle = "#11264D"; ctx.font = "italic 40px 'Brush Script MT','Segoe Script',cursive"; ctx.textBaseline = "middle"; ctx.fillText(val, 16, c.height / 2); onChange(c.toDataURL("image/png")); } else onChange(""); }; return (
{mode === "type" && ( commitTyped(e.target.value)} placeholder="Type your full name" style={{ width: "100%", boxSizing: "border-box", padding: "8px 10px", border: "1px solid #C8CCD2", borderRadius: 4, fontSize: 15, marginBottom: 6, fontFamily: "'Brush Script MT','Segoe Script',cursive" }} /> )}
Sign above — {mode === "draw" ? "use your mouse or finger" : "your typed name becomes your signature"}.
); }; // ── Advisor "send" modal ──────────────────────────────────────────────────── const RoEsignModal = ({ ro, act, onClose }) => { const cfg = roSignCfg(); const es = (cfg && cfg.ro && cfg.ro.texts && cfg.ro.texts.esignSend) || { via: "show", fillMobile: false }; const mobile = ro.customerPhone || (es.fillMobile && ro.cust && ro.cust.phone) || ""; const [via, setVia] = React.useState(es.via === "email" ? "email" : "text"); const [to, setTo] = React.useState(es.via === "email" && es.fillMobile && ro.cust && ro.cust.email ? ro.cust.email : mobile); const [sent, setSent] = React.useState(ro.signRequest && ro.signRequest.status === "sent" ? ro.signRequest : null); const link = sent ? (location.origin + location.pathname + "?rosign=" + sent.token + ((window.K2BStore && window.K2BStore.pageId()) ? "&s=" + window.K2BStore.pageId() : "")) : ""; // RC35 (brd): the link names its store const send = () => { const r = DMS.requestROSignature(ro.id, { via, to }); act(r); if (r.ok) setSent({ token: r.token, via, to, status: "sent" }); }; const copy = () => { try { navigator.clipboard.writeText(link); window.k2bToast && window.k2bToast("Link copied.", "ok"); } catch (e) {} }; return ( Close{!sent && Send Request}}> {!sent ? ( <>
Text or email the customer a secure link to review and authorize the approved repairs on {ro.id}.
{[["text", "📱 Text"], ["email", "✉ Email"]].map(([v, l]) => ( ))}
Live SMS/email delivery is wired through your messaging provider in production. In this build the link is generated for you to open the customer's signing screen.
) : ( <>
✓ {via === "email" ? "Email" : "Text"} sent to {to || "customer"} with the signing link.
When the customer signs, this RO is authorized automatically and the signature lands on the printed order.
)}
); }; // ── Customer signing screen (full page, ?rosign=TOKEN) ────────────────────── const RoSignScreen = ({ token }) => { const [, force] = React.useReducer(n => n + 1, 0); React.useEffect(() => (window.DMS && DMS.subscribe) ? DMS.subscribe(force) : undefined, []); const ro = window.DMS ? DMS.findROBySignToken(token) : null; React.useEffect(() => { try { if (typeof jbSvcViewport === "function") jbSvcViewport(true); } catch (e) {} }, []); // RC32: the page fits the customer's phone const cfg = roSignCfg(); const store = roSignStoreName(cfg); const [sig, setSig] = React.useState(""); const [name, setName] = React.useState(ro ? ro.customer : ""); const [agree, setAgree] = React.useState(false); if (!ro) return (
🔗
This signing link is invalid or has expired.
); const already = ro.authorization; const est = roSignEstimate(ro, cfg); const supplies = !!(cfg && cfg.money && cfg.money.shopSupplies && cfg.money.shopSupplies.on); const submit = () => { if (!sig) { window.k2bToast && window.k2bToast("Please sign to authorize.", "warn"); return; } if (!agree) { window.k2bToast && window.k2bToast("Please check the authorization box.", "warn"); return; } // RC32 fix (r32-aud M1, owner Q1): the customer signs the WHOLE estimate -> its jobs are approved first (ServiceTickets roOkWholeJobs), // and the RO records what this page showed (est.shows), not a setting read later. if (est.shows === "whole" && typeof window.roOkWholeJobs === "function") window.roOkWholeJobs(ro.id, "whole"); DMS.submitROSignature(token, { signature: sig, name: name || ro.customer, shown: est.shows }); force(); }; return (
{window.K2BStoreBand ? : null}
{window.K2BStoreLogo ? : null}
{store} — Service
Repair Authorization · {ro.id}
{already ? (
✅
Repairs Authorized
Thank you, {already.signedName || already.by}. Your service advisor has been notified and work will proceed.
Signed {new Date(already.at).toLocaleString()}
) : ( <>
{ro.customer} · {ro.vehicle}
Please review & approve your repairs
{est.lines.map(o => (
{o.desc}
{o.note &&
{o.note}
}
{roMoney(o.amount)}
))} {est.parts && (
Labor{roMoney(est.parts.labor)} Parts{roMoney(est.parts.parts)} {est.parts.sublet ? <>Sublet / outside work{roMoney(est.parts.sublet)} : null} {est.parts.fees ? <>Fees{roMoney(est.parts.fees)} : null} Tax{roMoney(est.parts.tax)}
)}
{est.shows === "whole" ? "Estimated total" : "Estimated total (approved)"}{roMoney(est.total)}
{est.shows === "whole" ? (supplies ? "Plus applicable shop supplies. " : "") : (supplies ? "Plus applicable shop supplies and tax. " : "Plus applicable tax. ")}Final charges may change only with your further authorization.
{window.SvcSunbitBox ? : null}{/* RC28 (svc): the store's Sunbit apply link + QR (no payment estimates) */}
{roSignAuthText(ro, cfg)}
setName(e.target.value)} style={{ width: "100%", boxSizing: "border-box", padding: "8px 10px", border: "1px solid #C8CCD2", borderRadius: 4, fontSize: 14, marginTop: 3, marginBottom: 12 }} />
)}
); }; Object.assign(window, { RoSignaturePad, RoEsignModal, RoSignScreen, RO_AUTH_DISCLAIMER, roSignEstimate, roSignAuthText, roSignStoreName });