/* 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. */ // K2B DMS — VIN look-up, shared by every screen (VIN Decode, Trade Appraisal, CRM trade, Stock-In, Trade-In Requests, Service RO, // Service appointment, Body Shop, Fulfillment Orders). // The browser NEVER decodes a VIN itself and never calls MarketCheck or NHTSA: every look-up goes to OUR server // (GET /api/vin/decode/:vin, GET /api/vin/recalls/:vin), which tries MarketCheck (a saved answer first), then NHTSA (the free // government decoder), then reads what it can from the VIN itself - and says which one answered. The server decides who may see // dollars (MSRP / options); this file never adds them. // Names here start with vl / Vl (every .jsx.txt shares ONE global scope). Registered on window.DMS.vin = { decode, recalls, fill, recent, remember }. const VL_RECENT_KEY = "jb_vin_recent"; // last 20 decodes on this station (F2-Lookup on the VIN Decode screen) const VL_RECENT_MAX = 20; const vlNormVin = (v) => String(v == null ? "" : v).toUpperCase().replace(/[\s-]+/g, ""); const vlIsVin = (v) => /^[A-HJ-NPR-Z0-9]{17}$/.test(vlNormVin(v)); const VL_TRANSLIT = { A: 1, B: 2, C: 3, D: 4, E: 5, F: 6, G: 7, H: 8, J: 1, K: 2, L: 3, M: 4, N: 5, P: 7, R: 9, S: 2, T: 3, U: 4, V: 5, W: 6, X: 7, Y: 8, Z: 9 }; const VL_WEIGHTS = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2]; // The same check-digit rule the server uses (a warning in the screen before the round trip; the server decides again). const vlCheckDigitOk = (v) => { const vin = vlNormVin(v); if (!vlIsVin(vin)) return false; let sum = 0; for (let i = 0; i < 17; i++) { const c = vin[i]; const val = /\d/.test(c) ? +c : VL_TRANSLIT[c]; if (val === undefined) return false; sum += val * VL_WEIGHTS[i]; } const r = sum % 11; return vin[8] === (r === 10 ? "X" : String(r)); }; const vlCall = (path) => { if (typeof apiFetch !== "function") return Promise.reject(Object.assign(new Error("offline"), { status: 0 })); return apiFetch("/vin" + path); }; const vlAge = (min, iso) => { let m = Number(min); if (!isFinite(m) || min == null) m = iso ? Math.round((Date.now() - new Date(iso).getTime()) / 60000) : NaN; if (!isFinite(m)) return ""; m = Math.max(0, m); if (m < 2) return "just now"; if (m < 90) return m + " min ago"; if (m < 36 * 60) return Math.round(m / 60) + " h ago"; return Math.round(m / 1440) + " d ago"; }; // Plain words for the person; never a status line, a file name or a stack trace. const vlErrText = (e, fallback) => { const b = (e && e.body) || {}; const t = String(b.error || ""); if (e && e.status === 0) return "The DMS server cannot be reached right now. Check the connection and try again."; if (b.code === "no_role" || e && e.status === 403) return t || "Your role does not have access to VIN look-ups. Ask a manager."; if (b.code === "bad_vin") return "A VIN has 17 letters and numbers (the letters I, O and Q are never used)."; if (b.code === "vehicle_unknown") return t; if (e && e.status === 404) return "VIN look-up is not on this server yet. Ask an administrator to install the update."; if (t && !/bash |\.sh\b|MARKETCHECK_|api_key|Error:|\bat \S+ \(/i.test(t)) return t; return fallback || "VIN look-up is not available right now. Try again in a minute."; }; const vlSourceLabel = (src) => (src === "marketcheck" ? "MarketCheck" : src === "nhtsa" ? "NHTSA (free government decoder)" : src === "local" ? "the VIN itself (partial)" : String(src || "")); // decode(vin, { confirm, paid }) -> the server answer { ok, vin, source, cached, partial, vehicle, marketcheck:{status,message}, warning, message }. // Rejects with an Error whose .text is plain English. const vlDecode = async (vin, opts) => { const v = vlNormVin(vin); if (!vlIsVin(v)) throw Object.assign(new Error("bad vin"), { text: "A VIN has 17 letters and numbers (the letters I, O and Q are never used).", code: "bad_vin" }); const qs = []; if (opts && opts.confirm) qs.push("confirm=1"); if (opts && opts.paid === false) qs.push("paid=0"); if (opts && opts.refresh) qs.push("refresh=1"); try { const r = await vlCall("/decode/" + v + (qs.length ? "?" + qs.join("&") : "")); if (!r || r.ok !== true) throw Object.assign(new Error("bad answer"), { text: "VIN look-up gave an answer that could not be read. Try again." }); return r; } catch (e) { if (e && e.text) throw e; throw Object.assign(new Error(e && e.message || "failed"), { text: vlErrText(e), code: e && e.body && e.body.code, status: e && e.status }); } }; // recalls(vin, { year, make, model }) -> { ok, vehicle, count, recalls:[{campaign, component, summary, consequence, remedy, reportDate, status}], note, message, cached, stale } const vlRecalls = async (vin, veh) => { const v = vlNormVin(vin); if (!vlIsVin(v)) throw Object.assign(new Error("bad vin"), { text: "A VIN has 17 letters and numbers (the letters I, O and Q are never used).", code: "bad_vin" }); const qs = []; ["year", "make", "model"].forEach(k => { const x = veh && veh[k]; if (x != null && String(x).trim()) qs.push(k + "=" + encodeURIComponent(String(x).trim())); }); try { const r = await vlCall("/recalls/" + v + (qs.length ? "?" + qs.join("&") : "")); if (!r || r.ok !== true) throw Object.assign(new Error("bad answer"), { text: "The recall look-up gave an answer that could not be read. Try again." }); return r; } catch (e) { if (e && e.text) throw e; throw Object.assign(new Error(e && e.message || "failed"), { text: vlErrText(e, "The recall look-up is not available right now. Try again in a minute."), code: e && e.body && e.body.code, status: e && e.status }); } }; // fill(form, decoded, typedKeys, keys, shape) -> { patch, filled:[{key, label, from, to}], skipped:[key] } // form the current form values (plain object) decoded the server's `vehicle` (or a vlDecode answer) // typedKeys a Set / array of form keys the PERSON typed - never overwritten // keys { formKey: decodeKey } (default: same names); shape { formKey: fn(value) } to reshape a value for that form // A field is filled only when it is blank (or holds what a previous decode put there) and the person has not typed it. const VL_LABEL = { year: "Year", make: "Make", model: "Model", trim: "Trim", body: "Body", engine: "Engine", cylinders: "Cylinders", displacement: "Displacement", transmission: "Transmission", drivetrain: "Drive", fuel: "Fuel", gvwr: "GVWR", doors: "Doors", plant: "Plant", exteriorColor: "Color", interiorColor: "Interior", ymm: "Vehicle", ymmt: "Vehicle" }; const vlPick = (dk, d) => { const t = (x) => (x == null || String(x).trim() === "" ? null : String(x).trim()); if (!d) return null; if (dk === "year") { const n = parseInt(d.year, 10); return n >= 1900 && n <= 2100 ? n : null; } if (dk === "make" || dk === "model" || dk === "trim" || dk === "body") return t(d[dk]) ? t(d[dk]).toUpperCase() : null; if (dk === "cylinders" || dk === "doors") { const n = parseInt(d[dk], 10); return n > 0 ? n : null; } // one-line description boxes ("2022 CHEVROLET SILVERADO 1500"): year + make + model (+ trim), only when the make or model is known if (dk === "ymm" || dk === "ymmt") { const parts = [vlPick("year", d), vlPick("make", d), vlPick("model", d)].concat(dk === "ymmt" ? [vlPick("trim", d)] : []).filter(Boolean); return vlPick("make", d) || vlPick("model", d) ? parts.join(" ") : null; } return t(d[dk]); }; // isBlank optional (formKey, value) -> true when the field counts as empty (a form's untouched default, say): it is filled and Undo puts the default back const vlFill = (form, decoded, typedKeys, keys, shape, wrote, isBlank) => { const veh = decoded && decoded.vehicle ? decoded.vehicle : decoded; const typed = typedKeys instanceof Set ? typedKeys : new Set(typedKeys || []); const map = keys || {}; const fks = Object.keys(map).length ? Object.keys(map) : Object.keys(VL_LABEL); const patch = {}, filled = [], skipped = []; fks.forEach(fk => { const dk = Object.keys(map).length ? map[fk] : fk; let val = vlPick(dk, veh); if (val == null) return; if (shape && shape[fk]) val = shape[fk](val); const cur = form ? form[fk] : undefined; const blank = cur == null || String(cur).trim() === "" || (dk === "year" && !(parseInt(cur, 10) > 0)) || (typeof isBlank === "function" && !!isBlank(fk, cur)); const ours = wrote && wrote[fk] !== undefined && String(cur) === String(wrote[fk]); // still exactly what the last decode put there const from = ours && wrote[fk + "|from"] !== undefined ? wrote[fk + "|from"] : cur; // Undo goes back to what the PERSON had, not to another VIN's value if (typed.has(fk) && !ours) { skipped.push(fk); return; } if (!blank && !ours) { skipped.push(fk); return; } // A field the last decode wrote that this VIN fills with the SAME value (a second Chevrolet, the same fuel or colour) is filled again, // not skipped: it now belongs to this VIN, so a screen that clears the previous VIN's leftovers keeps it and the note lists it. if (String(cur) === String(val) && !ours) return; patch[fk] = val; filled.push({ key: fk, label: VL_LABEL[dk] || fk, from, to: val }); }); return { patch, filled, skipped }; }; // Recent decodes on this station (localStorage; per browser, never sent anywhere). const vlRecent = () => { try { const a = JSON.parse(localStorage.getItem(VL_RECENT_KEY) || "[]"); return Array.isArray(a) ? a : []; } catch (e) { return []; } }; const vlRemember = (entry) => { if (!entry || !vlIsVin(entry.vin)) return; const vin = vlNormVin(entry.vin); const list = vlRecent().filter(x => x && vlNormVin(x.vin) !== vin); list.unshift({ vin, at: new Date().toISOString(), source: entry.source || "", label: String(entry.label || "").slice(0, 80), vehicle: entry.vehicle || null, recallCount: entry.recallCount == null ? null : entry.recallCount }); try { localStorage.setItem(VL_RECENT_KEY, JSON.stringify(list.slice(0, VL_RECENT_MAX))); } catch (e) {} }; const vlForget = (vin) => { const v = vlNormVin(vin); try { localStorage.setItem(VL_RECENT_KEY, JSON.stringify(vlRecent().filter(x => x && vlNormVin(x.vin) !== v))); } catch (e) {} }; // ── React hook: auto-fill a form from a VIN typed / pasted into it (Trade Appraisal, CRM trade) ────────────────────────── // cfg: { vin, enabled, keys:{ formKey: decodeKey }, read(), write(patch), typed: Set|fn, shape, isBlank(formKey, value) } // Decodes once the VIN is 17 valid characters (450 ms after the last keystroke), fills only blank fields the person has not typed, // remembers what it wrote so a later VIN replaces it and Undo can put the old values back. const VlUseVinFill = (cfg) => { const [stt, setStt] = React.useState({ phase: "idle" }); const R = React.useRef({}); R.current.cfg = cfg; const seq = React.useRef(0), doneVin = React.useRef(""), wrote = React.useRef({}); const vin = vlNormVin(cfg.vin); const ok = cfg.enabled !== false && vlIsVin(vin); const typedOf = () => { const t = R.current.cfg.typed; return typeof t === "function" ? t() : (t || new Set()); }; const run = React.useCallback(async (v, o) => { doneVin.current = v; const my = ++seq.current; const confirmed = !!(o && o.confirm); // the person ticked "VIN is correct": MarketCheck may be asked although the check digit fails setStt({ phase: "busy", vin: v }); let r = null, err = null; try { r = await vlDecode(v, { confirm: confirmed }); } catch (e) { err = e; } if (my !== seq.current || vlNormVin(R.current.cfg.vin) !== v) return; // the VIN changed while we waited const c = R.current.cfg; if (r) { const { patch, filled } = vlFill(c.read(), r, typedOf(), c.keys, c.shape, wrote.current, c.isBlank); if (filled.length) { c.write(patch); filled.forEach(f => { wrote.current[f.key] = f.to; wrote.current[f.key + "|from"] = f.from; }); } vlRemember({ vin: v, source: r.source, vehicle: r.vehicle, label: [r.vehicle && r.vehicle.year, r.vehicle && r.vehicle.make, r.vehicle && r.vehicle.model].filter(Boolean).join(" ") }); setStt({ phase: filled.length ? "filled" : (r.partial ? "partial" : "nothing"), vin: v, filled, source: r.source, cached: !!r.cached, partial: !!r.partial, how: r.cached ? "saved " + (vlAge(r.ageMinutes, r.fetchedAt) || "earlier") : "looked up just now", note: r.marketcheck && r.marketcheck.status !== "ok" && r.marketcheck.status !== "cached" ? r.marketcheck.message : null, warning: r.warning || null, message: r.message || "", cdOk: r.checkDigitOk !== false, confirmed }); return; } setStt({ phase: "msg", vin: v, text: err && err.text ? err.text : "VIN look-up is not available right now.", kind: err && /^(bad_vin)$/.test(String(err.code || "")) ? "warn" : "info" }); }, []); React.useEffect(() => { if (!ok) { if (doneVin.current && doneVin.current !== vin) { doneVin.current = ""; seq.current++; setStt({ phase: "idle" }); } return; } if (doneVin.current === vin) return; const t = setTimeout(() => { if (doneVin.current !== vin) run(vin); }, 450); return () => clearTimeout(t); }, [vin, ok]); const undo = () => { const c = R.current.cfg; if (!stt.filled) return; const cur = c.read(), back = {}; stt.filled.forEach(f => { if (String(cur[f.key]) === String(f.to)) { back[f.key] = f.from == null ? "" : f.from; delete wrote.current[f.key]; delete wrote.current[f.key + "|from"]; } }); // only fields untouched since c.write(back); const n = Object.keys(back).length; setStt({ phase: "undone", vin: stt.vin, count: n, kept: stt.filled.length - n }); }; const again = () => { if (ok) { doneVin.current = ""; run(vin); } }; const confirm = () => { if (ok) { doneVin.current = ""; run(vin, { confirm: true }); } }; // "VIN is correct": decode again, MarketCheck included return { stt, undo, again, confirm, onBlur: () => { if (ok && doneVin.current !== vin) run(vin); } }; }; // The one-line note under a VIN box: what was filled, from where, Undo. const VlVinNote = ({ fill, id }) => { const s = fill && fill.stt; if (!s || s.phase === "idle") return null; const box = (bg, bd, children, phase) => (