/* 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. */ // JBREW DMS — Desking Tool (DESK). Modern 4-square work tool. // • Trade · Price · Down · Payment, with three live payment scenarios. // • Lenders pulled from the shared Banks registry (window.jbLenders). // • F&I products pulled from the shared catalog (window.jbProducts) as a // toggle-able checklist — service contracts + all ancillary products. // • Its own Settings (defaults: doc fee, tax, pack, terms/APRs, comm). // • Print menu: Customer Presentation, 4-Square Worksheet, As-Is, Release // of Liability, Buyer's Order. // • Push to Cash Deal / Push to Finance hands the worked deal to its closing screen (ONE PATH). // • Saved Scenarios — 💾 snapshots the COMPLETE desk under a name into // DMS.deskScenarios (server-synced); 📂 lists / reopens / deletes them // from any workstation. // No demo presets (B-05): with no unit / customer picked the desk starts // BLANK — nothing invented ever reaches the Buyer's Order or a printed form. const DESK_EMPTY_UNIT = { yr: "", mk: "", mdl: "", stock: "", vin: "", msrp: 0, cost: 0, miles: 0, type: "", preset: true }; const DESK_EMPTY_CUST = { name: "", dealId: null, credit: 0, dti: "", income: 0, preset: true }; // Negatives render "-$1,234.00" (sign before the currency symbol), never "$-1,234.00". const dt$ = (n) => { const v = +n || 0; return (v < 0 ? "-$" : "$") + Math.abs(v).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); }; const dtRound2 = (n) => Math.round(n * 100) / 100; const dtPmt = (rate, n, pv) => { if (n <= 0) return 0; if (rate === 0) return pv / n; const r = rate / 12; return (pv * r) / (1 - Math.pow(1 + r, -n)); }; // ── Lease math (standard single-pay-per-month closed-end lease) ───────────── // Money factor MF = APR / 2400 (a .00250 MF = 6.00% APR) // Residual = MSRP × residual% // Depreciation = (adjusted cap cost − residual) / term // Rent charge = (adjusted cap cost + residual) × MF // Base payment = depreciation + rent charge (tax added on the payment) const dtAprToMf = (apr) => (+apr || 0) / 2400; const dtMfToApr = (mf) => (+mf || 0) * 2400; // Typical GM closed-end residuals by term at 12k/yr, adjusted for mileage. const DT_RESID_BASE = { 24: 68, 27: 66, 30: 64, 36: 60, 39: 58, 42: 56, 48: 51, 60: 42 }; const dtResidualPct = (term, milesPerYr) => { const terms = Object.keys(DT_RESID_BASE).map(Number).sort((a, b) => a - b); let base = DT_RESID_BASE[term]; if (base == null) { // interpolate off the nearest terms const lo = terms.filter(t => t <= term).pop() ?? terms[0]; const hi = terms.find(t => t >= term) ?? terms[terms.length - 1]; base = lo === hi ? DT_RESID_BASE[lo] : DT_RESID_BASE[lo] + (DT_RESID_BASE[hi] - DT_RESID_BASE[lo]) * ((term - lo) / (hi - lo)); } // GM-style mileage adjustment: ±1 point per 2,500 miles off 12k/yr. return Math.max(10, base - ((+milesPerYr || 12000) - 12000) / 2500); }; // The full closed-end lease engine lives in LeaseCalc.jsx.txt (loaded first): // window.jbLeaseCalc — GMF-aware: tier MF adds, dealer participation markup, // single-pay band reductions w/ MF floor, mileage residual adds, capitalized // vs upfront fee flags, and all three tax methods. // GM Financial program data loads from LeaseRates.js as window.GMF_LEASE: // { program (rate sheet + tier/mileage/fee tables), residualGuide, usedProgram }. const GMF_TIERS = ["A+/A1", "A2", "A3", "B1", "B2", "B3"]; const GMF_MILES = [7500, 10000, 12000, 15000]; const GMF_USED_TERMS = [24, 36, 48]; const dtGmfData = () => window.GMF_LEASE || { program: {}, residualGuide: [], usedProgram: {} }; const dtGmfTierLabel = (t) => { const f = (dtGmfData().program.tierFico || {})[t]; return f ? `${t} · FICO ${f}` : t; }; const dtNormLsc = (s) => ({ gap: false, vsc: false, all: false, resid: null, sp: false, aprIn: false, autoMf: false, autoRv: false, ...s }); // Compact display for a titling entity's legal name: // "ACAR Leasing Ltd. d/b/a GM Financial Leasing" → "ACAR Leasing Ltd. (GMF Leasing)" const dtEntityShort = (name) => { const parts = String(name || "").split(/\s+d\/b\/a\s+/i); return parts.length > 1 ? `${parts[0]} (${parts[1].replace(/GM Financial/i, "GMF")})` : String(name || ""); }; /* ── GMF Program Picker — searchable model/trim modal ── Two sources: the current-month rate sheet (MY2025 · MF + residual for 24/27/36/39/48) and the ALG-style residual guide (MY2026-27 + the odd 30/42-month terms · residual only). Picking auto-fills every option. */ const GmfProgramPicker = ({ onPick, onClose }) => { const [q, setQ] = React.useState(""); const [src, setSrc] = React.useState("rates"); // "rates" | "guide" const G = dtGmfData(); const qq = q.trim().toLowerCase(); const inp = { fontSize: 12.5, padding: "6px 8px", border: "1px solid #C8CCD2", borderRadius: 4 }; const rateRows = (G.program.rates || []).map(r => ({ source: "rates", model: r.model, modelYear: r.modelYear, leaseCash: +r.leaseCash || 0, label: `${r.modelYear} ${r.model}`, t36: r.t36 || {}, })); const guideRows = (G.residualGuide || []).map(g => ({ source: "guide", section: g.section, model: g.model, code: g.code, trim: g.trim, modelYear: (String(g.section || "").match(/\d{4}/) || [""])[0], label: `${g.section} ${g.model} ${g.trim} (${g.code})`, rv36: g.rv ? g.rv["36"] : null, })); const all = src === "rates" ? rateRows : guideRows; const rows = (qq ? all.filter(r => r.label.toLowerCase().includes(qq)) : all).slice(0, 400); return ( Cancel}>
setQ(e.target.value)} placeholder="🔍 Search model / trim… (e.g. EQUINOX, TRAX, SILVERADO)" style={{ ...inp, flex: 1, fontSize: 13.5, padding: "8px 10px", background: "#FFFEF4", border: "2px solid var(--jb-navy, #1B66A8)" }} />
{[["rates", `Rate Sheet — MF + residual (${rateRows.length})`], ["guide", `Residual Guide — RV only, 30/42 mo + MY26-27 (${guideRows.length})`]].map(([k, lab]) => ( ))} {G.program.effective ? `Program effective ${G.program.effective}` : ""}
{src === "rates" ? <> : <>} {rows.length === 0 ? ( ) : rows.map((r, i) => ( onPick(r)} style={{ background: i % 2 ? "var(--jb-surface-2)" : "#fff", cursor: "pointer" }}> {r.source === "rates" ? <> : <>} ))}
MYModel / Trim36-mo MF36-mo RV%Lease CashCode36-mo RV%
No program rows match — change the search or switch source.
{r.modelYear} {r.source === "rates" ? r.model : `${r.model} · ${r.trim}`}{r.t36.mf != null ? (+r.t36.mf).toFixed(5) : "—"} {r.t36.rv != null ? r.t36.rv + "%" : "—"} {r.leaseCash ? dt$(r.leaseCash) : "—"}{r.code} {r.rv36 != null ? r.rv36 + "%" : "—"}
Rate-sheet picks fill MF + residual for 24/27/36/39/48-month options. Residual-guide picks fill residual only (incl. 30/42-month) — the money factor stays as entered. Autofilled fields show a GMF badge and go manual the moment you edit them.
); }; // ── Desking defaults (editable in the in-tool Settings panel) ────────────── // Lease-desk tax rate default — the customer's COMBINED LOCAL rate the store // actually collects on lease monthly receipts (owner-confirmed, matches the // live RouteOne worksheet: 60.41 / 633.43 = 9.537%). Kept separate from the // finance-desk rate (taxRate, 7.225) — both stay user-editable. const DT_LEASE_TAX_DEFAULT = 9.537; const DESK_DEFAULTS = { docFee: 0, taxRate: 9.537, pack: 500, commPct: 25, cashDown: 2500, rebate: 1000, t1: 75, a1: 6.99, t2: 72, a2: 6.49, t3: 60, a3: 5.99, // lease defaults acqFee: 650, dispFee: 395, milesPerYr: 12000, leaseTaxOnPayment: true, lt1: 24, lt2: 36, lt3: 39, lmf1: 0.00225, lmf2: 0.00195, lmf3: 0.00185 }; function loadDeskSettings() { try { const s = { ...DESK_DEFAULTS, ...JSON.parse(localStorage.getItem("jb_desk_settings") || "{}") }; // One-time migration: doc fee now defaults to ZERO. Stores that still // carry the old shipped default (299) and never chose it get zeroed. if (s.docFee === 299 && !localStorage.getItem("jb_desk_docfee_v2")) { s.docFee = 0; localStorage.setItem("jb_desk_docfee_v2", "1"); saveDeskSettings(s); } return s; } catch (e) { return { ...DESK_DEFAULTS }; } } function saveDeskSettings(s) { try { localStorage.setItem("jb_desk_settings", JSON.stringify(s)); } catch (e) {} } // Working-desk persistence — the in-progress deal survives navigating in/out. // The blob is keyed to the deal it was worked on: switching to a DIFFERENT // deal starts a fresh desk instead of restoring the previous deal's numbers. function dtLoadWork() { try { const w = JSON.parse(localStorage.getItem("jb_desk_work") || "null") || {}; let d = window.jbDeskDeal; if (!d) { try { d = JSON.parse(localStorage.getItem("jb_desk_deal") || "null"); } catch (e) {} } const curDeal = d && d.id ? d.id : null; if ((w.dealId || null) !== curDeal) return { dealId: curDeal }; // new deal → clean desk return w; } catch (e) { return {}; } } // ── Payment options A–D ────────────────────────────────────────────────── // Every desk carries FOUR option slots (finance `sc` + lease `lsc`); a shared // visibility list (scOn) decides which letters are shown. Letters are stable: // removing C hides C (D keeps its letter). Each slot owns its term / rate / // products / DOWN PAYMENT. Older blobs (3 slots + one global cashDown) migrate // to A/B/C with down = that cashDown, D hidden as a copy of C. const DT_LETTERS = "ABCD"; const DT_MAX_OPTS = 4; const dtNormSc = (s) => ({ gap: false, vsc: false, all: false, ...s }); function dtMigrateSc(arr, down, defaults, norm = dtNormSc) { const src = (Array.isArray(arr) && arr.length) ? arr : defaults; const out = src.slice(0, DT_MAX_OPTS).map(s => { const n = norm(s || {}); return { ...n, down: n.down != null && n.down !== "" ? +n.down || 0 : (+down || 0) }; }); while (out.length < DT_MAX_OPTS) out.push({ ...out[out.length - 1] }); return out; } // Printed option comparison — one EQUAL-WIDTH column per visible option, the // same rows for every option. heads = ["A", "B", …]; sel = position of the ★ // option; rows = [[label, [cell per option], strong?]]. No placeholder columns. function dtOptGridHtml(heads, sel, rows) { const n = Math.max(1, heads.length); const labW = 26, colW = (100 - labW) / n; const td = (i, inner, strong) => `${inner}`; return `${heads.map(() => ``).join("")}${heads.map((h, i) => ``).join("")}${rows.map(([lab, cells, strong]) => `${cells.map((c, i) => td(i, c, strong)).join("")}`).join("")}
Option ${h}${i === sel ? " ★" : ""}
${lab}
`; } function dtMigrateOn(on, arr) { if (Array.isArray(on) && on.length === DT_MAX_OPTS) return on.map((v, i) => i < 2 ? true : !!v); const n = Array.isArray(arr) && arr.length ? Math.min(DT_MAX_OPTS, Math.max(2, arr.length)) : 3; return [0, 1, 2, 3].map(i => i < n); } // ── Unit Picker — searchable, filterable inventory modal (shared with Deals) ── const UnitPickerModal = ({ onPick, onClose, go, exceptDeal }) => { const [q, setQ] = React.useState(""); const [type, setType] = React.useState("All"); const [yrMin, setYrMin] = React.useState(""); const [yrMax, setYrMax] = React.useState(""); const [priceMax, setPriceMax] = React.useState(""); const [sort, setSort] = React.useState("stock"); const all = ((window.DMS && window.DMS.inventory && window.DMS.inventory()) || []).filter(u => window.DealCalc.unitOnLot(u)); // RC28 fix round: the ONE on-lot reading const qq = q.trim().toLowerCase(); const price = (u) => +u.asking || +u.list || 0; let units = all.filter(u => { if (type !== "All" && (type === "New" ? !/^n/i.test(u.type || "") : /^n/i.test(u.type || ""))) return false; if (yrMin && +u.yr < +yrMin) return false; if (yrMax && +u.yr > +yrMax) return false; if (priceMax && price(u) > +priceMax) return false; if (!qq) return true; return [u.stock, u.vin, u.yr, u.mk, u.mdl, u.color, u.body].join(" ").toLowerCase().includes(qq); }); units.sort((a, b) => sort === "priceAsc" ? price(a) - price(b) : sort === "priceDesc" ? price(b) - price(a) : sort === "yrDesc" ? (+b.yr || 0) - (+a.yr || 0) : sort === "age" ? (+b.age || 0) - (+a.age || 0) : String(a.stock).localeCompare(String(b.stock))); const inp = { fontSize: 12.5, padding: "6px 8px", border: "1px solid #C8CCD2", borderRadius: 4 }; return ( { onClose(); go && go("inventory"); }}>Browse Inventory ▸ Cancel }> {/* search + filters */}
setQ(e.target.value)} placeholder="🔍 Search stock #, VIN, year, make, model, color…" style={{ ...inp, flex: 1, fontSize: 13.5, padding: "8px 10px", background: "#FFFEF4", border: "2px solid var(--jb-navy, #1B66A8)" }} />
{["All", "New", "Used"].map(t => ( ))} Year setYrMin(e.target.value.replace(/[^0-9]/g, ""))} placeholder="min" style={{ ...inp, width: 52 }} /> setYrMax(e.target.value.replace(/[^0-9]/g, ""))} placeholder="max" style={{ ...inp, width: 52 }} /> Max $ setPriceMax(e.target.value.replace(/[^0-9]/g, ""))} placeholder="any" style={{ ...inp, width: 76 }} />
{units.length} of {all.length}
{/* results */}
{all.length === 0 ? (
No units in stock — stock one in first (Inventory → Stock-In).
) : units.length === 0 ? (
{q ? No units match '{q}' · : "No units match — clear a filter or change the search."}
) : ( {units.map((u, i) => { const chk = window.DMS && window.DMS.unitOpenItems ? window.DMS.unitOpenItems(u.stock, { exceptDeal }) : { clean: true, items: [] }; return ( { onPick(u); onClose(); }} style={{ background: i % 2 ? "var(--jb-surface-2)" : "#fff", cursor: "pointer" }}> ); })}
StockVehicleType AskingDaysReady
{u.stock} {u.yr} {String(u.mk || "").trim()} {u.mdl}{u.color ? · {u.color} : null} {u.type} {dt$(price(u))} = 60 ? "var(--jb-red)" : "#888" }}>{u.age || 0} {chk.clean ? "CLEAN" : `⚠ ${chk.items.length}`}
)}
); }; window.UnitPickerModal = UnitPickerModal; function dtLoadUnit() { let u = window.jbDeskUnit; if (!u) { try { u = JSON.parse(localStorage.getItem("jb_desk_unit") || "null"); } catch (e) {} } if (!u || !u.stock) return { ...DESK_EMPTY_UNIT }; return dtNormUnit(u); } // Normalize a unit hand-off. Cost and New/Used type come from the LIVE // inventory record when the hand-off omits them (DealReview only passes // stock/vin/yr/mk/mdl/list/cost) — B-01 / B-02. function dtNormUnit(u) { let inv = null; try { inv = (window.DMS && window.DMS._unit && u.stock) ? window.DMS._unit(u.stock) : null; } catch (e) { inv = null; } const cost = Number(u.cost) || (inv ? Number(inv.cost || inv.invoice) || 0 : 0); if (!cost) { try { console.warn("[Desking] unit " + u.stock + " has no cost — front gross / commission will be wrong until Inventory carries a cost."); } catch (e) {} } const type = String(u.type || u.condition || (inv ? inv.type || inv.condition || "" : "")).trim(); return { yr: u.yr || "", mk: String(u.mk || "").trim(), mdl: u.mdl || "", stock: u.stock, vin: u.vin || (inv ? inv.vin || "" : ""), msrp: Number(u.list ?? u.asking ?? u.msrp) || (inv ? Number(inv.asking || inv.list) || 0 : 0), cost, miles: Number(u.miles ?? u.odometer) || (inv ? Number(inv.miles) || 0 : 0), type, newUsed: type ? (/^n|new/i.test(type) ? "N" : "U") : "" }; } // Credit profile comes from the CUSTOMER / DEAL record (fico, income, dti) // when it carries one — never a preset. Blank otherwise (B-05). function dtLoadCust() { let d = window.jbDeskDeal; if (!d) { try { d = JSON.parse(localStorage.getItem("jb_desk_deal") || "null"); } catch (e) {} } if (!d || !d.buyer) return { ...DESK_EMPTY_CUST }; let rec = null; try { const deal = d.id && window.DMS && window.DMS.deals ? window.DMS.deals().find(x => x.id === d.id) : null; const custs = (window.DMS && window.DMS.customers && window.DMS.customers()) || {}; const c = deal && deal.custNum ? custs[deal.custNum] : null; rec = { ...(c || {}), ...(deal || {}) }; } catch (e) { rec = null; } return dtCustProfile(d.buyer, d.id || null, rec); } function dtCustProfile(name, dealId, rec) { const r = rec || {}; return { name: name || "", dealId: dealId || null, credit: +(r.fico || r.credit || r.creditScore) || 0, dti: r.dti != null && r.dti !== "" ? String(r.dti) : "", income: +(r.income || r.monthlyIncome || r.grossMonthly) || 0 }; } /* ── ONE-SCREEN 4-SQUARE SHEET (the desk) ──────────────────────────────── The desk is ONE sheet laid out like the paper 4-square every desk manager knows — 1 Trade · 2 Price / 3 Down · 4 Payment — sized to the screen so nothing scrolls. Secondary detail (products, fee itemization, lease program and per-option lease breakdown, credit profile) lives in a docked drawer on the right that shrinks the sheet instead of covering it. Dealer numbers (invoice, front / back / total gross, pack, commission, product costs, ACV and the trade-hold spread) are NOT rendered until a manager clicks "Show gross"; they hide again on Present, on another deal or scenario, and whenever the desk is reopened. While the desk is open (body.dkc-desk-on): • the app sidebar folds to a 56 px icon rail (hover to expand; More ▴ › "Keep the full menu open" pins it) and the top bar gets shorter, • the helper mascot is hidden and toasts are shown in the desk's own status line (bottom bar) instead of floating over the numbers. Pure layout: every figure below is read from the desk's existing calculations — nothing here computes a payment. */ const DKC_CSS = ` @media screen { body.dkc-desk-on [data-jb-helper] { display: none !important; } /* (the Sales view-only screen adds dkf-view-on: same quiet screen, but the menu does not fold) */ body.dkc-desk-on:not(:has([data-k2b-modal])) [data-k2b-toasts] { display: none !important; } body.dkc-desk-on .jb-topbar { height: 44px; } body.dkc-desk-on .jb-topbar .k2b-button { height: 30px; min-height: 30px; } body.dkc-desk-on .jb-screen { padding: 6px 8px; } body.dkc-desk-on:not(.dkc-nav-pin):not(.dkf-view-on) .jb-shell { grid-template-columns: 56px minmax(0, 1fr) !important; } body.dkc-desk-on:not(.dkc-nav-pin):not(.dkf-view-on) .jb-sidebar { position: relative; z-index: 850; width: 56px; min-width: 56px; overflow-x: hidden; transition: width .14s ease 0s, box-shadow .14s ease 0s; } body.dkc-desk-on:not(.dkc-nav-pin):not(.dkf-view-on) .jb-sidebar:hover { width: 232px; box-shadow: 10px 0 28px rgba(7, 11, 31, .35); transition: width .14s ease .35s, box-shadow .14s ease .35s; } body.dkc-desk-on:not(.dkc-nav-pin):not(.dkf-view-on) .jb-sidebar:not(:hover) .section-label { font-size: 0; height: 1px; padding: 0; margin: 9px 12px 6px; background: rgba(255,255,255,.12); } body.dkc-desk-on:not(.dkc-nav-pin):not(.dkf-view-on) .jb-sidebar:not(:hover) .brand { padding: 12px 9px; } body.dkc-desk-on:not(.dkc-nav-pin):not(.dkf-view-on) .jb-sidebar:not(:hover) > div:last-child { display: none; } body.dkc-desk-on:not(.dkc-nav-pin):not(.dkf-view-on) .jb-sidebar:not(:hover) > div:nth-last-child(2) { padding: 10px 12px; } body.dkc-desk-on:not(.dkc-nav-pin):not(.dkf-view-on) .jb-sidebar:not(:hover) > div:nth-last-child(2) > :not(:first-child) { display: none; } } .dkc-root { --dkc-fmin: 232px; --dkc-mw: 112px; --dkc-row: 28px; --dkc-in-h: 26px; --dkc-hero: 32px; --dkc-pmt: 17px; --dkc-qp: 4px 12px 8px; --dkc-fs: 13px; --dkc-line: #1E2A55; display: flex; flex-direction: column; background: var(--jb-bg); font-family: var(--jb-font); } .dkc-root[data-dens="roomy"] { --dkc-row: 34px; --dkc-in-h: 30px; --dkc-hero: 42px; --dkc-pmt: 21px; --dkc-qp: 8px 16px 12px; --dkc-fs: 14px; --dkc-ifs: 14px; --dkc-lfs: 13.5px; --dkc-fmin: 250px; --dkc-mw: 118px; } .dkc-root[data-dens="xl"] { --dkc-row: 40px; --dkc-in-h: 34px; --dkc-hero: 54px; --dkc-pmt: 26px; --dkc-qp: 10px 20px 14px; --dkc-fs: 16px; --dkc-ifs: 16px; --dkc-lfs: 15px; --dkc-fmin: 300px; --dkc-mw: 136px; } .dkc-root[data-dens="xl"] .dkc-qt { font-size: 15px; } .dkc-root[data-dens="xl"] .dkc-qn { width: 26px; height: 26px; font-size: 14px; } .dkc-root[data-dens="xl"] .dkc-hero .k, .dkc-root[data-dens="xl"] .dkc-col.lab > .dkc-c, .dkc-root[data-dens="xl"] .dkc-hero .s { font-size: 14.5px; } .dkc-root[data-dens="xl"] .dkc-pc { font-size: 13.5px; height: 30px; } .dkc-root[data-dens="compact"] { --dkc-fmin: 212px; --dkc-mw: 104px; --dkc-row: 26px; --dkc-in-h: 25px; --dkc-hero: 28px; --dkc-pmt: 16px; --dkc-qp: 2px 10px 6px; } .dkc-root[data-dens="tight"] { --dkc-fmin: 178px; --dkc-mw: 98px; --dkc-row: 24px; --dkc-in-h: 24px; --dkc-hero: 24px; --dkc-pmt: 15px; --dkc-qp: 1px 8px 4px; } .dkc-root[data-dens="compact"][data-lease="1"] { --dkc-row: 24px; --dkc-in-h: 24px; } /* lease carries 2 more rows: 1280 x 720 still fits */ /* fit step 3 (measured: a square would otherwise scroll) — shorter rows, same text size */ .dkc-root.dkf-fit-rows:not([data-dens="tight"]):not([data-dens="compact"]) { --dkc-row: 26px; --dkc-in-h: 26px; } .dkc-root.dkf-fit-rows[data-lease="1"]:not([data-dens="tight"]) { --dkc-row: 24px; --dkc-in-h: 24px; } .dkc-root.dkf-fit-hero .dkc-hero .s { display: none; } .dkc-root .k2b-button.small { font-size: 12.5px; height: 28px; min-height: 28px; } .dkc-root input[type="checkbox"] { width: 18px; height: 18px; } /* header + footer bars */ .dkc-head, .dkc-foot { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; padding: 6px 10px; box-sizing: border-box; background: var(--jb-surface); min-width: 0; } .dkc-head { min-height: 44px; border-bottom: 1px solid var(--jb-border); } .dkc-foot { min-height: 54px; border-top: 1px solid var(--jb-border); position: relative; } .dkc-head > *, .dkc-foot > * { flex: 0 0 auto; } .dkc-ib { width: 32px; height: 30px; border-radius: 8px; border: 1px solid var(--jb-border-strong); background: var(--jb-surface); color: var(--jb-text-dim); font-size: 15px; cursor: pointer; } .dkc-ib:hover { border-color: var(--jb-blue); color: var(--jb-blue); } .dkc-chip { display: inline-flex; align-items: center; gap: 6px; height: 30px; box-sizing: border-box; padding: 0 12px; border-radius: 999px; border: 1px solid var(--jb-border); background: var(--jb-blue-tint); color: var(--jb-blue); font: 600 13px var(--jb-font); cursor: pointer; min-width: 0; max-width: 100%; white-space: nowrap; } .dkc-chip > .t { overflow: hidden; text-overflow: ellipsis; min-width: 0; } .dkc-chip.ro { cursor: default; background: transparent; color: var(--jb-navy); font-size: 14px; } .dkc-chip.sm { height: 26px; padding: 0 10px; font-size: 12.5px; } .dkc-head > .dkc-chip { flex: 0 1 auto; min-width: 90px; } .dkc-fico { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--jb-text-dim); } .dkc-badge { display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 12px; border-radius: 999px; background: var(--jb-navy); color: var(--jb-yellow); font: 800 12.5px var(--jb-font); letter-spacing: .06em; text-transform: uppercase; white-space: nowrap; } .dkc-btn-main.k2b-button { background: var(--jb-yellow) !important; border-color: var(--jb-yellow-2) !important; color: var(--jb-navy) !important; font-weight: 700; } .dkc-root .dkc-btn-go.k2b-button { background: var(--jb-green) !important; border-color: var(--jb-green) !important; color: #fff !important; font-weight: 700; } .dkc-btn-on.k2b-button { background: var(--jb-navy) !important; border-color: var(--jb-navy) !important; color: #fff !important; } .dkc-root .k2b-button.dkc-gtog.on { background: #7A4E00 !important; border-color: #7A4E00 !important; color: #fff !important; } /* status line (replaces floating toasts while desking) */ .dkc-status { flex: 1 1 0px !important; min-width: 40px; position: relative; display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--jb-text-dim); height: 34px; padding: 0 8px; border-radius: 8px; cursor: pointer; } .dkc-status:hover { background: var(--jb-surface-2); } .dkc-status.fresh { color: var(--jb-text); background: var(--jb-surface-2); } .dkc-status .ic { width: 18px; height: 18px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; color: #fff; font: 700 12px var(--jb-font); flex: 0 0 auto; } .dkc-status .m { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; } .dkc-status .m.dim { color: var(--jb-text-muted); } .dkc-nodeal { display: inline-flex; align-items: center; gap: 8px; min-width: 0; flex: 0 1 auto !important; height: 34px; padding: 0 6px 0 12px; border-radius: 8px; background: var(--jb-amber-tint, #FBF3E0); border: 1px solid var(--jb-amber, #D9920A); color: #7A4E00; font-size: 13px; white-space: nowrap; overflow: hidden; } .dkc-nodeal > span { flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; min-width: 0; } .dkc-nodeal > b, .dkc-nodeal > button { flex: 0 0 auto; } .dkc-root .k2b-button.dkc-push { height: 42px; min-height: 42px; padding: 0 18px !important; font-size: 14.5px; font-weight: 800; color: #fff !important; border: 0 !important; } .dkc-root .k2b-button.dkc-push.cash { background: var(--jb-green) !important; } .dkc-root .k2b-button.dkc-push.fin { background: var(--jb-blue) !important; } .dkc-root .k2b-button.dkc-push:hover { filter: brightness(1.08); } /* footer summary — the chosen payment, total down and amount financed beside Push */ .dkc-sum { display: flex; align-items: stretch; gap: 2px; flex: 0 1 auto !important; min-width: 0; border: 1px solid var(--jb-border); border-radius: 10px; background: var(--jb-surface-2); padding: 2px; } .dkc-sumi { display: flex; flex-direction: column; justify-content: center; align-items: flex-end; gap: 0; padding: 2px 10px; border: 0; border-radius: 8px; background: transparent; cursor: pointer; min-width: 0; font-family: var(--jb-font); } .dkc-sumi:disabled { cursor: default; } .dkc-sumi:not(:disabled):hover, .dkc-sumi:focus-visible { background: var(--jb-blue-tint); outline: none; } .dkc-sumi > .k { font: 700 12px var(--jb-font); color: var(--jb-text-muted); white-space: nowrap; } .dkc-sumi > .v { font: 800 15px var(--jb-mono); color: var(--jb-navy); white-space: nowrap; } .dkc-sumi.pmt > .v { color: #8A5A00; font-size: 16.5px; } .dkc-root[data-dens="tight"] .dkc-sumi { padding: 1px 7px; } .dkc-root[data-dens="tight"] .dkc-sumi > .v { font-size: 14px; } .dkc-alert { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; padding: 4px 10px; background: var(--jb-red-tint); border-bottom: 1px solid var(--jb-red); color: var(--jb-red); font-size: 13px; min-width: 0; } .dkc-alert > .m { flex: 1 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .dkc-alert > * { flex: 0 0 auto; } .dkc-root .k2b-button.dkc-upill { background: var(--jb-red-tint) !important; border-color: var(--jb-red) !important; color: var(--jb-red) !important; font-weight: 800; } .dkc-unitpop { gap: 4px; } /* body: sheet (+ docked drawer) */ .dkc-body { flex: 1 1 0px; min-height: 0; display: grid; grid-template-rows: minmax(0, 1fr); gap: 8px; padding: 8px; overflow: auto; box-sizing: border-box; } .dkc-sheet { min-width: 0; min-height: 0; display: grid; grid-template-columns: minmax(0, 41fr) minmax(0, 59fr); grid-template-rows: auto minmax(0, 1fr); grid-template-areas: "trade price" "down pay"; background: var(--jb-surface); border: 2px solid var(--dkc-line); border-radius: 12px; overflow: hidden; box-shadow: var(--jb-shadow-sm); } .dkc-quad { min-width: 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; } .dkc-quad[data-quad="trade"] { grid-area: trade; border-right: 2px solid var(--dkc-line); border-bottom: 2px solid var(--dkc-line); } .dkc-quad[data-quad="price"] { grid-area: price; border-bottom: 2px solid var(--dkc-line); } .dkc-quad[data-quad="down"] { grid-area: down; border-right: 2px solid var(--dkc-line); } .dkc-quad[data-quad="pay"] { grid-area: pay; background: #FBFCFE; } .dkc-qhead { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; padding: 6px 12px 2px; min-height: 36px; box-sizing: border-box; min-width: 0; } .dkc-root[data-dens="tight"] .dkc-qhead, .dkc-root[data-dens="compact"] .dkc-qhead { min-height: 32px; padding-top: 3px; } .dkc-root[data-dens="tight"] .dkc-qhead { min-height: 29px; padding-top: 1px; padding-bottom: 0; } .dkc-root[data-dens="tight"] .dkc-gridwrap { padding-bottom: 3px; } .dkc-root[data-dens="tight"] .dkc-head { min-height: 40px; padding-top: 4px; padding-bottom: 4px; } .dkc-root[data-dens="tight"] .dkc-foot { min-height: 50px; padding-top: 4px; padding-bottom: 4px; } .dkc-root[data-dens="tight"] .dkc-body { padding: 5px 6px; gap: 6px; } @media screen and (max-height: 660px) { body.dkc-desk-on .jb-topbar { height: 40px; } body.dkc-desk-on .jb-screen { padding: 4px 6px; } } .dkc-qhead > * { flex: 0 0 auto; } .dkc-qhead > .dkf-thdesc { flex: 1 1 auto; min-width: 90px; display: flex; align-items: center; overflow: hidden; } .dkc-qhead .k2b-button.small { height: 28px; min-height: 28px; } .dkc-qn { width: 22px; height: 22px; border-radius: 50%; background: var(--dkc-line); color: #fff; font: 800 12px var(--jb-font); display: inline-flex; align-items: center; justify-content: center; } .dkc-qt { font: 800 13px var(--jb-font); letter-spacing: .1em; text-transform: uppercase; color: var(--dkc-line); white-space: nowrap; } .dkc-qbody { flex: 1 1 auto; min-height: 0; overflow: auto; padding: var(--dkc-qp); } .dkc-fields { display: grid; grid-template-columns: repeat(auto-fill, minmax(var(--dkc-fmin), 1fr)); column-gap: 22px; row-gap: 1px; align-content: start; } .dkc-f { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: var(--dkc-row); min-width: 0; } .dkc-f.wide { grid-column: 1 / -1; } .dkc-f.rule { border-top: 1px solid var(--jb-border); margin-top: 3px; padding-top: 2px; } .dkc-fl { font-size: var(--dkc-lfs, 13px); color: var(--jb-text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; } .dkc-f.strong .dkc-fl { font-weight: 700; color: var(--jb-text); } .dkc-hint { font-size: 12px; color: var(--jb-text-muted); margin-left: 6px; } .dkc-fv { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 4px; } .dkc-v { font: 700 var(--dkc-fs) var(--jb-mono); color: var(--jb-text); white-space: nowrap; } .dkc-v.big { font-size: calc(var(--dkc-fs) + 5px); } .dkc-v.dim { color: var(--jb-text-muted); font-weight: 600; } /* a total that jumps to the box that changes it */ .dkc-clk { border: 0; background: transparent; padding: 0 4px; margin: 0 -4px; min-height: 26px; border-radius: 6px; cursor: pointer; } .dkc-clk:hover, .dkc-clk:focus-visible { background: var(--jb-blue-tint); outline: none; } .dkc-flash { animation: dkc-flash .9s ease-out; } @keyframes dkc-flash { 0% { box-shadow: 0 0 0 4px rgba(27, 102, 168, .55); } 100% { box-shadow: 0 0 0 0 rgba(27, 102, 168, 0); } } .dkc-salefirst input { border: 2px solid var(--jb-blue) !important; box-shadow: 0 0 0 3px var(--jb-blue-tint); } .dkc-empty { display: flex; flex-direction: column; gap: 8px; align-items: center; justify-content: center; padding: 10px 0; color: var(--jb-text-dim); font-weight: 700; font-size: 13.5px; text-align: center; } .dkc-empty > div { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; } .dkc-tradenote { grid-column: 1 / -1; font-size: 12.5px; line-height: 1.35; color: var(--jb-red); background: var(--jb-red-tint); border-radius: 7px; padding: 3px 8px; margin: 2px 0; } .dkc-review { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; background: var(--jb-amber-tint, #FBF3E0); border: 1px solid var(--jb-amber, #D9920A); border-radius: 999px; padding: 2px 6px 2px 12px; font-size: 12.5px; color: #7A4E00; margin: 2px 0; } /* dealer numbers — only rendered after "Show gross" */ .dkc-profit { margin-top: 8px; border: 1.5px dashed #B07000; border-radius: 10px; background: #FFFBF0; padding: 4px 10px 6px; } .dkc-profit > .h { display: flex; align-items: center; gap: 8px; font: 800 12px var(--jb-font); letter-spacing: .08em; text-transform: uppercase; color: #7A4E00; min-height: 28px; } .dkc-profit .dkc-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 18px; } .dkc-profit { container-type: inline-size; } @container (max-width: 420px) { .dkc-profit .dkc-long { display: none; } .dkc-profit .dkc-short { display: inline; } .dkc-profit .dkc-fields { column-gap: 12px; } } /* "includes $X negative equity" drops under the label instead of being cut off */ .dkc-f.dkf-wrapl > .dkc-fl { white-space: normal; overflow: visible; line-height: 1.25; } .dkc-f.dkf-wrapl > .dkc-fl > .dkc-hint { display: inline-block; white-space: nowrap; } .dkc-root[data-dens="tight"] .dkc-profit { margin-top: 4px; padding: 2px 8px 4px; } .dkc-mgrrow .dkc-fl { color: #7A4E00; } /* payment square */ .dkc-seg { display: inline-flex; gap: 2px; padding: 2px; border-radius: 999px; background: var(--jb-surface-3, #E9EDF3); } .dkc-seg > button { border: 0; border-radius: 999px; padding: 0 12px; height: 26px; font: 800 12.5px var(--jb-font); background: transparent; color: var(--jb-text-dim); cursor: pointer; white-space: nowrap; } .dkc-seg > button.on { background: var(--jb-navy); color: var(--jb-yellow); } .dkc-seg.sm > button { height: 26px; padding: 0 10px; font-weight: 700; font-size: 12.5px; } .dkc-root select.dkc-sel { height: 28px; padding: 2px 26px 2px 8px; font-size: 13px; background-position: right 7px center; max-width: 190px; min-width: 0; } .dkc-root select.dkc-sel.sm { height: 26px; font-size: 12.5px; padding: 0 22px 0 6px; } .dkc-hero { flex: 0 0 auto; margin: 2px 10px 6px; border-radius: 10px; padding: 6px 14px; display: flex; align-items: center; gap: 16px; min-width: 0; background: linear-gradient(165deg, var(--jb-navy) 0%, var(--jb-navy-deep) 100%); color: #fff; } .dkc-hero .l { flex: 0 0 auto; } .dkc-hero .k { font: 700 12px var(--jb-font); letter-spacing: .12em; text-transform: uppercase; color: var(--jb-yellow); white-space: nowrap; } .dkc-hero .v { font: 800 var(--dkc-hero)/1.08 var(--jb-mono); white-space: nowrap; color: #fff; } .dkc-hero .v small { font-size: 14px; color: #aeb8d0; font-weight: 500; } .dkc-hero button.v { border: 0; background: transparent; padding: 0 6px; margin: 0 -6px; border-radius: 8px; cursor: pointer; text-align: left; display: block; } .dkc-hero button.v:hover, .dkc-hero button.v:focus-visible { background: rgba(255, 255, 255, .12); outline: none; } .dkc-hero .s { flex: 1 1 0px; min-width: 140px; font-size: 12.5px; color: #C9D1E6; line-height: 1.4; } .dkc-hero .s > div { white-space: normal; } .dkc-hero.stack { flex-wrap: wrap; row-gap: 2px; } .dkc-hero.stack .s { flex: 1 1 100%; order: 3; } .dkc-hero.stack > .dkc-present { margin-left: auto; } .dkc-hero .k2b-button.dkc-present { flex: 0 0 auto; height: 34px; min-height: 34px; font-weight: 800; } .dkc-root[data-dens="tight"] .dkc-hero { padding: 2px 12px; margin-bottom: 4px; gap: 12px; } .dkc-root[data-dens="tight"] .dkc-hero .l { display: flex; align-items: baseline; gap: 10px; } .dkc-root[data-dens="tight"] .dkc-hero .s { font-size: 12px; line-height: 1.3; } /* 125 % laptop: the hero detail repeats the ★ column (term, rate, residual, due at signing) — the grid keeps it */ .dkc-root[data-dens="tight"] .dkc-hero .s { display: none; } .dkc-root[data-dens="tight"] .dkc-pc { padding: 0 7px; } .dkc-root[data-dens="tight"] .dkc-fl > .dkc-hint { display: none; } .dkc-root[data-dens="tight"] .dkc-hero .k2b-button.dkc-present { height: 30px; min-height: 30px; } .dkc-strip { flex: 0 0 auto; display: flex; align-items: center; gap: 6px 10px; padding: 0 10px 6px; flex-wrap: wrap; min-width: 0; overflow: hidden; font-size: 12.5px; color: var(--jb-text-dim); } .dkc-root[data-dens="tight"] .dkc-strip { flex-wrap: nowrap; padding-bottom: 2px; } .dkc-strip > * { flex: 0 0 auto; } .dkc-strip .dkc-chip { flex: 1 1 150px; max-width: 250px; min-width: 96px; } .dkc-root[data-dens="tight"] .dkc-strip .dkc-chip { min-width: 96px; } .dkc-strip label { display: inline-flex; align-items: center; gap: 5px; color: var(--jb-text-dim); } .dkc-titled { color: var(--jb-text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 0 1 auto !important; min-width: 0; } .dkc-tg { height: 26px; border-radius: 999px; padding: 0 10px; border: 1px solid var(--jb-border-strong); background: transparent; color: var(--jb-text-dim); font: 700 12.5px var(--jb-font); cursor: pointer; white-space: nowrap; } .dkc-tg.on { background: var(--jb-green); border-color: var(--jb-green); color: #fff; } .dkc-gridwrap { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 0 10px 8px; } .dkc-grid { display: grid; column-gap: 6px; align-content: start; } .dkc-col { display: grid; grid-template-rows: subgrid; min-width: 0; border-radius: 8px; } .dkc-col.opt { border: 1px solid var(--jb-border); background: var(--jb-surface); cursor: pointer; } .dkc-col.opt:hover { border-color: #AFC4DE; } .dkc-col.opt.on { border: 2px solid var(--jb-yellow-2, #E5B82A); background: #FFF8E1; box-shadow: 0 2px 8px rgba(229, 184, 42, .25); } .dkc-c { display: flex; align-items: center; justify-content: flex-end; gap: 4px; padding: 1px 6px; min-height: var(--dkc-row); box-sizing: border-box; min-width: 0; } .dkc-col.lab > .dkc-c { justify-content: flex-start; color: var(--jb-text-dim); font-size: 12.5px; white-space: nowrap; padding-left: 0; } .dkc-c.h { justify-content: space-between; font: 800 14px var(--jb-font); color: var(--jb-text-dim); border-bottom: 1px solid var(--jb-border); } .dkc-oh { display: inline-flex; align-items: center; gap: 4px; height: 26px; padding: 0 8px; margin-left: -4px; border: 1px solid transparent; border-radius: 7px; background: transparent; font: 800 14px var(--jb-font); color: inherit; cursor: pointer; white-space: nowrap; } .dkc-oh:hover, .dkc-oh:focus-visible { border-color: var(--jb-blue); background: var(--jb-blue-tint); outline: none; } .dkc-col.on > .dkc-c.h { color: #8A5A00; } .dkc-c.ro { font: 600 12.5px var(--jb-mono); color: var(--jb-text-dim); white-space: nowrap; } .dkc-c.pmt { border-top: 1px solid var(--jb-border); flex-wrap: wrap; row-gap: 0; } .dkc-c.pmt > [data-pmt] { font: 800 var(--dkc-pmt) var(--jb-mono); color: var(--jb-navy); white-space: nowrap; } .dkc-col.on > .dkc-c.pmt > [data-pmt] { color: #8A5A00; } .dkc-c.prod { flex-wrap: wrap; row-gap: 3px; gap: 3px; container-type: inline-size; } .dkc-c.rv { container-type: inline-size; } .dkc-pc { font: 700 12.5px/24px var(--jb-font); height: 26px; box-sizing: border-box; padding: 0 9px; border-radius: 999px; border: 1px solid var(--jb-border-strong); background: transparent; color: var(--jb-text-dim); cursor: pointer; white-space: nowrap; } .dkc-pc.on { background: var(--jb-navy); border-color: var(--jb-navy); color: #fff; } .dkc-pc.sp.on { background: var(--jb-green); border-color: var(--jb-green); } .dkc-pc:disabled { opacity: .45; cursor: not-allowed; } .dkc-short { display: none; } @container (max-width: 236px) { .dkc-long { display: none; } .dkc-short { display: inline; } } @container (max-width: 150px) { .dkc-rvd { display: none; } } @container (max-width: 172px) { .dkc-pc { padding: 0 6px; } } .dkc-x { font-size: 12.5px; line-height: 1; min-width: 26px; height: 26px; padding: 0 6px; border-radius: 7px; cursor: pointer; border: 1px solid var(--jb-border-strong); background: transparent; color: var(--jb-red); } .dkc-add { font: 700 12.5px var(--jb-font); height: 26px; padding: 0 8px; border-radius: 7px; border: 1px dashed var(--jb-border-strong); background: transparent; color: var(--jb-blue); cursor: pointer; white-space: nowrap; } .dkc-auto { border-color: var(--jb-green) !important; box-shadow: inset 3px 0 0 var(--jb-green); } /* drawer */ .dkc-drawer { min-width: 0; min-height: 0; display: flex; flex-direction: column; background: var(--jb-surface); border: 1px solid var(--jb-border); border-radius: 12px; box-shadow: var(--jb-shadow); overflow: hidden; animation: dkc-in .16s ease-out; } .dkc-sheet.dkc-dleft { grid-template-areas: "drawer price" "drawer pay"; } .dkc-sheet.dkc-dleft > .dkc-drawer { grid-area: drawer; border: 0; border-right: 2px solid var(--dkc-line); border-radius: 0; box-shadow: none; } @keyframes dkc-in { from { opacity: 0; transform: translateX(14px); } to { opacity: 1; transform: none; } } .dkc-dtabs { flex: 0 0 auto; display: flex; flex-wrap: wrap; gap: 2px; align-items: center; padding: 6px; border-bottom: 1px solid var(--jb-border); background: var(--jb-surface-2); } .dkc-dtab { border: 0; border-radius: 7px; padding: 0 10px; height: 28px; font: 700 12.5px var(--jb-font); background: transparent; color: var(--jb-text-dim); cursor: pointer; white-space: nowrap; } .dkc-dtab.on { background: var(--jb-navy); color: #fff; } .dkc-dbody { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 8px 12px 12px; display: flex; flex-direction: column; gap: 4px; } .dkc-dbody .dkc-fields { grid-template-columns: minmax(0, 1fr); } .dkc-sec { display: flex; align-items: center; gap: 8px; font: 800 12px var(--jb-font); letter-spacing: .08em; text-transform: uppercase; color: var(--jb-text-muted); margin: 6px 0 2px; } .dkc-note { font-size: 12.5px; color: var(--jb-text-dim); line-height: 1.45; background: var(--jb-surface-2); border-radius: 8px; padding: 6px 9px; } .dkc-prod { display: flex; align-items: center; gap: 9px; padding: 4px 6px; border-radius: 7px; border: 1px solid transparent; } .dkc-prod.on { background: var(--jb-green-tint); border-color: var(--jb-green); } .dkc-prod > .n { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; cursor: pointer; } .dkc-prod > .n > b { font-size: 13px; font-weight: 600; color: var(--jb-text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .dkc-prod > .n > span { font-size: 12px; color: var(--jb-text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* Show gross: the product cost sits right under the customer price (its own line - never appended to the subtitle, which a ticked row's price box cut off at 1536) */ .dkc-prod > .dkc-pwrap { flex: 0 0 auto; display: flex; flex-direction: column; align-items: flex-end; gap: 1px; } .dkc-pcost { font: 700 12px var(--jb-mono); color: #7A4E00; white-space: nowrap; } .dkc-lopt { border: 1px solid var(--jb-border); border-radius: 10px; padding: 7px 10px; display: flex; flex-direction: column; gap: 2px; } .dkc-lopt.on { border: 2px solid var(--jb-yellow-2, #E5B82A); background: #FFF8E1; } .dkc-lopt .dkc-f { min-height: 23px; } .dkc-li { background: var(--jb-surface-2); border-radius: 8px; padding: 4px 8px; margin-top: 2px; } .dkc-li .dkc-f { min-height: 21px; } .dkc-li .dkc-fl, .dkc-li .dkc-v { font-size: 12px; } /* menus / popovers */ .dkc-menu-back { position: fixed; inset: 0; z-index: 60; } .dkc-menu { position: absolute; z-index: 61; background: var(--jb-surface); border: 1px solid var(--jb-border); border-radius: 10px; box-shadow: var(--jb-shadow-lg); padding: 6px; display: grid; gap: 2px; } .dkc-mi { display: flex; align-items: center; gap: 8px; text-align: left; padding: 7px 10px; border: 0; border-radius: 6px; background: transparent; font: 500 13px var(--jb-font); color: var(--jb-text); cursor: pointer; white-space: nowrap; } .dkc-mi:hover, .dkc-mi:focus-visible { background: var(--jb-blue-tint); outline: none; } .dkc-mi:disabled { opacity: .45; cursor: not-allowed; } .dkc-msep { height: 1px; background: var(--jb-border); margin: 3px 4px; } .dkc-log { max-height: 60vh; overflow: auto; } .dkc-log > div { display: flex; gap: 8px; align-items: flex-start; font-size: 12.5px; padding: 5px 8px; border-radius: 6px; white-space: pre-wrap; } .dkc-keys > div { display: flex; align-items: baseline; gap: 10px; font-size: 13px; padding: 4px 8px; white-space: normal; } .dkc-keys kbd { flex: 0 0 auto; } .dkf-offer { display: inline-flex; align-items: center; gap: 4px; flex: 0 1 auto !important; min-width: 0; } .dkf-offer > button:first-child { min-width: 0; max-width: 360px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: inline-block; } .dkc-keys kbd { font: 700 12px var(--jb-mono); background: var(--jb-surface-2); border: 1px solid var(--jb-border-strong); border-radius: 5px; padding: 1px 6px; min-width: 64px; display: inline-block; } /* customer view (Present) + the Sales view-only screen */ .dkc-root[data-present="1"] .dkc-sheet { border-color: var(--jb-navy); } .dkc-root[data-present="1"] .dkc-c.pmt > [data-pmt] { font-size: calc(var(--dkc-pmt) + 4px); } .dkc-root[data-present="1"] .dkc-v { font-size: calc(var(--dkc-fs) + 1px); } .dkc-root[data-present="1"] .dkc-col.opt { cursor: pointer; } .dkc-plist { display: flex; flex-direction: column; gap: 1px; margin-top: 6px; border-top: 1px solid var(--jb-border); padding-top: 4px; } .dkc-msg { margin: auto; max-width: 560px; text-align: center; display: flex; flex-direction: column; gap: 12px; align-items: center; padding: 30px; color: var(--jb-text-dim); font-size: 14px; line-height: 1.5; } .dkc-msg > b { font-size: 16px; color: var(--jb-text); } /* Phone-width Sales view-only screen (dkf-narrow: the window is under 640 px wide — the desk itself is desktop-only by owner decision). The four squares stand one under the other and the body scrolls; the option grid scrolls sideways inside the Payment square (readable columns instead of 20 px ones); header and footer wrap; Print customer copy sits in the footer. */ .dkc-root.dkf-narrow .dkc-head, .dkc-root.dkf-narrow .dkc-foot { flex-wrap: wrap; row-gap: 6px; } .dkc-root.dkf-narrow .dkc-head > .dkc-chip { flex: 0 1 auto; min-width: 0; max-width: 100%; height: auto; min-height: 30px; padding-top: 3px; padding-bottom: 3px; white-space: normal; } .dkc-root.dkf-narrow .dkc-head > .dkc-chip > .t { white-space: normal; overflow: visible; line-height: 1.25; } .dkc-root.dkf-narrow .dkc-body { display: block; } .dkc-root.dkf-narrow .dkc-sheet { display: flex; flex-direction: column; } .dkc-root.dkf-narrow .dkc-quad { flex: 0 0 auto; overflow: visible; border-right: 0; border-bottom: 2px solid var(--dkc-line); } .dkc-root.dkf-narrow .dkc-quad[data-quad="pay"] { border-bottom: 0; } .dkc-root.dkf-narrow .dkc-qbody { flex: 0 0 auto; overflow: visible; } .dkc-root.dkf-narrow .dkc-fields { grid-template-columns: minmax(0, 1fr); } .dkc-root.dkf-narrow .dkc-hero { flex-wrap: wrap; gap: 6px 12px; } .dkc-root.dkf-narrow .dkc-hero .s { flex: 1 1 100%; min-width: 0; } .dkc-root.dkf-narrow .dkc-gridwrap { flex: 0 0 auto; overflow-x: auto; overflow-y: hidden; -webkit-overflow-scrolling: touch; } .dkc-root.dkf-narrow .dkc-col.lab { position: sticky; left: 0; z-index: 1; background: #FBFCFE; padding-right: 4px; } /* the row labels stay while the options scroll */ .dkc-root.dkf-narrow .dkc-foot > .dkc-status { flex: 1 1 100% !important; order: 8; height: auto; min-height: 34px; } .dkc-root.dkf-narrow .dkc-foot > .dkc-status .m { white-space: normal; line-height: 1.3; } .dkc-root.dkf-narrow .dkc-foot > .dkc-sum { flex: 1 1 100% !important; order: 9; flex-wrap: wrap; justify-content: space-between; } .dkc-root.dkf-narrow .dkc-sumi { flex: 1 1 auto; align-items: flex-start; } `; const dkcIn = (extra) => Object.assign({ height: "var(--dkc-in-h)", fontSize: "var(--dkc-ifs, 13px)", fontWeight: 600, textAlign: "right", boxSizing: "border-box" }, extra || {}); // label · control row (the control is the child) const DkcF = ({ k, label, hint, title, wide, rule, strong, mgr, cls, children }) => (
{label}{hint ? {hint} : null} {children}
); // money input (read-only text in the customer view) const DkcMoney = ({ k, label, value, set, ro, hint, title, w = "var(--dkc-mw)", color, wide, rule, mgr, cls }) => ( {ro ? {dt$(value)} : } ); // decimal / integer input const DkcNum = ({ k, label, value, set, ro, dec, w = 72, hint, title, mgr, suffix }) => ( {ro ? {dec ? (+value || 0).toFixed(dec) : String(value)}{suffix || ""} : dec ? : } ); // read-out; with `go` the figure is a button that jumps to the box that changes it const DkcStat = ({ k, label, value, color, big, rule, wide, strong, title, mgr, hint, go, cls }) => ( {go ? : {value}} ); // one quadrant of the sheet const DkcQuad = ({ area, n, title, right, children }) => (
{n}{title}
{right}
{children}
); // popover menu with a click-away backdrop (press inside + release outside never closes it) const DkcPop = ({ open, onClose, style, className, children }) => !open ? null : ( <>
{ e.currentTarget.__dkcDown = true; }} onClick={(e) => { e.stopPropagation(); const d = e.currentTarget.__dkcDown; e.currentTarget.__dkcDown = false; if (d) onClose(); }} />
e.stopPropagation()}>{children}
); const dkcKind = (k) => k === "err" ? ["✕", "var(--jb-red)"] : k === "warn" ? ["!", "#B07000"] : k === "info" ? ["i", "var(--jb-blue)"] : ["✓", "var(--jb-green)"]; const dkcStore = (k, v) => { try { if (v === undefined) return localStorage.getItem(k); localStorage.setItem(k, v); } catch (e) {} return null; }; // Box size of a desk window → density (row height / type scale). Shared by the // desk and the read-only Sales view so both fit the screen the same way. const dkfDensOf = (b) => b.h < 600 ? "tight" : b.h < 690 ? "compact" : b.h < 880 ? "normal" : (b.h >= 1200 && b.w >= 2000) ? "xl" : "roomy"; function dkfUseBox(ref) { const [box, setBox] = React.useState({ w: 1300, h: 700 }); React.useLayoutEffect(() => { const el = ref.current; if (!el) return undefined; const measure = () => { const r = el.getBoundingClientRect(); const w = Math.round(r.width), h = Math.round(r.height); if (w && h) setBox(b => (b.w === w && b.h === h) ? b : { w, h }); }; measure(); if (!window.ResizeObserver) { window.addEventListener("resize", measure); return () => window.removeEventListener("resize", measure); } const ro = new ResizeObserver(measure); ro.observe(el); return () => ro.disconnect(); }, []); return box; } // A label with a long and a short form; CSS shows the short one in a narrow profit block. const dkfLS = (long, short) => <>{long}{short}; // A program's term list plus the stored term when it is off the list (so a select shows the real term). const dkfTermList = (list, t) => (list.includes(t) ? list.slice() : list.concat([t])).sort((a, b) => a - b); // Messages: every toast (k2btoast event) is kept for the screen's own status // line instead of floating over the figures (the desk and the Sales view hide // the toast stack while they are open). Toasts already on screen when the // screen opens (e.g. a sign-in sync warning) are adopted. function dkfUseMsgs() { const [msgs, setMsgs] = React.useState([]); const [, tick] = React.useReducer(n => n + 1, 0); React.useEffect(() => { const add = (msg, kind, ttl) => setMsgs(a => a.concat([{ id: Math.random().toString(36).slice(2), msg: String(msg), kind: kind || "ok", ttl: +ttl || 2400, at: Date.now() }]).slice(-8)); try { const host = document.querySelector("[data-k2b-toasts]"); if (host) Array.from(host.children).forEach(c => { const ic = c.children[0] ? c.children[0].textContent.trim() : "", tx = c.children[1] ? c.children[1].textContent.trim() : ""; if (tx) add(tx.replace(/\+\d+$/, ""), ic === "✕" ? "err" : ic === "!" ? "warn" : ic === "i" ? "info" : "ok", 6000); }); } catch (e) {} const on = (e) => { const d = (e && e.detail) || {}; if (d.msg) add(d.msg, d.kind, d.ttl); }; window.addEventListener("k2btoast", on); return () => window.removeEventListener("k2btoast", on); }, []); const last = msgs.length ? msgs[msgs.length - 1] : null; const hold = last ? Math.max(4000, last.ttl + 1500) : 0; React.useEffect(() => { if (!last) return undefined; const t = setTimeout(tick, hold + 50); return () => clearTimeout(t); }, [last ? last.id : null]); return { msgs, last, fresh: !!last && (Date.now() - last.at < hold) }; } // Put the cursor in a desk box (by data-f / data-opt selector) and flash it. function dkfFocusBox(root, sel) { try { const host = root && root.querySelector(sel); if (!host) return false; const inp = host.matches("input") ? host : host.querySelector("input:not([type='checkbox']), select"); const el = inp || host; if (el.focus) el.focus(); if (inp && inp.select) inp.select(); const fl = host.closest(".dkc-f, .dkc-c") || host; fl.classList.remove("dkc-flash"); void fl.offsetWidth; fl.classList.add("dkc-flash"); setTimeout(() => { try { fl.classList.remove("dkc-flash"); } catch (e) {} }, 950); return true; } catch (e) { return false; } } // ── RC27 (dsk): DEAL COST LINES (the Profit panel) — helpers ── // A line: { id, type, desc, amount ("595.00"), side "front" | "back", weOwe, passed (lender fee only), kind, points, lender, auto }. // The checks and the money are DealCalc's (= the server's lib/deal-calc.js): costAddsOf / profitPlan. const dkpId = () => "CA" + Date.now().toString(36).toUpperCase() + Math.random().toString(36).slice(2, 6).toUpperCase(); const dkpDealOf = (dealId) => { try { return dealId && window.DMS && window.DMS.deals ? (window.DMS.deals().find(x => x.id === dealId) || null) : null; } catch (e) { return null; } }; const dkpDealCostAdds = (dealId) => { const d = dkpDealOf(dealId); return d && Array.isArray(d.costAdds) ? JSON.parse(JSON.stringify(d.costAdds)) : []; }; const dkpPosted = (d) => !!d && !d.imported && (d.postedToGL === true || (window.DealCalc ? window.DealCalc.isPostedDeal(d) : false)); const DKP_TYPES = () => (window.DealCalc ? window.DealCalc.COST_ADD_ORDER.map(k => [k, window.DealCalc.COST_ADD_TYPES[k]]) : []); const DKP_EMPTY = { any: false, lines: [], problems: [], dpaCents: 0, feeCustCents: 0, feeDealerCents: 0, frontCents: 0, backCents: 0, weOweCents: 0, accrueCents: 0 }; const dkpCa = (list) => (window.DealCalc && Array.isArray(list) && list.length ? window.DealCalc.costAddsOf({ costAdds: list }) : DKP_EMPTY); // RC27 (dsk) fix round (audit): the PACK the posting uses — the unit's own pack in Inventory ($0.00 when Inventory has none), exactly what // the server's posting reads (deal-calc postPlan: the RC21 pay plan, pack counts). null = the unit is not in Inventory (the desk pack stands). const dkpInvPackOf = (stock) => { if (!stock || !window.DMS || !window.DMS._unit) return null; let u = null; try { u = window.DMS._unit(stock); } catch (e) { u = null; } if (!u) return null; const c = Math.round((+u.pack || 0) * 100); return isFinite(c) && c > 0 ? c / 100 : 0; }; const DkfDeskSheet = ({ onClose, go }) => { const settingsRef = React.useRef(loadDeskSettings()); const S = settingsRef.current; const [unit, setUnitState] = React.useState(dtLoadUnit); const [cust, setCust] = React.useState(dtLoadCust); const [picker, setPicker] = React.useState(null); // "unit" | "cust" const [settingsOpen, setSettingsOpen] = React.useState(false); const [printOpen, setPrintOpen] = React.useState(false); // Live registries const [, regForce] = React.useReducer(n => n + 1, 0); React.useEffect(() => { const offs = []; if (window.jbLenders) offs.push(window.jbLenders.subscribe(regForce)); if (window.jbProducts) offs.push(window.jbProducts.subscribe(regForce)); return () => offs.forEach(f => f && f()); }, []); // Titling entities (role: lienholder/lessor, e.g. AmeriCredit / ACAR Leasing) // stay out of the funding-lender menu — they're auto-selected per deal type. const lenderList = (window.jbLenders ? window.jbLenders.list().filter(l => l.active && !l.role) : []); const catalog = (window.jbProducts ? window.jbProducts.list().filter(p => p.active) : []); const [, dtForce] = React.useReducer(n => n + 1, 0); React.useEffect(() => (window.DMS && window.DMS.subscribe) ? window.DMS.subscribe(dtForce) : undefined, []); const unitCheck = (window.DMS && window.DMS.unitOpenItems && unit.stock) ? window.DMS.unitOpenItems(unit.stock, { exceptDeal: cust.dealId }) : { clean: true, items: [] }; const setUnit = (u) => { const n = dtNormUnit(u); setUnitState(n); window.jbDeskUnit = { ...n, list: n.msrp }; try { localStorage.setItem("jb_desk_unit", JSON.stringify(window.jbDeskUnit)); } catch (e) {} if (n.msrp) setPrice(n.msrp); setMsrpOv(n.msrp || 0); // Inception miles seed from the unit's odometer — the engine auto-applies // the GMF retro adjustment when it lands in the 1,250–7,500 mi window. setInceptionMiles(+n.miles || 0); setPicker(null); window.k2bToast && window.k2bToast(`Now desking ${n.yr} ${n.mk} ${n.mdl} · Stk ${n.stock}.`, "ok"); }; const pickCust = (c) => { setCust(c); dkpLoadedRef.current = c.dealId || null; setCostAdds(dkpDealCostAdds(c.dealId)); // RC27 (dsk): the picked deal's cost lines window.jbDeskDeal = c.dealId ? { id: c.dealId, buyer: c.name } : { buyer: c.name }; try { localStorage.setItem("jb_desk_deal", JSON.stringify(window.jbDeskDeal)); } catch (e) {} if (c.dealId) { try { localStorage.setItem("jb_fw_deal", c.dealId); } catch (e) {} } // Carry the deal's vehicle onto the desk automatically — picking a deal // should never require re-picking the unit it was written on. if (c.dealId && window.DMS && window.DMS.deals) { const d = window.DMS.deals().find(x => x.id === c.dealId); const u = d && d.stock ? (window.DMS.inventory() || []).find(x => x.stock === d.stock) : null; if (u) { const n = dtNormUnit({ ...u, list: u.asking }); setUnitState(n); window.jbDeskUnit = { ...n, list: n.msrp }; try { localStorage.setItem("jb_desk_unit", JSON.stringify(window.jbDeskUnit)); } catch (e) {} if (d.salePrice) setPrice(+d.salePrice); else if (n.msrp) setPrice(n.msrp); setMsrpOv(n.msrp || 0); setInceptionMiles(+n.miles || 0); } } setPicker(null); window.k2bToast && window.k2bToast(`Customer set: ${c.name}${c.dealId ? " · deal " + c.dealId + " (unit carried over)" : ""}.`, "ok"); }; const W = React.useRef(dtLoadWork()).current; // restore the in-progress desk const [price, setPrice] = React.useState(() => W.price != null ? W.price : (unit.msrp || 0)); // MSRP override — residual $ keys off THIS, never the sale price, so the // desk can discount the sell price while MSRP stays fixed for the residual // calc. Seeded from the unit's sticker; re-seeds whenever the unit changes. const [msrpOv, setMsrpOv] = React.useState(() => W.msrpOv != null ? W.msrpOv : (unit.msrp || 0)); const msrpVal = +msrpOv || unit.msrp || price; const [docFee, setDocFee] = React.useState(W.docFee != null ? W.docFee : S.docFee); const [taxRate, setTaxRate] = React.useState(W.taxRate != null ? W.taxRate : S.taxRate); const [tradeAllow, setTradeAllow] = React.useState(W.tradeAllow || 0); const [tradeAcv, setTradeAcv] = React.useState(W.tradeAcv || 0); const [tradePayoff, setTradePayoff] = React.useState(W.tradePayoff || 0); const [hasTrade, setHasTrade] = React.useState(!!W.hasTrade); const [tradeDesc, setTradeDesc] = React.useState(W.tradeDesc || ""); // Payoff lender (bank the trade payoff goes to) — from the CRM lead / deal, // editable here, carried to the Finance Deal + Buyer's Order. const [tradeLender, setTradeLender] = React.useState(W.tradeLender || ""); const removeTrade = () => { setHasTrade(false); setTradeAllow(0); setTradeAcv(0); setTradePayoff(0); setTradeDesc(""); setTradeLender(""); window.k2bToast && window.k2bToast("No trade on this deal.", "ok"); }; const loadAppraisal = () => { const ap = (window.DMS && window.DMS.appraisals && window.DMS.appraisals()[0]) || null; if (!ap) { window.k2bToast && window.k2bToast("No saved appraisals — use \u201c+ Enter manually,\u201d or run Trade Appraisal.", "warn", 3600); return; } setHasTrade(true); setTradeDesc(`${[ap.year, ap.make, ap.model].filter(Boolean).join(" ").toUpperCase()}${ap.mileage ? " \u00b7 " + Number(ap.mileage).toLocaleString() + " mi" : ""}`); setTradeAllow(+ap.allowance || 0); setTradePayoff(+ap.payoff || 0); setTradeAcv(ap.acv != null ? (+ap.acv || 0) : (+ap.allowance || 0)); if (ap.lender || ap.payoffLender || ap.lienholder) setTradeLender(String(ap.lender || ap.payoffLender || ap.lienholder).toUpperCase()); window.k2bToast && window.k2bToast("Trade loaded from latest appraisal.", "ok"); }; const addManualTrade = () => { setHasTrade(true); setTradeDesc(""); setTradeAllow(0); setTradePayoff(0); setTradeAcv(0); setTradeLender(""); }; // Seed the trade from THE deal (CRM → Desk It carries tradeDesc / payoff / // lender / appraisal onto it). Runs once per desk when the work blob is // fresh — dtLoadWork returns only { dealId } (hasTrade undefined) whenever // the desk opens on a DIFFERENT deal than the saved one. A deal "has a // trade" when any of allowance / payoff / description / appraisal is set. const tradeInitRef = React.useRef(false); React.useEffect(() => { if (tradeInitRef.current || W.hasTrade != null) return; tradeInitRef.current = true; const d = cust.dealId && window.DMS && window.DMS.deals ? window.DMS.deals().find(x => x.id === cust.dealId) : null; if (d && (+d.tradeAllow > 0 || +d.tradePayoff > 0 || d.tradeDesc || d.tradeYrMkMdl || d.tradeVeh || d.tradeApprId)) { setHasTrade(true); setTradeAllow(+d.tradeAllow || 0); setTradePayoff(+d.tradePayoff || 0); setTradeAcv(+d.tradeACV || +d.tradeAcv || +d.tradeAllow || 0); setTradeDesc(d.tradeYrMkMdl || d.tradeDesc || d.tradeVeh || ""); setTradeLender(String(d.tradeLender || "").toUpperCase()); } }, [cust.dealId]); // Down payment lives PER OPTION now (sc[i].down / lsc[i].down) — the legacy // single cashDown only seeds migration of an older work blob / saved scenario. const legacyDown = W.cashDown != null ? +W.cashDown || 0 : S.cashDown; const [rebateIn, setRebate] = React.useState(W.rebate != null ? W.rebate : S.rebate); // as entered (new units) // Rebates apply to NEW units only. When the unit turns used the entered // rebate is moved here (NOT applied anywhere) so the user can review it on // the way back to new; rebateReview = [{ key, label, amount }] pending Apply. const [rebateHeld, setRebateHeld] = React.useState(W.rebateHeld != null ? +W.rebateHeld || 0 : 0); const [leaseCashHeld, setLeaseCashHeld] = React.useState(W.leaseCashHeld != null ? +W.leaseCashHeld || 0 : 0); const [rebateReview, setRebateReview] = React.useState(Array.isArray(W.rebateReview) ? W.rebateReview : []); const [lender, setLender] = React.useState(() => W.lender || (window.jbLenders && window.jbLenders.activeNames()[0]) || "GM Financial"); const [pack, setPack] = React.useState(W.pack != null ? W.pack : S.pack); // RC27 (dsk) fix round (audit): the desk pack FOLLOWS the unit's Inventory pack (what the posting books) unless a manager typed a pack // by hand (a what-if — the Profit panel then also shows what the posting will book with the Inventory pack). const [packHand, setPackHand] = React.useState(() => !!W.packHand); const dkpInvPack = dkpInvPackOf(unit.stock); React.useEffect(() => { if (dkpInvPack != null && !packHand && Math.round((+pack || 0) * 100) !== Math.round(dkpInvPack * 100)) setPack(dkpInvPack); }, [unit.stock, dkpInvPack, packHand, pack]); const setPackByHand = (v) => { setPackHand(true); setPack(v); }; const [commPct, setCommPct] = React.useState(W.commPct != null ? W.commPct : S.commPct); // Credit profile: blank until the customer record / F&I supplies it (B-05). const [fico, setFico] = React.useState(() => W.fico != null ? W.fico : (+cust.credit || 0)); const [income, setIncome] = React.useState(() => W.income != null ? W.income : (+cust.income || 0)); const [dti, setDti] = React.useState(() => W.dti != null ? W.dti : String(cust.dti || "")); // Re-sync the credit profile ONLY when a different customer is picked — // never on mount (that was silently resetting edited FICO/income/DTI). const custSyncRef = React.useRef(false); React.useEffect(() => { if (!custSyncRef.current) { custSyncRef.current = true; return; } setFico(+cust.credit || 0); setIncome(+cust.income || 0); setDti(String(cust.dti || "")); }, [cust]); // Finance options A–D: { term, apr, gap, vsc, all, down }. Always 4 entries; // scOn says which letters are visible (A + B always on). Letters are stable. const [sc, setSc] = React.useState(() => dtMigrateSc(W.sc, legacyDown, [ { term: S.t1, apr: S.a1, gap: false, vsc: false, all: false }, { term: S.t2, apr: S.a2, gap: true, vsc: false, all: false }, { term: S.t3, apr: S.a3, gap: true, vsc: true, all: false }])); const setScK = (i, k, v) => setSc(arr => arr.map((s, j) => j === i ? { ...s, [k]: v } : s)); const [scOn, setScOn] = React.useState(() => dtMigrateOn(W.scOn, W.sc)); // ── FINANCE vs LEASE ────────────────────────────────────────── // One desk, two quote types. Lease scenarios carry their own term, money // factor and residual %; everything else (price, trade, products) is shared. const [mode, setMode] = React.useState(W.mode === "lease" ? "lease" : "finance"); const [milesPerYr, setMilesPerYr] = React.useState(W.milesPerYr != null ? W.milesPerYr : S.milesPerYr); // GMF program mode — ON by default; MF/residuals autofill off the rate sheet. const [gmfOn, setGmfOn] = React.useState(W.gmfOn != null ? !!W.gmfOn : true); const [gmfNewUsed, setGmfNewUsed] = React.useState(W.gmfNewUsed === "used" ? "used" : "new"); const [gmfPick, setGmfPick] = React.useState(W.gmfPick || null); const [gmfTier, setGmfTier] = React.useState(W.gmfTier || "A+/A1"); const [gmfLeaseCash, setGmfLeaseCash] = React.useState(W.gmfLeaseCash != null ? +W.gmfLeaseCash || 0 : 0); const [gmfPickerOpen, setGmfPickerOpen] = React.useState(false); const gmfFees = (dtGmfData().program.fees || {}); const [acqFee, setAcqFee] = React.useState(W.acqFee != null ? W.acqFee : (W.gmfOn === false ? S.acqFee : (+gmfFees.acquisition || 0))); const [acqFeeCapped, setAcqFeeCapped] = React.useState(W.acqFeeCapped != null ? !!W.acqFeeCapped : true); const [docFeeCapped, setDocFeeCapped] = React.useState(W.docFeeCapped != null ? !!W.docFeeCapped : true); const [titleLicFee, setTitleLicFee] = React.useState(W.titleLicFee != null ? W.titleLicFee : 0); const [titleLicCapped, setTitleLicCapped] = React.useState(W.titleLicCapped != null ? !!W.titleLicCapped : true); const [capRed, setCapRed] = React.useState(W.capRed != null ? W.capRed : 0); const [taxMethod, setTaxMethod] = React.useState(W.taxMethod || (S.leaseTaxOnPayment === false ? "upfront_on_payment_stream" : "monthly")); // Lease tax rate is SEPARATE from the finance-desk rate: the finance desk // keeps the dealership rate (7.225); the lease desk defaults to the // customer's combined local rate collected on lease receipts (9.537 — // validated against the live RouteOne deal). Both editable, both persisted. const [leaseTaxRate, setLeaseTaxRate] = React.useState(W.leaseTaxRate != null ? W.leaseTaxRate : DT_LEASE_TAX_DEFAULT); // Mileage factor group — extra mileage purchased up front, inception // (odometer) miles, and their $/mi rates. inceptionAdjust: null = AUTO // (engine applies the GMF 1,250–7,500 mi retro window), true/false = forced. const [extraMiles, setExtraMiles] = React.useState(W.extraMiles != null ? W.extraMiles : 0); const [extraMileageRate, setExtraMileageRate] = React.useState(W.extraMileageRate != null ? W.extraMileageRate : 0.20); const [inceptionMiles, setInceptionMiles] = React.useState(W.inceptionMiles != null ? W.inceptionMiles : (+unit.miles || 0)); const [inceptionAdjust, setInceptionAdjust] = React.useState(W.inceptionAdjust !== undefined ? W.inceptionAdjust : null); const [inceptionRate, setInceptionRate] = React.useState(W.inceptionRate != null ? W.inceptionRate : 0.20); const [secDep, setSecDep] = React.useState(W.secDep != null ? W.secDep : null); // null = auto (B2/B3 rule) const [dispFee, setDispFee] = React.useState(W.dispFee != null ? W.dispFee : (gmfFees.disposition != null ? +gmfFees.disposition : (S.dispFee != null ? S.dispFee : 395))); const [excessRate, setExcessRate] = React.useState(W.excessRate != null ? W.excessRate : (gmfFees.excessMileagePerMile != null ? +gmfFees.excessMileagePerMile : 0.25)); const [purchOptFee, setPurchOptFee] = React.useState(W.purchOptFee != null ? W.purchOptFee : (+gmfFees.purchaseOption || 0)); const [dealerMarkupMf, setDealerMarkupMf] = React.useState(W.dealerMarkupMf != null ? W.dealerMarkupMf : 0); const [leaseExpand, setLeaseExpand] = React.useState(null); // option index w/ open line-item breakdown const [lsc, setLsc] = React.useState(() => dtMigrateSc(W.lsc, legacyDown, [ dtNormLsc({ term: S.lt1, mf: S.lmf1 }), dtNormLsc({ term: S.lt2, mf: S.lmf2 }), dtNormLsc({ term: S.lt3, mf: S.lmf3, vsc: true })], dtNormLsc)); const setLscK = (i, k, v) => setLsc(arr => arr.map((s, j) => j === i ? { ...s, [k]: v } : s)); // Which option (A–D) is SELECTED for presentation — drives the highlight, // the hero payment, prints, and what goes to F&I. Click a row to choose. // Always a VISIBLE option. const [selSc, setSelScRaw] = React.useState(() => { const on = dtMigrateOn(W.scOn, W.sc); return (W.selSc >= 0 && W.selSc < DT_MAX_OPTS && on[W.selSc]) ? W.selSc : 1; }); const setSelSc = (i) => { if (scOn[i]) setSelScRaw(i); }; // Visible option indexes, in letter order — the ONLY options any total, // print or push ever reads. const visIdx = [0, 1, 2, 3].filter(i => scOn[i]); const addOption = () => { const i = scOn.findIndex(v => !v); if (i < 0) return; // New option = copy of the previous VISIBLE option (term, rate, products, down). const prev = [0, 1, 2, 3].filter(j => scOn[j] && j < i).pop(); const src = prev != null ? prev : 1; setSc(arr => arr.map((s, j) => j === i ? { ...arr[src] } : s)); setLsc(arr => arr.map((s, j) => j === i ? { ...arr[src] } : s)); setScOn(on => on.map((v, j) => j === i ? true : v)); window.k2bToast && window.k2bToast(`Option ${DT_LETTERS[i]} added (copy of ${DT_LETTERS[src]}).`, "ok"); }; const removeOption = (i) => { if (i < 2) return; // A and B always stay const on = scOn.map((v, j) => j === i ? false : v); setScOn(on); if (selSc === i) setSelScRaw([0, 1, 2, 3].filter(j => on[j] && j < i).pop() ?? 1); window.k2bToast && window.k2bToast(`Option ${DT_LETTERS[i]} removed — it no longer prints or pushes.`, "ok"); }; // F&I products — selected map keyed by product id → chosen retail price. const [selected, setSelected] = React.useState(W.selected || {}); // { [id]: price } const toggleProduct = (p) => setSelected(m => { const n = { ...m }; if (n[p.id] != null) delete n[p.id]; else n[p.id] = p.retail; return n; }); const setProductPrice = (id, v) => setSelected(m => ({ ...m, [id]: v })); const selectedProducts = catalog.filter(p => selected[p.id] != null); // ── RC27 (dsk): the deal's COST LINES (Profit panel). Saved with the deal (deal.costAdds — the server posts them) and in the working // desk / saved scenarios. A posted deal's lines are its own (frozen — the panel shows them read-only). const [costAdds, setCostAdds] = React.useState(() => (Array.isArray(W.costAdds) && W.dealId === (cust.dealId || null)) ? W.costAdds : dkpDealCostAdds(cust.dealId)); const dkpLoadedRef = React.useRef(cust.dealId || null); const dkpDeal = dkpDealOf(cust.dealId); const dkpIsPosted = dkpPosted(dkpDeal); const dkpLines = dkpIsPosted ? (Array.isArray(dkpDeal.costAdds) ? dkpDeal.costAdds : []) : costAdds; const CA = dkpCa(dkpLines); const setCaK = (i, k, v) => setCostAdds(arr => arr.map((l, j) => { if (j !== i) return l; if (k === "type") { const T = (window.DealCalc && window.DealCalc.COST_ADD_TYPES[v]) || {}; return Object.assign({}, l, { type: v, side: T.side || "front", weOwe: v === "weOwe", passed: false, kind: v === "lenderFee" ? (l.kind || "acquisition") : undefined, auto: false }); } return Object.assign({}, l, { [k]: v }, k === "amount" || k === "desc" || k === "passed" || k === "side" || k === "kind" ? { auto: false } : {}); })); const addCostLine = (type) => setCostAdds(arr => arr.length >= ((window.DealCalc && window.DealCalc.MAX_COST_ADDS) || 20) ? arr : arr.concat([{ id: dkpId(), type, desc: "", amount: "", side: (window.DealCalc && window.DealCalc.COST_ADD_TYPES[type] || {}).side || "front", weOwe: type === "weOwe", passed: false, kind: type === "lenderFee" ? "acquisition" : undefined }])); // a lender fee taken off by hand is not suggested again for that lender on this desk const [dkpFeeOff, setDkpFeeOff] = React.useState(() => (W.dkpFeeOff && W.dealId === (cust.dealId || null)) ? String(W.dkpFeeOff) : ""); const removeCostLine = (i) => setCostAdds(arr => { const l = arr[i]; if (l && l.type === "lenderFee") setDkpFeeOff(String(l.lender || lender || "")); return arr.filter((x, j) => j !== i); }); // ── Saved scenarios — snapshot / reopen the COMPLETE desk (server-synced // via DMS.deskScenarios so any workstation can pick the deal back up). ── const [scenModal, setScenModal] = React.useState(null); // "save" | "list" | null const [scenName, setScenName] = React.useState(""); const collectScenario = () => JSON.parse(JSON.stringify({ unit, cust, price, msrpOv, docFee, taxRate, hasTrade, tradeDesc, tradeAllow, tradeAcv, tradePayoff, tradeLender, cashDown, rebate: rebateIn, rebateHeld, leaseCashHeld, lender, pack, packHand, commPct, fico, income, dti, sc, scOn, selSc, selected, mode, milesPerYr, acqFee, capRed, lsc, gmfOn, gmfNewUsed, gmfPick, gmfTier, gmfLeaseCash, acqFeeCapped, docFeeCapped, titleLicFee, titleLicCapped, taxMethod, secDep, dispFee, excessRate, purchOptFee, dealerMarkupMf, leaseTaxRate, extraMiles, extraMileageRate, inceptionMiles, inceptionAdjust, inceptionRate, costAdds, // RC27 (dsk) })); const restoreScenario = (st) => { if (!st) { window.k2bToast && window.k2bToast("Scenario has no saved state.", "warn"); return; } tradeInitRef.current = true; // keep the deal-seed effect from clobbering the restored trade // Restore customer with credit fields matching the saved desk, so the // customer-resync effect re-derives the SAME fico / income / DTI. const c = { ...(st.cust || {}), credit: st.fico, income: st.income, dti: st.dti }; setCust(c); window.jbDeskDeal = c.dealId ? { id: c.dealId, buyer: c.name } : { buyer: c.name || "" }; try { localStorage.setItem("jb_desk_deal", JSON.stringify(window.jbDeskDeal)); } catch (e) {} if (st.unit && st.unit.stock) { setUnitState(st.unit); window.jbDeskUnit = { ...st.unit, list: st.unit.msrp }; try { localStorage.setItem("jb_desk_unit", JSON.stringify(window.jbDeskUnit)); } catch (e) {} } setPrice(+st.price || 0); setDocFee(+st.docFee || 0); setTaxRate(+st.taxRate || 0); setMsrpOv(st.msrpOv != null ? +st.msrpOv || 0 : ((st.unit && +st.unit.msrp) || 0)); setHasTrade(!!st.hasTrade); setTradeDesc(st.tradeDesc || ""); setTradeLender(st.tradeLender || ""); setTradeAllow(+st.tradeAllow || 0); setTradeAcv(+st.tradeAcv || 0); setTradePayoff(+st.tradePayoff || 0); // Rebate: the scenario's own figure; its held (used-unit) rebate comes back // as held — never applied — and no review chip carries over. const stUsed = (st.unit && st.unit.newUsed === "U") || (st.mode === "lease" && st.gmfOn !== false && st.gmfNewUsed === "used"); nuPrevRef.current = !!stUsed; // restored status is the baseline — no transition const stReb = +st.rebate || 0, stLc = +st.gmfLeaseCash || 0; setRebate(stUsed ? 0 : stReb); setRebateHeld(stUsed && stReb ? stReb : (+st.rebateHeld || 0)); setLeaseCashHeld(stUsed && stLc ? stLc : (+st.leaseCashHeld || 0)); setRebateReview([]); if (st.lender) setLender(st.lender); setPack(+st.pack || 0); setPackHand(!!st.packHand); setCommPct(+st.commPct || 0); setFico(+st.fico || 0); setIncome(+st.income || 0); setDti(String(st.dti || "")); // Options A–D (older 3-option scenarios migrate: down = their cashDown). const stDown = +st.cashDown || 0; if (Array.isArray(st.sc) && st.sc.length) setSc(dtMigrateSc(st.sc, stDown, st.sc)); const stOn = dtMigrateOn(st.scOn, st.sc); setScOn(stOn); setSelScRaw(st.selSc >= 0 && st.selSc < DT_MAX_OPTS && stOn[st.selSc] ? st.selSc : 1); setSelected(st.selected || {}); setMode(st.mode === "lease" ? "lease" : "finance"); setMilesPerYr(st.milesPerYr != null ? st.milesPerYr : S.milesPerYr); setAcqFee(st.acqFee != null ? st.acqFee : S.acqFee); setCapRed(+st.capRed || 0); if (Array.isArray(st.lsc) && st.lsc.length) setLsc(dtMigrateSc(st.lsc, stDown, st.lsc, dtNormLsc)); // GMF program state — every field the lease desk carries. setGmfOn(st.gmfOn != null ? !!st.gmfOn : true); setGmfNewUsed(st.gmfNewUsed === "used" ? "used" : "new"); setGmfPick(st.gmfPick || null); setGmfTier(st.gmfTier || "A+/A1"); setGmfLeaseCash(stUsed ? 0 : (+st.gmfLeaseCash || 0)); setAcqFeeCapped(st.acqFeeCapped != null ? !!st.acqFeeCapped : true); setDocFeeCapped(st.docFeeCapped != null ? !!st.docFeeCapped : true); setTitleLicFee(+st.titleLicFee || 0); setTitleLicCapped(st.titleLicCapped != null ? !!st.titleLicCapped : true); setTaxMethod(st.taxMethod || "monthly"); setSecDep(st.secDep != null ? st.secDep : null); setDispFee(st.dispFee != null ? +st.dispFee : 395); setExcessRate(st.excessRate != null ? +st.excessRate : 0.25); setPurchOptFee(+st.purchOptFee || 0); setDealerMarkupMf(+st.dealerMarkupMf || 0); // Mileage factor group + the lease-specific tax rate. setLeaseTaxRate(st.leaseTaxRate != null ? +st.leaseTaxRate : DT_LEASE_TAX_DEFAULT); setExtraMiles(+st.extraMiles || 0); setExtraMileageRate(st.extraMileageRate != null ? +st.extraMileageRate : 0.20); setInceptionMiles(st.inceptionMiles != null ? +st.inceptionMiles : ((st.unit && +st.unit.miles) || 0)); setInceptionAdjust(st.inceptionAdjust !== undefined ? st.inceptionAdjust : null); setInceptionRate(st.inceptionRate != null ? +st.inceptionRate : 0.20); // RC27 (dsk): the scenario's cost lines (a posted deal keeps its own — the panel shows the deal's) dkpLoadedRef.current = c.dealId || null; setCostAdds(Array.isArray(st.costAdds) ? JSON.parse(JSON.stringify(st.costAdds)) : []); }; const openSaveScenario = () => { const d = new Date(); const who = (!cust.preset && cust.name) ? cust.name : "Walk-in"; const veh = `${unit.yr || ""} ${unit.mk || ""} ${unit.mdl || ""}`.replace(/\s+/g, " ").trim(); setScenName(`${who} — ${veh || unit.stock || "No unit"} — ${d.getMonth() + 1}/${d.getDate()}`); setScenModal("save"); }; const saveScenario = () => { const name = scenName.trim() || "Untitled scenario"; const r = (window.DMS && window.DMS.saveDeskScenario) ? window.DMS.saveDeskScenario({ id: "DS-" + Date.now(), name, state: collectScenario(), view: dkfViewSafe() }) : { ok: false, msg: "DMS core not loaded." }; if (r && r.ok) { setScenModal(null); window.k2bToast && window.k2bToast("Scenario saved", "ok"); } else window.k2bToast && window.k2bToast((r && r.msg) || "Could not save scenario.", "warn"); }; // ── Computed ── // New vs used: from the desked unit (inventory type) — and, on a lease, the // GMF Used (CPO) program. Used units carry NO new-vehicle rebate / lease cash: // `rebate` below is the EFFECTIVE rebate every figure, print and push reads. const isUsedUnit = unit.newUsed === "U" || (mode === "lease" && gmfOn && gmfNewUsed === "used"); const rebate = isUsedUnit ? 0 : (+rebateIn || 0); const leaseCashEff = (!isUsedUnit && gmfOn && gmfNewUsed === "new") ? (+gmfLeaseCash || 0) : 0; // New ↔ used transitions (unit pick, deal pick, scenario restore, GMF // New / Used (CPO) buttons, finance ↔ lease with a GMF used program): // → used: the entered rebate / GMF lease cash move to HELD (not applied); // → new : nothing is re-applied — an amber review chip offers Apply / Discard. // All figures read the effective `rebate` above, so they recalc in the same // render the status flips (this effect only tidies the stored inputs). const nuPrevRef = React.useRef(null); React.useEffect(() => { const prev = nuPrevRef.current; nuPrevRef.current = isUsedUnit; if (prev === isUsedUnit) return; if (isUsedUnit) { const r = +rebateIn || 0, lc = +gmfLeaseCash || 0; if (r > 0) { setRebateHeld(r); setRebate(0); } if (lc > 0) { setLeaseCashHeld(lc); setGmfLeaseCash(0); } setRebateReview([]); if (prev === false && (r > 0 || lc > 0) && window.k2bToast) window.k2bToast(`Used unit — ${[r > 0 ? "rebate " + dt$(r) : null, lc > 0 ? "GMF lease cash " + dt$(lc) : null].filter(Boolean).join(" + ")} removed from every option (held for review if it goes back to new).`, "warn", 4800); } else if (prev === true) { const items = []; if (rebateHeld > 0) items.push({ key: "rebate", label: "Rebate", amount: dtRound2(rebateHeld) }); // GMF program data for the picked new model outranks the held lease cash. const progCash = gmfOn && gmfPick && +gmfPick.leaseCash > 0 ? +gmfPick.leaseCash : 0; const lc = progCash || leaseCashHeld; if (lc > 0) items.push({ key: "leaseCash", label: progCash ? "GMF lease cash (program)" : "GMF lease cash", amount: dtRound2(lc) }); setRebateReview(items); } }, [isUsedUnit]); const reviewApply = (it) => { if (it.key === "rebate") { setRebate(it.amount); setRebateHeld(0); } else { setGmfLeaseCash(it.amount); setLeaseCashHeld(0); } setRebateReview(list => list.filter(x => x.key !== it.key)); window.k2bToast && window.k2bToast(`${it.label} ${dt$(it.amount)} applied to every option.`, "ok"); }; const reviewDiscard = (it) => { if (it.key === "rebate") setRebateHeld(0); else setLeaseCashHeld(0); setRebateReview(list => list.filter(x => x.key !== it.key)); }; // Per-option down payment (finance + lease each keep their own per letter). const optDown = (i, lease = mode === "lease") => +(((lease ? lsc : sc)[i]) || {}).down || 0; const cashDown = optDown(selSc); // the SELECTED option's down const setCashDown = (v) => (mode === "lease" ? setLscK : setScK)(selSc, "down", +v || 0); // Tax: taxable = price + doc − trade allowance. The rebate is applied AFTER // tax (not a pre-tax reduction), so dropping it moves amount financed by // exactly the rebate amount. const taxable = price + docFee - tradeAllow; const tax = Math.max(0, taxable) * (taxRate / 100); const tradeEquity = tradeAllow - tradePayoff; // Trade equity split (retail + lease alike): positive equity is DOWN money; // negative equity (payoff > allowance) is ROLLED INTO the loan / cap cost. const negEquity = hasTrade ? Math.max(0, dtRound2(tradePayoff - tradeAllow)) : 0; const posEquity = hasTrade ? Math.max(0, dtRound2(tradeAllow - tradePayoff)) : 0; // Per-scenario product loads — GAP / Warranty(VSC) priced from the catalog // (or the picked price when selected); "All" = every selected F&I product. // The "Warranty" pill uses the service contract ticked in the checklist when // one is, else the first active one in the catalog (same for GAP). const isGapProd = (p) => /gap/i.test((p.name || "") + " " + (p.category || "")); const isVscProd = (p) => !isGapProd(p) && /vsc|service contract|warrant/i.test((p.name || "") + " " + (p.category || "")); const gapProd = catalog.find(p => isGapProd(p) && selected[p.id] != null) || catalog.find(isGapProd) || null; const vscProd = catalog.find(p => isVscProd(p) && selected[p.id] != null) || catalog.find(isVscProd) || null; const prodPrice = (p) => p ? (selected[p.id] != null ? +selected[p.id] || 0 : +p.retail || 0) : 0; const prodCost = (p) => p ? +p.cost || 0 : 0; // The protection products that are PART OF the chosen scenario, as the deal // record carries them: [{ id, code, name, category, provider, price, cost, termMo }]. // all → every ticked product (GAP + Warranty when nothing is ticked) // else → [Warranty if vsc, GAP if gap] const chosenProducts = (i = selSc) => { const s = (isLease ? lsc : sc)[i] || {}; let list = s.all ? (selectedProducts.length ? selectedProducts : [vscProd, gapProd]) : [s.vsc ? vscProd : null, s.gap ? gapProd : null]; const seen = {}; return list.filter(p => p && !seen[p.id] && (seen[p.id] = true)).map(p => ({ id: p.id, code: p.code || p.id, name: p.name || "", category: p.category || "", provider: p.provider || "", price: dtRound2(prodPrice(p)), cost: dtRound2(prodCost(p)), termMo: +p.termMo || 0 })); }; const scAdds = (i) => { const s = sc[i] || {}; if (s.all) { const sel = selectedProducts.reduce((sum, p) => sum + (+selected[p.id] || 0), 0); return sel || prodPrice(gapProd) + prodPrice(vscProd); } return (s.gap ? prodPrice(gapProd) : 0) + (s.vsc ? prodPrice(vscProd) : 0); }; // amtFin(i) = price + doc + tax + $30 title − trade allowance + trade payoff // − down(i) − rebate (new units only) + products(i) const baseFin = (i) => price + docFee + tax + 30 - tradeAllow + tradePayoff - optDown(i, false) - rebate; // RC27 (dsk): a lender fee PASSED TO THE CUSTOMER is the customer's fee line — in the amount financed (finance desk; never on a lease) const dkpFeeCust = mode === "lease" ? 0 : CA.feeCustCents / 100; const dkpDpa = CA.dpaCents / 100; // the store's down payment assistance — the customer pays this much less cash const amtFin = (i) => baseFin(i) + scAdds(i) + dkpFeeCust; const fiAdds = scAdds(selSc); // selected scenario's products const amtFinanced = amtFin(selSc); const frontGross = price - unit.cost - (tradeAllow - tradeAcv); const fiGross = (() => { const s = sc[selSc] || {}; if (s.all) return selectedProducts.reduce((sum, p) => sum + Math.max(0, (+selected[p.id] || 0) - (+p.cost || 0)), 0); let g = 0; if (s.gap) g += Math.max(0, prodPrice(gapProd) - prodCost(gapProd)); if (s.vsc) g += Math.max(0, prodPrice(vscProd) - prodCost(vscProd)); return g; })(); const totalGross = frontGross + fiGross - pack; // RC27 (dsk): the PROFIT with the cost lines — DealCalc.profitPlan, the same function the server's posting uses for the commission // (the pay plan the posting books: 25 % used / 20 % new of the front gross after pack and the FRONT cost lines, at least $150) // RC28 (fx, the reviewer's finding 3): the pay plan is the SERVER's (DMSCore.payPlanConfig — GET /api/pay-plan), the one the posting books const dkpPlan = window.DMS && window.DMS.payPlanConfig ? window.DMS.payPlanConfig() : undefined; const dkpPlanWords = dkpPlan ? (Math.round(dkpPlan.usedPct * 10000) / 100) + " % (used) / " + (Math.round(dkpPlan.newPct * 10000) / 100) + " % (new) of the net front, at least " + dt$(dkpPlan.miniFloor) + (dkpPlan.version ? " (pay plan version " + dkpPlan.version + ")" : " (the shipped pay plan)") : "the pay plan"; const dkpProfit = window.DealCalc ? window.DealCalc.profitPlan({ priceCents: Math.round((+price || 0) * 100), costCents: Math.round((+unit.cost || 0) * 100), overCents: Math.round((+tradeAllow || 0) * 100) - Math.round((+tradeAcv || 0) * 100), packCents: Math.round((+pack || 0) * 100), fiGrossCents: Math.round((+fiGross || 0) * 100), isNew: unit.newUsed === "N", ca: CA, payPlan: dkpPlan }) : null; // RC27 (dsk) fix round (audit): a pack typed by hand is a WHAT-IF — the posting books the unit's Inventory pack; show that figure too const dkpPackWhatIf = dkpInvPack != null && Math.round((+pack || 0) * 100) !== Math.round(dkpInvPack * 100); const dkpPostProfit = dkpPackWhatIf && window.DealCalc ? window.DealCalc.profitPlan({ priceCents: Math.round((+price || 0) * 100), costCents: Math.round((+unit.cost || 0) * 100), overCents: Math.round((+tradeAllow || 0) * 100) - Math.round((+tradeAcv || 0) * 100), packCents: Math.round(dkpInvPack * 100), fiGrossCents: Math.round((+fiGross || 0) * 100), isNew: unit.newUsed === "N", ca: CA, payPlan: dkpPlan }) : null; const dkpC = (c) => dt$((+c || 0) / 100); const pmt = (i) => dtPmt(sc[i].apr / 100, sc[i].term, amtFin(i)); // ── Lease side — GMF program glue + per-option engine calls ────────────── const lsAdds = (i) => { const s = lsc[i] || {}; if (s.all) { const sel = selectedProducts.reduce((sum, p) => sum + (+selected[p.id] || 0), 0); return sel || prodPrice(gapProd) + prodPrice(vscProd); } return (s.gap ? prodPrice(gapProd) : 0) + (s.vsc ? prodPrice(vscProd) : 0); }; // What the GMF program would auto-fill for a term (MF and/or residual %). const gmfAutoFor = (term) => { if (!gmfOn) return {}; const G = dtGmfData(); if (gmfNewUsed === "used") { const t = (G.usedProgram.tierMf || {})[gmfTier] || null; return { mf: t && t[String(term)] != null ? +t[String(term)] : null, rv: null }; } const p = gmfPick; if (!p) return {}; if (p.source === "rates") { const row = (G.program.rates || []).find(r => r.model === p.model && r.modelYear === p.modelYear); const cell = row && row["t" + term]; if (cell) return { mf: +cell.mf, rv: +cell.rv }; } if (p.source === "guide") { const g = (G.residualGuide || []).find(r => r.section === p.section && r.model === p.model && r.code === p.code && r.trim === p.trim); if (g && g.rv && g.rv[String(term)] != null) return { mf: null, rv: +g.rv[String(term)] }; } return {}; }; const mfOf = (i) => { const s = lsc[i] || {}; const a = gmfAutoFor(+s.term || 36); return (s.autoMf && a.mf != null) ? +a.mf : (+s.mf || 0); }; const residBaseOf = (i) => { const s = lsc[i] || {}; const a = gmfAutoFor(+s.term || 36); if (s.autoRv && a.rv != null) return +a.rv; if (s.resid != null && s.resid !== "") return +s.resid; return dtRound2(dtResidualPct(+s.term || 36, milesPerYr)); }; const mileageAddOf = (i) => { const s = lsc[i] || {}; return (gmfOn && s.autoRv && window.jbGmfMileageAdd) ? window.jbGmfMileageAdd(+s.term || 36, milesPerYr, dtGmfData().program) : 0; }; const tierAddOf = (i) => { const s = lsc[i] || {}; return (gmfOn && gmfNewUsed === "new" && !s.sp && window.jbGmfTierAdd) ? window.jbGmfTierAdd(gmfTier, dtGmfData().program) : 0; }; const autoMfOn = (i) => { const s = lsc[i] || {}; return !!(s.autoMf && gmfAutoFor(+s.term || 36).mf != null); }; const autoRvOn = (i) => { const s = lsc[i] || {}; return !!(s.autoRv && gmfAutoFor(+s.term || 36).rv != null); }; const setLeaseMf = (i, v) => setLsc(arr => arr.map((s, j) => j === i ? { ...s, mf: v, autoMf: false } : s)); const setLeaseApr = (i, v) => setLeaseMf(i, dtAprToMf(v)); const setLeaseResid = (i, v) => setLsc(arr => arr.map((s, j) => j === i ? { ...s, resid: v, autoRv: false } : s)); const setLeaseTerm = (i, v) => { let t = Math.max(1, Math.round(+v || 0)); if (gmfOn && gmfNewUsed === "used") t = GMF_USED_TERMS.reduce((b, c) => Math.abs(c - t) < Math.abs(b - t) ? c : b, 24); setLsc(arr => arr.map((s, j) => j === i ? { ...s, term: t, sp: s.sp && !(gmfOn && gmfNewUsed === "used" && t < 36) } : s)); }; const toggleSp = (i) => setLsc(arr => arr.map((s, j) => { if (j !== i) return s; if (!s.sp && gmfOn && gmfNewUsed === "used" && (+s.term || 0) < 36) { window.k2bToast && window.k2bToast("GMF used-lease single-pay is 36-48 month terms only.", "warn"); return s; } return { ...s, sp: !s.sp }; })); const applyGmfPick = (p) => { setGmfPick(p); setGmfPickerOpen(false); setLsc(arr => arr.map(s => ({ ...s, autoMf: true, autoRv: true }))); // Program lease cash is a NEW-unit incentive: on a used unit it is only // held (offered for review if the unit goes back to new). const pc = p && p.leaseCash != null ? +p.leaseCash || 0 : 0; if (isUsedUnit) { setGmfLeaseCash(0); setLeaseCashHeld(pc); } else setGmfLeaseCash(pc); window.k2bToast && window.k2bToast(`GMF program applied — ${p.modelYear || ""} ${p.model}${p.trim ? " " + p.trim : ""}. MF & residuals autofilled per term.`, "ok"); }; const toggleGmf = (on) => { setGmfOn(on); if (on) { const f = dtGmfData().program.fees || {}; setAcqFee(+f.acquisition || 0); setDispFee(f.disposition != null ? +f.disposition : 395); setPurchOptFee(+f.purchaseOption || 0); setExcessRate(f.excessMileagePerMile != null ? +f.excessMileagePerMile : 0.25); if (GMF_MILES.indexOf(+milesPerYr) < 0) setMilesPerYr(12000); setLsc(arr => arr.map(s => ({ ...s, gap: false }))); // GAP included in GMF lease window.k2bToast && window.k2bToast("GMF program mode ON — acq $0 · disposition $395 · 25¢/mi excess · GAP included.", "ok"); } }; const setNewUsed = (nu) => { setGmfNewUsed(nu); if (nu === "used") { setMilesPerYr(15000); setLsc(arr => arr.map((s, j) => { const t = GMF_USED_TERMS[j] || 36; return { ...s, term: t, autoMf: true, autoRv: false, sp: s.sp && t >= 36 }; })); window.k2bToast && window.k2bToast("GMF used lease — 24/36/48 terms, 15,000 mi/yr, residual entered manually off the used guide.", "ok", 4200); } else setLsc(arr => arr.map(s => ({ ...s, autoMf: !!gmfPick, autoRv: !!gmfPick }))); }; // Full engine quote for one option (LeaseCalc.jsx.txt). Returns the raw // engine output plus the legacy field aliases the prints / F&I push use. const leaseOf = (i) => { const s = lsc[i] || {}; const term = +s.term || 36; const sp = !!s.sp; const G = dtGmfData(); if (!window.jbLeaseCalc) return { term, residPct: 0, residual: 0, grossCap: 0, capCostRed: 0, adjCap: 0, dep: 0, rent: 0, base: 0, tax: 0, payment: 0, apr: 0, dueAtSigning: 0, totalOfPayments: 0, singlePay: sp, singlePayAmount: 0, effectiveMoneyFactor: 0, dueAtSigningItems: { total: 0 }, upsideDown: false, autoDep: 0, secDepUsed: 0 }; const spCfg = (gmfNewUsed === "used" ? G.usedProgram.singlePayMfReduction : G.program.singlePayMfReduction) || undefined; const args = { msrp: msrpVal, sellingPrice: price, term, milesPerYr, residualPct: residBaseOf(i) + mileageAddOf(i), moneyFactor: mfOf(i), acqFee, acqFeeCapitalized: acqFeeCapped, // 4-square Cash Down + any ADDITIONAL cap reduction entered on the rail. // GMF single-pay rule: NO cash CCR on a single-pay lease (rebates still apply). capReductionCash: sp ? 0 : optDown(i, true) + (+capRed || 0), // Rebate + GMF lease cash — both zero on a used unit. rebates: rebate + leaseCashEff, tradeAllowance: hasTrade ? tradeAllow : 0, tradePayoff: hasTrade ? tradePayoff : 0, docFee, docFeeCapitalized: docFeeCapped, titleLicenseFee: titleLicFee, titleLicenseCapitalized: titleLicCapped, addOns: lsAdds(i), // Lease-specific tax rate (customer's combined local rate) — NOT the // finance-desk taxRate. taxMethod, taxRate: +leaseTaxRate || 0, securityDeposit: 0, // Mileage factor group — extra mileage purchased + inception (odometer) // miles reduce the residual; null inceptionAdjust = engine AUTO window. extraMilesPurchased: +extraMiles || 0, extraMileageRate: +extraMileageRate || 0, inceptionMiles: +inceptionMiles || 0, inceptionMileageRate: +inceptionRate || 0, ...(inceptionAdjust != null ? { inceptionMileageAdjust: !!inceptionAdjust } : {}), dispositionFee: dispFee, excessMileageRate: excessRate, purchaseOptionFee: purchOptFee, singlePay: sp, tierMfAdd: tierAddOf(i), dealerMarkupMf: +dealerMarkupMf || 0, singlePayMfReduction: spCfg, }; let q = window.jbLeaseCalc(args); // B2/B3 security-deposit rule: payment rounded UP to $25 — overridable. const autoDep = (gmfOn && (gmfTier === "B2" || gmfTier === "B3") && !sp && window.jbGmfSecurityDeposit) ? window.jbGmfSecurityDeposit(q.totalMonthly) : 0; const depUsed = (secDep != null && secDep !== "") ? +secDep || 0 : autoDep; if (depUsed > 0) q = window.jbLeaseCalc({ ...args, securityDeposit: depUsed }); return { ...q, residPct: q.residualPct, residual: q.residualValue, grossCap: q.grossCapCost, capCostRed: q.totalCapReduction, adjCap: q.adjustedCapCost, dep: q.baseDepreciation, rent: q.rentCharge, base: q.baseMonthly, tax: q.monthlyTax, payment: q.totalMonthly, apr: q.aprEquivalent, dueAtSigning: q.dueAtSigningItems.total, autoDep, secDepUsed: depUsed }; }; const isLease = mode === "lease"; // ── GMF titling entity auto-selection ── // Lender + deal type → the legal titling entity from the Banks registry: // LEASE → ACAR Leasing Ltd. d/b/a GM Financial Leasing (lease-lessor — // vehicle titles to the lessor; the lessee is the registrant) // FINANCE → AmeriCredit Financial Services, Inc. d/b/a GM Financial // (retail-lienholder — customer owns, entity holds the lien) // Returns { name, address, city, state, zip, kind } or null (non-GMF lender). const titlingEntity = (asLease) => { try { return (window.jbLenders && window.jbLenders.lienholderFor) ? window.jbLenders.lienholderFor(lender, asLease ? "Lease" : "Retail") : null; } catch (e) { return null; } }; const heroPmt = (i) => isLease ? leaseOf(i).payment : dtRound2(pmt(i)); const heroTerm = (i) => isLease ? lsc[i].term : sc[i].term; const heroRate = (i) => isLease ? leaseOf(i).aprEquivalent : sc[i].apr; // Autosave the working desk (debounced) so nothing reverts between visits. React.useEffect(() => { const t = setTimeout(() => { try { localStorage.setItem("jb_desk_work", JSON.stringify({ price, msrpOv, docFee, taxRate, tradeAllow, tradeAcv, tradePayoff, hasTrade, tradeDesc, tradeLender, cashDown, rebate: rebateIn, rebateHeld, leaseCashHeld, rebateReview, lender, pack, packHand, commPct, fico, income, dti, sc, scOn, selSc, selected, mode, lsc, milesPerYr, acqFee, capRed, gmfOn, gmfNewUsed, gmfPick, gmfTier, gmfLeaseCash, acqFeeCapped, docFeeCapped, titleLicFee, titleLicCapped, taxMethod, secDep, dispFee, excessRate, purchOptFee, dealerMarkupMf, leaseTaxRate, extraMiles, extraMileageRate, inceptionMiles, inceptionAdjust, inceptionRate, costAdds, dkpFeeOff, dealId: cust.dealId || null })); } catch (e) {} }, 250); return () => clearTimeout(t); }); // ── RC27 (dsk): the cost lines are SAVED WITH THE DEAL as they are typed (debounced) — never onto a posted deal (frozen: the server // refuses it too). A line the server would refuse is still kept on the desk; the panel shows why. // A deal still on the desk has no down payment of its own yet (the CRM's Desk It creates it bare): when the deal on file can't carry the // lines yet (assistance above ITS down payment, a lender fee on a cash deal …) they wait on the desk and Push saves them with the figures. const dkpSaveSig = JSON.stringify(costAdds); const [dkpSaveNote, setDkpSaveNote] = React.useState(""); React.useEffect(() => { if (!cust.dealId || dkpLoadedRef.current !== cust.dealId) { dkpLoadedRef.current = cust.dealId || null; return undefined; } const t = setTimeout(() => { const d = dkpDealOf(cust.dealId); if (!d || dkpPosted(d) || !window.DMS || !window.DMS.updateDeal) return; const cur = Array.isArray(d.costAdds) ? d.costAdds : []; if (JSON.stringify(cur) === dkpSaveSig) { setDkpSaveNote(""); return; } if (dkpCa(costAdds).problems.length) return; // the panel lists the problem; nothing is saved until the line is right const pre = window.DealCalc ? window.DealCalc.saveProblems(Object.assign({}, d, { costAdds: JSON.parse(dkpSaveSig) })).filter(x => /^(dpa_over|lender_fee_)/.test(x.code)) : []; if (pre.length) { setDkpSaveNote(pre[0].msg + " — the deal on file does not have the desk's figures yet; Push to Finance / Cash saves the cost lines with them."); return; } const r = window.DMS.updateDeal(cust.dealId, { costAdds: JSON.parse(dkpSaveSig) }); if (r && !r.ok && r.msg) { setDkpSaveNote(r.msg); window.k2bToast && window.k2bToast(r.msg, "warn", 5200); } else setDkpSaveNote(""); }, 600); return () => clearTimeout(t); }, [dkpSaveSig, cust.dealId]); // ── RC27 (dsk): the LENDER FEE default (Banks / Lenders) is suggested when the lender is picked on a finance / lease deal — an "auto" // line that follows the lender; a lender fee line typed or changed by hand is never replaced. Not on a posted deal. React.useEffect(() => { try { if (window.DMS && window.DMS.loadLenderFees && window.DMS.can && (window.DMS.can("desk") || window.DMS.can("fi"))) window.DMS.loadLenderFees(); } catch (e) {} }, []); const dkpFeesAt = (window.DMS && window.DMS.lenderFeesCached) ? window.DMS.lenderFeesCached().at : 0; const dkpFeeBase = Math.round((baseFin(selSc) + scAdds(selSc)) * 100); React.useEffect(() => { if (dkpIsPosted || !window.DMS || !window.DMS.lenderFeeFor) return; const fee = dkpFeeOff && dkpFeeOff.toUpperCase() === String(lender || "").toUpperCase() ? null : window.DMS.lenderFeeFor(lender); setCostAdds(list => { if (list.some(l => l && l.type === "lenderFee" && !l.auto)) return list; const rest = list.filter(l => !(l && l.type === "lenderFee" && l.auto)); if (!fee) return rest.length === list.length ? list : rest; const amtC = fee.kind === "points" ? Math.round(Math.max(0, dkpFeeBase) * (+fee.points || 0) / 100) : (+fee.amountCents || 0); if (!(amtC > 0)) return rest.length === list.length ? list : rest; const kinds = (window.DealCalc && window.DealCalc.LENDER_FEE_KINDS) || {}; const line = { id: (list.find(l => l && l.type === "lenderFee" && l.auto) || {}).id || dkpId(), type: "lenderFee", desc: `${lender} — ${kinds[fee.kind] || "fee"}${fee.kind === "points" ? " " + fee.points + " pts" : ""}`.slice(0, 80), amount: (amtC / 100).toFixed(2), side: "front", weOwe: false, passed: !!fee.passed && mode !== "lease", kind: fee.kind, lender, auto: true }; const old = list.find(l => l && l.type === "lenderFee" && l.auto); if (old && JSON.stringify(old) === JSON.stringify(line)) return list; return rest.concat([line]); }); }, [lender, mode, dkpFeesAt, dkpIsPosted, dkpFeeBase, dkpFeeOff]); // Apply edited settings to live fields const applySettings = (next) => { saveDeskSettings(next); settingsRef.current = next; setDocFee(next.docFee); setTaxRate(next.taxRate); setPack(next.pack); setCommPct(next.commPct); // New default terms / rates for A–C (D follows C); each option keeps its // own down payment + products and the visible set is unchanged. const tr = [[next.t1, next.a1], [next.t2, next.a2], [next.t3, next.a3], [next.t3, next.a3]]; const lt = [[next.lt1, next.lmf1], [next.lt2, next.lmf2], [next.lt3, next.lmf3], [next.lt3, next.lmf3]]; setSc(arr => arr.map((s, i) => ({ ...s, term: tr[i][0], apr: tr[i][1] }))); setLsc(arr => arr.map((s, i) => ({ ...s, term: lt[i][0], mf: lt[i][1], resid: null, autoMf: false, autoRv: false }))); setAcqFee(next.acqFee); setMilesPerYr(next.milesPerYr); setSettingsOpen(false); window.k2bToast && window.k2bToast("Desking defaults saved.", "ok"); }; // Push current desk figures into the shared Buyer's Order draft — including // the SELECTED scenario's protection products (codes + prices) and the buyer. const pushToBuyersOrder = () => { const f2 = (n) => (+n || 0).toFixed(2); const products = chosenProducts(); const planFields = deskPlanFields(products, f2); const nm = String(cust.name || ""); const [ln, fn] = nm.includes(",") ? nm.split(",").map(x => x.trim()) : [nm.split(" ").slice(-1)[0], nm.split(" ").slice(0, -1).join(" ")]; // Lease deals use the lease engine's tax (monthly-receipts / upfront per the // tax method) — NEVER the retail (price − trade allowance) tax credit calc. const LQ = isLease ? leaseOf(selSc) : null; const dealTax = isLease ? (taxMethod === "monthly" ? LQ.tax : LQ.upfrontTax) : tax; window.saveJbForm && window.saveJbForm("buyersOrder", { ...(cust.preset ? {} : { lastName: (ln || "").toUpperCase(), firstName: (fn || "").toUpperCase() }), // Buyer address / phone / DL come from the CUSTOMER FILE the deal was // started from in the CRM (the Customers screen no longer fills the BO). ...(() => { try { const dd = cust.dealId && window.DMS ? window.DMS.deals().find(x => x.id === cust.dealId) : null; const cc = dd && dd.custNum && window.DMS.customers ? (window.DMS.customers()[dd.custNum] || null) : null; const src = cc || dd || null; if (!src) return {}; return { middle: String(cc && cc.middleName || "").toUpperCase().slice(0, 1), street: String(src.street || "").toUpperCase(), city: String(src.city || "").toUpperCase(), state: String(src.state || "").toUpperCase(), zip: src.postal || src.zip || "", phone: src.phone || (cc && (cc.cell || cc.homePhone)) || "", // RC30 (cn): a customer # is not a phone dlNum: String((cc && (cc.dl || cc.dlNumber)) || "").toUpperCase(), dlState: String((cc && (cc.dlState || cc.state)) || "").toUpperCase() }; } catch (e) { return {}; } })(), salePrice: price.toFixed(2), docFee: docFee.toFixed(2), salesTax: dealTax.toFixed(2), tradeAllow: tradeAllow.toFixed(2), tradePayoff: tradePayoff.toFixed(2), rebate: rebate.toFixed(2), cashDown: (isLease ? cashDown + capRed : cashDown).toFixed(2), // RC27 (dsk): the store's down payment assistance (the customer's credit toward the down payment) and a lender fee the customer pays — // customer-facing lines only; no cost line the store pays ever reaches the Buyer's Order downAssist: dkpDpa > 0 ? dkpDpa.toFixed(2) : "", customerCash: dkpDpa > 0 ? Math.max(0, (isLease ? cashDown + capRed : cashDown) - dkpDpa).toFixed(2) : "", lenderFeeCust: dkpFeeCust > 0 ? dkpFeeCust.toFixed(2) : "", // Vehicle: BLANK when no unit is picked — never a preset VIN/stock (B-05). stockNum: unit.stock || "", vin: unit.vin || "", yrMkMdl: unit.stock ? `${unit.yr} ${unit.mk} ${unit.mdl}`.replace(/\s+/g, " ").trim() : "", newUsed: unit.newUsed || "", odometer: unit.stock && unit.miles ? String(unit.miles) : "", license: (isLease ? titleLicFee : 30).toFixed(2), // Trade — description / VIN / miles / payoff lender from the deal record // (CRM lead → Desk It) and the desk's lender box; neg equity read-out. ...(() => { const d = (cust.dealId && window.DMS && window.DMS.deals) ? (window.DMS.deals().find(x => x.id === cust.dealId) || {}) : {}; return { tradeVin: hasTrade ? String(d.tradeVin || "").toUpperCase() : "", tradeYrMkMdl: hasTrade ? tradeDesc : "", tradeMiles: hasTrade && d.tradeMiles ? String(d.tradeMiles) : "", tradeLender: hasTrade ? tradeLender : "", tradeLien: hasTrade ? tradeLender : "", negEquity: f2(negEquity) }; })(), // Protection products of the chosen option: plan 1 = the legacy // serviceContract fields, then plan2..plan5 + GAP + the JSON list. serviceContract: planFields.plan1Price, serviceContractCode: planFields.plan1Code ? `${planFields.plan1Code} — ${planFields.plan1Name}` : "", ...planFields, gapIns: planFields.gapCode ? planFields.gapIns : "", gapCode: planFields.gapCode ? `${planFields.gapCode} — ${planFields.gapName}` : "", scenario: DT_LETTERS[selSc], amtFinanced: (isLease ? LQ.adjCap : amtFinanced).toFixed(2), dealType: isLease ? "Lease" : "Retail", // Lease figures for the Buyer's Order balance block (B-08): due at // signing + monthly incl. tax replace the retail "balance to finance". ...(isLease ? { dueAtSigning: LQ.dueAtSigning.toFixed(2), monthlyPayment: LQ.payment.toFixed(2), leaseTerm: String(LQ.term), residual: LQ.residual.toFixed(2), moneyFactor: LQ.effectiveMoneyFactor.toFixed(5), adjCapCost: LQ.adjCap.toFixed(2) } : { dueAtSigning: "", monthlyPayment: "", leaseTerm: "" }), lender, // GMF titling entity: LEASE → ACAR Leasing Ltd. d/b/a GM Financial Leasing // (lessor/titled owner); FINANCE → AmeriCredit Financial Services, Inc. // d/b/a GM Financial (lienholder). null for lenders with no entity on file. lienholder: titlingEntity(isLease) || null, }); }; // ── Lease + finance print snapshots (saved to the shared form store so the // Forms library can re-print the last desked figures too) ────────────── const leaseSnapshot = () => { const G = dtGmfData(); return JSON.parse(JSON.stringify({ customer: cust.preset ? "" : (cust.name || ""), dealId: cust.dealId || "", unit, printedAt: new Date().toLocaleString(), // Lessor / titled owner entity (ACAR Leasing Ltd. d/b/a GM Financial // Leasing when the lender is GM Financial) — prints on the worksheet // and disclosure; the lessee is the registrant. lessor: titlingEntity(true) || null, gmf: gmfOn ? { mode: gmfNewUsed, program: gmfNewUsed === "used" ? (G.usedProgram.program || "GMF Used Lease") : (G.program.program || "GMF Lease Program"), effective: gmfNewUsed === "used" ? (G.usedProgram.effective || "") : (G.program.effective || ""), pick: gmfPick, tier: gmfTier, tierFico: (G.program.tierFico || {})[gmfTier] || "", leaseCash: gmfLeaseCash, gapIncluded: G.program.gapIncluded !== false, } : null, shared: { msrp: msrpVal, price, docFee, docFeeCapped, acqFee, acqFeeCapped, titleLicFee, titleLicCapped, cashDown, capRed, rebate, gmfLeaseCash: leaseCashEff, usedUnit: isUsedUnit, hasTrade, tradeDesc, tradeAllow, tradePayoff, tradeLender, negEquity, milesPerYr, taxMethod, taxRate: +leaseTaxRate || 0, dispFee, excessRate, purchOptFee, dealerMarkupMf, lender, extraMiles: +extraMiles || 0, extraMileageRate: +extraMileageRate || 0, inceptionMiles: +inceptionMiles || 0, inceptionRate: +inceptionRate || 0 }, // VISIBLE options only; selSc = the selected option's POSITION in that list. selSc: Math.max(0, visIdx.indexOf(selSc)), selLetter: DT_LETTERS[selSc], options: visIdx.map(i => { const s = lsc[i] || {}; return { label: DT_LETTERS[i], letter: DT_LETTERS[i], down: optDown(i, true), singlePay: !!s.sp, products: [s.all ? "All Products" : null, !s.all && s.gap ? "GAP" : null, !s.all && s.vsc ? "Warranty" : null].filter(Boolean).join(" + ") || "—", addOns: lsAdds(i), q: leaseOf(i) }; }), })); }; const printLeaseWorksheet = () => { const snap = leaseSnapshot(); window.saveJbForm && window.saveJbForm("leaseDesk", snap); window.k2bPrintLeaseWorksheet && window.k2bPrintLeaseWorksheet(snap); }; // The disclosure covers the ★ option only — hand it just that option so the // printer can never land on a hidden or wrong letter. const printLeaseDisclosure = () => { const snap = leaseSnapshot(); window.saveJbForm && window.saveJbForm("leaseDesk", snap); const one = { ...snap, options: [snap.options[snap.selSc]].filter(Boolean), selSc: 0 }; window.k2bPrintLeaseDisclosure && window.k2bPrintLeaseDisclosure(one); }; // Contract Prep Sheet — mirrors the 553-MO Truth-in-Lending boxes from the // FINANCE desk's selected option, so F&I can copy onto the official form. const financeSnapshot = () => { const s = sc[selSc] || {}; const n = +s.term || 0; const p = dtRound2(pmt(selSc)); const top = dtRound2(p * n); const netTrade = hasTrade ? dtRound2(tradeAllow - tradePayoff) : 0; const totalDown = dtRound2(cashDown + rebate + Math.max(0, netTrade)); const inclGap = !!(s.gap || s.all), inclVsc = !!(s.vsc || s.all); const vscAmt = inclVsc ? dtRound2(prodPrice(vscProd)) : 0; const gapAmt = inclGap ? dtRound2(prodPrice(gapProd)) : 0; const otherProducts = Math.max(0, dtRound2(scAdds(selSc) - vscAmt - gapAmt)); return JSON.parse(JSON.stringify({ customer: cust.preset ? "" : (cust.name || ""), dealId: cust.dealId || "", unit, lender, // Retail lienholder entity (AmeriCredit Financial Services, Inc. d/b/a // GM Financial when the lender is GM Financial) for the 553-MO prep. lienholder: titlingEntity(false) || null, printedAt: new Date().toLocaleString(), apr: +s.apr || 0, term: n, payment: p, amtFinanced: dtRound2(amtFinanced), financeCharge: dtRound2(top - amtFinanced), totalOfPayments: top, totalDown, totalSalePrice: dtRound2(top + totalDown), firstDueDate: new Date(Date.now() + 45 * 86400000).toLocaleDateString("en-US"), itemization: { vehiclePrice: dtRound2(price), docFee: dtRound2(docFee), salesTax: dtRound2(tax), cashPrice: dtRound2(price + docFee + tax), cashDown: dtRound2(cashDown), rebate: dtRound2(rebate), tradeAllow: hasTrade ? dtRound2(tradeAllow) : 0, tradePayoff: hasTrade ? dtRound2(tradePayoff) : 0, netTrade, tradeDesc: hasTrade ? tradeDesc : "", tradeLender: hasTrade ? tradeLender : "", negEquity, unpaidBalance: dtRound2(price + docFee + tax - (cashDown + rebate + netTrade)), publicOfficials: 30, insurance: 0, serviceContract: vscAmt, gap: gapAmt, otherProducts, downAssist: dkpDpa > 0 ? dtRound2(dkpDpa) : 0, lenderFeeCust: dkpFeeCust > 0 ? dtRound2(dkpFeeCust) : 0 }, // RC27 (dsk) })); }; const printContractPrep = () => { const snap = financeSnapshot(); window.saveJbForm && window.saveJbForm("contractPrep", snap); window.k2bPrintContractPrep && window.k2bPrintContractPrep(snap); }; // Product label for an option — identical wording in every print. const optProdLabel = (s) => [s.all ? "All Products" : null, !s.all && s.gap ? "GAP" : null, !s.all && s.vsc ? "Warranty" : null].filter(Boolean).join(" + ") || "—"; const printCustomerPresentation = () => { const r = (n) => "$" + Number(n).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const heads = visIdx.map(i => DT_LETTERS[i]); const selPos = visIdx.indexOf(selSc); const L1 = DT_LETTERS[selSc]; if (isLease) { const L = leaseOf(selSc); const qs = visIdx.map(i => leaseOf(i)); const grid = dtOptGridHtml(heads, selPos, [ ["Down payment (cap reduction cash)", visIdx.map((i, k) => qs[k].singlePay ? "n/a (1-pay)" : r(optDown(i, true)))], ["Term", visIdx.map((i, k) => `${lsc[i].term} mo${qs[k].singlePay ? " · 1-PAY" : ""}`)], ["Money factor", qs.map(q => q.effectiveMoneyFactor.toFixed(5))], ["Residual", qs.map(q => `${q.residPct.toFixed(1)}% / ${r(q.residual)}`)], ["Protection products", visIdx.map(i => optProdLabel(lsc[i]))], ["Adjusted cap cost", qs.map((q, k) => dkfBad[visIdx[k]] ? "—" : r(q.adjCap))], ["Due at signing", qs.map((q, k) => dkfBad[visIdx[k]] ? "—" : r(q.dueAtSigning))], ["Payment", qs.map((q, k) => dkfBad[visIdx[k]] ? "—" : q.singlePay ? r(q.singlePayAmount) + " once" : r(q.payment) + "/mo")], ]); const html = `${dealerHeadHtml()}
LEASE PAYMENT OPTIONS
${_pEsc(cust.name)} · ${_pEsc(unit.yr + " " + unit.mk + " " + unit.mdl)} · Stk ${_pEsc(unit.stock)} · ${(+milesPerYr).toLocaleString()} miles/year

