/* 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 }) => ( ); const CASel = ({ label, value, set, options, onBlur, miss }) => ( ); // 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 ( ); }; const CASection = ({ title, right, children, id }) => (
{title}
{right}
{children}
); // 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.
: ( {list.map((c, i) => { const cc = jbCreditCompleteness(c); return ( onPick(c.custNum)}> ); })}
Name Customer # City Credit app
{[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) => `
${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}
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.")}
)}
{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).
) : (
{LENDERS.map((L, i) => { const checked = selectedLenders.includes(L.code); const s = feedByCode[L.code]; const st = s ? (s.status === "SUBMITTED" ? "PENDING…" : s.status) : "—"; return ( ); })}
Lender Tier Buy % Status Max Amt Term Stip
setSelectedLenders(x => e.target.checked ? [...x, L.code] : x.filter(c => c !== L.code))} /> {L.name} {s && s.tier ? s.tier : L.tier} {(s && s.buyRate ? s.buyRate : L.buyRt).toFixed(2)} {st} {s && s.maxAmt ? ca$(s.maxAmt) : "—"} {(s && s.term) || ""} {(s && s.stip) || ""}
Bureau Pull
{["experian", "equifax", "transunion"].map(b => ( ))}
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 });