/* eslint-disable */ /* © 2026 K2B NETWORK SOLUTIONS LLC. All rights reserved. Proprietary and confidential. Unauthorized copying, distribution, or use of this file, via any medium, is strictly prohibited. Original work product. */ // K2B Network Solutions — Schedule Sub-Ledgers (SCHED). // Classic dealer-accounting "schedules": for any control account that's // reconcilable (inventory, receivables, contracts-in-transit, floorplan // payable, accruals), this is the open-item detail that rolls up to the // GL balance. One row = one open document. // // Every schedule is hydrated LIVE from the DMS ledger (hydrateSchedules) — // nothing here is a fixture. A schedule with no live source (321 accrued // payroll, 324 sales-tax jurisdictions) simply shows "No entries yet." // Each schedule definition lists its column shape; rows + glBal are filled // from the ledger on every render. const SCHEDULES = { // 220 — Contracts in Transit (posted, unfunded, FINANCED deals) "220": { title: "Contracts in Transit", desc: "Financed deals posted (delivered) but not yet funded by the lender. Cash deals are paid at delivery and never sit here.", glBal: 0, columns: [ { key: "ctrl", label: "Deal #", w: 80 }, { key: "cust", label: "Customer", w: 200 }, { key: "stk", label: "Stock", w: 80 }, { key: "lender",label: "Lender", w: 130 }, { key: "date", label: "Delivered", w: 90 }, { key: "age", label: "Age", w: 50, align: "right", aging: true }, { key: "amt", label: "Amount", w: 110, align: "right", money: true }, ], rows: [], }, // 230 — Customer Receivables (service / parts / etc.) "230": { title: "Customer Receivables", desc: "A/R from service customers, fleet accounts, and warranty.", glBal: 0, columns: [ { key: "cust", label: "Customer", w: 220 }, { key: "inv", label: "Invoice", w: 90 }, { key: "dept", label: "Dept", w: 60 }, { key: "date", label: "Date", w: 90 }, { key: "age", label: "Age", w: 50, align: "right", aging: true }, { key: "amt", label: "Open Balance", w: 110, align: "right", money: true }, ], rows: [], }, // 240 — New Vehicle Inventory (each unit) "240": { title: "New Vehicle Inventory", desc: "Open new-vehicle units, costed to invoice + dealer adds.", glBal: 0, columns: [ { key: "stk", label: "Stock", w: 90 }, { key: "yr", label: "Yr", w: 44 }, { key: "mdl", label: "Year / Model",w: 240 }, { key: "vin", label: "VIN", w: 170 }, { key: "date", label: "Recv", w: 80 }, { key: "age", label: "Days", w: 50, align: "right", aging: true }, { key: "amt", label: "Cost", w: 110, align: "right", money: true }, ], rows: [], }, // 244 — Used Vehicle Inventory "244": { title: "Used Vehicle Inventory", desc: "Open used-vehicle units, ACV + recon costs.", glBal: 0, columns: [ { key: "stk", label: "Stock", w: 90 }, { key: "yr", label: "Yr", w: 44 }, { key: "mdl", label: "Year / Model",w: 240 }, { key: "miles", label: "Miles", w: 80, align: "right" }, { key: "src", label: "Source", w: 110 }, { key: "age", label: "Days", w: 50, align: "right", aging: true }, { key: "amt", label: "Cost+Recon", w: 110, align: "right", money: true }, ], rows: [], }, // 260 — Parts Inventory (rolled up by bin) "260": { title: "Parts Inventory by Bin", desc: "Active parts on hand, summarized by physical bin.", glBal: 0, columns: [ { key: "bin", label: "Bin", w: 70 }, { key: "desc", label: "Category", w: 200 }, { key: "qty", label: "Qty SKUs", w: 80, align: "right" }, { key: "obs", label: "Obs Risk", w: 80, align: "right" }, { key: "age", label: "Avg Age", w: 70, align: "right", aging: true }, { key: "amt", label: "Ext Cost", w: 110, align: "right", money: true }, ], rows: [], }, // 300 — Notes Payable, New Vehicle Floorplan "300": { title: "Floorplan Payable — New", desc: "Open floorplan advances by unit. Curtailments due at 60/90/120.", glBal: 0, columns: [ { key: "stk", label: "Stock", w: 90 }, { key: "mdl", label: "Year / Model",w: 220 }, { key: "bank", label: "Bank", w: 110 }, { key: "date", label: "Advanced", w: 90 }, { key: "age", label: "Days", w: 50, align: "right", aging: true }, { key: "rate", label: "Rate", w: 60, align: "right" }, { key: "amt", label: "Balance", w: 110, align: "right", money: true, neg: true }, ], rows: [], }, // 311 — Notes Payable, Used Vehicle "311": { title: "Floorplan Payable — Used", desc: "Open used-vehicle floorplan advances.", glBal: 0, columns: [ { key: "stk", label: "Stock", w: 90 }, { key: "mdl", label: "Year / Model",w: 220 }, { key: "bank", label: "Bank", w: 110 }, { key: "date", label: "Advanced", w: 90 }, { key: "age", label: "Days", w: 50, align: "right", aging: true }, { key: "amt", label: "Balance", w: 110, align: "right", money: true, neg: true }, ], rows: [], }, // 321 — Salaries, Wages & Commissions Payable (live: unpaid deal commissions) "321": { title: "Accrued Payroll & Commissions", desc: "Earned, unpaid commissions accrued at posting (DMS commissions).", glBal: 0, columns: [ { key: "emp", label: "Employee", w: 220 }, { key: "dept", label: "Dept", w: 70 }, { key: "kind", label: "Type", w: 110 }, { key: "date", label: "Earned", w: 90 }, { key: "amt", label: "Accrued", w: 110, align: "right", money: true, neg: true }, ], rows: [], }, // 324 — Sales Taxes Payable (live: tax collected on posted deals) "324": { title: "Sales Taxes Payable", desc: "Sales tax collected on posted deliveries.", glBal: 0, columns: [ { key: "ctrl", label: "Deal #", w: 90 }, { key: "cust", label: "Customer", w: 220 }, { key: "kind", label: "Type", w: 80 }, { key: "date", label: "Delivered", w: 90 }, { key: "amt", label: "Collected", w: 110, align: "right", money: true, neg: true }, ], rows: [], }, // 740 — F&I Income (product payables / production on posted deals) "740": { title: "F & I Income — Products", desc: "Per-deal F&I production on posted deals (service contract + GAP sold).", glBal: 0, columns: [ { key: "ctrl", label: "Deal", w: 80 }, { key: "cust", label: "Customer", w: 220 }, { key: "vsc", label: "VSC", w: 90, align: "right", money: true, neg: true }, { key: "gap", label: "GAP", w: 90, align: "right", money: true, neg: true }, { key: "maint", label: "Maint", w: 90, align: "right", money: true, neg: true }, { key: "amt", label: "Per-Deal", w: 110, align: "right", money: true, neg: true }, ], rows: [], }, }; // Hydrate schedules LIVE from the DMS ledger so every sub-ledger agrees with // inventory, deals, parts, A/R and floorplan — one source of truth. Called on // every ScheduleView render (cheap) + DMS.subscribe keeps it current. function hydrateSchedules() { if (typeof window === "undefined" || !window.DMS) return; const D = window.DMS; const fmtD = (iso) => iso ? new Date(iso).toLocaleDateString("en-US", { month: "2-digit", day: "2-digit", year: "2-digit" }) : "—"; const recv = (days) => { const d = new Date(); d.setDate(d.getDate() - (days || 0)); return ("0" + (d.getMonth() + 1)).slice(-2) + "/" + ("0" + d.getDate()).slice(-2) + "/" + d.getFullYear().toString().slice(-2); }; const ageOf = (iso) => iso ? Math.max(0, Math.floor((Date.now() - new Date(iso)) / 86400000)) : 0; const stageOf = (d) => (D.dealStage ? D.dealStage(d) : (d.stage || "")); const dealNo = (d) => String(d.id || "").replace("DR-", ""); const inv = D.inventory() || []; const deals = (D.deals() || []).filter(d => d.status !== "Dead"); const parts = (D.parts && D.parts()) || []; const posted = deals.filter(d => d.postedToGL); // 220 — Contracts in Transit: posted, unfunded, FINANCED deals (cash deals // were paid at delivery). Age clock starts at `delivered` (set by postDeal). const cit = posted.filter(d => stageOf(d) !== "Funded" && (+d.amtFinanced || 0) > 0); SCHEDULES["220"].rows = cit.map(d => ({ ctrl: dealNo(d), cust: d.buyer, stk: d.stock || "—", lender: d.lender || "—", date: fmtD(d.delivered || d.deliveredDate || d.created), age: ageOf(d.delivered || d.deliveredDate || d.created), amt: +d.amtFinanced || 0, })); SCHEDULES["220"].glBal = SCHEDULES["220"].rows.reduce((s, r) => s + r.amt, 0); // 230 — Customer receivables from the live A/R module. const ar = (D.arCustomers && D.arCustomers()) || []; const arBal = (c) => +(c.balance ?? c.bal ?? 0); SCHEDULES["230"].rows = ar.filter(c => Math.abs(arBal(c)) > 0.004).map(c => ({ cust: c.name || c.acct, inv: c.acct, dept: c.type || "A/R", date: fmtD(c.lastActivity || c.opened), age: +c.age || ageOf(c.lastActivity), amt: arBal(c), })); SCHEDULES["230"].glBal = SCHEDULES["230"].rows.reduce((s, r) => s + r.amt, 0); const inStock = inv.filter(u => window.DealCalc.unitOnLot(u)); // RC28 fix round: the ONE on-lot reading const newUnits = inStock.filter(u => /new/i.test(u.type || "")); const usedUnits = inStock.filter(u => !/new/i.test(u.type || "")); // 240 / 244 — vehicle inventory by unit (cost + recon), straight off the ledger. SCHEDULES["240"].rows = newUnits.map(u => ({ stk: u.stock, yr: u.yr, mdl: (u.mk || "").trim() + " " + (u.mdl || ""), vin: u.vin, date: recv(u.age), age: +u.age || 0, amt: +u.cost || 0, })); SCHEDULES["240"].glBal = SCHEDULES["240"].rows.reduce((s, r) => s + r.amt, 0); SCHEDULES["244"].rows = usedUnits.map(u => ({ stk: u.stock, yr: u.yr, mdl: (u.mk || "").trim() + " " + (u.mdl || ""), miles: (+u.miles || 0).toLocaleString(), src: u.floorLender ? "FLOORED" : "OWNED", age: +u.age || 0, amt: (+u.cost || 0) + (+u.recon || 0), })); SCHEDULES["244"].glBal = SCHEDULES["244"].rows.reduce((s, r) => s + r.amt, 0); // 260 — parts inventory rolled up by bin, from the live parts master. const bins = {}; parts.forEach(p => { const b = p.bin || "(no bin)"; if (!bins[b]) bins[b] = { bin: b, desc: "", qty: 0, obs: 0, age: 0, amt: 0 }; bins[b].qty += 1; if ((+p.onHand || 0) <= 0) bins[b].obs += 1; bins[b].amt += (+p.onHand || 0) * (+p.cost || 0); }); SCHEDULES["260"].rows = Object.values(bins) .map(b => ({ ...b, desc: b.desc || "Stocked parts", amt: +b.amt.toFixed(2) })) .sort((a, b2) => String(a.bin).localeCompare(String(b2.bin))); SCHEDULES["260"].glBal = SCHEDULES["260"].rows.reduce((s, r) => s + r.amt, 0); // 300 / 311 — floorplan payable: floored in-stock units + sold-awaiting-payoff. const fpDue = ((D.floorplanPayoffs && D.floorplanPayoffs()) || []).filter(f => !f.paid); SCHEDULES["300"].rows = newUnits.filter(u => +u.floorAmt > 0).map(u => ({ stk: u.stock, mdl: `${u.yr} ${(u.mk || "").trim()} ${u.mdl || ""}`, bank: u.floorLender || "—", date: recv(u.age), age: +u.age || 0, rate: u.floorRate != null ? u.floorRate + "%" : "—", amt: -(+u.floorAmt || 0), })); SCHEDULES["300"].glBal = SCHEDULES["300"].rows.reduce((s, r) => s + r.amt, 0); SCHEDULES["311"].rows = usedUnits.filter(u => +u.floorAmt > 0).map(u => ({ stk: u.stock, mdl: `${u.yr} ${(u.mk || "").trim()} ${u.mdl || ""}`, bank: u.floorLender || "—", date: recv(u.age), age: +u.age || 0, amt: -(+u.floorAmt || 0), })).concat(fpDue.map(f => ({ stk: f.stock, mdl: `SOLD — payoff due (${f.deal})`, bank: f.lender || "—", date: fmtD(f.soldDate), age: ageOf(f.soldDate), amt: -(+f.amount || 0), }))); SCHEDULES["311"].glBal = SCHEDULES["311"].rows.reduce((s, r) => s + r.amt, 0); // 321 — accrued, unpaid deal commissions (DMS.commissions, accrued at posting). const comms = ((D.commissions && D.commissions()) || []).filter(c => !c.paid); SCHEDULES["321"].rows = comms.map(c => ({ emp: c.salesperson || c.spId || "—", dept: "SAL", kind: "Commission", date: fmtD(c.created), amt: -(+c.commission || 0), })); SCHEDULES["321"].glBal = SCHEDULES["321"].rows.reduce((s, r) => s + r.amt, 0); // 324 — sales tax collected on posted deals (retail/cash tax; lease upfront tax). SCHEDULES["324"].rows = posted.filter(d => (+d.tax || 0) > 0 || (+d.leaseUpfrontTax || 0) > 0).map(d => { const lease = /lease/i.test(String(d.dealType || "")); return { ctrl: dealNo(d), cust: d.buyer, kind: lease ? "Lease" : (d.dealType || "Retail"), date: fmtD(d.delivered || d.created), amt: -(lease ? (+d.leaseUpfrontTax || 0) : (+d.tax || 0)) }; }); SCHEDULES["324"].glBal = SCHEDULES["324"].rows.reduce((s, r) => s + r.amt, 0); // 740 — F&I product production per posted deal (vscSale / gapSale). const fiDeals = posted.filter(d => ((+d.vscSale || 0) + (+d.gapSale || 0)) > 0); SCHEDULES["740"].rows = fiDeals.map(d => ({ ctrl: dealNo(d), cust: d.buyer, vsc: -(+d.vscSale || 0), gap: -(+d.gapSale || 0), maint: 0, amt: -((+d.vscSale || 0) + (+d.gapSale || 0)), })); SCHEDULES["740"].glBal = SCHEDULES["740"].rows.reduce((s, r) => s + r.amt, 0); } hydrateSchedules(); // ─── Schedule list (left rail in Schedule tab) ──────────────────────────── const SCHED_INDEX = [ { acct: "220", title: "Contracts in Transit", group: "Receivables" }, { acct: "230", title: "Customer Receivables", group: "Receivables" }, { acct: "240", title: "New Vehicle Inventory", group: "Inventory" }, { acct: "244", title: "Used Vehicle Inventory", group: "Inventory" }, { acct: "260", title: "Parts Inventory by Bin", group: "Inventory" }, { acct: "300", title: "Floorplan Payable — New", group: "Payables" }, { acct: "311", title: "Floorplan Payable — Used", group: "Payables" }, { acct: "321", title: "Accrued Payroll & Commissions", group: "Accruals" }, { acct: "324", title: "Sales Taxes Payable", group: "Accruals" }, { acct: "740", title: "F & I Income — Products", group: "Production" }, ]; const schedFmt = (n, neg) => { const a = Math.abs(n).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); return (neg || n < 0) ? `(${a})` : a; }; const agingTone = (d) => d >= 90 ? "var(--k2b-maroon)" : d >= 60 ? "#B26A00" : d >= 30 ? "#7A6A00" : "var(--k2b-blue-data-dk)"; // ── ScheduleView: full schedule for the given account ──────────────────── const ScheduleView = ({ acct, embedded = false }) => { // Re-hydrate from the live ledger on every render + on any DMS change. const [, force] = React.useReducer(n => n + 1, 0); React.useEffect(() => (window.DMS && window.DMS.subscribe) ? window.DMS.subscribe(force) : undefined, []); hydrateSchedules(); const sch = SCHEDULES[acct]; if (!sch) { return (
No schedule maintained for this account. (Acct {acct})
); } const total = sch.rows.reduce((s, r) => s + (typeof r.amt === "number" ? r.amt : 0), 0); const diff = total - sch.glBal; const recOK = Math.abs(diff) < 0.50; // Aging buckets for receivable/inventory schedules const buckets = [ ["0-30", sch.rows.filter(r => (r.age ?? 0) <= 30 ).reduce((s, r) => s + (r.amt || 0), 0)], ["31-60", sch.rows.filter(r => (r.age ?? 0) > 30 && (r.age ?? 0) <= 60).reduce((s, r) => s + (r.amt || 0), 0)], ["61-90", sch.rows.filter(r => (r.age ?? 0) > 60 && (r.age ?? 0) <= 90).reduce((s, r) => s + (r.amt || 0), 0)], ["91-120", sch.rows.filter(r => (r.age ?? 0) > 90 && (r.age ?? 0) <= 120).reduce((s, r) => s + (r.amt || 0), 0)], ["121+", sch.rows.filter(r => (r.age ?? 0) > 120).reduce((s, r) => s + (r.amt || 0), 0)], ]; return (
{/* Header band */}
Acct {acct} — {sch.title}
{sch.desc}
GL Bal: {schedFmt(sch.glBal)}
Detail: {schedFmt(total)}
Variance: {recOK ? "RECONCILED" : schedFmt(diff)}
{/* Aging bucket strip */}
{buckets.map(([k, v]) => (
{k}d {schedFmt(v)}
))}
{/* Detail rows */}
{sch.columns.map(c => ( ))} {sch.rows.length === 0 && ( )} {sch.rows.map((r, i) => ( {sch.columns.map(c => { let val = r[c.key]; let style = { padding: "1px 6px", textAlign: c.align || "left", color: "var(--k2b-blue-data-dk)" }; if (c.aging) { style.color = agingTone(val || 0); if ((val || 0) >= 60) style.fontWeight = 700; } else if (c.money) { style.color = (val < 0) ? "var(--k2b-maroon)" : "var(--k2b-blue-data-dk)"; style.fontWeight = 700; val = schedFmt(val); } return ; })} ))} {sch.columns.map((c, k) => ( ))}
{c.label}
No entries yet.
{val}
{k === 0 ? `${sch.rows.length} open items` : c.money ? schedFmt(total) : ""}
); }; Object.assign(window, { SCHEDULES, SCHED_INDEX, ScheduleView, schedFmt });