Capitalized Cost — Option ${L1}

${lsAdds(selSc) ? `` : ""} ${L.negativeEquity > 0 ? `` : ""}
MSRP${r(msrpVal)}
Agreed Upon Value (selling price)${r(price)}
Doc Fee${r(docFee)}
Acquisition Fee${r(acqFee)}
Protection Products${r(lsAdds(selSc))}
Negative Equity (trade payoff over allowance — capitalized)${r(L.negativeEquity)}
Gross Capitalized Cost${r(L.grossCap)}
Capitalized Cost Reduction (cash / trade equity${rebate + leaseCashEff > 0 ? " / rebate" : ""})− ${r(L.capCostRed)}
Adjusted Capitalized Cost${r(L.adjCap)}
Residual Value (${L.residPct.toFixed(1)}% of MSRP${L.residualAdjustments > 0 ? ` = ${r(L.residualValueUnadjusted)} − ${r(L.residualAdjustments)} mileage adjustments` : ""})${r(L.residual)}

${L.singlePay ? "Single Payment" : "Monthly Payment"} — Option ${L1}

${taxMethod === "monthly" ? `` : ``} ${L.dueAtSigningItems.capReductionTax > 0 ? `` : ""} ${L.singlePay ? `` : ``} ${dkpDpa > 0 ? `` : ""}
Depreciation & Amortized Amounts${r(L.dep)}
Rent Charge (money factor ${L.effectiveMoneyFactor.toFixed(5)} ≈ ${L.apr.toFixed(2)}% APR)${r(L.rent)}
Base Monthly Payment${r(L.base)}
Monthly Sales Tax (${(+leaseTaxRate).toFixed(3)}%)${r(L.tax)}
Tax paid up front (${(+leaseTaxRate).toFixed(3)}% ${taxMethod === "upfront_on_capred" ? "on cap reduction" : "on payment stream"})${r(L.upfrontTax)}
Tax on Cap Reduction (${(+leaseTaxRate).toFixed(3)}%, due at signing)${r(L.dueAtSigningItems.capReductionTax)}
SINGLE LEASE PAYMENT (${L.term} months prepaid)${r(L.singlePayAmount)}
TOTAL MONTHLY PAYMENT — ${L.remainingPayments} payments of ${r(L.payment)} (first payment at inception)${r(L.payment)}
Due at Signing${r(L.dueAtSigning)}
Down payment assistance (from the dealer)− ${r(dkpDpa)}
Your cash at signing${r(Math.max(0, L.dueAtSigning - dkpDpa))}
Total of Payments${r(L.totalOfPayments)}

