/* 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 — Deal Backend (internal / non-customer-facing). // The F&I manager's cost sheet for one deal: insurance & protection products // with DEALER COST visible, dealer-installed options/accessories, lender // reserve, and the resulting backend gross. Saved straight onto the deal // (DMS.updateDeal) so the recap, GL post and 740 schedule all agree. const BE_CATALOG = [ { key: "vsc36", cat: "VSC", name: "Service Contract 36mo/45k", provider: "GMPP", cost: 1095, sale: 1995 }, { key: "vsc60", cat: "VSC", name: "Service Contract 60mo/75k", provider: "GMPP", cost: 1495, sale: 2495 }, { key: "vsc84", cat: "VSC", name: "Service Contract 84mo/100k", provider: "GMPP", cost: 1895, sale: 3195 }, { key: "gap", cat: "GAP", name: "GAP Coverage", provider: "AAGI", cost: 350, sale: 895 }, { key: "tw", cat: "ANC", name: "Tire & Wheel", provider: "AAGI", cost: 285, sale: 699 }, { key: "maint", cat: "ANC", name: "Prepaid Maintenance 3yr", provider: "GMPP", cost: 420, sale: 899 }, { key: "appear", cat: "ANC", name: "Appearance Protection", provider: "ECP", cost: 195, sale: 595 }, { key: "theft", cat: "ANC", name: "Theft Deterrent / Etch", provider: "ECP", cost: 89, sale: 349 }, { key: "key", cat: "ANC", name: "Key Replacement", provider: "AAGI", cost: 110, sale: 399 }, { key: "cl", cat: "INS", name: "Credit Life", provider: "Protective", cost: 240, sale: 540 }, { key: "ah", cat: "INS", name: "Accident & Health", provider: "Protective", cost: 310, sale: 690 }, ]; const BE_OPT_CATALOG = [ { key: "bedliner", name: "Spray-In Bedliner", cost: 280, sale: 595 }, { key: "tint", name: "Window Tint", cost: 120, sale: 349 }, { key: "steps", name: "Running Boards / Steps", cost: 410, sale: 795 }, { key: "hitch", name: "Trailer Hitch & Wiring", cost: 330, sale: 649 }, { key: "mudflaps", name: "Splash Guards", cost: 45, sale: 139 }, { key: "cargo", name: "Cargo Mat / Liner Pkg", cost: 95, sale: 229 }, { key: "custom", name: "Custom / Other…", cost: 0, sale: 0 }, ]; const be$ = (n) => "$" + (+n || 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const beNum = (x) => { const n = parseFloat(String(x ?? "").replace(/[^0-9.\-]/g, "")); return isNaN(n) ? 0 : n; }; // Derive initial product rows from what the deal already carries. function beInitProducts(d) { // Desk / Finance write {category, price}; this sheet reads {cat, sale} — accept both. if (Array.isArray(d.products) && d.products.length) return d.products.map(p => ({ ...p, cat: p.cat || (/gap/i.test(String((p.category || "") + " " + (p.name || ""))) ? "GAP" : (p.category || "VSC")), sale: p.sale != null ? p.sale : (p.price != null ? p.price : (p.retail || 0)), cost: p.cost || 0 })); const rows = []; if ((+d.vscSale || 0) > 0) rows.push({ cat: "VSC", name: "Service Contract 60mo/75k", provider: "GMPP", cost: +d.vscCost || 0, sale: +d.vscSale || 0 }); if ((+d.gapSale || 0) > 0) rows.push({ cat: "GAP", name: "GAP Coverage", provider: "AAGI", cost: +d.gapCost || 0, sale: +d.gapSale || 0 }); return rows; } const DealBackend = ({ deal: d, go, onBack }) => { // POSTED deals are read-only here — every change goes through "Edit Deal // Backend" (DealAdjustModal), which posts an adjusting entry. const posted = !!d.postedToGL; const [productsState, setProducts] = React.useState(() => beInitProducts(d)); const products = posted ? beInitProducts(d) : productsState; const [optionsState, setOptions] = React.useState(() => Array.isArray(d.hardAdds) ? d.hardAdds.map(o => ({ ...o })) : []); const options = posted ? (Array.isArray(d.hardAdds) ? d.hardAdds : []) : optionsState; const [reserveState, setReserve] = React.useState(() => +d.reserve || 0); const reserve = posted ? (+d.reserve || 0) : reserveState; const [notes, setNotes] = React.useState(() => d.backendNotes || ""); const [dirty, setDirty] = React.useState(false); const prodGross = products.reduce((s, p) => s + (beNum(p.sale) - beNum(p.cost)), 0); const optGross = options.reduce((s, o) => s + (beNum(o.sale) - beNum(o.cost)), 0); const backendGross = prodGross + beNum(reserve); const touch = (fn) => (...a) => { if (posted) return; fn(...a); setDirty(true); }; const setProd = touch((i, k, v) => setProducts(ps => ps.map((p, j) => j === i ? { ...p, [k]: v } : p))); const delProd = touch((i) => setProducts(ps => ps.filter((_, j) => j !== i))); const addProd = touch((key) => { const c = BE_CATALOG.find(x => x.key === key); if (!c) return; setProducts(ps => [...ps, { cat: c.cat, name: c.name, provider: c.provider, cost: c.cost, sale: c.sale }]); }); const setOpt = touch((i, k, v) => setOptions(os => os.map((o, j) => j === i ? { ...o, [k]: v } : o))); const delOpt = touch((i) => setOptions(os => os.filter((_, j) => j !== i))); const addOpt = touch((key) => { const c = BE_OPT_CATALOG.find(x => x.key === key); if (!c) return; setOptions(os => [...os, { name: c.key === "custom" ? "" : c.name, cost: c.cost, sale: c.sale }]); }); const save = () => { if (!window.DMS || !window.DMS.updateDeal) { window.k2bToast && window.k2bToast("Ledger offline.", "warn"); return; } if (posted) return; // Convention shared with the desk: GAP → gap*, every other product → vsc*. const gap = products.filter(p => p.cat === "GAP" || /gap/i.test(String(p.category || "") + " " + (p.name || ""))); const vsc = products.filter(p => gap.indexOf(p) < 0); const patch = { products: products.map(p => ({ ...p, category: p.category || p.cat, cost: beNum(p.cost), sale: beNum(p.sale), price: beNum(p.sale) })), hardAdds: options.map(o => ({ ...o, cost: beNum(o.cost), sale: beNum(o.sale) })), reserve: beNum(reserve), backendNotes: notes, vscSale: vsc.reduce((s, p) => s + beNum(p.sale), 0), vscCost: vsc.reduce((s, p) => s + beNum(p.cost), 0), gapSale: gap.reduce((s, p) => s + beNum(p.sale), 0), gapCost: gap.reduce((s, p) => s + beNum(p.cost), 0), fiGross: +(prodGross + beNum(reserve)).toFixed(2), }; const r = window.DMS.updateDeal(d.id, patch); setDirty(false); window.k2bToast && window.k2bToast( r && r.ok === false ? (r.msg || "Save failed.") : `Backend saved to ${d.id} — F&I gross ${be$(backendGross)}. Finance / Cash Deal pick it up.`, r && r.ok === false ? "warn" : "ok", 4200); }; const card = { background: "var(--jb-surface)", border: "1px solid var(--jb-border)", borderRadius: "var(--jb-r-lg)", padding: "12px 14px" }; const head = { fontSize: 11, textTransform: "uppercase", letterSpacing: "0.06em", color: "var(--jb-text-dim)", fontWeight: 700, marginBottom: 8 }; const th = { padding: "3px 8px", textAlign: "left", fontSize: 11, color: "var(--k2b-maroon)", borderBottom: "1px solid var(--jb-border)", whiteSpace: "nowrap" }; const td = { padding: "3px 8px", fontSize: 12, fontFamily: "var(--jb-mono)" }; const inp = { width: 90, font: "12px var(--jb-mono)", border: "1px solid var(--jb-border)", borderRadius: 3, padding: "2px 6px", textAlign: "right", background: "#FFFFE1" }; const moneyCell = (row, i, k, set) => ( set(i, k, e.target.value)} onBlur={(e) => { if (!posted) set(i, k, beNum(e.target.value).toFixed(2)); }} /> ); return (
{/* header */}
‹ Deal Pipeline
{d.id} — Backend ({d.buyer}) {(window.DMS && window.DMS.dealStage) ? window.DMS.dealStage(d) : (d.stage || "Desked")}
{d.vehicle || ""}{d.stock ? ` · Stk ${d.stock}` : ""}
INTERNAL — DEALER COST VISIBLE · NOT CUSTOMER FACING
{/* RC23 (corr): Unwind deal / Cancel product / Refund customer (the office) */} {window.DealCorrectButtons ? : null} {posted ? (window.DealAdjustButton ? : null) : {dirty ? "● Save Backend" : "Save Backend"} }
{posted && (
Posted {d.delivered ? new Date(d.delivered).toLocaleDateString() : ""}. This sheet is read-only — its figures are on the books. To correct it the office uses ↺ Unwind deal (every entry reversed; change the deal and post it again) or ✕ Cancel product (one product, the deal stays posted).
)} {posted && window.DealAdjustHistory ? : null} {window.DealCorrectionHistory ? : null} {/* products */}
Insurance & F&I Products {!posted && }
{products.length === 0 ?
No products on this deal yet — add from the catalog above.
: ( {products.map((p, i) => ( ))}
ProductProvider Dealer Cost Sale Price Gross
{p.name}{p.cancelled ? — {p.cancelNote || "cancelled"} : null} {p.provider} {moneyCell(p, i, "cost", setProd)} {moneyCell(p, i, "sale", setProd)} = 0 ? "var(--jb-green)" : "var(--k2b-maroon)" }}> {be$(beNum(p.sale) - beNum(p.cost))} {!posted && delProd(i)}>✕}
Products total {be$(products.reduce((s, p) => s + beNum(p.cost), 0))} {be$(products.reduce((s, p) => s + beNum(p.sale), 0))} {be$(prodGross)}
)}
{/* options + reserve/notes */}
Dealer-Installed Options / Accessories {!posted && }
{options.length === 0 ?
No dealer-installed options on this deal.
: ( {options.map((o, i) => ( ))}
Option Cost Sale Gross
setOpt(i, "name", e.target.value)} /> {moneyCell(o, i, "cost", setOpt)} {moneyCell(o, i, "sale", setOpt)} {be$(beNum(o.sale) - beNum(o.cost))} {!posted && delOpt(i)}>✕}
Options total {be$(options.reduce((s, o) => s + beNum(o.cost), 0))} {be$(options.reduce((s, o) => s + beNum(o.sale), 0))} {be$(optGross)}
)}
Options count toward front gross; products + reserve toward backend gross.
Reserve & Totals
Lender reserve (participation) { setReserve(e.target.value); setDirty(true); }} onBlur={(e) => { if (!posted) setReserve(beNum(e.target.value).toFixed(2)); }} />
Products gross {be$(prodGross)}
Reserve {be$(beNum(reserve))}
BACKEND GROSS (F&I) {be$(backendGross)}
Options gross (front) {be$(optGross)}
Internal Notes
); }; /* ═══ EDIT DEAL BACKEND — post-sale adjustments on a POSTED deal ═════════ One modal shared by Funding, Deal Pipeline recap, Accounting Office and the Backend cost sheet (window.DealAdjustModal / window.DealAdjustButton). Figures, products and extra charges are edited here; the live preview mirrors DMS.adjustPostedDeal line-for-line, and "Post Adjustment" calls it (one ADJ--n entry for the difference + an F&I income delta). */ const dadjR2 = (n) => +(+n || 0).toFixed(2); const dadjIsGap = (p) => /gap/i.test(String((p.category || p.cat || "") + " " + (p.name || ""))); const dadjNormProd = (p) => ({ ...p, category: p.category || p.cat || "", name: p.name || "", price: dadjR2(p.price != null ? p.price : p.sale != null ? p.sale : p.retail), cost: dadjR2(p.cost) }); // Chart of accounts for the charge picker — DMS chart first, the built-in chart as fallback (a store whose chart is built in only - RC35). const dadjChart = () => { let rows = []; try { const g = window.DMS && window.DMS.glAccounts ? window.DMS.glAccounts() : []; if (Array.isArray(g) && g.length) rows = g.map(a => ({ acct: String(a.acct || ""), name: a.name || a.desc || a.title || "" })); } catch (e) {} if (!rows.length && Array.isArray(window.K2B_CHART_BUILTIN) && !(window.K2BStore && !window.K2BStore.builtinChart())) rows = window.K2B_CHART_BUILTIN.map(r => Array.isArray(r) ? { acct: String(r[0]), name: String(r[1] || "") } : { acct: String(r.acct || ""), name: r.name || r.desc || "" }); const seen = {}; return rows.filter(r => r.acct && !seen[r.acct] && (seen[r.acct] = true)); }; const DADJ_ACCT_DEF = { cashOnHand: "201", cit: "205", arCustomer: "220", payoffPayable: "303", salesTaxPay: "324A", titlePay: "905D", docFee: "905A", saleNewCar: "400A", saleUsedCar: "446B", fiNew: "806", fiUsed: "808", vscPayable: "302", gapPayable: "302G" }; const dadjAcctMap = () => { const m = { ...DADJ_ACCT_DEF }; try { (window.DMS.getAcctMap() || []).forEach(r => { if (r && r.key && r.acct) m[r.key] = r.acct; }); } catch (e) {} return m; }; const dadjIsPosted = (d) => !!(d && d.postedToGL); const dadjIsCashDeal = (d) => /cash/i.test(String(d.dealType || "")) || d.stage === "Cash" || (!/lease/i.test(String(d.dealType || "")) && !(dadjR2(d.amtFinanced) > 0) && !d.lender); // Same arithmetic as DMS.adjustPostedDeal — what you see is what posts. const dadjPreview = (d, p) => { const R2 = dadjR2, A = dadjAcctMap(); const isLease = /lease/i.test(String(d.dealType || "")); const unit = (window.DMS && window.DMS._unit) ? window.DMS._unit(d.stock) : null; const isNew = d.type === "New"; const saleAcct = unit && unit.saleAcct ? unit.saleAcct : (isNew ? A.saleNewCar : A.saleUsedCar); const funded = /^funded/i.test(String(d.status || "")); const isCash = !isLease && !(R2(d.amtFinanced) > 0); const lines = []; let crTotal = 0; const credit = (field, acct, label) => { if (p[field] === undefined) return; const delta = R2(R2(p[field]) - R2(d[field])); if (!delta) return; lines.push({ acct, dr: delta < 0 ? -delta : 0, cr: delta > 0 ? delta : 0, desc: `${label} ${delta > 0 ? "+" : ""}${delta.toFixed(2)}` }); crTotal += delta; }; credit("salePrice", saleAcct, "Sale price"); credit("tax", A.salesTaxPay, "Sales tax"); credit("docFee", A.docFee, "Doc fee"); credit("titleFee", A.titlePay, "Title / license"); credit("vscSale", A.vscPayable, "Service contracts"); credit("gapSale", A.gapPayable, "GAP"); credit("tradePayoff", A.payoffPayable, "Trade payoff"); (Array.isArray(p.addCharges) ? p.addCharges.filter(c => c && R2(c.amount)) : []).forEach(c => { const a = R2(c.amount); lines.push({ acct: String(c.acct || A.docFee), dr: a < 0 ? -a : 0, cr: a > 0 ? a : 0, desc: "Charge: " + (c.desc || "additional charge") }); crTotal += a; }); let cashDelta = 0; if (p.cashDown !== undefined) { cashDelta = R2(R2(p.cashDown) - R2(d.cashDown)); if (cashDelta) lines.push({ acct: A.cashOnHand, dr: cashDelta > 0 ? cashDelta : 0, cr: cashDelta < 0 ? -cashDelta : 0, desc: `Cash down ${cashDelta > 0 ? "+" : ""}${cashDelta.toFixed(2)}` }); } const plug = R2(crTotal - cashDelta); let plugAcct = A.cit, plugLabel = "Contracts in transit", plugKind = "CIT"; if (isCash) { plugAcct = A.cashOnHand; plugLabel = "Cash due from customer"; plugKind = "Cash"; } else if (funded) { plugAcct = A.arCustomer; plugLabel = "Due from lender / customer (deal already funded)"; plugKind = "Customer A/R"; } if (plug) lines.push({ acct: plugAcct, dr: plug > 0 ? plug : 0, cr: plug < 0 ? -plug : 0, desc: plugLabel + ` ${plug > 0 ? "+" : ""}${plug.toFixed(2)}` }); const oldFi = R2(d.fiGross); const v = (k) => p[k] !== undefined ? R2(p[k]) : R2(d[k]); const newFi = R2(Math.max(0, v("vscSale") - v("vscCost")) + Math.max(0, v("gapSale") - v("gapCost"))); const fiTouched = ["vscSale", "vscCost", "gapSale", "gapCost"].some(k => p[k] !== undefined); const fiDelta = fiTouched ? R2(newFi - oldFi) : 0; const fiLines = fiDelta ? [ { acct: A.arCustomer, dr: fiDelta > 0 ? fiDelta : 0, cr: fiDelta < 0 ? -fiDelta : 0, desc: "F&I income adjustment" }, { acct: isNew ? A.fiNew : A.fiUsed, dr: fiDelta < 0 ? -fiDelta : 0, cr: fiDelta > 0 ? fiDelta : 0, desc: "F&I income " + (fiDelta > 0 ? "+" : "") + fiDelta.toFixed(2) }, ] : []; const n = (d.adjustments || []).length + 1; const jeId = "ADJ-" + String(d.id).slice(-5) + "-" + n; const newAmtFin = (plug && !isCash && !funded) ? R2(R2(d.amtFinanced) + plug) : R2(d.amtFinanced); return { lines, fiLines, plug, plugAcct, plugLabel, plugKind, isCash, funded, isLease, oldFi, newFi, fiDelta, jeId, newAmtFin, nothing: !lines.length && !fiDelta }; }; const DealAdjustModal = ({ deal: dealIn, onClose, onDone }) => { const D = window.DMS; const d = (D && D.deals ? D.deals().find(x => x.id === dealIn.id) : null) || dealIn; const R2 = dadjR2, f2 = (n) => R2(n).toFixed(2); const [f, setF] = React.useState(() => ({ salePrice: f2(d.salePrice), tax: f2(d.tax), docFee: f2(d.docFee), titleFee: f2(d.titleFee), vscSale: f2(d.vscSale), vscCost: f2(d.vscCost), gapSale: f2(d.gapSale), gapCost: f2(d.gapCost), tradePayoff: f2(d.tradePayoff), cashDown: f2(d.cashDown), lender: d.lender || "", apr: d.apr != null && d.apr !== "" ? String(d.apr) : "", term: d.term != null && d.term !== "" ? String(d.term) : "", payment: f2(d.payment), })); const [prods, setProds] = React.useState(() => (Array.isArray(d.products) ? d.products : []).map(dadjNormProd)); const [prodDirty, setProdDirty] = React.useState(false); const [charges, setCharges] = React.useState([]); const [reason, setReason] = React.useState(""); const [err, setErr] = React.useState(""); const chart = React.useMemo(dadjChart, []); const acctName = (a) => { const r = chart.find(x => x.acct === String(a)); return r ? r.name : ""; }; const defChargeAcct = chart.find(a => a.acct === "905") ? "905" : dadjAcctMap().docFee; const catalog = (window.jbProducts && window.jbProducts.list) ? window.jbProducts.list().filter(p => p.active !== false) : []; const prodMode = prods.length > 0 || prodDirty; const set = (k) => (e) => setF(s => ({ ...s, [k]: e.target.value })); // ── the patch DMS.adjustPostedDeal receives ── const patch = {}; ["salePrice", "tax", "docFee", "titleFee", "tradePayoff", "cashDown"].forEach(k => { const v = R2(beNum(f[k])); if (v !== R2(d[k])) patch[k] = v; }); if (prodDirty) { const list = prods.map(p => ({ ...p, price: R2(beNum(p.price)), sale: R2(beNum(p.price)), cost: R2(beNum(p.cost)) })); const gap = list.filter(dadjIsGap), vsc = list.filter(p => !dadjIsGap(p)); const sum = (l, k) => R2(l.reduce((t, p) => t + (+p[k] || 0), 0)); patch.products = list; patch.vscSale = sum(vsc, "price"); patch.vscCost = sum(vsc, "cost"); patch.gapSale = sum(gap, "price"); patch.gapCost = sum(gap, "cost"); } else if (!prodMode) { ["vscSale", "vscCost", "gapSale", "gapCost"].forEach(k => { const v = R2(beNum(f[k])); if (v !== R2(d[k])) patch[k] = v; }); } const terms = {}; if (String(f.lender).trim() !== String(d.lender || "")) terms.lender = String(f.lender).trim(); if (f.apr !== "" && R2(beNum(f.apr)) !== R2(d.apr)) terms.apr = R2(beNum(f.apr)); if (f.term !== "" && Math.round(beNum(f.term)) !== Math.round(+d.term || 0)) terms.term = Math.round(beNum(f.term)); if (R2(beNum(f.payment)) !== R2(d.payment)) terms.payment = R2(beNum(f.payment)); Object.assign(patch, terms); const goodCharges = charges.filter(c => R2(beNum(c.amount))).map(c => ({ desc: String(c.desc || "").trim() || "Additional charge", amount: R2(beNum(c.amount)), acct: c.acct || defChargeAcct })); if (goodCharges.length) patch.addCharges = goodCharges; const pv = dadjPreview(d, patch); const bal = (D && D.jeBalance) ? D.jeBalance(pv.lines) : { dr: 0, cr: 0, ok: false, diff: 0 }; const termsOnly = pv.nothing && Object.keys(terms).length > 0; const canPost = !!reason.trim() && (!pv.nothing || termsOnly); const post = async () => { setErr(""); if (!reason.trim()) { setErr("A reason is required — it goes on the audit trail."); return; } if (!D || !D.adjustPostedDeal) { setErr("Ledger offline."); return; } let r; if (d && d.postedToGL && !d.imported) { // RC33 (mpx): a POSTED deal is corrected by the SERVER in one step — the posting reversed, the figures changed, the deal posted again // (POST /api/deals/correct). The preview below is the server's own (nothing is written until you confirm). const pv = D.correctDealPreview ? await D.correctDealPreview(d.id, patch, reason.trim()) : null; if (!pv || pv.ok === false) { const m = (pv && (pv.error || pv.msg)) || "The server could not prepare the correction."; setErr(m); window.k2bToast && window.k2bToast(m, "warn", 7000); return; } const lines = [`Correct ${d.id}?`, "", `The current posting is reversed (${((pv.unwind && pv.unwind.journals) || []).length} entr${((pv.unwind && pv.unwind.journals) || []).length === 1 ? "y" : "ies"}) and the deal is posted again with: ${Object.keys(pv.changes || {}).join(", ")}.`].concat((pv.warnings || []).slice(0, 4)); if (!window.confirm(lines.join("\n"))) return; r = await D.adjustPostedDeal(d.id, patch, reason.trim()); } else if (termsOnly) { // Lender / rate / term / payment only on a deal that is not posted — no money moves: save the terms on the deal r = D.updateDeal(d.id, terms); if (r && r.ok !== false) { D.logAudit && D.logAudit("Deal Terms Edited", `${d.id} · ${reason.trim()} · ${Object.keys(terms).map(k => k + " " + (d[k] ?? "—") + "→" + terms[k]).join(", ")}`); r = { ok: true, msg: `${d.id} terms updated (no money changed — no journal entry).` }; } } else { r = await D.adjustPostedDeal(d.id, patch, reason.trim()); } if (!r || !r.ok) { setErr((r && r.msg) || "Adjustment failed."); window.k2bToast && window.k2bToast((r && r.msg) || "Adjustment failed.", "warn", 4200); return; } window.k2bToast && window.k2bToast(r.msg, "ok", 5200); onDone && onDone(r); onClose && onClose(); }; const lbl = { fontSize: 10.5, color: "var(--jb-text-dim)", fontWeight: 700, textTransform: "uppercase", letterSpacing: 0.3 }; const inp = { width: "100%", boxSizing: "border-box", font: "13px var(--jb-mono)", border: "1px solid #C8CCD2", borderRadius: 3, padding: "4px 6px", textAlign: "right", background: "#FFFFE1" }; const sec = { background: "#FFF", border: "1px solid var(--jb-border)", borderRadius: "var(--jb-r)", padding: "10px 12px" }; const secHd = { fontSize: 11, fontWeight: 700, textTransform: "uppercase", letterSpacing: 0.4, color: "var(--k2b-maroon)", marginBottom: 8, display: "flex", alignItems: "center", gap: 8 }; const th = { padding: "3px 6px", textAlign: "left", fontSize: 10.5, color: "var(--jb-text-dim)", borderBottom: "1px solid var(--jb-border)", whiteSpace: "nowrap", fontWeight: 700 }; const td = { padding: "3px 6px", fontSize: 12, fontFamily: "var(--jb-mono)", verticalAlign: "middle" }; const field = (k, label, opts = {}) => { const cur = opts.cur !== undefined ? opts.cur : d[k]; const changed = opts.text ? String(f[k]).trim() !== String(cur || "") : R2(beNum(f[k])) !== R2(cur); return ( ); }; const setProd = (i, k, val) => { setProds(ps => ps.map((p, j) => j === i ? { ...p, [k]: val } : p)); setProdDirty(true); }; const addProd = (id) => { const c = catalog.find(x => String(x.id) === String(id)); if (!c) return; setProds(ps => [...ps, { id: c.id, code: c.code || c.id, name: c.name || "", category: c.category || "", provider: c.provider || "", price: R2(c.retail != null ? c.retail : c.price), cost: R2(c.cost), termMo: +c.termMo || 0 }]); setProdDirty(true); }; const lineRows = (ls) => ls.map((l, i) => ( {l.acct} {acctName(l.acct) || "—"}
{l.desc}
{l.dr ? be$(l.dr) : ""} {l.cr ? be$(l.cr) : ""} )); return ( {err || (pv.nothing ? (termsOnly ? "Terms only — saved on the deal, no journal entry." : "Change a figure, product or charge to preview the adjusting entry.") : `Posts ${pv.jeId}${pv.fiDelta ? " + " + pv.jeId + "F" : ""} to the GL.`)} Cancel }>
{/* ── left: what can change ── */}
POSTED {d.delivered ? new Date(d.delivered).toLocaleDateString() : ""} · {pv.isCash ? "CASH DEAL" : pv.isLease ? "LEASE" : "FINANCED"}{pv.funded ? " · ALREADY FUNDED" : ""} · changes post an adjusting entry — nothing is re-posted
{d.postedToGL && !d.imported && (
This deal is posted — its figures are on the books and can't be adjusted here (the preview below is for information). To correct it, close this and use ↺ Unwind deal (every entry reversed — change the deal and post it again) or ✕ Cancel product.
)}
Deal figures
{field("salePrice", "Sale price")}{field("tax", "Sales tax")}{field("docFee", "Doc fee")}{field("titleFee", "Title / license")} {field("tradePayoff", "Trade payoff")}{field("cashDown", pv.isCash ? "Cash collected" : "Cash down")}
Service contract & GAP{prodMode ? — totals come from the product list below : null}
{prodMode ? (() => { const gap = prods.filter(dadjIsGap), vsc = prods.filter(p => !dadjIsGap(p)); const s = (l, k) => l.reduce((t, p) => t + beNum(p[k]), 0); return [["Service contract price", s(vsc, "price"), d.vscSale], ["Service contract cost", s(vsc, "cost"), d.vscCost], ["GAP price", s(gap, "price"), d.gapSale], ["GAP cost", s(gap, "cost"), d.gapCost]].map(([l, v, was]) => (
{l}
{be$(v)}
was {be$(was)}
)); })() : <>{field("vscSale", "Service contract price")}{field("vscCost", "Service contract cost")}{field("gapSale", "GAP price")}{field("gapCost", "GAP cost")}}
Products on the deal
{prods.length === 0 ?
No products on this deal.{catalog.length ? "" : " (The F&I Products catalog is empty.)"}
: ( {prods.map((p, i) => ( ))}
ProductTypePriceCost
{p.name || p.code || "—"} {dadjIsGap(p) ? "GAP" : (p.category || "VSC")} setProd(i, "price", e.target.value)} /> setProd(i, "cost", e.target.value)} /> { setProds(ps => ps.filter((_, j) => j !== i)); setProdDirty(true); }}>✕
)}
Additional charges
{charges.length === 0 ?
Nothing added. Use “+ Add charge” for a fee or add-on missed at delivery.
: ( {charges.map((c, i) => ( ))}
DescriptionAmountGL account
setCharges(cs => cs.map((x, j) => j === i ? { ...x, desc: e.target.value } : x))} /> setCharges(cs => cs.map((x, j) => j === i ? { ...x, amount: e.target.value.replace(/[^0-9.\-]/g, "") } : x))} /> setCharges(cs => cs.filter((_, j) => j !== i))}>✕
)}
{!pv.isCash && (
Lender & terms
{field("lender", "Lender", { text: true })}{field("apr", "APR %", { plain: "%" })}{field("term", "Term (mo)", { plain: "mo" })}{field("payment", "Payment")}
)}
{/* ── right: live preview of the adjusting entry ── */}
Adjusting entry preview
{pv.lines.length ? pv.jeId : "—"}
{pv.lines.length === 0 ?
No money changes yet.
: ( {lineRows(pv.lines)}
AcctName / lineDebitCredit
{bal.ok ? ✓ Balanced : Out of balance {be$(Math.abs(bal.diff))}} {be$(bal.dr)} {be$(bal.cr)}
)} {pv.fiLines.length > 0 && ( <>
F&I income entry
{pv.jeId}F
{lineRows(pv.fiLines)}
F&I gross {be$(pv.oldFi)} → {be$(pv.newFi)}
)}
Where the difference lands
{pv.plug ? (
{pv.plugKind} ({pv.plugAcct}) {pv.plug > 0 ? "up" : "down"} {be$(Math.abs(pv.plug))}
{pv.plugLabel}{pv.isCash ? " — collect it from the customer." : pv.funded ? " — the lender has already paid; bill the difference." : " — the lender's check changes by this amount."}
) :
{pv.lines.length ? "Nets to zero — no change to what is owed." : "—"}
}
Amount financed {pv.isCash ? "— (cash deal)" : <>{be$(d.amtFinanced)} → {be$(pv.newAmtFin)}}
Reason *