/* 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 Network Solutions — Credit Application (CRAPP).
//
// ONE PATH: the customer record IS the credit application. This screen is
// bound to ONE customer from the shared ledger (DMS.customers(), keyed by
// custNum = the Customer #, RC30 (cn): the phone without the area code, 7 digits) and every field it shows is saved straight back
// onto that record with DMS.upsertCustomer — there is no separate
// "application" object floating around that has to be transferred.
//
// top level (server-encrypted at rest): ssn, dob, dl (dl mirrored to dlNumber)
// top level (plain): firstName, middleName, lastName, phone, email,
// street, city, state, postal/zip, dlState, dlExp
// nested `credit`: maritalStatus, dependents, residence{}, employment{},
// coBuyer{} | null, consent{}, updatedAt
//
// Customer resolution on open: jb_credit_cust (read-once) → the open deal
// (jb_fw_deal / jb_desk_deal) → its custNum → otherwise "Pick customer".
// Customers are CREATED in the CRM (+ New Lead) — never here.
//
// Lender routing goes through DMS.submitToLenders (decisions post back to
// DMS.lenderSubmissions); RouteOne through DMS.sendToRouteOne(dealId) after the
// deal gets a `creditApp` snapshot.
// Lender catalog — published programs only; live status comes from the feed.
const LENDERS = [
{ code: "GMF", name: "GM FINANCIAL", tier: "A", buyRt: 5.99 },
{ code: "EXET", name: "EXETER FINANCE", tier: "B", buyRt: 9.49 },
{ code: "ALLY", name: "ALLY FINANCIAL", tier: "A", buyRt: 6.49 },
{ code: "CHASE", name: "JPMORGAN CHASE", tier: "A+", buyRt: 5.79 },
{ code: "CAPONE",name: "CAPITAL ONE AUTO", tier: "A", buyRt: 6.24 },
{ code: "WLAKE", name: "WESTLAKE FINANCIAL", tier: "C", buyRt: 14.99 },
];
const ca$ = (n) => "$" + (n || 0).toLocaleString("en-US", { minimumFractionDigits: 0 });
const caDigits = (s) => String(s == null ? "" : s).replace(/\D/g, "");
const caNum = (v) => { const n = parseFloat(String(v == null ? "" : v).replace(/[^0-9.\-]/g, "")); return isFinite(n) ? n : 0; };
const caUp = (v) => String(v == null ? "" : v).toUpperCase();
const caFmtSSN = (s) => { const d = caDigits(s).slice(0, 9); return d.length === 9 ? `${d.slice(0, 3)}-${d.slice(3, 5)}-${d.slice(5)}` : d; };
const caFmtPhone = (s) => { const d = caDigits(s); return d.length === 10 ? `(${d.slice(0, 3)}) ${d.slice(3, 6)}-${d.slice(6)}` : String(s || ""); };
// Any date the file may hold (MM/DD/YYYY from a DL scan, or ISO) → YYYY-MM-DD for .
const caISODate = (s) => {
const t = String(s == null ? "" : s).trim();
let m = t.match(/^(\d{4})-(\d{1,2})-(\d{1,2})/);
if (m) return `${m[1]}-${m[2].padStart(2, "0")}-${m[3].padStart(2, "0")}`;
m = t.match(/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})$/);
if (m) return `${m[3]}-${m[1].padStart(2, "0")}-${m[2].padStart(2, "0")}`;
return t;
};
// Age from a MM/DD/YYYY (or YYYY-MM-DD) date of birth; "" when unparseable.
const caAgeFrom = (dob) => {
const s = String(dob || "").trim();
let y, m, d;
let mm = s.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
if (mm) { m = +mm[1]; d = +mm[2]; y = +mm[3]; }
else { mm = s.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/); if (mm) { y = +mm[1]; m = +mm[2]; d = +mm[3]; } }
if (!y || !m || !d) return "";
const now = new Date();
let age = now.getFullYear() - y;
if (now.getMonth() + 1 < m || (now.getMonth() + 1 === m && now.getDate() < d)) age--;
return age >= 0 && age < 130 ? String(age) : "";
};
// ─── the credit shape stored on the customer record ─────────────────────────
const caBlankResidence = () => ({ type: "", years: "", months: "", monthlyPayment: "", landlordOrMortgage: "",
prevStreet: "", prevCity: "", prevState: "", prevZip: "", prevYears: "" });
const caBlankEmployment = () => ({ employer: "", position: "", type: "", years: "", months: "", phone: "",
street: "", city: "", state: "", zip: "", grossMonthly: "", otherIncome: "", otherIncomeSource: "",
prevEmployer: "", prevPosition: "", prevYears: "" });
const caBlankCoBuyer = () => ({ firstName: "", lastName: "", relationship: "Spouse", phone: "", email: "", dob: "", ssn: "", dl: "", dlState: "",
street: "", city: "", state: "", zip: "", residenceType: "", years: "", employer: "", position: "", years2: "", grossMonthly: "" });
const caBlankConsent = () => ({ signed: false, at: "", by: "", method: "" });
const caBlankCredit = () => ({ maritalStatus: "", dependents: "", residence: caBlankResidence(), employment: caBlankEmployment(),
coBuyer: null, consent: caBlankConsent(), updatedAt: "" });
// Deep-fill whatever is on file into the full shape so every input is controlled.
const caNormCredit = (c) => {
const src = c && typeof c === "object" ? c : {};
const b = caBlankCredit();
return {
...b, ...src,
residence: { ...b.residence, ...(src.residence || {}) },
employment: { ...b.employment, ...(src.employment || {}) },
coBuyer: src.coBuyer ? { ...caBlankCoBuyer(), ...src.coBuyer } : null,
consent: { ...b.consent, ...(src.consent || {}) },
};
};
// Customer record → editable form state.
const caFormFromCustomer = (c) => {
const cust = c || {};
return {
firstName: caUp(cust.firstName), middleName: caUp(cust.middleName), lastName: caUp(cust.lastName),
dob: caISODate(cust.dob), ssn: caDigits(cust.ssn).slice(0, 9),
dl: String(cust.dl || cust.dlNumber || ""), dlState: caUp(cust.dlState || ""), dlExp: caISODate(cust.dlExp),
phone: String(cust.phone || cust.cell || ""), email: String(cust.email || ""), // RC30 (cn): a customer # is not a phone
street: caUp(cust.street), city: caUp(cust.city), state: caUp(cust.state), zip: String(cust.postal || cust.zip || ""),
credit: caNormCredit(cust.credit),
};
};
// Form state → the record we hand to DMS.upsertCustomer (merge). Top-level
// ssn / dob / dl are the server-encrypted fields; dl is mirrored to dlNumber
// so every existing reader (Customers, Title, Desking) keeps working.
const caRecordFromForm = (custNum, f) => ({
custNum,
firstName: caUp(f.firstName).trim(), middleName: caUp(f.middleName).trim(), lastName: caUp(f.lastName).trim(),
phone: f.phone || "", email: String(f.email || "").trim(), // RC30 (cn): never the customer # as the phone
street: caUp(f.street).trim(), city: caUp(f.city).trim(), state: caUp(f.state).trim().slice(0, 2),
postal: String(f.zip || "").trim(), zip: String(f.zip || "").trim(),
dob: caISODate(f.dob), ssn: caDigits(f.ssn).slice(0, 9),
dl: String(f.dl || "").trim().toUpperCase(), dlNumber: String(f.dl || "").trim().toUpperCase(),
dlState: caUp(f.dlState).trim().slice(0, 2), dlExp: caISODate(f.dlExp),
credit: { ...caNormCredit(f.credit), updatedAt: new Date().toISOString() },
});
// Application completeness — shared with Customers / CRM / Desking ("Credit app 70%").
const jbCreditCompleteness = (cust) => {
const c = cust || {};
const cr = c.credit || {};
const res = cr.residence || {}, emp = cr.employment || {}, con = cr.consent || {};
const checks = [
["Name", !!(String(c.firstName || "").trim() && String(c.lastName || "").trim())],
["Date of birth", !!String(c.dob || "").trim()],
["SSN", caDigits(c.ssn).length === 9],
["Driver's license",!!String(c.dl || c.dlNumber || "").trim()],
["Address", !!(c.street && c.city && c.state && (c.postal || c.zip))],
["Residence type", !!res.type],
["Employer", !!String(emp.employer || "").trim() || /retired/i.test(String(emp.type || ""))],
["Income", caNum(emp.grossMonthly) > 0 || caNum(emp.otherIncome) > 0],
["Credit inquiry consent", !!con.signed],
];
const missing = checks.filter(x => !x[1]).map(x => x[0]);
return { pct: Math.round(((checks.length - missing.length) / checks.length) * 100), missing, total: checks.length };
};
const caGet = (obj, path) => path.split(".").reduce((o, k) => (o == null ? undefined : o[k]), obj);
const caSet = (obj, path, v) => {
const ks = path.split("."); const out = { ...obj }; let cur = out;
for (let i = 0; i < ks.length - 1; i++) { cur[ks[i]] = { ...(cur[ks[i]] || {}) }; cur = cur[ks[i]]; }
cur[ks[ks.length - 1]] = v; return out;
};
// ─── field primitives (era look) ─────────────────────────────────────────────
// Audit P2-3: a field is either a REAL controlled input (set writes state) or
// explicitly readOnly + dimmed (computed value).
const CAField = ({ label, value, set, w = "100%", maxLength, hot, readOnly, onBlur, type = "text", placeholder, inputMode, miss }) => (
{label}{readOnly ? " (auto)" : ""}{miss ? * : null}
set(e.target.value)}
onBlur={readOnly ? undefined : onBlur}
style={{
background: readOnly ? "#E6E6E6" : hot ? "var(--k2b-yellow)" : "#FFFFE1",
border: "1px solid " + (miss ? "#B26A00" : "#808080"), padding: "1px 4px",
color: readOnly ? "#666" : "var(--k2b-navy)",
fontFamily: "'Courier Prime', monospace", fontSize: 13,
width: w, boxSizing: "border-box", minWidth: 0,
}}
/>
);
const CASel = ({ label, value, set, options, onBlur, miss }) => (
{label}{miss ? * : null}
set(e.target.value)} onBlur={onBlur}
style={{ background: "#FFFFE1", border: "1px solid " + (miss ? "#B26A00" : "#808080"), padding: "0 2px",
color: "var(--k2b-navy)", fontFamily: "'Courier Prime', monospace", fontSize: 13, height: 23 }}>
{options.map(o => {o === "" ? "— select —" : o} )}
);
// SSN entry: typed as 9 digits, stored as digits, masked on screen unless
// revealed (the reveal is audited like JbSecret's 20-second peek).
const CASSN = ({ label = "SSN", value, set, onBlur, miss, auditLabel }) => {
const [show, setShow] = React.useState(false);
const timer = React.useRef(null);
React.useEffect(() => () => { if (timer.current) clearTimeout(timer.current); }, []);
const toggle = () => {
if (show) { setShow(false); if (timer.current) clearTimeout(timer.current); return; }
setShow(true);
try { window.DMS && window.DMS.logAudit && window.DMS.logAudit("PII Revealed", "ssn · " + (auditLabel || "credit app")); } catch (e) {}
timer.current = setTimeout(() => setShow(false), 20000);
};
const d = caDigits(value).slice(0, 9);
return (
{label}{miss ? * : null}
{d.length ? d.length + "/9" : ""}
set(caDigits(e.target.value).slice(0, 9))} onBlur={onBlur}
style={{ background: "#FFFFE1", border: "1px solid " + (miss ? "#B26A00" : "#808080"), padding: "1px 4px",
color: "var(--k2b-navy)", fontFamily: "'Courier Prime', monospace", fontSize: 13, flex: 1, minWidth: 0, boxSizing: "border-box" }} />
{show ? "🙈" : "👁"}
);
};
const CASection = ({ title, right, children, id }) => (
);
// Customer picker (inline when nothing is bound; modal for "Change customer").
const CACustPicker = ({ onPick, compact }) => {
const [q, setQ] = React.useState("");
const all = Object.values((window.DMS && window.DMS.customers && window.DMS.customers()) || {});
const needle = q.trim().toLowerCase(), nd = caDigits(q);
const list = all.filter(c => !needle
|| (window.DMS && window.DMS.custQueryHit ? window.DMS.custQueryHit(q, c.custNum, [c.phone, c.cell]) : false) // RC31 (fx31, F6): an old number / a phone in any format
|| (c.lastName || "").toLowerCase().includes(needle) || (c.firstName || "").toLowerCase().includes(needle)
|| (nd && ((c.custNum || "").includes(nd) || caDigits(c.phone).includes(nd))))
.sort((a, b) => (b.saved || "").localeCompare(a.saved || "")).slice(0, 60);
return (
setQ(e.target.value)} placeholder="Search by phone or name…"
style={{ background: "#FFFFE1", border: "1px solid #808080", padding: "3px 6px", fontFamily: "'Courier Prime', monospace", fontSize: 13 }} />
Customers are created in the CRM (+ New Lead). {all.length} on file.
{!list.length ?
No customers match.
: (
Name
Customer #
City
Credit app
{list.map((c, i) => {
const cc = jbCreditCompleteness(c);
return (
onPick(c.custNum)}>
{[c.lastName, c.firstName].filter(Boolean).join(", ") || "—"}
{c.custNum}{c.phone ? · {c.phone} : null}
{[c.city, c.state].filter(Boolean).join(", ")}
= 80 ? "var(--k2b-green)" : "#888" }}>{cc.pct >= 80 ? "✓ " : "○ "}{cc.pct}%
);
})}
)}
);
};
// Which deal does this customer's credit app belong to? The open F&I deal
// when it is theirs, else their newest unposted deal.
const caDealFor = (custNum) => {
const D = window.DMS; if (!D || !custNum) return null;
const deals = (D.deals && D.deals()) || [];
let fw = null; try { fw = localStorage.getItem("jb_fw_deal"); } catch (e) {}
const open = fw && deals.find(d => d.id === fw);
if (open && open.custNum === custNum) return open;
return deals.find(d => d.custNum === custNum && !d.postedToGL) || deals.find(d => d.custNum === custNum) || null;
};
// ─── the bound application form ─────────────────────────────────────────────
const CreditAppForm = ({ custNum, go, onClose, onChangeCustomer }) => {
const D = window.DMS;
const cust0 = (D && D.customers && D.customers()[custNum]) || { custNum };
const [form, setForm] = React.useState(() => caFormFromCustomer(cust0));
const [selectedLenders, setSelectedLenders] = React.useState(["GMF", "EXET", "ALLY", "CAPONE"]);
const [bureausPulled, setBureausPulled] = React.useState({ experian: false, equifax: false, transunion: false });
const [lastSaved, setLastSaved] = React.useState(cust0.saved || "");
const dirty = React.useRef(false);
const formRef = React.useRef(form); formRef.current = form;
const timer = React.useRef(null);
const restricted = !!(D && D.isRestricted && D.isRestricted());
// Live ledger (lender feed / deal changes) — re-render, form state stays local.
const [, force] = React.useReducer(n => n + 1, 0);
React.useEffect(() => (D && D.subscribe) ? D.subscribe(force) : undefined, []);
const record = caRecordFromForm(custNum, form);
const done = jbCreditCompleteness(record);
const missing = new Set(done.missing);
const deal = caDealFor(custNum);
// Save = the customer record. Silent auto-save (debounced) on every blur;
// explicit F10 save toasts once + audits.
const save = (explicit) => {
if (timer.current) { clearTimeout(timer.current); timer.current = null; }
if (!D || !D.upsertCustomer) { if (explicit) window.k2bToast && window.k2bToast("Ledger offline — cannot save.", "warn"); return { ok: false }; }
const rec = caRecordFromForm(custNum, formRef.current);
const r = D.upsertCustomer(rec);
if (r && r.ok) { dirty.current = false; setLastSaved(new Date().toISOString()); }
if (explicit) {
if (r && r.ok) { D.logAudit && D.logAudit("Credit App Saved", custNum); window.k2bToast && window.k2bToast(`Credit application saved — ${rec.lastName}, ${rec.firstName} (${jbCreditCompleteness(rec).pct}% complete).`, "ok", 2600); }
else window.k2bToast && window.k2bToast((r && r.msg) || "Save failed.", "warn");
}
return r;
};
const scheduleSave = () => {
if (!dirty.current) return;
if (timer.current) clearTimeout(timer.current);
timer.current = setTimeout(() => save(false), 800);
};
// Nothing is lost when the screen is left mid-entry.
React.useEffect(() => () => { if (timer.current) clearTimeout(timer.current); if (dirty.current) save(false); }, []);
React.useEffect(() => {
const onKey = (e) => {
if (e.key === "F10" || (e.key.toLowerCase() === "s" && (e.ctrlKey || e.metaKey))) { e.preventDefault(); save(true); }
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, []);
const set = (path, up) => (v) => { dirty.current = true; setForm(f => caSet(f, path, up ? caUp(v) : v)); };
const setBool = (path) => (v) => { dirty.current = true; setForm(f => caSet(f, path, v)); };
const blur = scheduleSave;
// Field factories — every one is a real controlled input bound to the record.
const F = (label, path, o = {}) => ;
const S = (label, path, options, o = {}) => ;
const ST = ["", "AL","AK","AZ","AR","CA","CO","CT","DE","FL","GA","HI","ID","IL","IN","IA","KS","KY","LA","ME","MD","MA","MI","MN","MS","MO","MT","NE","NV","NH","NJ","NM","NY","NC","ND","OH","OK","OR","PA","RI","SC","SD","TN","TX","UT","VT","VA","WA","WV","WI","WY"];
const res = form.credit.residence, emp = form.credit.employment, co = form.credit.coBuyer, con = form.credit.consent;
const resShort = caNum(res.years) < 2;
const empShort = caNum(emp.years) < 2 && !/retired/i.test(emp.type || "");
const nameLF = [record.lastName, record.firstName].filter(Boolean).join(", ") || "(unnamed)";
// FICO from the deal record (bureau pull) — never a hard-coded score.
const fico = deal ? (+(deal.fico || deal.creditScore || 0) || null) : null;
const ficoText = fico == null ? "—" : String(fico);
const ficoColor = fico == null ? "#888" : fico >= 720 ? "var(--k2b-green)" : fico >= 660 ? "var(--k2b-blue-data-dk)" : fico >= 600 ? "#B26A00" : "var(--k2b-maroon)";
// Lender feed for THIS deal (or this buyer when the deal has none yet).
const feed = (D && D.lenderSubmissions) ? D.lenderSubmissions() : [];
const feedByCode = {};
feed.filter(s => (deal && s.deal === deal.id) || (!deal && s.buyer === nameLF)).forEach(s => { if (!feedByCode[s.lenderCode]) feedByCode[s.lenderCode] = s; });
const sentCount = Object.values(feedByCode).length;
const pendingCount = Object.values(feedByCode).filter(s => s.status === "SUBMITTED").length;
const consentGate = () => {
if (!con.signed) { window.k2bToast && window.k2bToast("Applicant must authorize the credit inquiry first (Consent section).", "warn", 3600); return false; }
if (done.pct < 80) { window.k2bToast && window.k2bToast(`Application is ${done.pct}% complete — missing: ${done.missing.join(", ")}.`, "warn", 4200); return false; }
return true;
};
const snapshotToDeal = () => {
if (!deal || !D.updateDeal) return null;
const rec = caRecordFromForm(custNum, formRef.current);
const cr = JSON.parse(JSON.stringify(rec.credit));
// Co-buyer identifiers are not server-encrypted on the deal — keep last-4 only in the snapshot.
if (cr.coBuyer) { cr.coBuyer.ssn = cr.coBuyer.ssn ? "***-**-" + caDigits(cr.coBuyer.ssn).slice(-4) : ""; cr.coBuyer.dl = cr.coBuyer.dl ? "****" + String(cr.coBuyer.dl).slice(-4) : ""; }
const creditApp = { custNum, name: nameLF, firstName: rec.firstName, middleName: rec.middleName, lastName: rec.lastName,
street: rec.street, city: rec.city, state: rec.state, zip: rec.zip, phone: rec.phone, email: rec.email,
dlState: rec.dlState, ssnLast4: rec.ssn.slice(-4), completeness: jbCreditCompleteness(rec).pct,
...cr, submittedAt: new Date().toISOString(), submittedBy: (D.userId && D.userId()) || "" };
D.updateDeal(deal.id, { creditApp });
return creditApp;
};
const submitSelected = () => {
if (!D || !D.submitToLenders) { window.k2bToast && window.k2bToast("Ledger offline — cannot route.", "warn"); return; }
if (!consentGate()) return;
save(false);
const L = LENDERS.filter(x => selectedLenders.includes(x.code));
const bo = (!deal && window.loadJbForm && window.loadJbForm("buyersOrder")) || {};
snapshotToDeal();
const r = D.submitToLenders({
deal: deal ? deal.id : "", buyer: nameLF,
vehicle: deal ? (deal.vehicle || "") : (bo.yrMkMdl || ""), stock: deal ? (deal.stock || "") : (bo.stockNum || ""),
amount: deal ? (+deal.amtFinanced || +deal.salePrice || 0) : (parseFloat(bo.salePrice) || 0),
fico: fico || 0, lenders: L,
});
window.k2bToast && window.k2bToast(r.msg, r.ok ? "ok" : "warn", 3200);
};
const sendRouteOne = () => {
if (!D || !D.sendToRouteOne) return;
if (!deal) { window.k2bToast && window.k2bToast("No deal for this customer — start it in the CRM (+ New Lead → Desk It) first.", "warn", 4200); return; }
if (!consentGate()) return;
save(false);
snapshotToDeal();
const r = D.sendToRouteOne(deal.id);
window.k2bToast && window.k2bToast(r.msg, r.ok ? "ok" : "warn", 4600);
};
const printApp = () => {
const rec = caRecordFromForm(custNum, formRef.current);
const cr = rec.credit, R = cr.residence, E = cr.employment, C = cr.coBuyer;
const cell = (lab, val) => ``;
const row = (...cells) => `${cells.join("")}
`;
const html = `${window.dealerHeadHtml ? window.dealerHeadHtml() : ""}
CREDIT APPLICATION
For the financing of a motor vehicle · Customer # ${custNum}${deal ? " · Deal " + deal.id : ""} · ECOA notice on reverse
Applicant
${row(cell("Last / First / MI", [rec.lastName, rec.firstName, rec.middleName].filter(Boolean).join(" / ")), cell("Date of Birth", rec.dob), cell("Marital Status", cr.maritalStatus), cell("Dependents", cr.dependents))}
${row(cell("SSN", rec.ssn ? "•••-••-" + rec.ssn.slice(-4) + " (on file)" : ""), cell("DL # / State / Exp", [rec.dl, rec.dlState, rec.dlExp].filter(Boolean).join(" / ")), cell("Phone", caFmtPhone(rec.phone)), cell("Email", rec.email))}
Residence
${row(cell("Street / City / State / Zip", [rec.street, rec.city, rec.state, rec.zip].filter(Boolean).join(", ")), cell("Own / Rent", R.type), cell("Time there", [R.years && R.years + " yr", R.months && R.months + " mo"].filter(Boolean).join(" ")), cell("Monthly payment", R.monthlyPayment && ca$(caNum(R.monthlyPayment))))}
${row(cell("Landlord / Mortgage", R.landlordOrMortgage), cell("Previous address", [R.prevStreet, R.prevCity, R.prevState, R.prevZip].filter(Boolean).join(", ")), cell("Years there", R.prevYears))}
Employment & Income
${row(cell("Employer", E.employer), cell("Position", E.position), cell("Type", E.type), cell("Time there", [E.years && E.years + " yr", E.months && E.months + " mo"].filter(Boolean).join(" ")))}
${row(cell("Employer phone", caFmtPhone(E.phone)), cell("Employer address", [E.street, E.city, E.state, E.zip].filter(Boolean).join(", ")), cell("Gross monthly", E.grossMonthly && ca$(caNum(E.grossMonthly))), cell("Other income / source", [E.otherIncome && ca$(caNum(E.otherIncome)), E.otherIncomeSource].filter(Boolean).join(" · ")))}
${row(cell("Previous employer", E.prevEmployer), cell("Position", E.prevPosition), cell("Years", E.prevYears))}
${C ? `Co-Buyer
${row(cell("Name / Relationship", [[C.lastName, C.firstName].filter(Boolean).join(", "), C.relationship].filter(Boolean).join(" · ")), cell("DOB", C.dob), cell("SSN", C.ssn ? "•••-••-" + caDigits(C.ssn).slice(-4) : ""), cell("DL / State", [C.dl, C.dlState].filter(Boolean).join(" / ")))}
${row(cell("Address", [C.street, C.city, C.state, C.zip].filter(Boolean).join(", ")), cell("Own / Rent · Years", [C.residenceType, C.years].filter(Boolean).join(" · ")), cell("Employer / Position", [C.employer, C.position].filter(Boolean).join(" · ")), cell("Years · Gross monthly", [C.years2, C.grossMonthly && ca$(caNum(C.grossMonthly))].filter(Boolean).join(" · ")))}
` : ""}
${deal ? `Vehicle
${row(cell("Year / Make / Model", deal.vehicle || ""), cell("VIN / Stock", [deal.vin, deal.stock].filter(Boolean).join(" / ")), cell("Sale price", deal.salePrice ? ca$(+deal.salePrice) : ""), cell("Amount financed", deal.amtFinanced ? ca$(+deal.amtFinanced) : ""))}
` : ""}
Authorization
I authorize the dealer and any lender to which this application is submitted to obtain consumer reports for the purpose of evaluating this application. I certify the information given is true and complete.
${cr.consent && cr.consent.signed ? ` Authorized ${cr.consent.method || ""} ${cr.consent.at ? new Date(cr.consent.at).toLocaleString() : ""}${cr.consent.by ? " · taken by " + cr.consent.by : ""}. ` : ""}
Applicant Signature · Date
Co-Buyer Signature · Date
`;
window.k2bPrint && window.k2bPrint(html, { title: "Credit Application — " + nameLF });
};
const goFinance = () => {
// ONE PATH: navigate to the deal the desk pushed — never create one here.
let id = null; try { id = localStorage.getItem("jb_fw_deal"); } catch (e) {}
if (!id && deal && D.dealStage && D.dealStageIndex(deal) >= 1) { id = deal.id; try { localStorage.setItem("jb_fw_deal", id); } catch (e) {} }
if (!id) { window.k2bToast && window.k2bToast("No deal open — start in the CRM and Desk It, then push it to Finance.", "warn", 3800); return; }
save(false);
go && go("finance");
};
const scrollTo = (id) => { const el = document.getElementById(id); if (el && el.scrollIntoView) el.scrollIntoView({ behavior: "smooth", block: "start" }); };
const grid4 = { display: "grid", gridTemplateColumns: "repeat(4, minmax(0, 1fr))", gap: 8 };
const sub = (t) => {t}
;
const note = (t) => {t}
;
return (
{/* identity strip + completeness meter */}
Credit Application —
{nameLF}
· Customer # {custNum}
{deal ?
· Deal {deal.id} {deal.vehicle ? ({deal.vehicle}) : null}
:
· no deal yet — go && go("crm")}>CRM → Desk It }
Change customer
FICO:
{ficoText}
{lastSaved ? "saved " + new Date(lastSaved).toLocaleTimeString() : "not saved yet"}
Application completeness
= 80 ? "var(--k2b-green)" : done.pct >= 50 ? "#E0B400" : "#B26A00", transition: "width .2s" }} />
= 80 ? "var(--k2b-green)" : "var(--k2b-maroon)" }}>{done.pct}%
{done.missing.length ? "Missing: " + done.missing.join(" · ") : "✓ Ready to submit"}
{F("FIRST NAME", "firstName", { up: true, hot: true, miss: missing.has("Name") && !form.firstName })}
{F("MIDDLE", "middleName", { up: true })}
{F("LAST NAME", "lastName", { up: true, hot: true, miss: missing.has("Name") && !form.lastName })}
{F("PHONE", "phone", {})}
{F("DATE OF BIRTH", "dob", { type: "date", miss: missing.has("Date of birth") })}
{F("AGE", "x", { readOnly: true, value: caAgeFrom(form.dob) })}
{S("MARITAL STATUS", "credit.maritalStatus", ["", "Single", "Married", "Separated", "Unmarried"])}
{F("DL #", "dl", { up: true, miss: missing.has("Driver's license") })}
{S("DL STATE", "dlState", ST)}
{F("DL EXPIRATION", "dlExp", { type: "date" })}
{F("DEPENDENTS", "credit.dependents", { inputMode: "numeric", maxLength: 2 })}
{F("EMAIL", "email", {})}
{note(`SSN, date of birth and DL # are encrypted at rest on the server. Customer # ${custNum} never changes (the phone without the area code; only the owner can renumber).`)}
{F("STREET ADDRESS", "street", { up: true, miss: missing.has("Address") && !form.street })}
{F("CITY", "city", { up: true, miss: missing.has("Address") && !form.city })}
{S("STATE", "state", ST, { miss: missing.has("Address") && !form.state })}
{F("ZIP", "zip", { maxLength: 10, miss: missing.has("Address") && !form.zip })}
{S("OWN / RENT", "credit.residence.type", ["", "Own", "Rent", "Family", "Other"], { miss: missing.has("Residence type") })}
{F("YEARS THERE", "credit.residence.years", { inputMode: "numeric", maxLength: 2 })}
{F("MONTHS", "credit.residence.months", { inputMode: "numeric", maxLength: 2 })}
{F("MONTHLY PAYMENT", "credit.residence.monthlyPayment", { inputMode: "decimal" })}
{F("LANDLORD / MORTGAGE CO.", "credit.residence.landlordOrMortgage", { up: true })}
{resShort ? <>
{sub("PREVIOUS RESIDENCE (less than 2 years at current)")}
{F("PREV STREET", "credit.residence.prevStreet", { up: true })}
{F("PREV CITY", "credit.residence.prevCity", { up: true })}
{S("PREV STATE", "credit.residence.prevState", ST)}
{F("PREV ZIP", "credit.residence.prevZip", { maxLength: 10 })}
{F("YEARS THERE", "credit.residence.prevYears", { inputMode: "numeric", maxLength: 2 })}
> : null}
{F("EMPLOYER", "credit.employment.employer", { up: true, hot: true, miss: missing.has("Employer") })}
{F("POSITION / TITLE", "credit.employment.position", { up: true })}
{S("TYPE", "credit.employment.type", ["", "Full-time", "Part-time", "Self-employed", "Retired", "Other"])}
{F("EMPLOYER PHONE", "credit.employment.phone", { inputMode: "tel" })}
{F("YEARS THERE", "credit.employment.years", { inputMode: "numeric", maxLength: 2 })}
{F("MONTHS", "credit.employment.months", { inputMode: "numeric", maxLength: 2 })}
{F("GROSS MONTHLY INCOME", "credit.employment.grossMonthly", { hot: true, inputMode: "decimal", miss: missing.has("Income") })}
{F("ANNUAL", "x2", { readOnly: true, value: caNum(emp.grossMonthly) ? ca$(Math.round(caNum(emp.grossMonthly) * 12)) : "" })}
{F("EMPLOYER STREET", "credit.employment.street", { up: true })}
{F("EMPLOYER CITY", "credit.employment.city", { up: true })}
{S("EMPLOYER STATE", "credit.employment.state", ST)}
{F("EMPLOYER ZIP", "credit.employment.zip", { maxLength: 10 })}
{F("OTHER INCOME / MO", "credit.employment.otherIncome", { inputMode: "decimal" })}
{F("OTHER INCOME SOURCE", "credit.employment.otherIncomeSource", { up: true })}
{note("Alimony, child support or separate maintenance income need not be revealed unless you wish it considered.")}
{empShort ? <>
{sub("PREVIOUS EMPLOYER (less than 2 years at current)")}
{F("PREV EMPLOYER", "credit.employment.prevEmployer", { up: true })}
{F("POSITION", "credit.employment.prevPosition", { up: true })}
{F("YEARS THERE", "credit.employment.prevYears", { inputMode: "numeric", maxLength: 2 })}
> : null}
{ dirty.current = true; setForm(f => caSet(f, "credit.coBuyer", e.target.checked ? caBlankCoBuyer() : null)); scheduleSave(); }} />
Add co-buyer
}>
{!co ? No co-buyer on this application. Married applicants in MO: spouse income may be included if available for repayment.
: (
{F("FIRST NAME", "credit.coBuyer.firstName", { up: true, hot: true })}
{F("LAST NAME", "credit.coBuyer.lastName", { up: true, hot: true })}
{S("RELATIONSHIP", "credit.coBuyer.relationship", ["Spouse", "Parent", "Child", "Sibling", "Partner", "Other"])}
{F("PHONE", "credit.coBuyer.phone", { inputMode: "tel" })}
{F("DATE OF BIRTH", "credit.coBuyer.dob", { type: "date" })}
{F("DL #", "credit.coBuyer.dl", { up: true })}
{S("DL STATE", "credit.coBuyer.dlState", ST)}
{F("EMAIL", "credit.coBuyer.email", {})}
{F("STREET", "credit.coBuyer.street", { up: true })}
{F("CITY", "credit.coBuyer.city", { up: true })}
{S("STATE", "credit.coBuyer.state", ST)}
{F("ZIP", "credit.coBuyer.zip", { maxLength: 10 })}
{S("OWN / RENT", "credit.coBuyer.residenceType", ["", "Own", "Rent", "Family", "Other"])}
{F("YEARS THERE", "credit.coBuyer.years", { inputMode: "numeric", maxLength: 2 })}
{F("EMPLOYER", "credit.coBuyer.employer", { up: true })}
{F("POSITION", "credit.coBuyer.position", { up: true })}
{F("YEARS THERE", "credit.coBuyer.years2", { inputMode: "numeric", maxLength: 2 })}
{F("GROSS MONTHLY", "credit.coBuyer.grossMonthly", { inputMode: "decimal" })}
{note("Note: co-buyer SSN / DOB / DL live inside the credit record and are not server-encrypted at rest — masked on screen only.")}
)}
{
dirty.current = true;
const on = e.target.checked;
setForm(f => caSet(f, "credit.consent", on
? { signed: true, at: new Date().toISOString(), by: (D && D.userId && D.userId()) || "", method: f.credit.consent.method || "In person" }
: { ...caBlankConsent(), method: f.credit.consent.method }));
scheduleSave();
}} />
Applicant authorizes credit inquiry. The applicant authorizes the dealer and the lenders it selects to obtain consumer credit reports and verify the information on this application (ECOA / FCRA).
{S("METHOD", "credit.consent.method", ["In person", "Phone", "Online", "E-sign"])}
{con.signed ? `Signed ${con.at ? new Date(con.at).toLocaleString() : ""}${con.by ? " · taken by " + con.by : ""}` : "Not yet authorized — required before lender submission."}
{deal ? `Deal ${deal.id} · ${deal.vehicle || ""}` : "No deal — lender routing needs a desked deal"}}>
{restricted ? (
Lender submission and RouteOne are handled by a manager or F&I — save the application and let them know it's ready ({done.pct}% complete).
) : (
Bureau Pull
{["experian", "equifax", "transunion"].map(b => (
setBureausPulled(s => ({ ...s, [b]: e.target.checked }))} />
{b.toUpperCase()}
))}
FICO (mid)
{ficoText}
Score is recorded on the deal when the bureau replies.
Submission Status
{deal && deal.routeOne ?
RouteOne {deal.routeOne.ref} · {deal.routeOne.status}
: null}
{sentCount
? <>Routed to
{sentCount} lender{sentCount !== 1 ? "s" : ""} ·{" "}
{pendingCount ?
{pendingCount} pending… :
all decisions in }>
: <>Not yet submitted. Route to {selectedLenders.length} selected lender{selectedLenders.length !== 1 ? "s" : ""}.>}
F5-Submit Selected Lenders
⇪ Send to RouteOne
)}
{/* F-keys */}
{ save(false); onClose && onClose(); }}>ESC-Close
alert("Credit App (CR)\n\nThe customer record IS the credit application — every field saves to the customer file (auto-save on blur).\n\nESC Close\nF2 Change customer\nF3 Co-buyer section\nF5 Submit to lenders\nF6 Print application\nF7 Back to Desking\nF8 MO Title App\nF9 Finance Deal\nF10 Save now (Ctrl+S)")}>F1-Help
F2-Change Cust
{ if (!co) { dirty.current = true; setForm(f => caSet(f, "credit.coBuyer", caBlankCoBuyer())); } setTimeout(() => scrollTo("ca-cobuyer"), 50); }}>F3-Co-Buyer
{!restricted &&
scrollTo("ca-submit")}>F5-Submit Lenders }
🖨 F6-Print App
{ save(false); go && go("desking"); }}>F7-Back to Desk
{ save(false); go && go("titlemo"); }}>F8-MO Title App
F9-Finance Deal ▸
save(true)}>F10-Save
);
};
// ─── screen: resolve the customer, then render the bound form ──────────────
const CreditApp = ({ onClose, go }) => {
const [custNum, setCustNum] = React.useState(() => {
const D = window.DMS;
const custs = (D && D.customers && D.customers()) || {};
// 1. explicit hand-off (Customers / CRM / Desking "Credit App ▸"), read once.
try {
const cn = localStorage.getItem("jb_credit_cust");
if (cn) { localStorage.removeItem("jb_credit_cust"); if (custs[cn]) return cn; }
} catch (e) {}
// 2. the open deal → its customer.
try {
const deals = (D && D.deals && D.deals()) || [];
let id = localStorage.getItem("jb_fw_deal");
if (!id) { const dd = JSON.parse(localStorage.getItem("jb_desk_deal") || "null"); id = dd && dd.id; }
if (!id && window.jbDeskDeal) id = window.jbDeskDeal.id;
const d = id && deals.find(x => x.id === id);
if (d) {
if (d.custNum && custs[d.custNum]) return d.custNum;
const ph = caDigits(d.phone); if (ph && custs[ph]) return ph;
const nm = String(d.buyer || "").toUpperCase();
const hit = Object.values(custs).find(c => [c.lastName, c.firstName].filter(Boolean).join(", ").toUpperCase() === nm);
if (hit) return hit.custNum;
}
} catch (e) {}
return null;
});
const [picking, setPicking] = React.useState(false);
const [, force] = React.useReducer(n => n + 1, 0);
React.useEffect(() => (window.DMS && window.DMS.subscribe) ? window.DMS.subscribe(force) : undefined, []);
const custs = (window.DMS && window.DMS.customers && window.DMS.customers()) || {};
const bound = custNum && custs[custNum] ? custNum : null;
return (
{}} onMax={() => {}} />
{picking && (
setPicking(false)}
buttons={ setPicking(false)}>ESC-Cancel }>
{ setCustNum(cn); setPicking(false); }} />
)}
{bound ? (
setPicking(true)} />
) : (
Pick the customer this credit application belongs to.
The customer record is the credit application — SSN, DOB, employment and residence are saved on the customer file.
Customers are created in the CRM (+ New Lead).
setCustNum(cn)} />
ESC-Close
go && go("crm")}>🎯 Open CRM ▸
)}
);
};
Object.assign(window, { CreditApp, LENDERS, jbCreditCompleteness });