Lease Options

${grid} ${gmfOn ? `

${_pEsc(gmfNewUsed === "used" ? (dtGmfData().usedProgram.program || "GMF Used Lease") : (dtGmfData().program.program || "GMF Lease Program"))} · Tier ${_pEsc(dtGmfTierLabel(gmfTier))} · Effective ${_pEsc(gmfNewUsed === "used" ? (dtGmfData().usedProgram.effective || "") : (dtGmfData().program.effective || ""))}${dtGmfData().program.gapIncluded !== false ? " · GAP included in GMF lease" : ""}

` : ""}

★ Option ${L1} selected. Mileage allowance ${(+milesPerYr).toLocaleString()}/year; excess mileage ${"$" + (+excessRate).toFixed(2)}/mile and excess wear charges apply per the lease agreement. Disposition fee ${r(dispFee || 0)} at lease end. Estimates only — subject to credit approval and lessor program terms.

${K2BPrint.footLeft()}Lease Options · ${_pEsc(new Date().toLocaleDateString("en-US"))}
`; window.k2bPrint(html, { title: "Lease Payment Options" }); return; } const grid = dtOptGridHtml(heads, selPos, [ ["Down payment", visIdx.map(i => r(optDown(i, false)))], ["Term", visIdx.map(i => `${sc[i].term} mo`)], ["APR", visIdx.map(i => `${(+sc[i].apr || 0).toFixed(2)}%`)], ["Protection products", visIdx.map(i => optProdLabel(sc[i]))], ["Amount financed", visIdx.map(i => dkfBad[i] ? "—" : r(amtFin(i)))], ["Monthly payment", visIdx.map(i => dkfBad[i] ? "—" : r(pmt(i))), true], ]); const prodRows = selectedProducts.map(p => `${p.name}${r(selected[p.id])}`).join(""); const html = `
CUSTOMER PRESENTATION
Prepared for ${cust.name} · ${new Date().toLocaleDateString()}

