/* 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 (