onPick(p)} title="Click to add this product to the deal"
style={{ background: current && current === p.id ? "#FFF7DF" : (i % 2 ? "var(--jb-surface-2, #F7F8FA)" : "#fff"), cursor: "pointer" }}>
{p.code || p.id}
{p.name}{current && current === p.id ? ON DEAL : null}
{p.provider}
{p.termMo ? p.termMo + " mo" : "—"}
{money(p.retail)}
{money(p.cost)}
))}
);
})}
)}
GAP products fill the GAP row; everything else lands in the first empty plan slot (up to {FW_MAX_PLANS}). Prices are editable on the row after adding.
);
};
/* ── Plan table — Plan 1..N rows (code · product · term · price · cost) + the
GAP row + the computed Warranties total. formId-driven so the Finance Deal
and the Cash Deal render the same block over their own forms. Boxes are
real widths: code 92, product grows (min 230), price 114, cost 96. ── */
const FwPlanTable = ({ formId, live, tick, openPicker, removePlan, removeGap, totalLabel, onPriceChange, accent = "#7C3AED" }) => {
const planTotal = fwPlanTotal(live);
const planCount = fwPlanCount(live);
const slots = []; for (let n = 1; n <= FW_MAX_PLANS; n++) if (n <= 3 || !fwSlotEmpty(live, n)) slots.push(n);
const planCat = (n) => { const c = fwCatalogFind(live[`plan${n}Id`], live[`plan${n}Code`]); return c ? c.category : "Service Contract"; };
let costTotal = 0; for (let n = 1; n <= FW_MAX_PLANS; n++) costTotal += fwNum(live[`plan${n}Cost`]);
const F = (p) => ;
return (
PROTECTION PRODUCTS{planCount ? `${planCount} plan${planCount === 1 ? "" : "s"} · $${fwFmt(planTotal)}` : "no service plans yet"}{live.gapCode ? ` · GAP${/^gap$/i.test(live.gapCode) ? "" : " " + live.gapCode} $${fwFmt(fwNum(live.gapIns))}` : ""} — from the desked option; edit or add here. openPicker({})}>Details openPicker({})}>+ Add product
Lease: the engine capitalizes negative equity into the gross cap cost and applies positive equity as cap reduction — see F5-Recalculate.
: Math.abs(amt - n("amountFinanced")) > 0.01 ?
Worksheet shows ${fwFmt(n("amountFinanced"))} — press F5-Recalculate to refresh it from these figures.
: null}
);
};
const FinanceWorksheet = ({ onClose, go }) => {
const [, force] = React.useReducer(n => n + 1, 0);
React.useEffect(() => (window.DMS && window.DMS.subscribe) ? window.DMS.subscribe(force) : undefined, []);
const [dealId, setDealId] = React.useState(() => {
try { return localStorage.getItem("jb_fw_deal") || null; } catch (e) { return null; }
});
const [showLookup, setShowLookup] = React.useState(false);
const [esignOpen, setEsignOpen] = React.useState(false);
const [formKey, setFormKey] = React.useState(0); // bump to remount fields after programmatic save
const [, tick] = React.useReducer(n => n + 1, 0); // live read-outs (neg equity, plan total) as the user types
const [picker, setPicker] = React.useState(null); // { slot?, category?, current? } → product picker
const [breakdown, setBreakdown] = React.useState(null); // "Cash Price" | "Deposit" → breakdown modal
const deals = (window.DMS && window.DMS.deals) ? window.DMS.deals() : [];
const deal = deals.find(d => d.id === dealId) || null;
// Lease vs retail — the deal record wins, then the form draft (the desk's
// F9-Send writes dealType:"Lease" into both).
const formDraft = window.loadJbForm ? window.loadJbForm(FW_FORM) : {};
const isLease = /lease/i.test(String((deal && (deal.dealType || deal.mode)) || formDraft.dealType || ""));
// RC27 (dsk): the deal's cost lines (desk › Profit panel): a lender fee PASSED TO THE CUSTOMER is the customer's fee line (in the amount
// financed); the dealer's DOWN PAYMENT ASSISTANCE lowers the customer's cash due (the deposit stays the whole down payment)
const fwCa = deal && window.DealCalc ? window.DealCalc.costAddsOf(deal) : null;
const fwFeeCust = fwCa && !isLease ? fwCa.feeCustCents / 100 : 0;
const fwDpa = fwCa ? fwCa.dpaCents / 100 : 0;
// Write a deal's figures into the worksheet form (single source of truth = the
// DMS deal record, which Desking's push patched with trade/rebate/down/lender/
// products). Stamps _srcDealId/_srcRev so a remount can tell "already holding
// this revision" from "a newer revision arrived" (B-04).
const writeForm = (d) => {
const lease = /lease/i.test(String(d.dealType || d.mode || ""));
// Products: the itemized list when the desk sent one; a legacy deal that only
// carries the lumped vscSale gets a single "SERVICE CONTRACT" plan row.
let products = Array.isArray(d.products) ? d.products : [];
if (!products.length && (+d.vscSale > 0 || +d.gapSale > 0)) {
products = [];
if (+d.vscSale > 0) products.push({ id: "P-VSC-LEGACY", code: "VSC", name: "SERVICE CONTRACT (desk)", category: "Service Contract", provider: "", price: +d.vscSale, cost: +d.vscCost || 0, termMo: 0 });
if (+d.gapSale > 0) products.push({ id: "", code: "GAP", name: "GAP", category: "GAP", provider: "", price: +d.gapSale, cost: +d.gapCost || 0, termMo: 0 });
}
window.saveJbForm(FW_FORM, {
_srcDealId: d.id, _srcRev: +d.rev || 0,
dealType: lease ? "Lease" : (d.dealType || "Retail"),
cashPrice: fwFmt(d.salePrice), tradeValue: fwFmt(d.tradeAllow || 0),
tradePayoff: fwFmt(d.tradePayoff || 0), tradeAcv: fwFmt(d.tradeACV != null ? d.tradeACV : (d.tradeAcv != null ? d.tradeAcv : d.tradeAllow || 0)),
tradeDesc: d.tradeDesc || d.tradeYrMkMdl || d.tradeVeh || "", tradeLender: d.tradeLender || "",
negEquity: fwFmt(Math.max(0, (+d.tradePayoff || 0) - (+d.tradeAllow || 0))),
rebates: fwFmt(d.rebate || 0),
// Down = the deal's down payment only. K2B-003 (RC18): never a cash deal's old "deposit" figure; on a cash deal
// only the payments actually recorded count as money down.
deposit: fwFmt(window.DMS && window.DMS.isCashDeal && window.DMS.isCashDeal(d) ? window.DMS.cashDealFigures(d).received : (d.cashDown || 0)), salesTax: fwFmt(d.tax || 0),
license: fwFmt(d.titleFee || 0), docFee: fwFmt(d.docFee || 0),
...fwPlanFieldsFromProducts(products),
otherIns: d.otherSale != null ? fwFmt(d.otherSale) : "0.00",
amountFinanced: fwFmt(d.amtFinanced || 0),
apr: (fwDealAprSet(d) ? +d.apr : (lease ? 0 : fwDefaultApr())).toFixed(2), term: String(d.term || (lease ? 36 : 72)),
payment: fwFmt(d.payment || 0), lienName: d.lender || "GM FINANCIAL",
...(lease ? {
moneyFactor: d.moneyFactor != null ? (+d.moneyFactor).toFixed(5) : undefined,
residual: d.residual != null ? fwFmt(d.residual) : undefined,
residualPct: d.residualPct != null ? (+d.residualPct).toFixed(2) : undefined,
milesPerYr: d.milesPerYr != null ? String(d.milesPerYr) : undefined,
dueAtSigning: d.dueAtSigning != null ? fwFmt(d.dueAtSigning) : undefined,
leaseTaxRate: d.leaseTaxRate != null ? String(d.leaseTaxRate) : undefined,
taxMethod: d.leaseTaxMethod || undefined,
totalOfPayments: d.totalOfPayments != null ? fwFmt(d.totalOfPayments) : undefined,
} : {}),
});
// T-03: the figures just mirrored are the deal's own — current for these inputs only when the deal carries real terms.
fwStampCalc(fwDealHasTerms(d));
};
// Mirror the linked deal ONLY when the form does not already hold this deal
// at this revision (open from Desking F9, F2-Lookup, Deal Review). A mere
// remount of the route never overwrites F&I's edited APR/term/deposit/lender
// (B-04); a newer desk push (rev bump) does refresh the figures.
React.useEffect(() => {
if (!deal) return;
const f = window.loadJbForm(FW_FORM);
const held = f._srcDealId || null, heldRev = f._srcRev != null ? +f._srcRev : null;
if (held === deal.id && heldRev === (+deal.rev || 0)) {
// T-03: figures that arrived from outside for this revision (a desk push) count as calculated only when they are
// exactly the deal's; otherwise Save / Post / Print recalculate first.
if (f._calcFor !== deal.id + "@" + heldRev) fwStampCalc(fwDealHasTerms(deal) && fwFormMatchesDeal(f, deal));
return;
}
writeForm(deal);
setFormKey(k => k + 1);
}, [deal && deal.id, deal && deal.rev]);
// `warranties` is COMPUTED from the plan rows — keep it in step on every mount
// / remount (a desk push, an F2 load, a picker add / remove all remount).
React.useEffect(() => {
const f = window.loadJbForm(FW_FORM);
const w = fwFmt(fwPlanTotal(f));
if (w !== String(f.warranties || "")) fwSyncWarranties(FW_FORM);
}, [formKey]);
// Load a deal's figures into the worksheet form (F2-Lookup picker — an
// explicit user action, so it always overwrites).
const loadDeal = (d) => {
writeForm(d);
setDealId(d.id);
try { localStorage.setItem("jb_fw_deal", d.id); } catch (e) {}
setFormKey(k => k + 1); setShowLookup(false);
window.k2bToast && window.k2bToast(`${d.id} — ${d.buyer} loaded into worksheet.`, "ok");
};
// Build the lease-engine input from the form draft (the desk saved every
// jbLeaseCalc input on F9-Send; F&I can edit any of them here).
const leaseArgsFromForm = (f) => ({
msrp: fwNum(f.msrp) || fwNum(f.cashPrice), sellingPrice: fwNum(f.cashPrice),
term: Math.max(1, Math.round(fwNum(f.term)) || 36), milesPerYr: fwNum(f.milesPerYr) || 12000,
residualPct: fwNum(f.residualPct) || (fwNum(f.residual) && (fwNum(f.msrp) || fwNum(f.cashPrice)) ? fwNum(f.residual) / (fwNum(f.msrp) || fwNum(f.cashPrice)) * 100 : 0),
moneyFactor: f.moneyFactor != null && f.moneyFactor !== "" ? fwNum(f.moneyFactor) : undefined,
apr: f.moneyFactor != null && f.moneyFactor !== "" ? undefined : fwNum(f.apr),
acqFee: fwNum(f.acqFee), acqFeeCapitalized: f.acqFeeCapped != null ? !!f.acqFeeCapped : true,
// Deposit on a lease = cash cap reduction (cash down + additional CCR).
capReductionCash: f.singlePay ? 0 : fwNum(f.deposit),
rebates: f.leaseRebates != null && f.leaseRebates !== "" ? fwNum(f.leaseRebates) : fwNum(f.rebates),
tradeAllowance: fwNum(f.tradeValue), tradePayoff: fwNum(f.tradePayoff),
docFee: fwNum(f.docFee), docFeeCapitalized: f.docFeeCapped != null ? !!f.docFeeCapped : true,
titleLicenseFee: fwNum(f.titleLicFee) || fwNum(f.license), titleLicenseCapitalized: f.titleLicCapped != null ? !!f.titleLicCapped : true,
addOns: fwPlanTotal(f) + fwNum(f.gapIns) + fwNum(f.otherIns),
taxMethod: f.taxMethod || "monthly", taxRate: fwNum(f.leaseTaxRate),
securityDeposit: fwNum(f.securityDeposit), dispositionFee: fwNum(f.dispFee),
excessMileageRate: fwNum(f.excessRate), purchaseOptionFee: fwNum(f.purchOptFee),
extraMilesPurchased: fwNum(f.extraMiles), extraMileageRate: f.extraMileageRate != null && f.extraMileageRate !== "" ? fwNum(f.extraMileageRate) : undefined,
inceptionMiles: fwNum(f.inceptionMiles), inceptionMileageRate: f.inceptionRate != null && f.inceptionRate !== "" ? fwNum(f.inceptionRate) : undefined,
...(f.inceptionAdjust === "1" || f.inceptionAdjust === "0" ? { inceptionMileageAdjust: f.inceptionAdjust === "1" } : {}),
singlePay: !!f.singlePay,
});
// ── T-03 (RC18): Save / Post / Print / RouteOne always work from the inputs on screen NOW. _calcSig is the fingerprint
// of the inputs AND the amount financed / payment as last calculated (F5, the deal mirror or a matching desk push); any
// difference — a price, down, trade, product or rate typed without F5, or a figure typed straight into the amount
// financed / payment boxes — recalculates first, so an old payment or amount financed is never stored or printed.
const fwCalcSig = (f) => JSON.stringify({ lease: isLease,
i: isLease ? leaseArgsFromForm(f)
: [fwNum(f.cashPrice), fwNum(f.tradeValue), fwNum(f.tradePayoff), fwNum(f.rebates), fwNum(f.deposit), fwNum(f.salesTax), fwNum(f.license),
fwNum(f.docFee), fwPlanTotal(f), fwNum(f.gapIns), fwNum(f.otherIns), fwHasNum(f.apr) ? fwNum(f.apr) : null, Math.round(fwNum(f.term)), fwFeeCust],
o: [fwR2(fwNum(f.amountFinanced)), fwR2(fwNum(f.payment))] });
// current = true → the form's figures are calculated for its inputs; false → the next Save / Post / Print recalculates.
function fwStampCalc(current) {
const f = window.loadJbForm(FW_FORM);
window.saveJbForm(FW_FORM, { _calcSig: current ? fwCalcSig(f) : "", _calcFor: (f._srcDealId || "") + "@" + (f._srcRev != null ? +f._srcRev : "") });
}
function fwDealHasTerms(d) { return /lease/i.test(String(d.dealType || d.mode || "")) ? (+d.payment || 0) > 0 : (+d.term || 0) > 0 && ((+d.payment || 0) !== 0 || (+d.amtFinanced || 0) !== 0); }
function fwFormMatchesDeal(f, d) {
const eq = (a, b) => Math.abs(fwR2(a) - fwR2(+b || 0)) < 0.006;
return eq(fwNum(f.cashPrice), d.salePrice) && eq(fwNum(f.tradeValue), d.tradeAllow) && eq(fwNum(f.tradePayoff), d.tradePayoff) && eq(fwNum(f.rebates), d.rebate)
&& eq(fwNum(f.deposit), d.cashDown) && eq(fwNum(f.salesTax), d.tax) && eq(fwNum(f.license), d.titleFee) && eq(fwNum(f.docFee), d.docFee)
&& eq(fwPlanTotal(f), d.vscSale) && eq(fwNum(f.gapIns), d.gapSale) && eq(fwNum(f.amountFinanced), d.amtFinanced) && eq(fwNum(f.payment), d.payment)
&& Math.round(fwNum(f.term)) === Math.round(+d.term || 0) && eq(fwNum(f.apr), d.apr);
}
// spec 6.3 valid-input gate (same wording style as the desk's): a negative amount financed / cap cost is refused, inputs kept.
const fwNegMsg = (over, what) => `Down payment too high — the down payment, trade equity and rebates are $${fwFmt(over)} more than the whole ${isLease ? "cap cost" : "balance"} — lower the down payment.${what ? " " + what + "; your entries are kept." : " Your entries are kept."}`;
// Compute amount financed, payment and finance charge from the form AS IT IS (pure: returns the form patch, writes nothing).
// LEASE deals go through the closed-end lease engine (LeaseCalc.jsx.txt — window.jbLeaseCalc), never retail amortization (B-03).
const fwCompute = (f, what) => {
const negEq = Math.max(0, fwNum(f.tradePayoff) - fwNum(f.tradeValue));
if (isLease) {
if (!window.jbLeaseCalc) return { ok: false, msg: "Lease engine not loaded (LeaseCalc)." };
const q = window.jbLeaseCalc(leaseArgsFromForm(f));
const pay = q.singlePay ? q.singlePayAmount : q.totalMonthly;
if (q.adjustedCapCost < -0.005) return { ok: false, neg: true, msg: fwNegMsg(-q.adjustedCapCost, what) };
if (pay < -0.005) return { ok: false, neg: true, msg: `Lease payment would be below $0.00 ($${fwFmt(pay)}) — lower the cap cost reduction.${what ? " " + what + "; your entries are kept." : " Your entries are kept."}` };
return { ok: true, warn: q.upsideDown, patch: {
dealType: "Lease", negEquity: fwFmt(negEq),
amountFinanced: fwFmt(q.adjustedCapCost), // adjusted cap cost
payment: fwFmt(q.singlePay ? q.singlePayAmount : q.totalMonthly), // monthly incl. tax
basePayment: fwFmt(q.baseMonthly), monthlyTax: fwFmt(q.monthlyTax),
financeCharge: fwFmt(q.rentCharge * q.term), // total rent charge
rentCharge: fwFmt(q.rentCharge), residual: fwFmt(q.residualValue), residualPct: q.residualPct.toFixed(2),
moneyFactor: q.effectiveMoneyFactor.toFixed(5), apr: q.aprEquivalent.toFixed(2), term: String(q.term),
salesTax: fwFmt(q.taxMethod === "monthly" || !q.upfrontTax ? q.monthlyTax : q.upfrontTax),
dueAtSigning: fwFmt(q.dueAtSigningItems.total), totalOfPayments: fwFmt(q.totalOfPayments),
leaseUpfrontTax: fwFmt(q.taxItemization.estimatedUpfrontTotal), grossCapCost: fwFmt(q.grossCapCost),
}, toast: `Lease recalculated — $${fwFmt(q.totalMonthly)}/mo incl. tax · ${q.term}mo · MF ${q.effectiveMoneyFactor.toFixed(5)} · residual $${fwFmt(q.residualValue)}${q.upsideDown ? " · ⚠ adj. cap below residual" : ""}.` };
}
const amt = fwNum(f.cashPrice) - fwNum(f.tradeValue) + fwNum(f.tradePayoff)
- fwNum(f.rebates) - fwNum(f.deposit)
+ fwNum(f.salesTax) + fwNum(f.license) + fwNum(f.docFee)
+ fwPlanTotal(f) + fwNum(f.gapIns) + fwNum(f.otherIns) + fwFeeCust; // RC27 (dsk): + a lender fee passed to the customer
if (fwR2(amt) < 0) return { ok: false, neg: true, msg: fwNegMsg(-fwR2(amt), what) };
const aprPct = fwHasNum(f.apr) ? fwNum(f.apr) : fwDefaultApr(); // blank → the ONE default; an entered 0.00 stays 0% (K2B-005)
const n = Math.max(1, Math.round(fwNum(f.term)) || 72);
const r = aprPct / 100 / 12;
const pmt = r === 0 ? amt / n : (amt * r) / (1 - Math.pow(1 + r, -n));
return { ok: true, patch: { amountFinanced: fwFmt(amt), payment: fwFmt(pmt), negEquity: fwFmt(negEq),
financeCharge: fwFmt(pmt * n - amt), apr: aprPct.toFixed(2), term: String(n) },
toast: `Recalculated — $${fwFmt(pmt)} / ${n}mo @ ${aprPct.toFixed(2)}%${negEq > 0 ? " · incl. $" + fwFmt(negEq) + " negative equity" : ""}.` };
};
// F5-Recalculate.
const recalc = () => {
fwSyncWarranties(FW_FORM);
const c = fwCompute(window.loadJbForm(FW_FORM));
if (!c.ok) { window.k2bToast && window.k2bToast(c.msg, "warn", 6500); return; } // nothing written — the entries stay as typed
window.saveJbForm(FW_FORM, c.patch);
fwStampCalc(true);
setFormKey(k => k + 1);
window.k2bToast && window.k2bToast(c.toast, c.warn ? "warn" : "ok", isLease ? 5000 : undefined);
};
// Save / Post / Print / RouteOne: recalculate first when anything changed since the figures were calculated.
const ensureCalc = (what) => {
fwSyncWarranties(FW_FORM);
const f = window.loadJbForm(FW_FORM);
if (f._calcSig && f._calcSig === fwCalcSig(f)) return { ok: true, recalculated: false };
const c = fwCompute(f, what);
if (!c.ok) return { ok: false, msg: c.msg };
window.saveJbForm(FW_FORM, c.patch);
fwStampCalc(true);
setFormKey(k => k + 1);
return { ok: true, recalculated: true, toast: c.toast };
};
// The deal patch every save/post writes: the worksheet's edited figures,
// the protection products (itemized `products` + vscSale/gapSale for the GL,
// B-07), the trade lender / neg equity, and on a lease the lease structure
// postDeal books (B-03).
const dealPatchFromForm = (f) => ({
salePrice: fwNum(f.cashPrice), tradeAllow: fwNum(f.tradeValue),
tradePayoff: fwNum(f.tradePayoff), rebate: fwNum(f.rebates),
...(f.tradeAcv != null && f.tradeAcv !== "" ? { tradeACV: fwNum(f.tradeAcv), tradeAcv: fwNum(f.tradeAcv) } : {}),
...(f.tradeDesc != null ? { tradeDesc: String(f.tradeDesc || "") } : {}),
tradeLender: String(f.tradeLender || ""), negEquity: Math.max(0, fwR2(fwNum(f.tradePayoff) - fwNum(f.tradeValue))),
cashDown: fwNum(f.deposit), tax: fwNum(f.salesTax),
titleFee: fwNum(f.license), docFee: fwNum(f.docFee),
...fwProductPatch(f), otherSale: fwNum(f.otherIns),
amtFinanced: fwNum(f.amountFinanced), apr: fwNum(f.apr),
term: Math.round(fwNum(f.term)), payment: fwNum(f.payment),
lender: f.lienName || (deal && deal.lender) || "",
dealType: isLease ? "Lease" : (f.dealType || (deal && deal.dealType) || "Retail"),
...(isLease ? {
moneyFactor: fwNum(f.moneyFactor), residual: fwNum(f.residual), residualPct: fwNum(f.residualPct),
milesPerYr: fwNum(f.milesPerYr), dueAtSigning: fwNum(f.dueAtSigning),
leaseUpfrontTax: fwNum(f.leaseUpfrontTax), leaseTaxMethod: f.taxMethod || "monthly",
leaseTaxRate: fwNum(f.leaseTaxRate), totalOfPayments: fwNum(f.totalOfPayments),
} : {}),
});
// Write the worksheet to the linked deal + re-stamp the revision (B-04).
// Shared by F10-Save, F9-Post and Send to RouteOne. Returns the update result.
const writeDeal = (what) => {
const c = ensureCalc(what || "Nothing was saved"); // T-03: never store the old payment / amount financed
if (!c.ok) return { ok: false, refused: true, msg: c.msg };
const f = window.loadJbForm(FW_FORM);
const r = window.DMS.updateDeal(deal.id, dealPatchFromForm(f));
if (r && r.ok) { window.saveJbForm(FW_FORM, { _srcDealId: deal.id, _srcRev: r.rev }); fwStampCalc(true); r.recalculated = c.recalculated; }
return r;
};
// F10-Save — actually persists: the form draft is already written field-by-
// field; this patches the linked deal with the edited figures and re-stamps
// the form with the new revision so the next mount does not re-mirror (B-04).
const saveNow = () => {
if (!deal || !window.DMS || !window.DMS.updateDeal) {
window.k2bToast && window.k2bToast(FW_NO_DEAL, deal ? "warn" : "info", 4200);
return;
}
// RC21 (deals): a POSTED deal's figures are on the books — F10 never changes them (the server refuses it too)
if (deal.postedToGL && !deal.imported) {
window.k2bToast && window.k2bToast(`${deal.id} is posted — its figures are on the books and can't be changed here. Correcting a posted deal (reverse and re-post) is not part of this release; ask the office. Nothing was saved; your entries are kept on the screen.`, "warn", 7000);
return;
}
const r = writeDeal();
if (r && r.ok) {
const f = window.loadJbForm(FW_FORM);
window.k2bToast && window.k2bToast(`${deal.id} saved${r.recalculated ? ` — recalculated from the figures on screen first: $${f.payment || "0.00"} / ${f.term}mo @ ${f.apr}% on $${f.amountFinanced || "0.00"}` : ""} — worksheet figures written to the deal${window.DMS.isOnline && window.DMS.isOnline() ? " and the server" : ""}.`, "ok", r.recalculated ? 5200 : 3600);
} else window.k2bToast && window.k2bToast((r && r.msg) || "Save failed.", "warn", r && r.refused ? 6500 : undefined);
};
// Patch the linked deal with the worksheet figures and post to accounting
// through the ONE shared gate (window.jbPostDeal: compliance, truth box,
// open items, stage checks — one confirm, logged override). Never
// DMS.postDeal directly.
const postToGL = async () => {
if (!window.DMS || !window.jbPostDeal) { window.k2bToast && window.k2bToast("Ledger offline.", "warn"); return; }
if (!deal) { window.k2bToast && window.k2bToast(FW_NO_DEAL, "warn", 4200); return; }
const u = writeDeal("Nothing was posted");
if (!(u && u.ok)) { window.k2bToast && window.k2bToast((u && u.msg) || "Could not write the worksheet to the deal — nothing was posted.", "warn", 6500); return; }
const f = window.loadJbForm(FW_FORM);
const r = await window.jbPostDeal(deal.id); // RC21: the server posts (or refuses with the reason — nothing changed)
if (r && r.ok) {
// Posting bumps the deal revision — stamp the form so the mount effect
// does not re-mirror over the worksheet (B-04).
const posted = (window.DMS.deals() || []).find(d => d.id === deal.id);
window.saveJbForm(FW_FORM, { _srcDealId: deal.id, _srcRev: posted ? (+posted.rev || 0) : (u && u.rev) });
fwStampCalc(true);
}
// Ease of flow: a booked deal goes straight to the print packet — the
// Report Selection dialog opens pre-checked from the forms-sequencing rules.
// A lease resolves as "Lease" (lease forms in, retail RIC / 553 prep out) — B-03.
if (r && r.ok) setTimeout(() => {
const posted = (window.DMS.deals() || []).find(d => d.id === deal.id) || deal;
window.k2bPrintSelectDeal && window.k2bPrintSelectDeal({
dealType: isLease ? "Lease" : (/^n|new/i.test(String(posted.type || posted.newUsed || "")) ? "New" : "Used"),
newUsed: posted.type || "",
bank: f.lienName || deal.lender || "", state: "" });
}, 700);
};
// Send to RouteOne — the package is built from the DEAL record, so the
// worksheet (products, trade lender, neg equity, lender/APR/term) is written
// to it first, exactly like F10-Save.
const sendRouteOne = () => {
if (!deal) { window.k2bToast && window.k2bToast("Load a deal first (F2-Lookup).", "warn"); return; }
if (window.DMS && window.DMS.updateDeal) { const u = writeDeal("Nothing was sent"); if (!(u && u.ok)) { window.k2bToast && window.k2bToast((u && u.msg) || "Could not write the worksheet to the deal.", "warn"); return; } }
const r = window.DMS.sendToRouteOne(deal.id);
window.k2bToast && window.k2bToast(r.msg, r.ok ? "ok" : "warn", 4600);
};
// Product picker actions
const openPicker = (opts) => setPicker(opts || {});
const onPickProduct = (p) => {
const r = fwPlacePlan(FW_FORM, p, picker && picker.slot);
setPicker(null);
if (!r.ok) { window.k2bToast && window.k2bToast(r.msg, "warn", 3600); return; }
setFormKey(k => k + 1);
window.k2bToast && window.k2bToast(`${p.code || p.id} — ${p.name} ${r.slot === "gap" ? "set as GAP" : "added as plan " + r.slot} · $${fwFmt(p.retail)}. Press F5 to refresh the payment.`, "ok", 3600);
};
const removePlan = (n) => { fwRemovePlan(FW_FORM, n); setFormKey(k => k + 1); };
const removeGap = () => { fwRemoveGap(FW_FORM); setFormKey(k => k + 1); };
const onPlanPriceChange = () => { fwSyncWarranties(FW_FORM); tick(); };
// Live read-outs off the persisted draft (re-read on every render — the
// JBFields persist per keystroke and call tick()).
const live = window.loadJbForm ? window.loadJbForm(FW_FORM) : {};
const negEquityLive = Math.max(0, fwR2(fwNum(live.tradePayoff) - fwNum(live.tradeValue)));
const planTotalLive = fwPlanTotal(live);
const planCount = fwPlanCount(live);
return (
// R-4: never a fixed 490px box — the window grows to its content and the
// form body scrolls inside the viewport, so every field and F10-Save are
// reachable at 1366×768 as well as 1600×1000.
{/* Purpose banner — distinguishes this from the Sales Desk */}
F&I OFFICEFinalize & post. Confirm lender, APR & term, add the insurance & service-plan products, then F9-Post to GL books the deal. Dimmed figures carried from the Sales Desk — edit only to override.
{/* Body = [scrolling form | fixed F-key column]. Overlap fix: the F-key
column is OUTSIDE the scroll area and never shrinks, so no field can
slide under it; the form's two columns wrap (Lender/Payment drops below
Pricing) when the window is narrow (1366 @125 %, pop-outs). */}
{/* LEFT COLUMN — Pricing */}
{"Cash Price.....: "}desk setBreakdown("Cash Price")} title="How the amount financed is built from these figures">Details
{/* PROTECTION PRODUCTS — full-width plan table (was three 20-px "Svc Plan" boxes).
Rows come from the desk's chosen option; Details / + Add product open the
real catalog picker; prices edit in place and roll into `warranties`. */}
{/* RIGHT COLUMN — Action buttons stacked (own column; scrolls on short screens) */}
ESC-Finished alert(FW_HELP)}>F1-Help setShowLookup(true)}>F2-Lookup {
go && go("crm");
window.k2bToast && window.k2bToast("Deals start in the CRM (+ New Lead → Desk It)", "info", 3500);
}}>F3-CRM openPicker({})} title="Add a protection product from the F&I catalog">F4-InsuranceF5-Recalculate {
const pc = ensureCalc("Nothing was printed"); // T-03: print the figures for the inputs on screen now
if (!pc.ok) { window.k2bToast && window.k2bToast(pc.msg, "warn", 6500); return; }
if (pc.recalculated) window.k2bToast && window.k2bToast("Recalculated before printing — " + pc.toast, "ok", 4200);
const d = window.loadJbForm(FW_FORM);
const fmt = (k, lab) => `
${lab}
$${d[k] || "0.00"}
`;
const esc = (s) => String(s == null ? "" : s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
const negEq = Math.max(0, fwNum(d.tradePayoff) - fwNum(d.tradeValue));
let planRows = ""; for (let n = 1; n <= FW_MAX_PLANS; n++) if (!fwSlotEmpty(d, n)) planRows += `