Vehicle

${unit.yr} ${unit.mk} ${unit.mdl}${unit.newUsed ? (unit.newUsed === "U" ? " (Used)" : " (New)") : ""}Stock ${unit.stock}
MSRP${r(msrpVal)}
Sale Price${r(price)}
${hasTrade ? `

Trade

${negEquity > 0 ? `` : ``}
${tradeDesc}
Allowance${r(tradeAllow)}
Payoff${tradeLender ? ` (${_pEsc(tradeLender)})` : ""}${r(tradePayoff)}
Negative Equity (financed)${r(negEquity)}
Net Equity${r(tradeAllow - tradePayoff)}
` : ``} ${selectedProducts.length ? `

Protection Products

${prodRows}
Total Products — Option ${L1}${r(fiAdds)}
` : ``}

Your Investment — Option ${L1}

${dkpDpa > 0 ? `` : ""} ${dkpFeeCust > 0 ? `` : ""} ${rebate > 0 ? `` : ""} ${posEquity > 0 ? `` : ""} ${negEquity > 0 ? `` : ""}
Down Payment${r(cashDown)}
Down payment assistance (from the dealer)− ${r(dkpDpa)}
Your cash down${r(Math.max(0, cashDown - dkpDpa))}
Lender fee${r(dkpFeeCust)}
Rebate Applied${r(rebate)}
Trade Equity Applied${r(posEquity)}
Negative Equity (financed)${r(negEquity)}
Doc Fee${r(docFee)}
Sales Tax (${taxRate.toFixed(3)}%)${r(tax)}
Amount Financed — Option ${L1}${r(amtFinanced)}

