/* 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 — Scan Driver's License. // Reads the PDF417 barcode on the back of a US/CA driver's license, // parses the AAMVA fields, and saves the customer to the customer file. // ONE PATH: this is an INPUT METHOD for the CRM's New Lead → New Customer // form — it never creates a deal (app.jsx upserts the customer and fires // "k2b-customer-scanned"; the CRM selects that customer for the lead). // // • Camera mode — uses @zxing/library (loaded from CDN) to live-scan the barcode // • Upload mode — drop a photo of the back of the DL // • Manual mode — paste the raw barcode string OR type fields directly // // Customer numbers (RC30, cn): the phone WITHOUT the area code — 7 digits (CustNum.jsx.txt == the server's rule). The 10-digit phone is the contact number. const AAMVA_FIELDS = { DAA: "fullName", DAB: "lastName", DAC: "firstName", DAD: "middleName", DCS: "lastName", DAC2: "firstName", DCT: "firstName", DAD2: "middleName", DBA: "expiration", // YYYYMMDD or MMDDYYYY DBB: "dob", // YYYYMMDD or MMDDYYYY DBC: "sex", // 1=M, 2=F DBD: "issued", DAG: "street", DAH: "street2", DAI: "city", DAJ: "state", DAK: "postal", DAQ: "dlNumber", DCG: "country", DAU: "height", DAY: "eyes", DAZ: "hair", }; // Parse an AAMVA PDF417 string into a structured object. // Sample raw input: "@\n\u001eANSI 636026080002DL00410288ZV03290015DLDAQ..." function parseAAMVA(raw) { if (!raw || typeof raw !== "string") return null; const out = {}; // Lines may be separated by \n or by the AAMVA element delimiters. // Walk every "DXX" subfile pattern. const re = /(D[A-Z]{2})([^\n\r]*?)(?=D[A-Z]{2}|$)/g; let m; while ((m = re.exec(raw)) !== null) { const code = m[1]; const value = m[2].trim(); const key = AAMVA_FIELDS[code]; if (key && value && !out[key]) out[key] = value; } // Format DOB / expiration into a friendly form if it looks like YYYYMMDD or MMDDYYYY const fmt = (d) => { if (!d) return ""; if (/^\d{8}$/.test(d)) { // Could be YYYYMMDD (newer) or MMDDYYYY (older). Use the year heuristic. const a = d.slice(0, 4), b = d.slice(4, 6), c = d.slice(6, 8); if (+a > 1900 && +a < 2100) return `${b}/${c}/${a}`; // YYYYMMDD const yy = d.slice(4, 8), mm = d.slice(0, 2), dd = d.slice(2, 4); if (+yy > 1900 && +yy < 2100) return `${mm}/${dd}/${yy}`; // MMDDYYYY } return d; }; if (out.dob) out.dob = fmt(out.dob); if (out.expiration) out.expiration = fmt(out.expiration); if (out.issued) out.issued = fmt(out.issued); if (out.sex === "1") out.sex = "M"; if (out.sex === "2") out.sex = "F"; // If name fields are missing but fullName is present (older formats), split it. if (!out.lastName && out.fullName) { const parts = out.fullName.split(","); if (parts.length >= 2) { out.lastName = parts[0].trim(); const rest = parts[1].trim().split(/\s+/); out.firstName = rest[0] || ""; out.middleName = rest.slice(1).join(" "); } } return out; } // Lazy-load ZXing only when the user opens the scanner. let _zxingPromise = null; function loadZXing() { if (_zxingPromise) return _zxingPromise; _zxingPromise = new Promise((resolve, reject) => { if (window.ZXing) return resolve(window.ZXing); const s = document.createElement("script"); s.src = "https://unpkg.com/@zxing/library@0.21.3/umd/index.min.js"; s.onload = () => resolve(window.ZXing); s.onerror = () => reject(new Error("Could not load barcode scanner library.")); document.head.appendChild(s); }); return _zxingPromise; } // Phone → customer # (RC30, cn): the 7 digits WITHOUT the area code — the owner's rule ("the 7-digit number is the customer number"). // A leading US country code ("1 573 555 0123") is dropped first. "" when the phone is not 10 digits. const phoneTo10 = (p) => { let d = String(p || "").replace(/\D/g, ""); if (d.length === 11 && d[0] === "1") d = d.slice(1); return d.length === 10 ? d : ""; }; const phoneToCustNum = (p) => (window.CustNum ? (window.CustNum.custNumFromPhone(p).num7 || "") : phoneTo10(p).slice(-7)); const fmtPhone = (digits) => { const d = digits.replace(/\D/g, ""); if (d.length === 10) return `(${d.slice(0,3)}) ${d.slice(3,6)}-${d.slice(6)}`; return digits; }; const ScanDL = ({ onClose, onCreate }) => { const [tab, setTab] = React.useState("camera"); // camera | upload | manual const [parsed, setParsed] = React.useState(null); const [error, setError] = React.useState(""); const [busy, setBusy] = React.useState(false); const [phone, setPhone] = React.useState(""); // Camera scan state const videoRef = React.useRef(null); const readerRef = React.useRef(null); // Cleanup on unmount or tab change React.useEffect(() => () => { if (readerRef.current && readerRef.current.reset) readerRef.current.reset(); }, []); React.useEffect(() => { if (tab !== "camera") return; let cancelled = false; setError(""); setBusy(true); loadZXing() .then((Z) => { if (cancelled) return; const reader = new Z.BrowserMultiFormatReader(); readerRef.current = reader; return reader.decodeFromConstraints( { video: { facingMode: { ideal: "environment" } } }, videoRef.current, (result, err) => { if (result) { const text = result.getText(); const data = parseAAMVA(text); if (data && (data.lastName || data.firstName || data.dlNumber)) { setParsed(data); setPhone(""); reader.reset(); } } } ); }) .catch((e) => !cancelled && setError(e.message || "Camera unavailable.")) .finally(() => !cancelled && setBusy(false)); return () => { cancelled = true; if (readerRef.current && readerRef.current.reset) readerRef.current.reset(); }; }, [tab]); // File upload — decode the image const onFile = async (file) => { if (!file) return; setError(""); setBusy(true); try { const Z = await loadZXing(); const url = URL.createObjectURL(file); const reader = new Z.BrowserMultiFormatReader(); const result = await reader.decodeFromImageUrl(url); const text = result.getText(); const data = parseAAMVA(text); if (!data || (!data.lastName && !data.dlNumber)) { setError("Couldn't read the barcode. Try a sharper photo or use Manual."); } else { setParsed(data); } } catch (e) { setError("Couldn't decode the image. " + (e?.message || "Try Manual entry.")); } finally { setBusy(false); } }; // Manual paste of raw AAMVA text const [rawText, setRawText] = React.useState(""); const parseRaw = () => { const data = parseAAMVA(rawText); if (!data || (!data.lastName && !data.dlNumber)) { setError("Couldn't read AAMVA fields in that text."); } else { setError(""); setParsed(data); } }; // Manual form (typed) const [manualForm, setManualForm] = React.useState({ firstName: "", lastName: "", middleName: "", street: "", city: "", state: "", postal: "", dob: "", sex: "", dlNumber: "", expiration: "", }); const useManualForm = () => { if (!manualForm.firstName || !manualForm.lastName) { setError("First and last name are required."); return; } setParsed({ ...manualForm }); setError(""); }; const create = () => { if (!parsed) return; const p10 = phoneTo10(phone); const custNum = phoneToCustNum(phone); if (!p10 || !custNum) { const n = String(phone || "").replace(/\D/g, "").length; setError(`A 10-digit phone number is required here (its last 7 digits become the customer #) — you entered ${n} digit${n === 1 ? "" : "s"}. A customer with no phone is added in the CRM, where K2B issues a number.`); return; } onCreate({ custNum, ...parsed, phone: fmtPhone(p10), }); }; return (
{ e.currentTarget.__k2bDownOnBackdrop = (e.target === e.currentTarget); }} onClick={(e) => { const d = e.currentTarget.__k2bDownOnBackdrop; e.currentTarget.__k2bDownOnBackdrop = false; if (d && e.target === e.currentTarget) onClose && onClose(); }}>
e.stopPropagation()} style={{ width: 760, maxWidth: "92vw", maxHeight: "94vh", background: "var(--jb-surface)", border: "1px solid var(--jb-border)", borderRadius: "var(--jb-r-lg)", boxShadow: "var(--jb-shadow-lg)", overflow: "hidden", display: "flex", flexDirection: "column", }}> {/* Header */}
📷
Scan Driver's License
Reads the PDF417 barcode on the back of the DL and saves the customer — the CRM's New Lead picks them up.
{parsed ? ( /* ─── Confirmation step ────────────────────────────────── */
✓ License read. Review and add a phone number to create the customer.
📞
Phone number (its last 7 digits become the customer #)
The customer # is the phone without the area code — that is how the accounts stay straight.
setPhone(e.target.value)} placeholder="(573) 555-0123" autoFocus style={{ flex: 1, fontSize: 15 }} onKeyDown={(e) => e.key === "Enter" && create()} /> Customer # → {phoneToCustNum(phone) || "—"}
{error &&
{error}
}
{ setParsed(null); setPhone(""); setError(""); }}>← Re-scan Save Customer → CRM Lead ▸
) : ( /* ─── Capture step ─────────────────────────────────────── */ <> {/* Tabs */}
{[["camera", "📷 Camera"], ["upload", "🖼 Upload photo"], ["manual", "✎ Manual"]].map(([k, l]) => ( ))}
{tab === "camera" && (
{error && (
{error}
Tip: try Upload photo or Manual instead.
)}
)} {tab === "upload" && ( )} {tab === "manual" && ( )}
)}
); }; const Field = ({ label, value, multiline }) => (
{label}
{value || "—"}
); const UploadTab = ({ onFile, busy, error }) => { const fileRef = React.useRef(null); const [drag, setDrag] = React.useState(false); return (
fileRef.current && fileRef.current.click()} onDragOver={(e) => { e.preventDefault(); setDrag(true); }} onDragLeave={() => setDrag(false)} onDrop={(e) => { e.preventDefault(); setDrag(false); const file = e.dataTransfer.files && e.dataTransfer.files[0]; if (file) onFile(file); }} style={{ border: "2px dashed " + (drag ? "var(--jb-navy)" : "var(--jb-border-strong)"), background: drag ? "var(--jb-blue-tint)" : "var(--jb-surface-2)", borderRadius: "var(--jb-r-lg)", padding: "44px 20px", textAlign: "center", cursor: "pointer", }}>
🖼
{busy ? "Decoding…" : "Drop a photo of the back of the DL, or click to choose"}
Aim for a sharp photo with the full barcode visible. JPG/PNG.
onFile(e.target.files && e.target.files[0])} />
{error &&
{error}
}
); }; const ManualTab = ({ rawText, setRawText, parseRaw, manualForm, setManualForm, useManualForm, error }) => (
Paste AAMVA barcode text
If you've already scanned with an external reader, paste the raw output. The system will parse the AAMVA fields automatically.