/* 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 (
{ setMode("draw"); clear(); }} className="k2b-button small" style={mode === "draw" ? { background: "var(--jb-navy, #1B66A8)", color: "#fff", fontWeight: 700 } : {}}>✍ Draw
{ setMode("type"); clear(); }} className="k2b-button small" style={mode === "type" ? { background: "var(--jb-navy, #1B66A8)", color: "#fff", fontWeight: 700 } : {}}>⌨ Type
{ clear(); setTyped(""); }} className="k2b-button small">Clear
{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]) => (
{ setVia(v); if (es.fillMobile) setTo(v === "email" ? ((ro.cust && ro.cust.email) || "") : mobile); }} style={via === v ? { background: "var(--jb-navy, #1B66A8)", color: "#fff", fontWeight: 700 } : {}}>{l}
))}
{via === "text" ? "Mobile number" : "Email address"}
setTo(e.target.value)} autoComplete="off" name="k2b-esign-to" placeholder={via === "text" ? "(573) 555-0134" : "customer@email.com"}
style={{ padding: "8px 10px", border: "1px solid #C8CCD2", borderRadius: 4, fontSize: 13 }} />
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.
Signing link
Copy
window.open(link, "_blank")}>Open Customer Signing Screen ↗
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)}
setAgree(e.target.checked)} style={{ width: 18, height: 18, marginTop: 1 }} />
I have read and agree to the authorization above, and approve the estimated repairs.
Full name
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 }} />
Authorize Repairs
>
)}
);
};
Object.assign(window, { RoSignaturePad, RoEsignModal, RoSignScreen, RO_AUTH_DISCLAIMER, roSignEstimate, roSignAuthText, roSignStoreName });