Payment Options — ${lender}

${grid}

★ Option ${L1} selected — each option's Amount Financed reflects that option's own down payment and protection products.

Payments are estimates; final figures are subject to lender approval and verification of stated information. Tax, title, license and dealer fees additional except where noted.

Customer Acknowledgement  ·  Date
Salesperson  ·  Date
`; window.k2bPrint(`${window.dealerHeadHtml ? window.dealerHeadHtml() : ""}${html}`, { title: "Customer Presentation" }); }; const printFourSquare = () => { const r = (n) => "$" + Number(n).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const cell = (t, rows) => `
${t}
${rows}
`; const ln = (l, v) => `
${l}${v}
`; const L1 = DT_LETTERS[selSc]; const heads = visIdx.map(i => DT_LETTERS[i]); const selPos = visIdx.indexOf(selSc); const qs = isLease ? visIdx.map(i => leaseOf(i)) : null; const grid = isLease ? dtOptGridHtml(heads, selPos, [ ["Down", visIdx.map((i, k) => qs[k].singlePay ? "n/a (1-pay)" : r(optDown(i, true)))], ["Term", visIdx.map(i => `${lsc[i].term} mo`)], ["Money factor", qs.map(q => q.effectiveMoneyFactor.toFixed(5))], ["Products", visIdx.map(i => optProdLabel(lsc[i]))], ["Adj. cap cost", qs.map((q, k) => dkfBad[visIdx[k]] ? "—" : r(q.adjCap))], ["Payment", qs.map((q, k) => dkfBad[visIdx[k]] ? "—" : q.singlePay ? r(q.singlePayAmount) + " once" : r(q.payment)), true], ]) : dtOptGridHtml(heads, selPos, [ ["Down", visIdx.map(i => r(optDown(i, false)))], ["Term", visIdx.map(i => `${sc[i].term} mo`)], ["APR", visIdx.map(i => `${(+sc[i].apr || 0).toFixed(2)}%`)], ["Products", visIdx.map(i => optProdLabel(sc[i]))], ["Amt financed", visIdx.map(i => dkfBad[i] ? "—" : r(amtFin(i)))], ["Payment", visIdx.map(i => dkfBad[i] ? "—" : r(pmt(i))), true], ]); const selAmt = isLease ? leaseOf(selSc).adjCap : amtFinanced; const html = `${window.dealerHeadHtml ? window.dealerHeadHtml() : ""}
4-SQUARE WORKSHEET
${cust.name} · ${unit.yr} ${unit.mk} ${unit.mdl}${unit.newUsed ? (unit.newUsed === "U" ? " (Used)" : " (New)") : ""} · Stk ${unit.stock} · ${new Date().toLocaleDateString()}
${cell("TRADE", hasTrade ? ln("Allowance", r(tradeAllow)) + ln("Payoff" + (tradeLender ? " · " + _pEsc(tradeLender) : ""), r(tradePayoff)) + ln(negEquity > 0 ? "Negative Equity" : "Net Equity", (negEquity > 0 ? "-" : "") + r(Math.abs(tradeEquity))) : `
No trade-in
`)} ${cell("PRICE", ln("Sale Price", r(price)) + ln("Doc Fee", r(docFee)) + ln("Sales Tax", r(tax)))} ${cell("DOWN", ln(`Down Payment (${L1})`, r(cashDown)) + (dkpDpa > 0 ? ln("Down payment assistance", "-" + r(dkpDpa)) + ln("Customer cash down", r(Math.max(0, cashDown - dkpDpa))) : "") + (rebate > 0 ? ln("Rebate", r(rebate)) : "") + (isLease && leaseCashEff > 0 ? ln("GMF Lease Cash", r(leaseCashEff)) : "") + (posEquity > 0 ? ln("Trade Equity", r(posEquity)) : "") + (negEquity > 0 ? `
Negative Equity (financed)${r(negEquity)}
` : "") + ln(`${isLease ? "Adj. Cap Cost" : "Amount Financed"} (${L1})`, r(selAmt)))} ${cell("PAYMENT", ln(`Option ${L1} ★ · ${heroTerm(selSc)} mo @ ${isLease ? leaseOf(selSc).effectiveMoneyFactor.toFixed(5) + " MF" : (+sc[selSc].apr || 0).toFixed(2) + "%"}`, r(isLease && lsc[selSc].sp ? leaseOf(selSc).singlePayAmount : heroPmt(selSc))) + `
${visIdx.length} option${visIdx.length !== 1 ? "s" : ""} compared below
`)}
PAYMENT OPTIONS
${grid}
Customer Initials
Sales Manager
`; window.k2bPrint(html, { title: "4-Square Worksheet" }); }; // ONE PATH — the desk pushes THE deal (created by CRM → Desk It) forward to // exactly one of two closing screens: kind = "cash" → Cash Deal, // kind = "finance" → Finance Deal (lease = finance). Both write the deal // record + the target worksheet form, stamp the stage, set jb_fw_deal, go. const pushDeal = (kind) => { if (!cust.dealId) { window.k2bToast && window.k2bToast("No deal open — deals start in the CRM: pick the lead → Desk It.", "warn", 4200); return; } if (dkfNoFig) { window.k2bToast && window.k2bToast("Enter the sale price first — nothing to push yet.", "warn", 4200); return; } // K2B-013 if (kind === "cash" ? dkfOverSel : dkfBadSel) { window.k2bToast && window.k2bToast(dkfBadMsg(selSc), "warn", 5200); return; } // K2B-013 / 6.3 if (kind === "cash" && !dkpIsPosted && costAdds.some(l => l && l.type === "lenderFee" && !l.auto)) { window.k2bToast && window.k2bToast("A cash deal has no lender — take the lender fee line off the Profit panel first (Show gross › 💲 Profit).", "warn", 5200); return; } // RC27 (dsk) // RC27 (dsk) fix round (audit): a cost line that is not right yet (no amount, …) stops the push HERE — before any form or the deal // changes — with the Profit panel's own words (the deal save would refuse the whole figures patch and the deal would sit in F&I bare) if (!dkpIsPosted) { const pushLines = kind === "cash" ? costAdds.filter(l => !(l && l.type === "lenderFee")) : costAdds; const pc = dkpCa(pushLines); if (pc.problems.length) { window.k2bToast && window.k2bToast(`Nothing was pushed — a cost line on the Profit panel is not right yet: ${pc.problems[0].msg} Fix it or take the line off (Show gross › 💲 Profit), then push again.`, "warn", 8000); if (showGross) openDrawer("profit"); return; } } if (kind === "cash" && !dkpIsPosted && costAdds.some(l => l && l.type === "lenderFee")) setCostAdds(arr => arr.filter(l => !(l && l.type === "lenderFee"))); // the suggested lender fee goes with the lender if (kind === "cash") return pushCash(); return pushFinance(); }; // Shared vehicle / price / trade / tax / fee / product fields both pushes write. const deskDealCore = () => { const products = chosenProducts(); const gapList = products.filter(p => /gap/i.test(p.category + " " + p.name)); const vscList = products.filter(p => !/gap/i.test(p.category + " " + p.name)); const sum = (l, k) => dtRound2(l.reduce((t, p) => t + (+p[k] || 0), 0)); return { vehicle: `${unit.yr} ${unit.mk} ${unit.mdl}`.replace(/\s+/g, " ").trim(), stock: unit.stock, vin: unit.vin || "", // B-01 / B-02: cost + New/Used ride on the deal from the desk on. cost: dtRound2(unit.cost || 0), type: unit.type ? (/^n|new/i.test(unit.type) ? "New" : "Used") : undefined, salePrice: price, tradeAllow: hasTrade ? tradeAllow : 0, tradePayoff: hasTrade ? tradePayoff : 0, rebate, // B-07: ACV under BOTH spellings so every reader (GL, recap, desk) agrees. tradeACV: hasTrade ? tradeAcv : 0, tradeAcv: hasTrade ? tradeAcv : 0, tradeDesc: hasTrade ? tradeDesc : "", tradeLender: hasTrade ? tradeLender : "", negEquity, docFee, // The chosen scenario's protection products, itemized with codes — the // Finance Deal / Cash Deal / Buyer's Order read THIS list. products, // B-07: the same products lumped as vscSale (every non-GAP product) / // gapSale (+ costs) so postDeal's payables and the pay plan see them. vscSale: sum(vscList, "price"), vscCost: sum(vscList, "cost"), gapSale: sum(gapList, "price"), gapCost: sum(gapList, "cost"), fiGross: dtRound2(fiGross), // Which desk option (A–D) was pushed, and whether the rebate was dropped // because the unit is used (rebate above is then 0). deskOption: DT_LETTERS[selSc], rebateNewOnly: isUsedUnit, // RC27 (dsk): the deal's cost lines (Profit panel) ride with the push (a posted deal keeps its own) ...(dkpIsPosted ? {} : { costAdds: JSON.parse(JSON.stringify(costAdds)) }), }; }; // Form patches (Finance Deal / Cash Deal / Buyer's Order) carry the plan rows // as plan1..plan5 {Code, Name, Price, Cost, Term} for the non-GAP products, // GAP as gapIns/gapCode, and the whole list as a JSON string. const deskPlanFields = (products, f2) => { const gap = products.find(p => /gap/i.test(p.category + " " + p.name)) || null; const plans = products.filter(p => !/gap/i.test(p.category + " " + p.name)); const out = { products: JSON.stringify(products), gapIns: gap ? f2(gap.price) : "0.00", gapCode: gap ? gap.code : "", gapName: gap ? gap.name : "", gapCost: gap ? f2(gap.cost) : "" }; for (let n = 1; n <= 5; n++) { const p = plans[n - 1]; out[`plan${n}Code`] = p ? p.code : ""; out[`plan${n}Name`] = p ? p.name : ""; out[`plan${n}Price`] = p ? f2(p.price) : ""; out[`plan${n}Cost`] = p ? f2(p.cost) : ""; out[`plan${n}Term`] = p && p.termMo ? String(p.termMo) : ""; out[`plan${n}Id`] = p ? p.id : ""; } out.warranties = f2(plans.reduce((t, p) => t + (+p.price || 0), 0)); return out; }; // Push to Cash Deal → the customer pays the balance at delivery. No lender, // no amount financed, no payment; the Cash Deal screen finalizes + posts. const pushCash = () => { pushToBuyersOrder(); const f2 = (n) => (n || 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const core = deskDealCore(); if (Array.isArray(core.costAdds)) core.costAdds = core.costAdds.filter(l => !(l && l.type === "lenderFee")); // RC27 (dsk): a cash deal has no lender fee if (!unit.cost) { try { console.warn("[Desking] Push to Cash: unit " + (unit.stock || "(none)") + " has no cost — deal.cost will be 0 until Inventory carries one."); } catch (e) {} } window.saveJbForm && window.saveJbForm("cashWorksheet", { dealType: "Cash", cashPrice: f2(price), tradeValue: f2(core.tradeAllow), tradePayoff: f2(core.tradePayoff), tradeAcv: f2(core.tradeAcv), tradeDesc: core.tradeDesc, tradeLender: core.tradeLender, negEquity: f2(negEquity), rebates: f2(rebate), deposit: f2(cashDown), salesTax: f2(tax), docFee: f2(docFee), license: f2(30), ...deskPlanFields(core.products, f2), other: "0.00", taxRate: String(taxRate), }); let rev = null; if (window.DMS && window.DMS.updateDeal) { const r = window.DMS.updateDeal(cust.dealId, { ...core, cashDown, tax: dtRound2(tax), titleFee: 30, dealType: "Cash", amtFinanced: 0, apr: 0, term: 0, payment: 0, lender: "", lienholder: null, stage: "Cash", deskView: dkfViewSafe("cash", core), // customer-safe snapshot for the Sales view-only screen }); // RC27 (dsk) fix round (audit): the deal refused the figures — no stage change, no "pushed", stay on the desk and say why if (r && r.ok === false) { window.k2bToast && window.k2bToast(`${r.msg || "The deal did not take the figures."} Nothing was pushed — the deal stays where it is; fix it and push again.`, "warn", 8000); return; } if (r && r.ok) rev = r.rev; if (window.DMS.setDealStage) { const st = window.DMS.setDealStage(cust.dealId, "Cash"); if (st && st.ok) rev = st.rev; } // Stamp the form with the deal revision it now mirrors so the Cash Deal // screen does not re-copy the same figures on mount (B-04). if (rev != null && window.saveJbForm) window.saveJbForm("cashWorksheet", { _srcDealId: cust.dealId, _srcRev: rev }); } try { localStorage.setItem("jb_fw_deal", cust.dealId); } catch (e) {} window.k2bToast && window.k2bToast(`${cust.dealId} pushed to Cash Deal — balance due at delivery ${dt$(Math.max(0, price + tax + docFee + 30 + core.vscSale + core.gapSale + core.tradePayoff - core.tradeAllow - rebate - cashDown - dkpDpa))}${dkpDpa > 0 ? " (after " + dt$(dkpDpa) + " down payment assistance)" : ""}.`, "ok", 4200); go && go("cashworksheet"); }; // Push to Finance (retail or lease) → the Finance Deal screen (F&I office). const pushFinance = () => { pushToBuyersOrder(); const f2 = (n) => (n || 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const s = (isLease ? lsc : sc)[selSc] || {}; const core = deskDealCore(); const L = isLease ? leaseOf(selSc) : null; if (!unit.cost) { try { console.warn("[Desking] Push to Finance: unit " + (unit.stock || "(none)") + " has no cost — deal.cost will be 0 until Inventory carries one."); } catch (e) {} } // Lease inputs the Finance Worksheet needs to RE-RUN the lease engine on // F5 (B-03) — every field jbLeaseCalc reads, as the desk had them. const leaseForm = isLease ? { moneyFactor: L.effectiveMoneyFactor.toFixed(5), residual: f2(L.residual), residualPct: (L.residPct + 0).toFixed(2), msrp: f2(msrpVal), acqFee: f2(acqFee), acqFeeCapped: acqFeeCapped ? "1" : "", docFeeCapped: docFeeCapped ? "1" : "", titleLicFee: f2(titleLicFee), titleLicCapped: titleLicCapped ? "1" : "", capRed: f2(capRed), cashDownOnly: f2(cashDown), leaseRebates: f2(rebate + leaseCashEff), milesPerYr: String(milesPerYr), taxMethod, leaseTaxRate: String(leaseTaxRate), extraMiles: String(extraMiles || 0), extraMileageRate: String(extraMileageRate), inceptionMiles: String(inceptionMiles || 0), inceptionRate: String(inceptionRate), inceptionAdjust: inceptionAdjust == null ? "" : (inceptionAdjust ? "1" : "0"), securityDeposit: f2(L.secDepUsed), dispFee: f2(dispFee), excessRate: String(excessRate), purchOptFee: f2(purchOptFee), singlePay: s.sp ? "1" : "", dueAtSigning: f2(L.dueAtSigning), totalOfPayments: f2(L.totalOfPayments), rentCharge: f2(L.rent), leaseUpfrontTax: f2(L.taxItemization ? L.taxItemization.estimatedUpfrontTotal : 0), } : {}; window.saveJbForm && window.saveJbForm("financeWorksheet", { dealType: isLease ? "Lease" : "Retail", cashPrice: f2(price), tradeValue: f2(core.tradeAllow), tradePayoff: f2(core.tradePayoff), tradeAcv: f2(core.tradeAcv), tradeDesc: core.tradeDesc, tradeLender: core.tradeLender, negEquity: f2(negEquity), rebates: f2(rebate), deposit: f2(isLease ? cashDown + capRed : cashDown), salesTax: f2(isLease ? L.tax : tax), docFee: f2(docFee), license: f2(isLease ? titleLicFee : 30), // Protection products of the chosen option — plan rows + GAP + JSON list. ...deskPlanFields(core.products, f2), amountFinanced: f2(isLease ? L.adjCap : amtFinanced), apr: (isLease ? L.apr : sc[selSc].apr).toFixed(2), term: String(heroTerm(selSc)), payment: f2(heroPmt(selSc)), lienName: lender, // Titling entity for the deal jacket: lease → ACAR (lessor/titled owner), // finance → AmeriCredit/GMF (lienholder). Editable downstream. lienholder: titlingEntity(isLease) || null, ...leaseForm, }); if (cust.dealId && window.DMS && window.DMS.updateDeal) { const r = window.DMS.updateDeal(cust.dealId, { // Vehicle / price / trade (+ lender, neg equity) / products (itemized // with codes + lumped vscSale / gapSale for the GL) — shared with Push to Cash. ...core, cashDown: isLease ? cashDown + capRed : cashDown, tax: dtRound2(isLease ? L.tax : tax), titleFee: dtRound2(isLease ? titleLicFee : 30), amtFinanced: dtRound2(isLease ? L.adjCap : amtFinanced), apr: isLease ? L.apr : sc[selSc].apr, term: heroTerm(selSc), payment: heroPmt(selSc), lender, dealType: isLease ? "Lease" : "Retail", // deal.lienholder = { name, address, city, state, zip, kind } — the // titling entity (lease-lessor / retail-lienholder) for this deal. lienholder: titlingEntity(isLease) || null, stage: "F&I", deskView: dkfViewSafe("finance"), // customer-safe snapshot for the Sales view-only screen ...(isLease ? { moneyFactor: L.effectiveMoneyFactor, residual: L.residual, residualPct: L.residPct, milesPerYr, dueAtSigning: dtRound2(L.dueAtSigning), leaseUpfrontTax: dtRound2(L.taxItemization ? L.taxItemization.estimatedUpfrontTotal : 0), leaseTaxMethod: taxMethod, leaseTaxRate: +leaseTaxRate || 0, totalOfPayments: dtRound2(L.totalOfPayments) } : {}), }); // RC27 (dsk) fix round (audit): the deal refused the figures — no stage change, no "pushed", stay on the desk and say why if (r && r.ok === false) { window.k2bToast && window.k2bToast(`${r.msg || "The deal did not take the figures."} Nothing was pushed — the deal stays where it is; fix it and push again.`, "warn", 8000); return; } let rev = r && r.ok ? r.rev : null; if (window.DMS.setDealStage) { const st = window.DMS.setDealStage(cust.dealId, "F&I"); if (st && st.ok) rev = st.rev; } // Stamp the form with the deal revision it now mirrors so the Finance // Deal screen does not re-copy the same figures on mount (B-04). if (rev != null && window.saveJbForm) window.saveJbForm("financeWorksheet", { _srcDealId: cust.dealId, _srcRev: rev }); try { localStorage.setItem("jb_fw_deal", cust.dealId); } catch (e) {} } window.k2bToast && window.k2bToast(`${cust.dealId} pushed to Finance Deal${isLease ? " (Lease)" : ""} \u2014 F&I office finalizes & posts.`, "ok"); go && go("finance"); }; // No FICO on file → no verdict (never "DIFFICULT" for a blank profile). const approval = !fico ? "— (no FICO on file)" : fico >= 700 ? "STRONG" : fico >= 640 ? "LIKELY" : fico >= 580 ? "SUBPRIME" : "DIFFICULT"; const approvalColor = !fico ? "var(--jb-text-muted)" : fico >= 700 ? "var(--jb-green)" : fico >= 640 ? "var(--jb-blue)" : "var(--jb-red)"; const ptiEst = income ? (pmt(selSc) / income * 100) : 0; // Group catalog by category for the product checklist const cats = (window.jbProducts ? window.jbProducts.CATEGORIES : []).filter(c => catalog.some(p => p.category === c)); // ── One-screen 4-square sheet: VIEW state only (no figures live here) ── // Box size of the desk window → density (row height / type scale) + how // much header text fits. Measured, so 1366×768, 125 % scaling and 1080p // each get a layout that fits without scrolling. const dkcRootRef = React.useRef(null); const dkcBox = dkfUseBox(dkcRootRef); const dkcDens = dkfDensOf(dkcBox); const dkcWide = dkcBox.w >= 1400, dkcNarrow = dkcBox.w < 1150; // Customer view (Present): the same 4 squares, no inputs, no dealer numbers. const [dkcPresent, setDkcPresent] = React.useState(false); // Dealer numbers ("Show gross"): remembered for THIS customer / deal object // only — picking another deal or reopening a scenario hands the desk a new // cust object, so the numbers are hidden again in that same render. Never // stored: a reopened desk always starts hidden. Present hides them too. const [dkfGrossCust, setDkfGrossCust] = React.useState(null); const showGross = !dkcPresent && dkfGrossCust != null && dkfGrossCust === cust; const toggleGross = () => setDkfGrossCust(g => (g != null && g === cust) ? null : cust); // Messages that arrive while the customer view is on are held (the status line shows only a count). const dkfPresAtRef = React.useRef(0); const togglePresent = () => { const nv = !dkcPresent; setDkcPresent(nv); if (nv) { setDkfGrossCust(null); dkfPresAtRef.current = Date.now(); } setPrintOpen(false); setDkcMenu(null); }; // Details drawer — docked on the right (it shrinks the sheet, never covers // the payment square). null = automatic: open on wide screens. const [dkcDrawer, setDkcDrawer] = React.useState(() => { const s = dkcStore("dkc_drawer"); return s === "open" ? true : s === "closed" ? false : null; }); const [dkcTab, setDkcTab] = React.useState(() => { const t = dkcStore("dkc_drawer_tab"); return t && t !== "profit" ? t : "products"; }); const drawerOpen = !dkcPresent && (dkcDrawer == null ? dkcBox.w >= 1500 : dkcDrawer); const drawerTab = (dkcTab === "lease" && !isLease) ? "products" : (dkcTab === "profit" && !showGross) ? "products" : dkcTab; // RC27 (dsk): the Profit tab only while Show gross is on const openDrawer = (tab) => { if (tab) { setDkcTab(tab); dkcStore("dkc_drawer_tab", tab); } setDkcDrawer(true); dkcStore("dkc_drawer", "open"); }; const closeDrawer = () => { setDkcDrawer(false); dkcStore("dkc_drawer", "closed"); }; const toggleDrawer = (tab) => { if (drawerOpen && (!tab || tab === drawerTab)) closeDrawer(); else openDrawer(tab); }; const dkcDrawerW = Math.round(Math.max(300, Math.min(440, dkcBox.w * 0.27))); // Payment-square width (sheet columns 41 / 59; 37 / 63 when the payment // square would be narrow and there is height to spare) → compact labels. // Docked beside the sheet when the sheet keeps >= 900 px; on smaller screens // the drawer takes the Trade / Down half while open (Price + Payment stay). // A lease payment square carries more rows, so it needs a wider sheet // (1366 × 768 lease: the drawer takes Trade / Down instead of squeezing it). const drawerDock = drawerOpen && (dkcBox.w - 20 - dkcDrawerW - 8 >= (isLease ? 1000 : 900)); const drawerLeft = drawerOpen && !drawerDock; const dkcSheetW = drawerLeft ? (dkcBox.w - 20) : dkcBox.w - 20 - (drawerOpen ? dkcDrawerW + 8 : 0); const payNarrow = dkcSheetW * 0.59 < 660; const dkcSplit = (payNarrow && dkcDens !== "tight") ? [37, 63] : [41, 59]; const dkcPayW = Math.round(dkcSheetW * dkcSplit[1] / 100); // ── Fit: what the sheet gives up, in order, when a square would need scrolling ── // Measured after every render (before paint); starts again from 0 whenever what // fills the squares changes (screen size, lease / finance, trade, gross, unit // alert, details panel, options, customer view …). Nothing is ever lost — // each step's figures are elsewhere on the sheet or in the details panel. // 1 · lease: the roomy per-option breakdown rows (Residual $ … Tax / mo — all in Lease detail) // 2 · unit not clean: the red pill in the Price square instead of the full-width line // 3 · shorter rows (the compact row height — text size unchanged) // 4 · the negative-equity sentence under the trade (the Down square keeps "Neg. equity →") // 5 · the detail lines under the big payment (the option columns carry the same figures) // 6 · the trade description moves up into the Trade square's header row const DKF_FIT_MAX = 6; const dkfFitKey = [dkcBox.w, dkcBox.h, mode, hasTrade ? 1 : 0, negEquity > 0 ? 1 : 0, showGross ? 1 : 0, unitCheck.clean ? 1 : 0, drawerOpen ? (drawerDock ? 2 : 1) : 0, visIdx.length, dkcPresent ? 1 : 0, rebateReview.length, leaseCashEff > 0 ? 1 : 0, isUsedUnit ? 1 : 0, gmfOn ? 1 : 0, dkcPresent ? chosenProducts(selSc).length : 0].join(","); const [dkfFitSt, setDkfFitSt] = React.useState({ key: "", lv: 0 }); const dkfFit = dkfFitSt.key === dkfFitKey ? dkfFitSt.lv : 0; React.useLayoutEffect(() => { const root = dkcRootRef.current; if (!root || dkfFit >= DKF_FIT_MAX) return; const over = Array.from(root.querySelectorAll(".dkc-sheet .dkc-gridwrap, .dkc-sheet .dkc-qbody")).some(el => el.scrollHeight - el.clientHeight > 1); if (over) setDkfFitSt({ key: dkfFitKey, lv: dkfFit + 1 }); }); // Unit-readiness alert: a full-width line when the screen has height to spare, else a pill in the Price square. const dkfAlertPill = !(dkcDens === "roomy" || dkcDens === "xl") || dkfFit >= 2; const [dkcMenu, setDkcMenu] = React.useState(null); // "more" | "log" | "keys" | null (Print uses printOpen) const [dkcModOpen, setDkcModOpen] = React.useState(false); // module gear (was the title bar ⚙) const dkcMod = window.jbModSettings && window.jbModSettings.forTitle ? window.jbModSettings.forTitle("Desking — 4-Square Work Tool") : null; // Sidebar folds to an icon rail while desking; More ▴ › "Keep the full menu open" pins it. const [dkcPin, setDkcPin] = React.useState(() => dkcStore("dkc_nav_pin") === "1"); React.useEffect(() => { document.body.classList.toggle("dkc-nav-pin", !!dkcPin); }, [dkcPin]); // Messages: every toast (k2btoast event) is shown in the bottom status line // while the desk is open — nothing pops up over the numbers (dkfUseMsgs). const { msgs: dkcMsgs, last: dkcLast, fresh: dkcFresh } = dkfUseMsgs(); React.useEffect(() => { const B = document.body; B.classList.add("dkc-desk-on"); return () => { B.classList.remove("dkc-desk-on"); B.classList.remove("dkc-nav-pin"); }; }, []); // Keyboard: Alt+1..4 pick option A–D · Alt+P customer view · Alt+G show / // hide gross · Alt+M details drawer · Esc closes an open menu / leaves the // customer view. React.useEffect(() => { const onKey = (e) => { if (document.querySelector("[data-k2b-modal]")) return; if (e.key === "Escape" && !e.altKey && !e.ctrlKey && !e.metaKey) { if (printOpen || dkcMenu) { setPrintOpen(false); setDkcMenu(null); e.preventDefault(); } else if (dkcPresent) { setDkcPresent(false); e.preventDefault(); } return; } if (!e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return; const c = e.code || ""; const dg = /^(Digit|Numpad)([1-4])$/.exec(c); if (dg) { const i = +dg[2] - 1; if (scOn[i]) { setSelSc(i); e.preventDefault(); } return; } if (c === "KeyP") { e.preventDefault(); togglePresent(); return; } if (c === "KeyG") { e.preventDefault(); if (!dkcPresent) toggleGross(); return; } if (c === "KeyM") { e.preventDefault(); toggleDrawer(); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }); const noDeal = !cust.dealId; // K2B-013: with NO deal open (a unit picked or not), or with no sale price yet, the desk shows NO payment / // total / amount-financed figure — the defaults alone (down, rebate, title) would compute made-up or negative // ones — and nothing can be pushed or printed from it. Figures appear once a deal is open (CRM → Desk It). const dkfNoFig = noDeal || !(+price > 0); const dkfFig = (s) => dkfNoFig ? "—" : s; // K2B-013 / spec 6.3 valid-input gate: an option whose amount financed comes out at or below $0 (the down // payment, rebate and trade equity cover the whole balance), or a lease option whose adjusted cap cost or // payment comes out below $0, is not a valid loan / lease. It shows "—" instead of a figure; when it is the // ★ option, Push to Finance, Print and Present are held. A retail ★ option at exactly $0 is paid in full and can // still go to Cash; one BELOW $0 (down payment more than the whole balance = a refund) cannot go anywhere. const dkfBadOf = (i) => { if (dkfNoFig) return false; if (isLease) { const q = leaseOf(i); return dtRound2(q.adjCap) < 0 || dtRound2(q.singlePay ? q.singlePayAmount : q.payment) < 0; } return dtRound2(amtFin(i)) <= 0; }; const dkfBad = [0, 1, 2, 3].map(dkfBadOf); const dkfBadSel = !!dkfBad[selSc]; const dkfOverSel = dkfBadSel && !isLease && dtRound2(amtFin(selSc)) < 0; // retail ★ balance below $0 → no Cash push either const dkfOptFig = (i, s) => (dkfNoFig || dkfBad[i]) ? "—" : s; const dkfSelFig = (s) => (dkfNoFig || dkfBadSel) ? "—" : s; const dkfBadMsg = (i) => isLease ? `Option ${DT_LETTERS[i]}'s down payment is more than this lease can take — the payment would be below $0. Lower the down payment.` : dtRound2(amtFin(i)) < 0 ? `Option ${DT_LETTERS[i]}'s down payment is ${dt$(-dtRound2(amtFin(i)))} more than the whole balance — lower the down payment.` : `Option ${DT_LETTERS[i]}'s down payment covers the whole balance — nothing to finance. Lower the down payment, or push it as a cash deal (paid in full).`; const P = dkcPresent; const G = dtGmfData(); const usedMode = gmfOn && gmfNewUsed === "used"; const tierList = usedMode ? Object.keys(G.usedProgram.tierMf || {}) : GMF_TIERS; const gmfReady = !!(window.GMF_LEASE && window.GMF_LEASE.program && Array.isArray(window.GMF_LEASE.program.rates) && window.jbLeaseCalc); const markupMax = +G.program.dealerMarkupMaxMf || 0.001; const anySp = isLease && visIdx.some(i => lsc[i] && lsc[i].sp); const leaseTe = isLease ? titlingEntity(true) : null; const LQs = isLease ? leaseOf(selSc) : null; // ★ option's lease quote (read-only use) const heroSp = isLease && lsc[selSc] && lsc[selSc].sp; const L1 = DT_LETTERS[selSc]; // Figures the footer / hero repeat (read from the existing calculations). // Total down = cash down + rebate + positive equity. A single-pay lease takes no // cash as cap reduction (GMF rule — its Down cell reads "n/a (1-pay)"), so its // total leaves the greyed-out cash out. const dkfSpOf = (i) => isLease && !!(lsc[i] && lsc[i].sp); const dkfTotalDownOf = (i) => (dkfSpOf(i) ? 0 : optDown(i)) + rebate + posEquity; const dkfTotalDown = dkfTotalDownOf(selSc); const dkfAmt = isLease ? LQs.adjCap : amtFinanced; const dkfHeroAmt = heroSp ? LQs.singlePayAmount : heroPmt(selSc); // Title / registration fee as the amount-financed calculation itself carries // it: base amount financed minus every other itemized line. const dkfTitleFee = dtRound2(baseFin(selSc) - (price + docFee + tax - tradeAllow + tradePayoff - optDown(selSc, false) - rebate)); // Clicking a total puts the cursor in the box that changes it. On a small // screen the details drawer may sit over Trade / Down — it is closed first. const dkfJump = (sel, inTradeDown) => () => { if (P) return; const run = () => dkfFocusBox(dkcRootRef.current, sel); if (inTradeDown && drawerLeft) { closeDrawer(); setTimeout(run, 80); } else run(); }; const jumpPmt = dkfJump(`[data-opt="${L1}"] [data-c="down"]`); const jumpDown = dkfJump('[data-f="down"]', true); const jumpAmt = dkfJump('[data-f="salePrice"]'); // ── Customer-safe snapshot of the desk (what a salesperson may show) ── // Computed figures only — no inputs to recompute from, no invoice, gross, // pack, commission, ACV, product cost or credit data. Saved with each // scenario (scenario.view) and with each push (deal.deskView); the Sales // view-only screen renders nothing else. // Amounts are stored as the desk holds them (the view formats them with the same // dt$ the desk uses, so a half-cent amount rounds the same way); payments are // the rounded figures the desk shows. const dkfViewOf = (pushedAs, core) => { const lease = isLease; const options = visIdx.filter(i => !dkfBad[i]).map(i => { // K2B-013 / 6.3: an option below $0 is not shown to Sales const s = (lease ? lsc : sc)[i] || {}; const prods = chosenProducts(i).map(p => ({ name: p.name, price: p.price })); if (lease) { const q = leaseOf(i); return { letter: DT_LETTERS[i], term: +s.term || 0, singlePay: !!s.sp, mf: q.effectiveMoneyFactor, aprEq: q.aprEquivalent, residPct: q.residPct, residual: q.residual, down: s.sp ? null : optDown(i, true), prodLabel: optProdLabel(s), prodTotal: lsAdds(i), products: prods, amount: q.adjCap, dueAtSigning: q.dueAtSigning, payment: s.sp ? q.singlePayAmount : q.payment, remaining: q.remainingPayments, totalOfPayments: q.totalOfPayments, totalDown: (s.sp ? 0 : optDown(i, true)) + rebate + posEquity }; } return { letter: DT_LETTERS[i], term: +s.term || 0, apr: +s.apr || 0, down: optDown(i, false), prodLabel: optProdLabel(s), prodTotal: scAdds(i), products: prods, amount: amtFin(i), payment: dtRound2(pmt(i)), totalDown: optDown(i, false) + rebate + posEquity }; }); const v = { v: 1, savedAt: new Date().toISOString(), dealId: cust.dealId || null, customer: cust.preset ? "" : (cust.name || ""), unit: { yr: unit.yr || "", mk: unit.mk || "", mdl: unit.mdl || "", stock: unit.stock || "", vin: unit.vin || "", newUsed: unit.newUsed || "", miles: +unit.miles || 0 }, mode, lender, titledTo: lease && leaseTe ? dtEntityShort(leaseTe.name) : "", salePrice: price, docFee, taxRate, salesTax: tax, msrp: msrpVal, leaseTaxRate: lease ? (+leaseTaxRate || 0) : null, milesPerYr: lease ? (+milesPerYr || 0) : null, taxMethod: lease ? taxMethod : null, trade: hasTrade ? { desc: tradeDesc || "", allowance: tradeAllow, payoff: tradePayoff, lender: tradeLender || "", equity: tradeEquity, negEquity, posEquity } : null, rebate, leaseCash: lease ? leaseCashEff : 0, capRed: lease ? (+capRed || 0) : 0, usedUnit: isUsedUnit, sel: DT_LETTERS[selSc], options, products: selectedProducts.map(p => ({ name: p.name, price: +selected[p.id] || 0 })), pushedAs: pushedAs || null, // RC27 (dsk): customer-facing only — the store's down payment assistance (a credit toward the down) and a lender fee the customer pays downAssist: dkpDpa > 0 ? dkpDpa : 0, lenderFeeCust: dkpFeeCust > 0 ? dkpFeeCust : 0, }; if (pushedAs === "cash" && core) v.cashDue = Math.max(0, price + tax + docFee + 30 + core.vscSale + core.gapSale + core.tradePayoff - core.tradeAllow - rebate - cashDown - dkpDpa); return JSON.parse(JSON.stringify(v)); }; const dkfViewSafe = (pushedAs, core) => { try { return dkfViewOf(pushedAs, core); } catch (e) { return null; } }; // ── A desk opened on a PC that never worked this deal starts from the defaults // (the working desk lives in each PC's own storage). When the deal has a // saved scenario, the footer offers it: one click loads the newest one. ── const dkfFreshRef = React.useRef(!!(W && W.dealId && !Array.isArray(W.sc))); const [dkfOfferOff, setDkfOfferOff] = React.useState(false); const dkfOffer = (() => { if (dkfOfferOff || dkcPresent || !dkfFreshRef.current || !cust.dealId || String(cust.dealId) !== String(W.dealId)) return null; try { return ((window.DMS && window.DMS.deskScenarios && window.DMS.deskScenarios()) || []) .filter(r => r && r.state && r.state.cust && String(r.state.cust.dealId || "") === String(cust.dealId)) .sort((a, b) => String(b.savedAt || "").localeCompare(String(a.savedAt || "")))[0] || null; } catch (e) { return null; } })(); const dkfLoadOffer = () => { const r = dkfOffer; if (!r) return; restoreScenario(r.state); setDkfOfferOff(true); window.k2bToast && window.k2bToast(`Saved desk “${r.name || r.id}” loaded.`, "ok"); }; // ── option grid helpers (layout only) ── const stopEv = (e) => e.stopPropagation(); const chipFor = (i, k, lab, short) => { const s = (isLease ? lsc : sc)[i] || {}; const L = DT_LETTERS[i]; return ( ); }; const roomyLease = isLease && (dkcDens === "roomy" || dkcDens === "xl") && dkfFit < 1; // fit step 1 drops these rows // Rows of the options grid (label + one cell per visible option). const optRows = [ ["h", ], ["term", isLease ? "Term · 1-pay" : "Term (mo)"], ["rate", isLease ? "Money factor" : "APR %"], ...(isLease ? [["resid", "Residual %"]] : []), ...(roomyLease ? [["residv", "Residual $"]] : []), ["down", "Down $"], ["prod", "Products"], ...(roomyLease ? [["dep", "Depreciation"], ["rent", "Rent charge"], ["base", "Base payment"], ["ltax", taxMethod === "monthly" ? "Tax / mo" : "Tax (upfront)"]] : []), ["amt", isLease ? "Adj. cap cost" : "Amt financed"], ...(isLease ? [["due", "Due at signing"]] : []), ["pmt", "Payment"], ]; const optCell = (row, i) => { const s = (isLease ? lsc : sc)[i] || {}; const L = DT_LETTERS[i]; const on = i === selSc; const q = isLease ? leaseOf(i) : null; switch (row) { case "h": return (
{/* the option letter is a real button: Tab reaches it, Enter / Space picks it */} {!P && i >= 2 ? : null}
); case "term": return (
{P ? {s.term} mo{q && s.sp ? " · 1-PAY" : ""} : ( {isLease && usedMode ? (() => { const t0 = +s.term || 36, off = !GMF_USED_TERMS.includes(t0); // a stored term off the 24 / 36 / 48 program is SHOWN, never replaced by the first option return ; })() : isLease ? setLeaseTerm(i, v) : setScK(i, "term", v)} width={50} style={dkcIn()} />})} {isLease && !P ? (() => { const spDisabled = usedMode && (+s.term || 0) < 36; return ( ); })() : null}
); case "rate": return (
{P ? {isLease ? q.effectiveMoneyFactor.toFixed(5) : (+s.apr || 0).toFixed(2) + "%"} : ( {isLease ? (s.aprIn ? <> setLeaseApr(i, v)} width={62} decimals={2} style={dkcIn(autoMfOn(i) ? { border: "1px solid var(--jb-green)", boxShadow: "inset 3px 0 0 var(--jb-green)" } : null)} />% : setLeaseMf(i, v)} width={74} decimals={5} style={dkcIn(autoMfOn(i) ? { border: "1px solid var(--jb-green)", boxShadow: "inset 3px 0 0 var(--jb-green)" } : null)} />) : setScK(i, "apr", v)} width={62} decimals={2} style={dkcIn()} />})}
); case "resid": return (
{P ? {q.residPct.toFixed(1)}% : ( {!roomyLease ? {dt$(q.residual)} : null} {mileageAddOf(i) ? +{mileageAddOf(i)} : null} setLeaseResid(i, v)} width={56} decimals={1} style={dkcIn(autoRvOn(i) ? { border: "1px solid var(--jb-green)", boxShadow: "inset 3px 0 0 var(--jb-green)" } : null)} /> )}
); case "residv": return
{dt$(q.residual)}
; case "down": return (
{P ? {isLease && s.sp ? "n/a (1-pay)" : dt$(+s.down || 0)} : ( isLease ? setLscK(i, "down", +v || 0) : setScK(i, "down", +v || 0)} width={"100%"} style={dkcIn()} /> )}
); case "prod": return P ?
{optProdLabel(s)}
: (
{chipFor(i, "gap", "GAP")}{chipFor(i, "vsc", "Warranty", "Warr")}{chipFor(i, "all", "All Products", "All")}
); case "dep": return
{dkfOptFig(i, dt$(q.dep))}
; case "rent": return
{dkfOptFig(i, dt$(q.rent))}
; case "base": return
{dkfOptFig(i, dt$(q.base))}
; case "ltax": return
{dkfOptFig(i, dt$(taxMethod === "monthly" ? q.tax : q.upfrontTax))}
; case "amt": return (
{isLease ? dkfOptFig(i, dt$(q.adjCap)) : {dkfOptFig(i, dt$(amtFin(i)))}}
); case "due": return
{dkfOptFig(i, dt$(q.dueAtSigning))}
; case "pmt": return (
{isLease && s.sp ? once : null} {dkfOptFig(i, isLease && s.sp ? dt$(q.singlePayAmount) : dt$(heroPmt(i)))}
); default: return
; } }; // Print menu entries (unchanged list + actions). const printItems = [ // Packet resolver gets "Lease" for a lease (B-03) — the sequencing rules // then add the lease forms and drop the retail RIC / 553 prep. ["Deal Forms Packet… (sequenced)", () => { pushToBuyersOrder(); const bo = window.loadJbForm ? window.loadJbForm("buyersOrder") : {}; window.k2bPrintSelectDeal && window.k2bPrintSelectDeal({ dealType: isLease ? "Lease" : (/^n|new/i.test(unit.type || bo.newUsed || "") ? "New" : "Used"), newUsed: unit.newUsed || bo.newUsed || "", bank: lender, state: bo.state || "", dealId: cust.dealId || null }); }], ["Due Bill — We Owe", () => { const d = dkpDealOf(cust.dealId); window.k2bPrintDueBill && window.k2bPrintDueBill(Object.assign({}, d || { id: cust.dealId, buyer: cust.name }, { costAdds: dkpLines })); }], // RC27 (dsk): the customer's due bill (no amounts) ["Customer Presentation", () => printCustomerPresentation()], ["4-Square Worksheet", () => printFourSquare()], ["Lease Worksheet (Dealer)", () => printLeaseWorksheet()], ["Lease Disclosure (Customer)", () => printLeaseDisclosure()], ["Contract Prep — 553-MO (F&I)", () => printContractPrep()], ["Buyer's Order", () => { pushToBuyersOrder(); window.k2bPrintBuyersOrder && window.k2bPrintBuyersOrder(); }], ["Sold “As-Is” — No Warranty", () => { pushToBuyersOrder(); window.k2bPrintAsIs && window.k2bPrintAsIs(); }], ["Release of Liability", () => { pushToBuyersOrder(); window.k2bPrintRelease && window.k2bPrintRelease(); }], ]; const goCreditApp = () => { try { const dd = cust.dealId && window.DMS ? window.DMS.deals().find(x => x.id === cust.dealId) : null; if (dd && dd.custNum) localStorage.setItem("jb_credit_cust", dd.custNum); } catch (e) {} go && go("creditapp"); }; const moreItems = [ ["🚗 Pick Unit", () => setPicker("unit")], ...(unit.vin && window.K2BApps ? [["📄 Carfax", () => { try { window.K2BApps.open("carfax", { vin: unit.vin, stock: unit.stock }); } catch (e) { window.k2bToast && window.k2bToast("Carfax app not available.", "warn"); } }, "Carfax report for VIN " + unit.vin]] : []), ["🧾 Credit App", goCreditApp], ["📝 Save to Buyer's Order", () => { if (dkfNoFig || dkfBadSel) { window.k2bToast && window.k2bToast(noDeal ? "No deal open — deals start in the CRM: pick the lead → Desk It." : dkfNoFig ? "Enter the sale price first — nothing to save yet." : dkfBadMsg(selSc), "warn", 4200); return; } pushToBuyersOrder(); window.k2bToast && window.k2bToast("Figures saved to Buyer's Order draft.", "ok"); }], ["📋 Buyer's Order", () => go && go("buyersorder")], null, ["🏦 Banks / lenders", () => go && go("banks"), "Manage lenders / banks"], ["⚙ Desking settings & defaults", () => setSettingsOpen(true)], ["🛡 Manage F&I catalog", () => go && go("fiproducts")], ...(dkcMod && window.K2BModSettingsPanel ? [["⚙ Module settings", () => setDkcModOpen(true)]] : []), null, [dkcPin ? "☰ Fold the menu while desking" : "☰ Keep the full menu open", () => { const v = !dkcPin; setDkcPin(v); dkcStore("dkc_nav_pin", v ? "1" : "0"); }], ]; const dkfKeys = [["Alt+1 … 4", "Pick option A – D"], ["Tab", "Price → Trade → Down → Payment"], ["Enter / Space", "On an option letter: pick it"], ["Alt+P", "Customer view on / off"], ["Alt+G", "Show / hide gross"], ["Alt+M", "Details panel"], ["Esc", "Close a menu · leave the customer view"]]; // ── the four squares (numbered in reading order: 1 Trade · 2 Price / 3 Down · 4 Payment) ── const unitPill = (!unitCheck.clean && !P && dkfAlertPill) ? ( setDkcMenu(null)} className="dkc-unitpop" style={{ right: 0, top: "calc(100% + 6px)", width: 380 }}>
⚠ UNIT NOT CLEAN — Stk {unit.stock}
{unitCheck.items.map((it, i) =>
{it.label}
)}
) : null; const priceSq = ( {unitPill}}>
{isLease && }
); // fit step 6: the trade description moves up into the square's header row const tradeDescIn = hasTrade && dkfFit >= 6; const tradeDescEl = P ? {tradeDesc || "Trade-in"} : setTradeDesc(e.target.value)} placeholder="Year / Make / Model" aria-label="Trade description" style={{ width: "100%", boxSizing: "border-box", fontSize: "var(--dkc-ifs, 13px)", height: "var(--dkc-in-h)", padding: "0 8px" }} />; const tradeSq = ( {tradeDescIn ?
{tradeDescEl}
: null} {(!P && hasTrade) ? : null}}> {!hasTrade ? (
No trade-in on this deal {!P &&
}
) : (
{!tradeDescIn &&
{tradeDescEl}
} {P ? {tradeLender || "—"} : setTradeLender(e.target.value.toUpperCase())} placeholder="Bank holding the payoff" aria-label="Payoff lender" title={tradeLender || ""} style={{ width: "calc(var(--dkc-mw) + 96px)", maxWidth: "100%", boxSizing: "border-box", fontSize: "var(--dkc-ifs, 13px)", fontWeight: 600, textAlign: "right", height: "var(--dkc-in-h)", padding: "0 6px" }} />} 0 ? "Negative equity" : "Net Equity"} title={negEquity > 0 ? "Negative equity — rolled into the deal" : "Trade equity"} value={dt$(tradeEquity)} color={tradeEquity < 0 ? "var(--jb-red)" : "var(--jb-green)"} strong /> {/* ACV (book) and Trade Hold live in the profit block under Down (Show gross) — they no longer make this row taller */} {negEquity > 0 && dkcDens !== "tight" && dkfFit < 4 &&
Payoff is over the allowance — {dt$(negEquity)} is rolled into the {isLease ? "lease" : "loan"} on every option.
}
)}
); const downSq = (
{isUsedUnit ?
Rebates apply to new units only—
: } {!isUsedUnit && !P && rebateReview.map(it => (
{it.label} {dt$(it.amount)} {it.key === "leaseCash" && /program/.test(it.label) ? "available" : "from before"}{it.key === "leaseCash" ? " (lease)" : ""}
))} {isLease && } {isLease && leaseCashEff > 0 && } {/* RC27 (dsk): customer-facing lines only — the dealer's down payment assistance lowers the customer's cash; a lender fee passed to the customer is their fee */} {dkpDpa > 0 && } {dkpDpa > 0 && } {dkpFeeCust > 0 && } {negEquity > 0 && } {isLease && } 0 ? `includes ${dt$(negEquity)} negative equity` : null} />
{P && chosenProducts(selSc).length ? (
Protection products — option {L1}
{chosenProducts(selSc).map((pp, i) => )}
) : null} {showGross && !dkfNoFig && (
💲 {dkfLS("Deal profit · managers only", "Deal profit")}
{/* long labels, or short ones when the Down square is narrow (container query) — never cut off */}
{CA.frontCents ? : null} {CA.backCents ? : null} {dkpPackWhatIf && dkpPostProfit ? : null} {hasTrade && } {hasTrade && 0 ? "var(--jb-amber)" : "var(--jb-green)"} mgr />}
)} ); const payGridRows = optRows.length; const presentBtn = ( ); const paySq = (
4Payment {P ? {isLease ? "Lease" : "Finance"} · {lender} : ( <> {[["finance", "💰 Finance"], ["lease", "🔑 Lease"]].map(([m, lab]) => ( ))} )}
{!P && } {!P && isLease && }
{isLease ? (heroSp ? "Single-Pay Lease" : "Lease") : "Finance"} Payment — Option {L1} ★
{dkfNoFig || dkfBadSel ?
—
: P ? (heroSp ?
{dt$(LQs.singlePayAmount)} one-pay
:
{dt$(heroPmt(selSc))}/mo
) : }
{dkfNoFig && (
{noDeal ? "No deal open" : "No sale price yet"} — {noDeal ? "deals start in the CRM: pick the lead → Desk It." : "enter the sale price to see payments."}
)} {dkfBadSel && (
{isLease ? "Payment below $0" : dkfOverSel ? "Down payment too high" : "Nothing to finance"} — {dkfBadMsg(selSc)}
)} {!isLease && !dkfNoFig && !dkfBadSel &&
{heroTerm(selSc)} months @ {sc[selSc].apr.toFixed(2)}% · {lender}
} {presentBtn} {isLease && !dkfNoFig && !dkfBadSel && (
{heroTerm(selSc)} months · MF {LQs.effectiveMoneyFactor.toFixed(5)} ({heroRate(selSc).toFixed(2)}% APR equiv) · Residual {dt$(LQs.residual)} ({LQs.residPct.toFixed(1)}%{LQs.residualAdjustments > 0 ? " − mi adj" : ""}) · Due at signing {dt$(LQs.dueAtSigning)}
{heroSp ? "1 payment at inception" : `${LQs.remainingPayments} payments of ${dt$(LQs.payment)} — first paid at inception`} · Total of payments {dt$(LQs.totalOfPayments)} · {(+milesPerYr).toLocaleString()} mi/yr{gmfOn ? ` · ${gmfNewUsed === "used" ? "GMF Used" : "GMF"} Tier ${gmfTier}` : ""}
)}
{isLease && !P && (
{!gmfOn && } {gmfOn && ( {["new", "used"].map(nu => )} )} {gmfOn && !usedMode && ( )} {leaseTe && dkcPayW >= 900 && ( Titled to: {dtEntityShort(leaseTe.name)} )}
)}
{optRows.map(([k, lab]) =>
{lab}
)}
{visIdx.map(i => (
setSelSc(i)}> {optRows.map(([k]) => optCell(k, i))}
))}
); // ── drawer (secondary detail) ── const drawerTabs = [["products", "Products"], ["fees", "Fees"], ...(isLease ? [["lease", "Lease"]] : []), ["credit", "Credit"], ...(showGross ? [["profit", "💲 Profit"]] : [])]; const drawerBody = (() => { // ── RC27 (dsk): the PROFIT panel — managers only (inside Show gross; never on Present / a customer copy) ── if (drawerTab === "profit" && showGross) { const pp = dkpProfit || {}; const ro = dkpIsPosted; const kinds = (window.DealCalc && window.DealCalc.LENDER_FEE_KINDS) || {}; const segBtn = (i, side, on) => ; return (
Deal profit — managers only
{ro ? posted — frozen : null}
{dkpPackWhatIf && dkpPostProfit ?
What-if pack: this desk uses {dt$(+pack || 0)}, but the posting books the unit's pack in Inventory, {dt$(dkpInvPack)} — net front {dkpC(dkpPostProfit.netFrontCents)}, total gross {dkpC(dkpPostProfit.totalGrossCents)}, commission {dkpC(dkpPostProfit.commissionCents)}. {!ro ? <> : null}
: null}
Cost adds — costs the store takes on this deal
{!ro && }
{!dkpLines.length ?
No cost adds on this deal. Add down payment assistance, a lender fee the store pays, a we-owe (it prints a due bill), a dealer-paid fee, an F&I item or another cost.
: null} {dkpLines.map((l, i) => { const T = (window.DealCalc && window.DealCalc.COST_ADD_TYPES[l.type]) || { label: l.type }; const isFee = l.type === "lenderFee"; const forcedWo = l.type === "weOwe"; const woAllowed = !(l.type === "dpa" || isFee); return (
setCaK(i, "desc", e.target.value)} style={{ flex: "1 1 120px", minWidth: 90, height: "var(--dkc-in-h)", fontSize: 13, padding: "0 6px" }} /> {ro ? {dt$(+l.amount || 0)} : setCaK(i, "amount", (+v || 0) > 0 ? (+v).toFixed(2) : "")} width={96} style={dkcIn()} />} {!ro && }
{segBtn(i, "front", (l.side || T.side) !== "back")}{segBtn(i, "back", (l.side || T.side) === "back")} {woAllowed && } {isFee && } {isFee && } {l.auto && !ro ? from {l.lender || "the lender"} : null} {isFee && l.passed ? customer's fee — not a cost : null}
); })} {!ro && dkpSaveNote && !CA.problems.length ?
Not saved to the deal yet: {dkpSaveNote}
: null} {CA.problems.length ?
{CA.problems.map((x, k) =>
⚠ {x.msg}
)}
Nothing is saved to the deal until the line is right; your entries stay here.
: null}
Front lowers the front gross and the commission the pay plan figures on it; Back lowers the F&I gross. Down payment assistance is the store's money toward the down payment — the customer pays that much less cash and the deal still balances. A lender fee the store pays comes out of the funding; switched to passed to customer it is the customer's fee line (in the amount financed) and not a cost. A we owe prints a due bill with the deal documents and goes on the We-Owes list when the deal posts. None of this ever shows on Present or a customer copy.
{ro ?
This deal is posted — its cost adds are on the books. To change them the office unwinds the deal (Deal Backend › Unwind deal), changes it and posts it again.
: null}
); } if (drawerTab === "products") return ( <>
F&I Products & Service Contracts
Option chips: GAP / Warranty = the ticked one of that kind · All = every ticked product (GAP + Warranty if none).
{catalog.length === 0 ? (
No active products — add them in the F&I Products catalog.
) : cats.map(cat => (
{cat}
{catalog.filter(p => p.category === cat).map(p => { const on = selected[p.id] != null; const sub = `${p.provider}${p.termMo ? " · " + p.termMo + "mo" : ""}`; // Show gross: the cost is its own line under the customer price (a ticked row's price box left the subtitle no room for it) return (
toggleProduct(p)} aria-label={p.name} /> toggleProduct(p)} title={`${p.name} — ${sub}${showGross ? " · cost " + dt$(p.cost) : ""}`}>{p.name}{sub} {on ? setProductPrice(p.id, v)} width={96} style={dkcIn()} /> : {dt$(p.retail)}} {showGross ? cost {dt$(p.cost)} : null}
); })}
))}
{showGross && }
); if (drawerTab === "fees") return isLease ? ( <>
Lease fees & tax
{secDep != null && } markupMax ? { color: "var(--jb-red)" } : null}>Dealer markup MF}> markupMax ? { color: "var(--jb-red)", fontWeight: 700 } : null)} /> {(+dealerMarkupMf || 0) > markupMax &&
⚠ Markup exceeds GMF max participation of {markupMax.toFixed(5)} MF.
} {anySp &&
💵 Single-pay option on the desk — GMF rule: cash cannot be used as cap-cost reduction on a single-pay lease (rebates / lease cash still apply); pricing is at A+/A1 with the band MF reduction.
}
) : ( <>
How option {L1}'s amount financed is built
{hasTrade && } {hasTrade && } {rebate > 0 && }
Taxable = sale price + doc fee − trade allowance; the rebate is applied after tax. Each option uses its own down payment and products.
); if (drawerTab === "lease") { const mq = LQs; return ( <> {!gmfReady &&
⚠ GMF program data (LeaseRates.js) or the lease engine (LeaseCalc) did not load — lease figures show $0.00. Check the script tags in index.html.
}
{gmfOn ? (usedMode ? (G.usedProgram.program || "GMF Used Lease") : (G.program.program || "GMF Program")) : "Manual program"}
{gmfOn ? `Effective ${usedMode ? (G.usedProgram.effective || "—") : (G.program.effective || "—")}` : "Manual MF & residuals"} {leaseCashEff > 0 ? <>
Lease cash {dt$(leaseCashEff)} applied as rebate : null} {gmfOn && G.program.gapIncluded !== false ? <>
🛡 GAP included in GMF lease — GAP product defaults off : null} {leaseTe ? <>
Titled to: {dtEntityShort(leaseTe.name)} : null}
Mileage factor · retro adj 1,250–7,500 mi
@ Inception miles {inceptionAdjust == null ? {mq.inceptionMileageAdjust ? "AUTO ADJ ON" : "auto · no adj"} : {inceptionAdjust ? "ADJ FORCED ON" : "adj forced off"}}} title="Odometer at signing — seeds from the desked unit; the retro adjustment auto-applies between 1,250 and 7,500 miles"> @ setInceptionAdjust(e.target.checked)} /> {inceptionAdjust != null && } 0 ? "− " : "") + dt$(mq.residualAdjustments)} color={mq.residualAdjustments > 0 ? "var(--jb-amber, #A66B00)" : null} />
Options — breakdown
{dkfNoFig ?
{noDeal ? "No deal open — deals start in the CRM: pick the lead → Desk It." : "Enter the sale price to see the lease figures."}
: visIdx.map(i => { const s = lsc[i] || {}; const q = leaseOf(i); const sel = i === selSc; const spDisabled = usedMode && (+s.term || 0) < 36; return (
setSelSc(i)} title={"Present option " + DT_LETTERS[i]}> {sel ? "★ " : ""}{DT_LETTERS[i]} {s.term} mo · {s.sp ? "single-pay lease" : "monthly lease"}
{dkfBad[i] ? ⚠ BELOW $0 — LOWER THE DOWN : q.upsideDown && ⚠ UPSIDE DOWN}
eff MF {q.effectiveMoneyFactor.toFixed(5)} ≈ {q.aprEquivalent.toFixed(2)}% APR{tierAddOf(i) ? ` (tier +${tierAddOf(i).toFixed(5)})` : ""}{s.sp ? ` (1-pay −${q.singlePayMfReduction.toFixed(5)})` : ""}
0 ? " − mi adj" : ""})`} value={dt$(q.residual)} /> {s.sp ? : }
{dkfBad[i] ? dkfBadMsg(i) : s.sp ? "1 payment at inception" : `${q.remainingPayments} payments of ${dt$(q.payment)} — first at inception`}
{leaseExpand === i && (
{lsAdds(i) > 0 && } {acqFeeCapped && acqFee > 0 && } {docFeeCapped && docFee > 0 && } {titleLicCapped && titleLicFee > 0 && } {q.negativeEquity > 0 && } {q.capReductionCash > 0 && } {(rebate + leaseCashEff > 0) && } {q.positiveEquity > 0 && } {q.residualAdjustments > 0 && ( <>
Residual
{q.extraMileageCost > 0 && } {q.inceptionMileageAdjustment > 0 && } )}
Due at signing
{s.sp ? : } {q.dueAtSigningItems.capReductionCash > 0 && } {q.dueAtSigningItems.acqFee > 0 && } {q.dueAtSigningItems.docFee > 0 && } {q.dueAtSigningItems.titleLicense > 0 && } {q.dueAtSigningItems.securityDeposit > 0 && } {q.dueAtSigningItems.upfrontTax > 0 && } {q.dueAtSigningItems.capReductionTax > 0 && } {q.dueAtSigningItems.taxOnUpfrontFees > 0 && }
)}
); })} ); } return ( <>
Customer profile
setDti(e.target.value)} aria-label="DTI" style={{ width: 70, textAlign: "right", fontSize: 13, height: "var(--dkc-in-h)", padding: "0 6px" }} /> 20 ? "var(--jb-amber)" : null} /> = 730 ? "A+ · Tier 1" : fico >= 690 ? "A · Tier 1" : fico >= 640 ? "B · Tier 2" : fico >= 580 ? "C · Tier 3" : "D · Tier 4"} color="var(--jb-blue)" />
); })(); const drawerEl = !drawerOpen ? null : ( ); const why = noDeal ? "No deal open — deals start in the CRM: pick the lead → Desk It." : dkfNoFig ? "Enter the sale price first — nothing to push yet." : ""; const whyFin = why || (dkfBadSel ? dkfBadMsg(selSc) : ""); // footer read-out: the chosen payment, total down and amount financed (click = jump to the box) const sumEl = (
); return (
= 3 ? " dkf-fit-rows" : "") + (dkfFit >= 5 ? " dkf-fit-hero" : "")} data-fit={dkfFit} data-dens={dkcDens} data-lease={isLease ? "1" : "0"} data-present={P ? "1" : "0"} data-gross={showGross ? "1" : "0"} style={{ width: "100%", height: 800, margin: "0 auto" }}> {/* ── header: who · what · save / print / gross / customer view / details ── */}
{P ? 👁 Customer view : null} {!P && ( FICO )} {!P && unit.vin && !dkcNarrow && }
{!P && } {!P && } {!P && (
setPrintOpen(false)} className="dkc-printmenu" style={{ right: 0, top: "calc(100% + 6px)", gridTemplateColumns: dkcBox.w >= 900 ? "repeat(3, 224px)" : "repeat(2, 224px)" }}> {printItems.map(([label, fn], i) => ( ))}
)} {!P && } {!P && }
{/* unit-readiness alert — one line on a big screen; on smaller screens a red pill in the Price square (no height taken) */} {!unitCheck.clean && !P && !dkfAlertPill && (
"• " + it.label).join("\n")}> ⚠ UNIT NOT CLEAN — Stk {unit.stock} has {unitCheck.items.length} open item{unitCheck.items.length !== 1 ? "s" : ""}: {unitCheck.items.map((it, i) => {i ? " · " : ""}{it.label})}
)} {/* ── the sheet (+ docked drawer) ── */}
{priceSq} {drawerLeft ? null : tradeSq} {drawerLeft ? null : downSq} {paySq} {drawerLeft ? drawerEl : null}
{drawerDock ? drawerEl : null}
{/* ── footer: close · more · status line · chosen payment / down / financed · PUSH (always on screen) ── */}
{!P && (
setDkcMenu(null)} style={{ left: -72, bottom: "calc(100% + 6px)", minWidth: 250 }}> {moreItems.map((it, i) => it ? ( ) :
)}
)} {noDeal && ( ⚠ No deal open. Deals start in the CRM — pick the lead → Desk It.{dkcWide ? " (Payments show once a deal is open.)" : ""} )} {dkfOffer && !P && ( {/* below ~1700 px of desk the scenario name stays in the tooltip so the status line beside it keeps room for its message */} )} {/* the customer view shows no message text (they can name other customers) — only a count, read after Exit */} {P ? (() => { const held = dkcMsgs.filter(m => m.at >= dkfPresAtRef.current).length; return (
{held ? ● {held} : }
); })() : (
{ if (dkcMsgs.length) setDkcMenu(m => m === "log" ? null : "log"); }} title={dkcMsgs.length ? "Messages — click for the last few" : ""}> {dkcLast ? (() => { const [ic, col] = dkcKind(dkcLast.kind); return ( <>{ic}{dkcLast.msg.replace(/\n/g, " · ")}); })() : }
)} {/* message log — opens over the Trade / Down side, never over the payment square */} setDkcMenu(null)} className="dkc-log" style={{ left: 10, bottom: "calc(100% + 6px)", boxSizing: "border-box", width: Math.max(280, Math.round(dkcSheetW * dkcSplit[0] / 100) - 20) }}> {dkcMsgs.slice().reverse().map(m => { const [ic, col] = dkcKind(m.kind); return (
{ic}{m.msg}{new Date(m.at).toLocaleTimeString()}
); })}
{!P && ( )} {/* the shortcut list opens over the Trade / Down side like the message log — never over the payment square */} setDkcMenu(null)} className="dkc-keys" style={{ left: 10, bottom: "calc(100% + 6px)", boxSizing: "border-box", width: Math.max(300, Math.round(dkcSheetW * dkcSplit[0] / 100) - 20) }}> {dkfKeys.map(([k, t]) =>
{k}{t}
)}
{/* the read-out beside Push; a narrow no-deal calculator gives its room to the "No deal open → Open CRM" notice */} {/* K2B-013: an empty desk has no figures to read out — the notice gets the room at every size */} {noDeal && (dkcNarrow || dkfNoFig) ? null : sumEl} {/* ONE PATH: the desk pushes THE deal to exactly one closing screen. */} {!isLease && !P && ( )} {!P && }
{dkcModOpen && dkcMod && window.K2BModSettingsPanel ? React.createElement(window.K2BModSettingsPanel, { module: dkcMod, onClose: () => setDkcModOpen(false) }) : null} {/* ── Settings panel ── */} {settingsOpen && setSettingsOpen(false)} go={go} />} {/* ── Pickers ── */} {picker === "unit" && setPicker(null)} go={go} exceptDeal={cust.dealId} />} {gmfPickerOpen && setGmfPickerOpen(false)} />} {picker === "cust" && ( setPicker(null)} buttons={<> { setPicker(null); go && go("crm"); }}>Start in CRM ▸ setPicker(null)}>Cancel }> {/* ONE PATH: only OPEN deals are pickable here. Deals (and customers) are created in the CRM (New Lead → Desk It) — never from the desk. RC19: search box + chips over the list (DsDeskPickList, DealSearch.jsx.txt). */} pickCust(dtCustProfile(d.buyer, d.id, { ...((custs.find(c => c.custNum === d.custNum)) || {}), ...d }))} /> )} {/* ── Save Scenario modal ── */} {scenModal === "save" && ( setScenModal(null)} buttons={<> setScenModal(null)}>Cancel Save }>
Scenario name setScenName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") saveScenario(); }} style={{ width: "100%", boxSizing: "border-box", fontSize: 13, padding: "7px 9px" }} />
Saves the complete desk — customer, unit, price, trade, down, all finance & lease options and F&I products — to the shared store, so any workstation can reopen it from 📂 Scenarios.
)} {/* ── Saved Scenarios list modal ── */} {scenModal === "list" && ( setScenModal(null)} buttons={ setScenModal(null)}>Close}> {(() => { const list = ((window.DMS && window.DMS.deskScenarios && window.DMS.deskScenarios()) || []) .slice().sort((a, b) => String(b.savedAt || "").localeCompare(String(a.savedAt || ""))); if (!list.length) return
No saved scenarios yet — work a deal, then hit 💾 Save Scenario.
; return (
{list.map((r, i) => ( ))}
ScenarioSaved by When
{r.name || r.id} {r.savedBy || "—"} {r.savedAt ? new Date(r.savedAt).toLocaleString() : "—"} {" "}
); })()}
)}
); }; /* ── Desking Settings panel ── */ const DeskSettings = ({ settings, onSave, onClose, go }) => { const [s, setS] = React.useState({ ...settings }); const set = (k, v) => setS(prev => ({ ...prev, [k]: v })); return ( { saveDeskSettings(DESK_DEFAULTS); setS({ ...DESK_DEFAULTS }); }}>Reset Defaults Cancel onSave(s)}>Save }>
Deal Defaults
Default Payment Scenarios
{[["A", "t1", "a1"], ["B", "t2", "a2"], ["C", "t3", "a3"]].map(([opt, tk, ak]) => (
{opt} Term set(tk, v)} width={50} /> APR set(ak, v)} width={60} decimals={2} />
))}
Catalogs
{/* ── Lease defaults ── */}
🔑 Lease Defaults
Default lease scenarios — term & money factor
{[["A", "lt1", "lmf1"], ["B", "lt2", "lmf2"], ["C", "lt3", "lmf3"]].map(([opt, tk, mk]) => (
{opt} Term set(tk, v)} width={50} /> MF set(mk, v)} width={70} decimals={5} /> ≈{dtMfToApr(s[mk]).toFixed(2)}%
))}
Residuals auto-derive from term + mileage (GM closed-end table) and can be overridden per option on the desk.
); }; const DkSetFld = ({ label, k, dec = 2, money, s, set }) => (
{label} {money ? set(k, v)} width={110} style={{ textAlign: "right" }} /> : set(k, v)} width={80} decimals={dec} style={{ textAlign: "right" }} />}
); /* ── SALES VIEW-ONLY DESK (route "deskview") ────────────────────────────── A salesperson may LOOK at the latest saved desk of THEIR OWN deal, the way a customer would see it: vehicle, price, trade allowance / payoff, down, every option's term / rate / down / payment, the chosen products at their customer prices, and the lender. Nothing else is ever rendered — no invoice, gross, cost, ACV, pack or commission, no inputs, no Push, no Save, no dealer prints (a customer copy may be printed). Source: the customer-safe snapshot the desk stores with every Save Scenario (scenario.view) and every Push (deal.deskView) — computed figures only. It is read from GET /api/desk-view/ when the server has that route (it returns the snapshot alone); otherwise from what this session already holds (own deal / saved scenarios). Either way it passes dkfSafeView, a whitelist copy, before anything is shown. */ const DKF_VIEW_PTR = "dkf_view_deal"; const dkfNum = (n) => (typeof n === "number" && isFinite(n)) ? n : (n != null && n !== "" && isFinite(+n) ? +n : null); const dkfStr = (s) => (s == null ? "" : String(s)).slice(0, 200); // Whitelist copy of a stored snapshot — unknown keys are dropped. function dkfSafeView(v) { if (!v || typeof v !== "object") return null; const opt = (o) => ({ letter: dkfStr(o && o.letter).slice(0, 1), term: dkfNum(o.term) || 0, apr: dkfNum(o.apr), mf: dkfNum(o.mf), aprEq: dkfNum(o.aprEq), singlePay: !!o.singlePay, residPct: dkfNum(o.residPct), residual: dkfNum(o.residual), down: dkfNum(o.down), prodLabel: dkfStr(o.prodLabel), prodTotal: dkfNum(o.prodTotal) || 0, products: Array.isArray(o.products) ? o.products.slice(0, 12).map(p => ({ name: dkfStr(p && p.name), price: dkfNum(p && p.price) || 0 })) : [], amount: dkfNum(o.amount), dueAtSigning: dkfNum(o.dueAtSigning), payment: dkfNum(o.payment) || 0, remaining: dkfNum(o.remaining), totalOfPayments: dkfNum(o.totalOfPayments), totalDown: dkfNum(o.totalDown) }); const u = v.unit || {}, t = v.trade; const options = (Array.isArray(v.options) ? v.options : []).filter(o => o && typeof o === "object").slice(0, 4).map(opt).filter(o => /^[A-D]$/.test(o.letter)); if (!options.length) return null; return { savedAt: dkfStr(v.savedAt), dealId: dkfStr(v.dealId), customer: dkfStr(v.customer), unit: { yr: dkfStr(u.yr), mk: dkfStr(u.mk), mdl: dkfStr(u.mdl), stock: dkfStr(u.stock), vin: dkfStr(u.vin), newUsed: dkfStr(u.newUsed), miles: dkfNum(u.miles) || 0 }, mode: v.mode === "lease" ? "lease" : "finance", lender: dkfStr(v.lender), titledTo: dkfStr(v.titledTo), salePrice: dkfNum(v.salePrice) || 0, docFee: dkfNum(v.docFee) || 0, taxRate: dkfNum(v.taxRate) || 0, salesTax: dkfNum(v.salesTax) || 0, msrp: dkfNum(v.msrp) || 0, leaseTaxRate: dkfNum(v.leaseTaxRate), milesPerYr: dkfNum(v.milesPerYr), taxMethod: dkfStr(v.taxMethod), trade: t && typeof t === "object" ? { desc: dkfStr(t.desc), allowance: dkfNum(t.allowance) || 0, payoff: dkfNum(t.payoff) || 0, lender: dkfStr(t.lender), equity: dkfNum(t.equity) || 0, negEquity: dkfNum(t.negEquity) || 0, posEquity: dkfNum(t.posEquity) || 0 } : null, rebate: dkfNum(v.rebate) || 0, leaseCash: dkfNum(v.leaseCash) || 0, capRed: dkfNum(v.capRed) || 0, usedUnit: !!v.usedUnit, sel: options.some(o => o.letter === v.sel) ? v.sel : options[0].letter, options, products: Array.isArray(v.products) ? v.products.slice(0, 20).map(p => ({ name: dkfStr(p && p.name), price: dkfNum(p && p.price) || 0 })) : [], pushedAs: v.pushedAs === "cash" || v.pushedAs === "finance" ? v.pushedAs : null, cashDue: dkfNum(v.cashDue), downAssist: dkfNum(v.downAssist) || 0, lenderFeeCust: dkfNum(v.lenderFeeCust) || 0, // RC27 (dsk): customer-facing only (twin of routes/desk-view.js) }; } const dkfMe = () => { try { return String((window.DMS && window.DMS.userId && window.DMS.userId()) || ""); } catch (e) { return ""; } }; const dkfIsSales = () => { try { return !!(window.DMS && window.DMS.roleIs && window.DMS.roleIs("Sales")); } catch (e) { return false; } }; const dkfCanDesk = () => { try { return !!(window.DMS && window.DMS.can && window.DMS.can("desk")); } catch (e) { return false; } }; // Is this deal the signed-in salesperson's own? (their lead points at it, or the deal names them) function dkfOwnDeal(dealId) { try { const me = dkfMe(); if (!me || !dealId || !window.DMS) return false; const leads = (window.DMS.crmLeads && window.DMS.crmLeads()) || []; if (leads.some(l => l && String(l.dealId || "") === String(dealId) && String(l._owner || "") === me)) return true; const d = ((window.DMS.deals && window.DMS.deals()) || []).find(x => x && x.id === dealId); if (!d || d._limited) return false; if (String(d.spId || "") === me) return true; const mine = ((window.DMS.roster && window.DMS.roster()) || []).find(u => u && String(u.id) === me); const sp = String(d.salesperson || "").split("/")[0].trim().toLowerCase(); return !!(mine && mine.name && sp && sp === String(mine.name).trim().toLowerCase()); } catch (e) { return false; } } // Newest snapshot this session already holds for the deal (own deal's push snapshot, saved scenarios). function dkfLocalView(dealId) { const cands = []; try { const d = ((window.DMS && window.DMS.deals && window.DMS.deals()) || []).find(x => x && x.id === dealId); if (d && !d._limited && d.deskView && typeof d.deskView === "object") cands.push(d.deskView); ((window.DMS && window.DMS.deskScenarios && window.DMS.deskScenarios()) || []).forEach(s => { if (s && s.view && typeof s.view === "object" && String(s.view.dealId || "") === String(dealId)) cands.push(s.view); }); } catch (e) {} cands.sort((a, b) => String(b.savedAt || "").localeCompare(String(a.savedAt || ""))); for (const c of cands) { const v = dkfSafeView(c); if (v) return v; } return null; } // Server first (GET /api/desk-view/ returns the snapshot only), then local. async function dkfLoadView(dealId) { let tok = null; try { tok = sessionStorage.getItem("jbrews-auth-token") || localStorage.getItem("jbrews-auth-token"); } catch (e) {} if (tok) { try { const res = await fetch(location.origin + "/api/desk-view/" + encodeURIComponent(dealId), { headers: Object.assign({ Authorization: "Bearer " + tok }, window.K2BStore ? window.K2BStore.stamp({}) : {}), credentials: "omit" }); // RC35 (brd): + the page's store / protocol let j = null; try { j = await res.json(); } catch (e) {} if (res.ok && j && j.ok) return { view: dkfSafeView(j.view), source: "server" }; if (j && j.error === "not_yours") return { view: null, source: "server", notYours: true }; if (res.status === 404 && j && j.error === "no_desk") return { view: null, source: "server" }; // any other answer = this server has no desk-view route yet → local copy } catch (e) {} } return { view: dkfLocalView(dealId), source: "local" }; } // Customer copy print (the customer presentation's content, from the snapshot). function dkfPrintCustomerCopy(v) { if (!v || !window.k2bPrint) return; const r = (n) => "$" + Number(n || 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const esc = (s) => String(s == null ? "" : s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); const lease = v.mode === "lease"; const heads = v.options.map(o => o.letter), selPos = Math.max(0, heads.indexOf(v.sel)); const sel = v.options[selPos]; const rows = lease ? [ ["Down payment (cap reduction cash)", v.options.map(o => o.singlePay ? "n/a (1-pay)" : r(o.down))], ["Term", v.options.map(o => `${o.term} mo${o.singlePay ? " · 1-PAY" : ""}`)], ["Money factor", v.options.map(o => o.mf != null ? o.mf.toFixed(5) : "—")], ["Residual", v.options.map(o => `${(o.residPct || 0).toFixed(1)}% / ${r(o.residual)}`)], ["Protection products", v.options.map(o => esc(o.prodLabel || "—"))], ["Adjusted cap cost", v.options.map(o => r(o.amount))], ["Due at signing", v.options.map(o => r(o.dueAtSigning))], ["Payment", v.options.map(o => o.singlePay ? r(o.payment) + " once" : r(o.payment) + "/mo"), true], ] : [ ["Down payment", v.options.map(o => r(o.down))], ["Term", v.options.map(o => `${o.term} mo`)], ["APR", v.options.map(o => `${(o.apr || 0).toFixed(2)}%`)], ["Protection products", v.options.map(o => esc(o.prodLabel || "—"))], ["Amount financed", v.options.map(o => r(o.amount))], ["Monthly payment", v.options.map(o => r(o.payment)), true], ]; const t = v.trade; const prods = (sel && sel.products) || []; const html = `${window.dealerHeadHtml ? window.dealerHeadHtml() : ""}
${lease ? "LEASE PAYMENT OPTIONS" : "CUSTOMER PRESENTATION"}
Prepared for ${esc(v.customer)} · ${esc(new Date().toLocaleDateString())} · customer copy

Vehicle

${esc([v.unit.yr, v.unit.mk, v.unit.mdl].filter(Boolean).join(" "))}${v.unit.newUsed ? (v.unit.newUsed === "U" ? " (Used)" : " (New)") : ""}Stock ${esc(v.unit.stock)}
MSRP${r(v.msrp)}
Sale Price${r(v.salePrice)}
${t ? `

Trade

${t.negEquity > 0 ? `` : ``}
${esc(t.desc)}
Allowance${r(t.allowance)}
Payoff${t.lender ? ` (${esc(t.lender)})` : ""}${r(t.payoff)}
Negative Equity (financed)${r(t.negEquity)}
Net Equity${r(t.equity)}
` : ""} ${prods.length ? `

Protection Products — Option ${esc(v.sel)}

${prods.map(p => ``).join("")}
${esc(p.name)}${r(p.price)}
Total Products — Option ${esc(v.sel)}${r(sel.prodTotal)}
` : ""}

Payment Options — ${esc(v.lender)}

${dtOptGridHtml(heads, selPos, rows)}

★ Option ${esc(v.sel)} selected. Payments are estimates; final figures are subject to ${lease ? "lessor" : "lender"} approval and verification of stated information.

Customer Acknowledgement  ·  Date
Salesperson  ·  Date
`; window.k2bPrint(html, { title: lease ? "Lease Payment Options (customer copy)" : "Customer Presentation (customer copy)" }); } // CRM hook: "View desk" for the salesperson's own lead (rendered by CRM next to // "Ask a manager to desk this lead"). Sales only — managers have Desk It. const DkfViewDeskButton = ({ lead, go }) => { if (!lead || !lead.dealId || !dkfIsSales()) return null; const open = () => { const ptr = { dealId: String(lead.dealId), leadId: lead.id || null, by: dkfMe() }; // tied to who opened it window.dkfViewDeal = ptr; try { sessionStorage.setItem(DKF_VIEW_PTR, JSON.stringify(ptr)); } catch (e) {} go && go("deskview"); }; return ( ); }; // Role gate at render time. The shell bounces a screen the role may not open // only after that screen's first render, so a deep link (?popout=desking) would // mount — and paint — the full desk for one frame for a Sales user. The desk // never mounts for a role the shell would not show it to. // RC19 — the "Pick Customer / Deal" list with the shared deal search (DealSearch.jsx.txt). The list is the same as before // (open deals: not posted, not dead, in the store's order); the box + chips only narrow it. Chips: My deals / All, stage. const DS_DESK_STAGE_TONE = { Desked: "var(--jb-amber)", Cash: "var(--jb-green)", "F&I": "var(--jb-blue)", Posted: "var(--jb-navy)", Funded: "var(--jb-green)" }; const dsDeskStageOf = (d) => (window.DMS && window.DMS.dealStage) ? window.DMS.dealStage(d) : (d.stage || "Desked"); const DS_DESK_CHIPS = [ { key: "mine", group: "who", label: "My deals", title: "Deals you sold (you are the salesperson, or your CRM lead)", test: (d) => dsIsMine(d) }, { key: "desked", group: "stage", label: "Desked", test: (d) => dsDeskStageOf(d) === "Desked" }, { key: "cash", group: "stage", label: "Cash", test: (d) => dsDeskStageOf(d) === "Cash" }, { key: "fi", group: "stage", label: "F&I", test: (d) => dsDeskStageOf(d) === "F&I" }, ]; const DsDeskPickList = ({ onPick }) => { const [, dsForce] = React.useReducer(n => n + 1, 0); React.useEffect(() => (window.DMS && window.DMS.subscribe) ? window.DMS.subscribe(dsForce) : undefined, []); const deals = ((window.DMS && window.DMS.deals && window.DMS.deals()) || []).filter(d => !d.postedToGL && d.status !== "Dead"); const custs = Object.values((window.DMS && window.DMS.customers && window.DMS.customers()) || {}); const pick = (d) => onPick && onPick(d, custs); const ds = dsUseDealSearch({ deals, chips: DS_DESK_CHIPS, onOpen: pick }); if (!deals.length) return (
No open deals. Deals start in the CRM: pick the lead → Desk It.
); return (
Open deals — pick one to desk
{ds.list.map((d, i) => { const act = d.id === ds.activeId; // the row Enter picks (none in an empty box until ↑ ↓) return ( pick(d)} style={{ background: act ? "var(--jb-blue-tint, #E5EFFA)" : (i % 2 ? "var(--jb-surface-2)" : "#fff"), cursor: "pointer", outline: act ? "2px solid var(--jb-blue, #1B66A8)" : "none", outlineOffset: -2 }}> ); })}
{d.id} {d.buyer} {d.vehicle || "—"} {dsDeskStageOf(d)}
); }; const DeskingTool = (props) => (window.jbNavAllowed && !window.jbNavAllowed("desking")) ? null : ; const DkfDeskView = ({ onClose, go }) => { const rootRef = React.useRef(null); const box = dkfUseBox(rootRef); const dens = dkfDensOf(box); const narrow = box.w < 640; // phone-width window: squares stacked, option grid scrolls sideways, Print in the footer (CSS .dkf-narrow) const ptr = React.useMemo(() => { let p = null; if (window.dkfViewDeal && window.dkfViewDeal.dealId) p = window.dkfViewDeal; else { try { p = JSON.parse(sessionStorage.getItem(DKF_VIEW_PTR) || "null"); } catch (e) {} } try { delete window.dkfViewDeal; } catch (e) {} // the session copy is enough return p && p.by && p.by !== dkfMe() ? null : p; // a pointer left by someone else is ignored }, []); const dealId = ptr && ptr.dealId ? String(ptr.dealId) : ""; // Desk roles may look at any deal's customer view; everyone else (Sales, and // any other role the route lets in) only at a deal that is their own. const allowed = !!dealId && (dkfCanDesk() || dkfOwnDeal(dealId)); const [st, setSt] = React.useState({ loading: !!dealId && allowed, view: null, notYours: false, source: "" }); const [selL, setSelL] = React.useState(null); // Same quiet screen as the desk (mascot hidden, toasts in the status line — // see msgs below), but the app menu stays open (dkf-view-on: no fold). const { msgs, last: msgLast, fresh: msgFresh } = dkfUseMsgs(); const [logOpen, setLogOpen] = React.useState(false); React.useEffect(() => { const B = document.body; B.classList.add("dkc-desk-on"); B.classList.add("dkf-view-on"); return () => { B.classList.remove("dkc-desk-on"); B.classList.remove("dkf-view-on"); }; }, []); React.useEffect(() => { if (!dealId || !allowed) return undefined; let live = true; dkfLoadView(dealId).then((r) => { if (live) setSt({ loading: false, view: r.view, notYours: !!r.notYours, source: r.source }); }); return () => { live = false; }; }, [dealId, allowed]); const v = st.view; const sel = v ? (v.options.find(o => o.letter === (selL || v.sel)) || v.options[0]) : null; const lease = v && v.mode === "lease"; // phone width: keep the ★ option's column in view inside the sideways-scrolling grid (the grid alone scrolls - never the body) React.useEffect(() => { if (!narrow || !v) return; try { const root = rootRef.current; const gw = root && root.querySelector(".dkc-gridwrap"), c = root && root.querySelector(".dkc-col.opt.on"), lab = root && root.querySelector(".dkc-col.lab"); if (!gw || !c) return; const gr = gw.getBoundingClientRect(), cr = c.getBoundingClientRect(), labW = lab ? lab.getBoundingClientRect().width : 0; if (cr.right > gr.right) gw.scrollLeft += Math.ceil(cr.right - gr.right) + 4; else if (cr.left < gr.left + labW) gw.scrollLeft -= Math.ceil(gr.left + labW - cr.left) + 4; } catch (e) {} }, [narrow, v, sel && sel.letter]); const backCrm = () => { if (onClose) onClose(); else if (go) go("crm"); }; const msg = (title, text) => (
{title}{text}
); let bodyEl = null; if (!dealId) bodyEl = msg("No deal picked", "Open the CRM, pick your lead and press View desk."); else if (!allowed || st.notYours) bodyEl = msg("Not your deal", "Only the salesperson on this deal can view its desk. Ask a manager."); else if (st.loading) bodyEl =
Loading the saved desk…
; else if (!v) bodyEl = msg("No saved desk for this deal yet", "The figures show here after a manager saves the desk (Save Scenario) or pushes the deal. Ask a manager to desk it."); if (!bodyEl) { const t = v.trade; const u = v.unit; const vehicle = [u.yr, u.mk, u.mdl].filter(Boolean).join(" "); const F = (k, label, value, opts) => React.createElement(DkcStat, Object.assign({ k, label, value }, opts || {})); const rows = [ ["h", ""], ["term", lease ? "Term" : "Term (mo)"], ["rate", lease ? "Money factor" : "APR %"], ...(lease ? [["resid", "Residual"]] : []), ["down", "Down $"], ["prod", "Products"], ["amt", lease ? "Adj. cap cost" : "Amt financed"], ...(lease ? [["due", "Due at signing"]] : []), ["pmt", "Payment"], ]; const cell = (k, o) => { const on = o.letter === sel.letter; switch (k) { case "h": return
; case "term": return
{o.term} mo{o.singlePay ? " · 1-PAY" : ""}
; case "rate": return
{lease ? (o.mf != null ? o.mf.toFixed(5) : "—") : (o.apr || 0).toFixed(2) + "%"}
; case "resid": return
{(o.residPct || 0).toFixed(1)}% · {dt$(o.residual)}
; case "down": return
{o.singlePay ? "n/a (1-pay)" : dt$(o.down)}
; case "prod": return
{o.prodLabel || "—"}
; case "amt": return
{dt$(o.amount)}
; case "due": return
{dt$(o.dueAtSigning)}
; case "pmt": return
{o.singlePay ? once : null}{dt$(o.payment)}
; default: return
; } }; const tradeSq = ( {!t ?
No trade-in on this deal
: (
{t.desc || "Trade-in"}
{F("tradeAllow", "Allowance", dt$(t.allowance))} {F("tradePayoff", "Pay-off", dt$(t.payoff))} {F("tradeLender", "Payoff lender", t.lender || "—", { wide: true })} {F("tradeEquity", t.negEquity > 0 ? "Negative equity" : "Net Equity", dt$(t.equity), { color: t.equity < 0 ? "var(--jb-red)" : "var(--jb-green)", strong: true })} {t.negEquity > 0 &&
Payoff is over the allowance — {dt$(t.negEquity)} is rolled into the {lease ? "lease" : "loan"} on every option.
}
)}
); const priceSq = (
{F("salePrice", "Sale Price", dt$(v.salePrice))} {F("docFee", "Doc Fee", dt$(v.docFee))} {F("taxRate", "Tax Rate %", (v.taxRate || 0).toFixed(3) + "%")} {F("salesTax", "Sales Tax", dt$(v.salesTax))} {lease && v.leaseTaxRate != null && F("leaseTaxRate", "Lease tax %", v.leaseTaxRate.toFixed(3) + "%")} {F("msrp", "MSRP", dt$(v.msrp))}
); // Total down: a single-pay lease takes no cash as cap reduction (its Down reads "n/a (1-pay)"). const tdOf = (o) => (lease && o.singlePay) ? (v.rebate || 0) + (t ? t.posEquity || 0 : 0) : o.totalDown; const downSq = (
{F("down", `Down (option ${sel.letter})`, sel.singlePay ? "n/a (1-pay)" : dt$(sel.down))} {v.usedUnit ?
Rebates apply to new units only—
: F("rebate", "Rebate", dt$(v.rebate))} {lease && v.capRed > 0 && F("capRed", "Add'l cap red.", dt$(v.capRed))} {lease && v.leaseCash > 0 && F("leaseCash", "GMF lease cash (as rebate)", dt$(v.leaseCash), { color: "var(--jb-green)" })} {t && F("tradeEqDown", "Trade Equity (down)", dt$(t.posEquity))} {v.downAssist > 0 && F("dpa", "Down payment assistance", "− " + dt$(v.downAssist), { color: "var(--jb-green)" })} {v.downAssist > 0 && !sel.singlePay && F("custCash", lease ? "Customer cash at signing" : "Customer cash down", dt$(Math.max(0, (sel.down || 0) - v.downAssist)), { strong: true })} {v.lenderFeeCust > 0 && F("lenderFeeCust", "Lender fee (customer pays)", dt$(v.lenderFeeCust))} {t && t.negEquity > 0 && F("negEq", "Neg. equity → " + (lease ? "cap cost" : "loan"), dt$(t.negEquity), { color: "var(--jb-red)" })} {F("totalDown", lease && sel.singlePay ? "Total Down (1-pay: no cash)" : "Total Down", dt$(tdOf(sel)), { color: "var(--jb-blue)", big: true, rule: true, wide: true, strong: true })} {F("amtFinanced", lease ? "Adj. Cap Cost" : "Amount Financed", dt$(sel.amount), { color: "var(--jb-navy)", big: true, wide: true, strong: true })}
{sel.products.length ? (
Protection products — option {sel.letter}
{sel.products.map((p, i) => )}
) : null}
); const paySq = (
4Payment {lease ? "Lease" : "Finance"} · {v.lender || "—"}
{v.pushedAs === "cash" ? (
Cash deal — balance due at delivery
{dt$(v.cashDue)}
The deal was pushed as a cash deal. The desk's payment options are shown below for reference.
) : (
{lease ? (sel.singlePay ? "Single-Pay Lease" : "Lease") : "Finance"} Payment — Option {sel.letter} ★
{dt$(sel.payment)}{sel.singlePay ? " one-pay" : "/mo"}
{sel.term} months {lease ? <>· MF {sel.mf != null ? sel.mf.toFixed(5) : "—"}{sel.aprEq != null ? ` (${sel.aprEq.toFixed(2)}% APR equiv)` : ""} · Due at signing {dt$(sel.dueAtSigning)} : <>@ {(sel.apr || 0).toFixed(2)}% · {v.lender}}
{lease &&
{sel.singlePay ? "1 payment at inception" : `${sel.remaining || 0} payments of ${dt$(sel.payment)} — first paid at inception`} · Total of payments {dt$(sel.totalOfPayments)}{v.milesPerYr ? ` · ${(+v.milesPerYr).toLocaleString()} mi/yr` : ""}{v.titledTo ? ` · Titled to ${v.titledTo}` : ""}
}
)}
{rows.map(([k, lab]) =>
{lab}
)}
{v.options.map(o => (
setSelL(o.letter)}> {rows.map(([k]) => cell(k, o))}
))}
); bodyEl = (
{priceSq}{tradeSq}{downSq}{paySq}
); } const savedWhen = v && v.savedAt ? (() => { try { return new Date(v.savedAt).toLocaleString(); } catch (e) { return ""; } })() : ""; // the customer copy print: in the header on a desk-size window, beside Close in the footer on a phone (where the header wraps) const printBtn = v ? ( ) : null; return (
👁 View only {v ? 👤 {v.customer || "Customer"} : null} {v ? 🚗 {[v.unit.yr, v.unit.mk, v.unit.mdl].filter(Boolean).join(" ")}{v.unit.stock ? " · Stk " + v.unit.stock : ""} : null}
{v && v.pushedAs ? {v.pushedAs === "cash" ? "Pushed to Cash Deal" : "Pushed to F&I"} : null} {narrow ? null : printBtn}
{bodyEl}
{narrow ? printBtn : null} {/* status line: messages (bell alerts, sync warnings …) show here for a few seconds, then the view-only note; click for the last few */}
{ if (msgs.length) setLogOpen(o => !o); }} title={msgs.length ? "Messages — click for the last few" : ""}> {msgFresh ? (() => { const [ic, col] = dkcKind(msgLast.kind); return ( <>{ic}{msgLast.msg.replace(/\n/g, " · ")}); })() : {v ? `View only — the desk as a manager saved it${savedWhen ? " on " + savedWhen : ""}${dealId ? " · " + dealId : ""}. Changes are made on the desk by a manager.` : ""}}
0} onClose={() => setLogOpen(false)} className="dkc-log" style={{ left: 10, bottom: "calc(100% + 6px)", boxSizing: "border-box", width: 420, maxWidth: "calc(100% - 20px)" }}> {msgs.slice().reverse().map(m => { const [ic, col] = dkcKind(m.kind); return (
{ic}{m.msg}{new Date(m.at).toLocaleTimeString()}
); })}
{v && sel ? (
Option {sel.letter} ★ {sel.singlePay ? "one-pay" : "payment"}{dt$(sel.payment)}{sel.singlePay ? "" : "/mo"}
{lease && sel.singlePay ? "Total down · 1-pay" : "Total down"}{dt$((lease && sel.singlePay) ? (v.rebate || 0) + (v.trade ? v.trade.posEquity || 0 : 0) : sel.totalDown)}
{lease ? "Adj. cap cost" : "Amount financed"}{dt$(sel.amount)}
) : null}
); }; Object.assign(window, { DeskingTool, DkfDeskView, DkfViewDeskButton });