/* 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 — Accounting / General Ledger Inquiry (GLINQ). // Classic-gray Win9x surface — GM standard chart of accounts. // Tabs: Chart of Accounts · COA Manager · Schedule Detail · Journal Entries // Account numbers and names follow the GM-US-AC v3.0 manual. // All data persists to localStorage so journal entries, custom accounts and // edits survive page reloads. // ─── Persistence keys ──────────────────────────────────────────────────── const ACCT_LS_KEYS = { COA: "jbrews-acct-coa-v1", // editable working chart of accounts JE: "jbrews-acct-journal-v1", // posted JE history SESSION_JE: "jbrews-acct-session-je-v1", // unposted (session) JEs }; const _acctLoad = (k, fallback) => { try { const raw = localStorage.getItem(k); return raw ? JSON.parse(raw) : fallback; } catch (e) { return fallback; } }; const _acctSave = (k, v) => { try { localStorage.setItem(k, JSON.stringify(v)); } catch (e) {} }; // ── DEFAULT working chart of accounts ─────────────────────────────────── // Account numbers are CONFIGURATION; balances are DATA. A fresh station // therefore opens with every account at $0.00 and zero journal history // (audit C-05). In a store whose chart is built in (RC35: K2BStore.builtinChart(), ChartTemplates.jsx.txt - // window.K2B_CHART_BUILTIN) we seed the numbers from there so its actual 450+ accounts show up; any other store // uses ONLY its own chart from the server (never a template - SPEC 4.8). // Demo balances / journal entries only appear when an admin explicitly // clicks Controls → Load Sample Data (JEStore.loadDemoJournal()). const _acctBuiltinOk = () => !window.K2BStore || window.K2BStore.builtinChart(); const _seedFromBuiltin = () => { const src = (typeof window !== "undefined" && _acctBuiltinOk() && window.K2B_CHART_BUILTIN) || []; return src.map(r => ({ acct: r[0], name: r[1], type: ({ A: "Asset", L: "Liability", E: "Expense", S: "Sales", C: "Cost", I: "Income" }[r[2]] || r[2]), bal: 0, sched: ["205", "210", "220", "230", "231", "237", "240", "241", "242", "263", "300", "310", "311", "321", "323", "324"].includes(r[0]), group: r[4], })); }; const GL_ACCOUNTS_DEFAULT = !_acctBuiltinOk() ? [] : (typeof window !== "undefined" && window.K2B_CHART_BUILTIN && window.K2B_CHART_BUILTIN.length) ? _seedFromBuiltin() : [ { acct: "200", name: "Cash on Hand (Petty Cash)", bal: 0, type: "Asset" }, { acct: "201", name: "Cash on Hand", bal: 0, type: "Asset" }, { acct: "202", name: "Cash in Bank — Operating", bal: 0, type: "Asset", sched: true }, { acct: "205", name: "Contracts in Transit", bal: 0, type: "Asset", sched: true }, { acct: "220", name: "Customer Receivables", bal: 0, type: "Asset", sched: true }, { acct: "231", name: "New Cars Inventory", bal: 0, type: "Asset", sched: true }, { acct: "237", name: "New Trucks Inventory", bal: 0, type: "Asset", sched: true }, { acct: "240", name: "Used Cars Inventory", bal: 0, type: "Asset", sched: true }, { acct: "241", name: "Used Trucks Inventory", bal: 0, type: "Asset", sched: true }, { acct: "242", name: "Parts & Accessories Inventory", bal: 0, type: "Asset", sched: true }, { acct: "263", name: "Warranty Claims Receivable", bal: 0, type: "Asset", sched: true }, { acct: "310", name: "Notes Payable - New Vehicle & Demos", bal: 0, type: "Liability", sched: true }, { acct: "311", name: "Notes Payable - Used Vehicles", bal: 0, type: "Liability", sched: true }, { acct: "321", name: "Salaries, Wages & Commissions Payable", bal: 0, type: "Liability", sched: true }, { acct: "323", name: "Payroll Taxes Payable", bal: 0, type: "Liability", sched: true }, { acct: "324", name: "Sales Taxes Payable", bal: 0, type: "Liability", sched: true }, { acct: "340", name: "Allowance for Doubtful Accounts", bal: 0, type: "Contra" }, { acct: "370", name: "Retained Earnings", bal: 0, type: "Equity" }, ]; // Load + persist. Bump key to v2 so users with a stale v1 (the old 33-acct // seed) get refreshed to the store's real built-in chart. const COA_VERSION = "v2"; const COA_KEY = "jbrews-acct-coa-" + COA_VERSION; // Migrate: remove the old v1 key if present so users don't carry over stale data try { localStorage.removeItem("jbrews-acct-coa-v1"); } catch (e) {} // The default chart is ALWAYS zero-balance (audit C-05). Balances come only // from posted journal activity, the server-backed COA, or an explicit admin // "Load Sample Data". let GL_ACCOUNTS = _acctLoad(COA_KEY, GL_ACCOUNTS_DEFAULT.map(a => ({ ...a, bal: 0 }))); // If user has saved a tiny stub from an earlier build, refresh to the full chart if (Array.isArray(GL_ACCOUNTS) && GL_ACCOUNTS.length < 100 && GL_ACCOUNTS_DEFAULT.length >= 100) { GL_ACCOUNTS = GL_ACCOUNTS_DEFAULT; } // If the shared DMS has a non-empty server-backed COA, prefer that (preserves the // local demo seed when the server side is empty). if (typeof window !== "undefined" && window.DMS && typeof window.DMS.glAccounts === "function") { const dmsCoa = window.DMS.glAccounts(); if (Array.isArray(dmsCoa) && dmsCoa.length) GL_ACCOUNTS = dmsCoa.slice(); } // Persist the working COA. If the DMS is wired, route through it (server + // other-station sync); otherwise fall back to the legacy localStorage cache. const persistCoa = () => { if (typeof window !== "undefined" && window.DMS && typeof window.DMS.resetGlAccounts === "function") { window.DMS.resetGlAccounts(GL_ACCOUNTS); } else { _acctSave(COA_KEY, GL_ACCOUNTS); } }; const fmt$ = (n) => { const a = Math.abs(n).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); return n < 0 ? `(${a})` : ` ${a} `; }; // ── Journal-entry mirror ────────────────────────────────────────────── // The ledger lives in DMSCore (state.journalEntries). JOURNAL_ENTRIES is only a // same-reference mirror of DMS.journalEntries() for legacy consumers. There is // NO demo seed: the GL shows exactly what has been posted — nothing else. const JOURNAL_ENTRIES = []; if (typeof window !== "undefined" && window.DMS && typeof window.DMS.journalEntries === "function") { (window.DMS.journalEntries() || []).forEach(j => JOURNAL_ENTRIES.push(j)); } else { _acctLoad(ACCT_LS_KEYS.JE, []).forEach(j => JOURNAL_ENTRIES.push(j)); // offline fallback only } // Note: with DMS wired, posted entries persist through the DMS layer; the // localStorage cache is only used when DMS isn't present. GL_ACCOUNTS / JE are // kept in sync via _syncFromDMS(). const persistJE = () => _acctSave(ACCT_LS_KEYS.JE, JOURNAL_ENTRIES); // Pull the latest GL_ACCOUNTS / JOURNAL_ENTRIES from the DMS singleton. // Returns true if either changed (so callers can force a re-render). // Mutates the arrays in place to keep all closures pointing at the same refs. function _syncFromDMS() { if (typeof window === "undefined" || !window.DMS) return false; let changed = false; if (typeof window.DMS.glAccounts === "function") { const next = window.DMS.glAccounts(); if (next && next.length && JSON.stringify(next) !== JSON.stringify(GL_ACCOUNTS)) { GL_ACCOUNTS.length = 0; next.forEach(a => GL_ACCOUNTS.push(a)); changed = true; } } if (typeof window.DMS.journalEntries === "function") { const next = window.DMS.journalEntries(); if (next && JSON.stringify(next) !== JSON.stringify(JOURNAL_ENTRIES)) { JOURNAL_ENTRIES.length = 0; next.forEach(j => JOURNAL_ENTRIES.push(j)); changed = true; } } return changed; } // ── Shared journal-entry store ────────────────────────────────────────────── // DocEntry posts into this and GLINQ reads from it so the workflow is // end-to-end (post a JE in DocEntry → see it in GLINQ → drill in). // Session entries persist to localStorage so a reload during entry doesn't // lose work. Once "Release Batch" is hit they're promoted to JOURNAL_ENTRIES. const JEStore = (() => { if (typeof window !== "undefined" && window.__k2bJEStore) return window.__k2bJEStore; const listeners = new Set(); let session = _acctLoad(ACCT_LS_KEYS.SESSION_JE, []); // persisted draft entries const persistSession = () => _acctSave(ACCT_LS_KEYS.SESSION_JE, session); const store = { getAll: () => [...session, ...JOURNAL_ENTRIES], getSession: () => [...session], /** * Add a journal entry. * - default (no opts): draft / session entry that survives reload but * stays local until "Release Batch" is hit. * - { posted: true }: complete posted entry (came from postGL cascade * e.g. postDeal/closeRO). Goes straight into JOURNAL_ENTRIES and * into the shared DMS collection so other stations see it. */ add(entry, opts) { if (opts && opts.posted) { // Promote straight to posted history. DMSCore.postGL already wrote it // to state.journalEntries; just mirror it here so the screen re-renders // immediately, and avoid a duplicate when _syncFromDMS runs. const exists = JOURNAL_ENTRIES.some(j => j && entry && String(j.id) === String(entry.id)); if (!exists) JOURNAL_ENTRIES.unshift({ ...entry }); // No DMS.upsertJournalEntry here — postGL already mutated state and // queued the diff-based persist. Calling it again would double-notify. listeners.forEach(fn => fn()); return; } session.unshift({ ...entry, _session: true }); persistSession(); listeners.forEach(fn => fn()); }, /** Promote all session entries to posted JE history. */ releaseAll() { // Legacy drafts only — every release goes through DMS.postJournalEntry // (balanced, period-checked, audited). Unbalanced drafts stay put. const D = typeof window !== "undefined" ? window.DMS : null; const released = [...session].reverse(); let n = 0; const keep = []; released.forEach(e => { if (D && D.postJournalEntry) { const r = D.postJournalEntry({ date: e.date, desc: e.desc, ref: e.ref || e.id, jrn: e.jrn, lines: e.lines, src: e.src || "DocEntry" }); if (r && r.ok) n++; else keep.push(e); } else { const rest = Object.assign({}, e); delete rest._session; JOURNAL_ENTRIES.unshift(rest); n++; } }); session = keep.reverse(); persistSession(); if (!(D && D.postJournalEntry)) persistJE(); listeners.forEach(fn => fn()); return n; }, /** Remove a single session entry by id (e.g. user cancels). */ removeSession(id) { session = session.filter(e => e.id !== id); persistSession(); listeners.forEach(fn => fn()); }, /** Wipe everything (for the Settings → Data wipe). */ clearAll() { session = []; JOURNAL_ENTRIES.length = 0; persistSession(); persistJE(); listeners.forEach(fn => fn()); }, clearSession() { session = []; persistSession(); listeners.forEach(fn => fn()); }, /** Demo journal loading is retired — the GL never injects sample rows. Kept as a no-op so older callers (Controls / Dashboard) stay harmless. */ loadDemoJournal() { return 0; }, subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); }, }; if (typeof window !== "undefined") window.__k2bJEStore = store; return store; })(); // Hook React components subscribe to the store const useJEStore = () => { const [, force] = React.useReducer(n => n + 1, 0); React.useEffect(() => JEStore.subscribe(force), []); return JEStore; }; // ── Live account activity ──────────────────────────────────────────────────────── // Σ(debit − credit) per account across every POSTED journal entry. Every // balance shown in this module derives from opening bal + this activity, so // each cascade (deal post, RO close, parts ticket, A/R, released doc entry) // moves the chart of accounts in real time — accounts affect each other. const acctActivity = () => { const act = {}; JOURNAL_ENTRIES.forEach(je => (je.lines || []).forEach(l => { const k = String(l.acct); act[k] = (act[k] || 0) + (+l.dr || 0) - (+l.cr || 0); })); return act; }; // ──────────────────────────────────────────────────────────────────────────── // ─── Parts Accounting tab ───────────────────────────────────────────────── // The accounting-side view of the Parts department: the Parts Inventory asset // account (242) plus every parts sales / cost account, with live gross. const PARTS_ACCT_ROWS = [ { acct: "467", name: "Parts — Repair Orders (sale)", kind: "sale" }, { acct: "667", name: "Parts — Repair Orders (cost)", kind: "cost" }, { acct: "482", name: "Parts — Counter (sale)", kind: "sale" }, { acct: "682", name: "Parts — Counter (cost)", kind: "cost" }, { acct: "483", name: "Parts — Wholesale (sale)", kind: "sale" }, { acct: "683", name: "Parts — Wholesale (cost)", kind: "cost" }, ]; const PartsAccountingTab = ({ vp, fig, onSchedule, onJournal, onOpenJe, onOpenAcct, go }) => { const invRow = GL_ACCOUNTS.find(x => x.acct === "242") || { acct: "242", name: "Parts & Accessories Inventory", bal: 0 }; const f242 = fig["242"] || { before: 0, dr: 0, cr: 0 }; const invBal = (+invRow.bal || 0) + f242.before + f242.dr - f242.cr; const activityOf = (acct) => { const f = fig[String(acct)]; return f ? f.dr - f.cr : 0; }; const saleTotal = PARTS_ACCT_ROWS.filter(r => r.kind === "sale").reduce((s, r) => s + Math.abs(activityOf(r.acct)), 0); const costTotal = PARTS_ACCT_ROWS.filter(r => r.kind === "cost").reduce((s, r) => s + Math.abs(activityOf(r.acct)), 0); const gross = saleTotal - costTotal; const grossPct = saleTotal ? (gross / saleTotal) * 100 : 0; const partsAccts = new Set(["242", ...PARTS_ACCT_ROWS.map(r => r.acct)]); const partsJE = glJEsIn(vp).filter(j => (j.lines || []).some(l => partsAccts.has(String(l.acct)))).slice(0, 40); const card = { background: "#FFFFFF", border: "1px solid #808080", padding: "10px 14px" }; const big = { fontFamily: "'Courier Prime', monospace", fontWeight: 700, fontSize: 22, color: "var(--k2b-navy)" }; return (
PARTS · ACCOUNTING The accounting side of the Parts department — inventory value, sales, cost, and gross, straight from the ledger.
go && go("partsdept")}>Open Parts Dept ▸
Parts Inventory (242)
{fmt$(invBal)}
onSchedule("242")}>View Schedule ▸
Parts Sales ({glPerShort(vp)})
{fmt$(saleTotal)}
Parts Cost ({glPerShort(vp)})
{fmt$(costTotal)}
Parts Gross
= 0 ? "#0E7A37" : "var(--k2b-maroon)" }}>{fmt$(gross)}
{grossPct.toFixed(1)}% margin
Parts Accounts
{PARTS_ACCT_ROWS.map(r => ( ))}
AcctDescriptionActivity
{ e.preventDefault(); onOpenAcct("242"); }}>242Parts & Accessories Inventory {fmt$(invBal)} onSchedule("242")}>Schedule
{ e.preventDefault(); onOpenAcct(r.acct); }}>{r.acct} {r.name} {fmt$(Math.abs(activityOf(r.acct)))}
Recent Parts Journal Activity
All Journals ▸
{partsJE.length === 0 ? (
No parts journal entries in {glPerLabel(vp)}. Selling parts at the counter or closing a repair order posts here automatically.
) : ( {partsJE.map(j => ( onOpenJe(j.id)} style={{ borderBottom: "1px solid #EEE", cursor: "pointer" }} title="Open this journal entry"> ))}
EntryDateSourceDescription
{j.id} {j.date} {j.src} {j.desc}
)}
); }; // ════════════════════════════════════════════════════════════════════════ // SHARED GL PIECES — period math, JE detail, JE editor, account ledger. // Exported on window (JeDetailModal, JeEditorModal, AcctLedgerModal, // GlModals, AcctPeriodPicker, PeriodEndDialog) so DocEntry / GM Statement / // P&L use the exact same click-through. Everything reads DMSCore — no // literals, no demo rows. // ════════════════════════════════════════════════════════════════════════ const glD = () => (typeof window !== "undefined" ? window.DMS : null); const glPad2 = (n) => String(n).padStart(2, "0"); // "MM/DD/YY" | "MM/DD/YYYY" | ISO → local Date (never UTC-shifted) const glParseDate = (s) => { if (!s) return null; if (s instanceof Date) return s; const m = String(s).match(/^(\d{1,2})\/(\d{1,2})\/(\d{2,4})/); if (m) { let y = +m[3]; if (y < 100) y += 2000; return new Date(y, +m[1] - 1, +m[2]); } const iso = String(s).match(/^(\d{4})-(\d{2})-(\d{2})/); if (iso) return new Date(+iso[1], +iso[2] - 1, +iso[3]); const d = new Date(s); return isNaN(d) ? null : d; }; const glPerOfDate = (d) => d ? d.getFullYear() + "-" + glPad2(d.getMonth() + 1) : ""; const glJePeriod = (je) => (je && je.period) || glPerOfDate(glParseDate(je && je.date)); const glNowPeriod = () => glPerOfDate(new Date()); const glOpenPeriod = () => { const D = glD(); try { return (D && D.periodStatus && D.periodStatus().open) || glNowPeriod(); } catch (e) { return glNowPeriod(); } }; const glPerLabel = (p) => { const [y, m] = String(p || glNowPeriod()).split("-").map(Number); return new Date(y, m - 1, 1).toLocaleDateString("en-US", { month: "long", year: "numeric" }); }; const glPerShort = (p) => { const [y, m] = String(p || "").split("-"); return m + " / " + y; }; const glPerShift = (p, n) => { const [y, m] = String(p).split("-").map(Number); const d = new Date(y, m - 1 + n, 1); return glPerOfDate(d); }; const glRange = (p) => { const D = glD(); if (D && D.periodRange) return D.periodRange(p); const [y, m] = String(p).split("-").map(Number); const start = new Date(y, m - 1, 1), end = new Date(y, m, 0); const f = (d) => d.toLocaleDateString("en-US", { month: "2-digit", day: "2-digit", year: "2-digit" }); return { start, end, startLabel: f(start), endLabel: f(end) }; }; // Where does period p stand? open | closed | ahead (after open) | prior (before open, not closed) const glPerState = (p) => { const D = glD(); const open = glOpenPeriod(); if (D && D.periodIsClosed && D.periodIsClosed(p)) return "closed"; if (p === open) return "open"; return p > open ? "ahead" : "prior"; }; const glAllJEs = () => { const D = glD(); return (D && D.journalEntries ? D.journalEntries() : JOURNAL_ENTRIES) || []; }; const glJEsIn = (p) => glAllJEs().filter(j => j && glJePeriod(j) === p); const glGetJE = (id) => { const D = glD(); return (D && D.journalEntry) ? D.journalEntry(id) : glAllJEs().find(j => String(j.id) === String(id)) || null; }; const glMoney = (n) => (+n || 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const glJeTotal = (je) => (je && je.lines || []).reduce((s, l) => s + (+l.dr || 0), 0); const glBal = (lines) => { const D = glD(); if (D && D.jeBalance) return D.jeBalance(lines); const dr = (lines || []).reduce((s, l) => s + (+l.dr || 0), 0), cr = (lines || []).reduce((s, l) => s + (+l.cr || 0), 0); return { dr: +dr.toFixed(2), cr: +cr.toFixed(2), diff: +(dr - cr).toFixed(2), ok: Math.abs(dr - cr) < 0.005 && dr > 0 }; }; // Chart lookup: live COA first, then the built-in chart (a store whose chart is built in) / the GM standard library. const glChart = () => { const D = glD(); const live = (D && D.glAccounts && D.glAccounts()) || []; return live.length ? live : GL_ACCOUNTS; }; const glAcctName = (acct) => { const k = String(acct || ""); const r = glChart().find(a => String(a.acct) === k) || GL_ACCOUNTS.find(a => String(a.acct) === k); if (r) return r.name || r.desc || ""; const lib = ((typeof window !== "undefined" && _acctBuiltinOk() && window.K2B_CHART_BUILTIN) || []).concat((typeof window !== "undefined" && window.GM_COA) || []); const x = lib.find(row => String(row[0]) === k); return x ? x[1] : ""; }; const glAcctKnown = (acct) => !!glAcctName(acct); const glTodayIso = () => { const d = new Date(); return d.getFullYear() + "-" + glPad2(d.getMonth() + 1) + "-" + glPad2(d.getDate()); }; const glIsoFromJe = (s) => { const d = glParseDate(s); return d ? d.getFullYear() + "-" + glPad2(d.getMonth() + 1) + "-" + glPad2(d.getDate()) : glTodayIso(); }; // ISO (from ) → "MM/DD/YYYY" so DMSCore parses it as LOCAL time. const glUsFromIso = (iso) => { const m = String(iso || "").match(/^(\d{4})-(\d{2})-(\d{2})$/); return m ? `${m[2]}/${m[3]}/${m[1]}` : iso; }; // A sensible default date for a new entry in the open period: today when today // is in the open period (or ahead of it — post-ahead), else the period's last day. const glDefaultEntryIso = () => { const open = glOpenPeriod(); const today = glTodayIso(); if (today.slice(0, 7) >= open && glPerState(today.slice(0, 7)) !== "closed") return today; const e = glRange(open).end; return e.getFullYear() + "-" + glPad2(e.getMonth() + 1) + "-" + glPad2(e.getDate()); }; // Per-account figures for a period: opening (bal + everything before p), period dr/cr, ending. const glAcctFigures = (p) => { const fig = {}; const get = (k) => (fig[k] = fig[k] || { before: 0, dr: 0, cr: 0 }); glAllJEs().forEach(je => { const jp = glJePeriod(je); if (!jp || jp > p) return; (je.lines || []).forEach(l => { const f = get(String(l.acct)); if (jp < p) f.before += (+l.dr || 0) - (+l.cr || 0); else { f.dr += (+l.dr || 0); f.cr += (+l.cr || 0); } }); }); return fig; }; const glToast = (msg, kind, ms) => { if (typeof window !== "undefined" && window.k2bToast) window.k2bToast(msg, kind || "ok", ms || 3200); }; const glEsc = (s) => String(s == null ? "" : s).replace(/&/g, "&").replace(//g, ">"); const glPrint = (title, bodyHtml) => { if (typeof window !== "undefined" && window.k2bPrint) window.k2bPrint(bodyHtml, { title }); else glToast("Printing isn't available on this station.", "warn"); }; const glDownload = (name, text) => { try { const url = URL.createObjectURL(new Blob([text], { type: "text/csv" })); const a = document.createElement("a"); a.href = url; a.download = name; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 1500); } catch (e) { glToast("Download failed: " + e.message, "warn"); } }; // Re-render on any DMS change (period close, new entry, another station). const useGlLive = () => { const [, force] = React.useReducer(n => n + 1, 0); React.useEffect(() => { const D = glD(); return (D && D.subscribe) ? D.subscribe(force) : undefined; }, []); return force; }; // ─── Period picker (◀ month ▶ + "open period" jump) ────────────────────── const AcctPeriodPicker = ({ value, onChange, compact }) => { useGlLive(); const open = glOpenPeriod(); const st = glPerState(value); const r = glRange(value); const chip = { open: ["OPEN", "#0E7A37", "#E7F5EA"], closed: ["CLOSED", "#8A1F1F", "#FBE9E9"], ahead: ["POST-AHEAD", "#9A6700", "#FBF3E0"], prior: ["PRIOR · not closed", "#555", "#EEE"] }[st]; return (
Period: { if (/^\d{4}-\d{2}$/.test(e.target.value)) onChange(e.target.value); }} style={{ fontFamily: "'Courier Prime', monospace", fontSize: 13, padding: "1px 4px", border: "1px solid #808080", background: "#FFFFE1", color: "var(--k2b-navy)" }} /> {value !== open && } {chip[0]} {!compact && ( Period beginning {r.startLabel} · ending {r.endLabel} )}
); }; // Small status chips for an entry. const glJeChips = (je) => { if (!je) return []; const out = []; const corrector = glAllJEs().find(j => j && j.corrects === je.id); if (je.reversedBy) out.push(["Reversed by " + je.reversedBy, "#8A1F1F", je.reversedBy]); if (je.reverses) out.push(["Reversal of " + je.reverses, "#8A1F1F", je.reverses]); if (corrector) out.push(["Corrected → " + corrector.id, "#9A6700", corrector.id]); if (je.corrects) out.push(["Correction of " + je.corrects, "#9A6700", je.corrects]); if (je.edits && je.edits.length) out.push([`Edited ${je.edits.length} time${je.edits.length === 1 ? "" : "s"}`, "#1F4E8A", null]); if (je.postAhead) out.push(["Post-ahead " + glPerShort(glJePeriod(je)), "#9A6700", null]); return out; }; const GlChip = ({ label, color, onClick }) => ( {label} ); const glTh = { padding: "3px 6px", textAlign: "left", color: "var(--k2b-maroon)", background: "#E4E4E4", position: "sticky", top: 0, zIndex: 1, borderBottom: "1px solid #808080", whiteSpace: "nowrap" }; const glTd = { padding: "2px 6px", borderBottom: "1px dotted #D0D0D0" }; // ─── JE detail ────────────────────────────────────────────────────────── const JeDetailModal = ({ id, go, onClose, onOpenJe, onOpenAcct, onEdit, onReversed }) => { useGlLive(); const D = glD(); const je = glGetJE(id); if (!je) { return (
Entry {id} was not found in the ledger.
); } const b = glBal(je.lines); const per = glJePeriod(je); const src = D && D.jeSource ? D.jeSource(je) : null; const deal = src && src.kind === "deal" && D && D.deals ? D.deals().find(d => d.id === src.id) : null; const canReverse = !je.reversedBy && !je.reverses; const canEdit = !je.reversedBy; const manual = false; // (ledger lane) posted entries are never edited in place — every correction is reverse + corrected copy const openDeal = () => { try { localStorage.setItem("jb_deal_focus", src.id); } catch (e) {} window.jbDealFocus = src.id; onClose && onClose(); go && go("deal"); }; const reverse = () => { const reason = window.prompt(`Reverse ${je.id}?\n\nThis posts RV-${je.id} with every debit and credit swapped. The original stays on the books, marked "Reversed".\n\nReason (required):`, ""); if (reason == null) return; if (!String(reason).trim()) { glToast("A reason is required to reverse an entry.", "warn"); return; } const r = D.reverseJournalEntry(je.id, String(reason).trim()); glToast(r.msg, r.ok ? "ok" : "warn", 4200); if (r.ok) { onReversed ? onReversed(r.id) : (onOpenJe && onOpenJe(r.id)); } }; const print = () => { const rows = (je.lines || []).map(l => `${glEsc(l.acct)}${glEsc(glAcctName(l.acct))}${glEsc(l.desc || "")}${l.dr ? glMoney(l.dr) : ""}${l.cr ? glMoney(l.cr) : ""}`).join(""); glPrint("Journal Entry " + je.id, `${window.dealerHeadHtml ? window.dealerHeadHtml() : ""}
JOURNAL ENTRY ${glEsc(je.id)}
${glEsc(je.date)} · Period ${glEsc(glPerLabel(per))} · Source ${glEsc(je.src || "")} · Jrn ${glEsc(je.jrn || "")}${je.ref ? " · Ref " + glEsc(je.ref) : ""}${je.by ? " · By " + glEsc(je.by) : ""}

${glEsc(je.desc || "")}

${rows}
AcctAccountMemoDebitCredit
TOTALS ${b.ok ? "(in balance)" : "(OUT OF BALANCE " + glMoney(Math.abs(b.diff)) + ")"}${glMoney(b.dr)}${glMoney(b.cr)}
`); }; const lab = { color: "var(--k2b-maroon)", fontWeight: 700 }; return ( {src && src.kind === "deal" && {`Open Deal ${src.id} ▸`}} {src && src.kind === "ro" && go && { onClose && onClose(); go("service"); }}>{`Repair Orders (${src.id}) ▸`}} {(!(window.DMS && window.DMS.can) || window.DMS.can("glEdit")) && glToast(je.reversedBy ? `${je.id} was already reversed by ${je.reversedBy}.` : `${je.id} is itself a reversal — post a new entry instead.`, "warn")}>Reverse…} {(!(window.DMS && window.DMS.can) || window.DMS.can("glEdit")) && onEdit && onEdit(je.id) : () => glToast(`${je.id} was reversed — edit its replacement instead.`, "warn")}>Edit / Correct…} Print Close }>
Entry{je.id} Date{je.date} Period{glPerLabel(per)}{D && D.periodIsClosed && D.periodIsClosed(per) ? " (closed)" : ""} Source{je.src || "—"}{manual ? " (manual)" : ""} Journal{je.jrn || "—"} Reference{je.ref || "—"} Posted by{je.by || "—"} Deal / RO{src ? `${src.kind === "deal" ? "Deal" : "RO"} ${src.id}${deal ? " — " + (deal.buyer || "") : ""}` : "—"}
Description {je.desc || "—"}
{glJeChips(je).length > 0 && (
{glJeChips(je).map(([t, c, target]) => onOpenJe(target) : undefined} />)}
)} {(je.lines || []).map((l, i) => ( ))}
AcctAccount nameMemo DebitCredit
{ e.preventDefault(); onOpenAcct && onOpenAcct(String(l.acct), per); }} style={{ color: "var(--k2b-blue-data-dk)" }} title="Open this account's ledger">{l.acct} {glAcctName(l.acct) || (not in chart)} {l.desc || ""} {+l.dr ? glMoney(l.dr) : ""} {+l.cr ? glMoney(l.cr) : ""}
Totals {glMoney(b.dr)} {glMoney(b.cr)}
{Math.abs(b.diff) < 0.005 ? "✓ Entry is in balance." : `⚠ Out of balance by $${glMoney(Math.abs(b.diff))}`}
{je.reverseReason &&
Reversal reason {je.reverseReason}
} {je.edits && je.edits.length > 0 && (
Edit history
{je.edits.map((ed, i) => (
• {new Date(ed.at).toLocaleString()} — {ed.by || "?"}{ed.reason ? ": " + ed.reason : ""}
))}
)} {canEdit && (
Edit / Correct: {manual && glPerState(per) !== "closed" ? "this entry is corrected by a reversing entry plus a corrected copy; the original stays on the books." : "this is a system-posted (or closed-period) entry — saving posts a reversal plus a corrected copy, so the trail stays complete."}
)}
); }; // ─── JE editor (new manual entry, or edit / correct an existing one) ───── const glBlankLine = () => ({ acct: "", dr: "", cr: "", desc: "" }); const JeEditorModal = ({ editId, src, jrn, defaults, onClose, onSaved }) => { const D = glD(); const orig = editId ? glGetJE(editId) : null; const [date, setDate] = React.useState(() => orig ? glIsoFromJe(orig.date) : ((defaults && defaults.dateIso) || glDefaultEntryIso())); const [desc, setDesc] = React.useState(() => orig ? (orig.desc || "") : ((defaults && defaults.desc) || "")); const [ref, setRef] = React.useState(() => orig ? (orig.ref || "") : ((defaults && defaults.ref) || "")); const [jr, setJr] = React.useState(() => String(orig ? (orig.jrn || 80) : (jrn || (defaults && defaults.jrn) || 80))); const [reason, setReason] = React.useState(""); const [err, setErr] = React.useState(""); const [lines, setLines] = React.useState(() => { const base = orig ? orig.lines : (defaults && defaults.lines); const ls = (base || []).map(l => ({ acct: String(l.acct || ""), dr: +l.dr ? String(+l.dr) : "", cr: +l.cr ? String(+l.cr) : "", desc: l.desc || l.note || "" })); while (ls.length < 2) ls.push(glBlankLine()); return ls; }); const setLine = (i, k, v) => setLines(arr => arr.map((l, j) => j === i ? { ...l, [k]: v } : l)); const num = (v) => String(v).replace(/[^0-9.]/g, ""); const used = lines.filter(l => l.acct || +l.dr || +l.cr); const b = glBal(used.map(l => ({ dr: +l.dr || 0, cr: +l.cr || 0 }))); const per = String(date).slice(0, 7); const perSt = /^\d{4}-\d{2}$/.test(per) ? glPerState(per) : "bad"; const unknown = used.filter(l => l.acct && !glAcctKnown(l.acct)).map(l => l.acct); const missingAcct = used.some(l => !l.acct && (+l.dr || +l.cr)); const manual = !orig; // (ledger lane) a NEW entry posts; an existing one is never changed in place — saving posts reverse + corrected copy const needReason = !!orig; const blockers = []; if (!b.ok) blockers.push(b.dr || b.cr ? `Out of balance by $${glMoney(Math.abs(b.diff))}` : "Enter at least one debit and one credit"); if (unknown.length) blockers.push("Unknown account " + unknown.join(", ")); if (missingAcct) blockers.push("Every amount needs an account"); if (perSt === "bad") blockers.push("Pick a date"); if (perSt === "closed") blockers.push(`${glPerLabel(per)} is closed — use a date in the open period`); if (needReason && !reason.trim()) blockers.push("Reason for the change is required"); const canSave = blockers.length === 0; const chart = glChart(); const save = () => { if (!canSave) return; const payloadLines = used.map(l => ({ acct: String(l.acct).trim(), dr: +l.dr || 0, cr: +l.cr || 0, desc: l.desc || "" })); let r; if (orig) r = D.editJournalEntry(orig.id, { date: glUsFromIso(date), desc, ref, lines: payloadLines }, reason.trim()); else r = D.postJournalEntry({ date: glUsFromIso(date), desc: desc || "Manual journal entry", ref, jrn: +jr || 80, lines: payloadLines, src: src || "Accounting" }); if (!r || !r.ok) { setErr((r && r.msg) || "The ledger refused this entry."); return; } glToast(r.msg, "ok", 5200); onSaved && onSaved(r); }; const inp = { fontFamily: "'Courier Prime', monospace", fontSize: 12.5, padding: "1px 4px", border: "1px solid #808080", background: "#FFFFE1", color: "var(--k2b-navy)" }; const lab = { color: "var(--k2b-maroon)", fontWeight: 700 }; return ( {canSave ? `✓ Balanced — $${glMoney(b.dr)}` : blockers[0]} Cancel }>
Date setDate(e.target.value)} style={inp} /> Ref setRef(e.target.value)} placeholder="invoice / check / memo #" style={inp} /> Jrn # setJr(e.target.value.replace(/[^0-9]/g, "").slice(0, 3))} style={{ ...inp, width: 60 }} />
Description setDesc(e.target.value)} placeholder="What is this entry for?" style={inp} />
Posts to period {perSt === "bad" ? "—" : glPerLabel(per)} {perSt === "open" && " (open)"} {perSt === "ahead" && ` — POST-AHEAD: ${glPerLabel(glOpenPeriod())} is still open; this releases when it closes.`} {perSt === "prior" && " (prior month, not closed)"} {perSt === "closed" && " — CLOSED. Reopen it in F6-Period End, or date the entry in the open period."}
{chart.map(a => )} {lines.map((l, i) => { const nm = l.acct ? glAcctName(l.acct) : ""; return ( ); })}
AccountAccount nameMemo DebitCredit
{ const v = e.target.value; const m = v.match(/^(\S+) — /); setLine(i, "acct", (m ? m[1] : v).toUpperCase()); }} style={{ ...inp, width: "100%", fontWeight: 700 }} /> {l.acct ? (nm || "not in chart") : ""} setLine(i, "desc", e.target.value)} style={{ ...inp, width: "100%", background: "#FFF" }} /> setLine(i, "dr", num(e.target.value))} style={{ ...inp, width: "100%", textAlign: "right" }} /> setLine(i, "cr", num(e.target.value))} style={{ ...inp, width: "100%", textAlign: "right" }} />
Totals {glMoney(b.dr)} {glMoney(b.cr)}
{b.ok ? "✓ Debits equal credits." : (b.dr || b.cr) ? `Out of balance by $${glMoney(Math.abs(b.diff))}` : "Enter at least one debit and one credit."}
{orig && (
Reason for change * setReason(e.target.value)} placeholder="required — goes in the audit trail" style={inp} /> {manual && glPerState(glJePeriod(orig)) !== "closed" ? `${orig.id} is corrected by a reversing entry plus a corrected copy — the original stays on the books.` : `${orig.id} is a ${orig.src || "system"} entry${glPerState(glJePeriod(orig)) === "closed" ? " in a closed period" : ""} — saving posts RV-${orig.id} (reversal) and a corrected copy.`}
)} {err &&
⚠ {err}
}
); }; // ─── Account ledger (every line hitting one account in a period) ──────── const AcctLedgerModal = ({ acct, period, onClose, onOpenJe }) => { useGlLive(); const [p, setP] = React.useState(period || glOpenPeriod()); const row = glChart().find(a => String(a.acct) === String(acct)) || GL_ACCOUNTS.find(a => String(a.acct) === String(acct)) || { acct, name: glAcctName(acct), bal: 0 }; const fig = glAcctFigures(p)[String(acct)] || { before: 0, dr: 0, cr: 0 }; const opening = (+row.bal || 0) + fig.before; const lines = []; glJEsIn(p).slice().sort((a, b2) => (glParseDate(a.date) || 0) - (glParseDate(b2.date) || 0)) .forEach(je => (je.lines || []).forEach(l => { if (String(l.acct) === String(acct)) lines.push({ je, l }); })); let run = opening; const withRun = lines.map(x => { run += (+x.l.dr || 0) - (+x.l.cr || 0); return { ...x, run }; }); const csv = () => { const out = [["Date", "Entry", "Source", "Description", "Debit", "Credit", "Balance"]]; withRun.forEach(x => out.push([x.je.date, x.je.id, x.je.src || "", (x.je.desc || "").replace(/"/g, "'"), x.l.dr || 0, x.l.cr || 0, x.run.toFixed(2)])); glDownload(`GL_${acct}_${p}.csv`, out.map(r => r.map(c => `"${c}"`).join(",")).join("\n")); }; const print = () => { const body = withRun.map(x => `${glEsc(x.je.date)}${glEsc(x.je.id)}${glEsc(x.je.desc || "")}${x.l.dr ? glMoney(x.l.dr) : ""}${x.l.cr ? glMoney(x.l.cr) : ""}${glMoney(x.run)}`).join(""); const r = glRange(p); glPrint(`GL ${acct} ${p}`, `${window.dealerHeadHtml ? window.dealerHeadHtml() : ""}
ACCOUNT ${glEsc(acct)} — ${glEsc(row.name || "")}
Period beginning ${r.startLabel} · ending ${r.endLabel}
${body}
Beginning balance${glMoney(opening)}
Period totals${glMoney(fig.dr)}${glMoney(fig.cr)}${glMoney(opening + fig.dr - fig.cr)}
`); }; return ( Export CSV Print Close }>
{withRun.length === 0 && ( )} {withRun.map((x, i) => ( onOpenJe && onOpenJe(x.je.id)} style={{ cursor: "pointer" }} title="Open the journal entry"> ))}
DateEntrySourceDescription DebitCreditBalance
Beginning balance {glRange(p).startLabel} {glMoney(opening)}
No activity in account {acct} during {glPerLabel(p)}.
{x.je.date} {x.je.id} {x.je.src || ""} {x.je.desc || ""}{x.l.desc ? · {x.l.desc} : null} {+x.l.dr ? glMoney(x.l.dr) : ""} {+x.l.cr ? glMoney(x.l.cr) : ""} {glMoney(x.run)}
Period totals · ending balance {glRange(p).endLabel} {glMoney(fig.dr)} {glMoney(fig.cr)} {glMoney(opening + fig.dr - fig.cr)}
); }; // ─── Modal stack host: one place that wires detail ↔ ledger ↔ editor ──── // stack items: {t:"je", id} | {t:"acct", acct, period} | {t:"edit", id} | {t:"new", src, jrn, defaults} const GlModals = ({ stack, setStack, go, src, onPosted }) => { if (!stack || !stack.length) return null; const top = stack[stack.length - 1]; const pop = () => setStack(s => s.slice(0, -1)); const push = (item) => setStack(s => [...s, item]); const replaceTop = (item) => setStack(s => [...s.slice(0, -1), item]); if (top.t === "je") return push({ t: "je", id })} onReversed={(id) => replaceTop({ t: "je", id })} onOpenAcct={(acct, period) => push({ t: "acct", acct, period })} onEdit={(id) => push({ t: "edit", id })} />; if (top.t === "acct") return push({ t: "je", id })} />; if (top.t === "edit") return { setStack(s => [...s.slice(0, -2), { t: "je", id: r.id }]); onPosted && onPosted(r); }} />; if (top.t === "new") return { replaceTop({ t: "je", id: r.id }); onPosted && onPosted(r); }} />; return null; }; // ───────────────────────────────────────────────────────────────── const Accounting = ({ onClose, go }) => { const [, force] = React.useReducer(n => n + 1, 0); const [tab, setTab] = React.useState(() => { const hint = (typeof window !== "undefined" && window.__acctTab) || "ledger"; if (typeof window !== "undefined") window.__acctTab = null; return hint; }); // ledger | coa | sched | parts | journal const [vp, setVp] = React.useState(() => glOpenPeriod()); // VIEWED period (any month) const [selAcct, setSelAcct] = React.useState(null); const [schedAcct, setSchedAcct] = React.useState("240"); const [stack, setStack] = React.useState([]); // GL modal stack const [modal, setModal] = React.useState(null); // 'period' | 'help' | 'post' const lastOpen = React.useRef(glOpenPeriod()); // Live: re-render on any DMS change; follow the open period when it moves // (close month / set period) if we were looking at the old open period. React.useEffect(() => { const D = glD(); if (_syncFromDMS()) force(); if (!D || !D.subscribe) return; return D.subscribe(() => { _syncFromDMS(); const now = glOpenPeriod(); if (now !== lastOpen.current) { setVp(v => (v === lastOpen.current ? now : v)); lastOpen.current = now; } force(); }); }, []); useJEStore(); const openJe = (id) => setStack(s => [...s, { t: "je", id }]); const openAcct = (acct) => setStack(s => [...s, { t: "acct", acct, period: vp }]); const newJe = () => setStack(s => [...s, { t: "new", src: "Accounting" }]); // F-keys (only when no dialog is up). React.useEffect(() => { const onKey = (e) => { if (document.querySelector("[data-k2b-modal]")) return; const map = { F1: () => setModal("help"), F2: () => (!(window.DMS && window.DMS.can) || window.DMS.can("glEdit")) && newJe(), F6: () => (!(window.DMS && window.DMS.can) || window.DMS.can("period")) && setModal("period"), F7: () => printJournal(), F9: () => go && go("docentry") }; if (map[e.key]) { e.preventDefault(); map[e.key](); } }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }); const ps = glD() && glD().periodStatus ? glD().periodStatus() : null; const fig = glAcctFigures(vp); const periodJEs = glJEsIn(vp); const drafts = JEStore.getSession().length; const printJournal = () => { const r = glRange(vp); const rows = periodJEs.slice().reverse().map(je => `${glEsc(je.date)}${glEsc(je.id)}${glEsc(je.src || "")}${glEsc(je.desc || "")}${glMoney(glJeTotal(je))}`).join(""); glPrint("General Journal " + vp, `${window.dealerHeadHtml ? window.dealerHeadHtml() : ""}
GENERAL JOURNAL — ${glEsc(glPerLabel(vp))}
Period beginning ${r.startLabel} · ending ${r.endLabel} · ${periodJEs.length} entries
${rows || ''}
DateEntrySourceDescriptionAmount
No entries.
`); }; const tabBtn = (k, label) => ( setTab(k)} style={{ background: tab === k ? "#D0E2F0" : undefined, fontWeight: tab === k ? 700 : 400 }}>{label} ); return (
{/* RC20 (fops): office only — ROs / parts sales since RC16 with no journal entry, "Post it now" per item */} {window.FopsNotInBooks && } {/* Period bar — every label derives from DMS.periodStatus() / periodRange() */}
0) ? "#FBF3E0" : "#E7F5EA", borderBottom: "1px solid #808080", fontFamily: "'Courier Prime', monospace", fontSize: 12, color: "var(--k2b-navy)" }}> {ps && Open period: {ps.openLabel}} {ps && ps.inNewMonth && · POST-AHEAD active ({glPerLabel(ps.nowPeriod)} entries wait for {ps.openLabel} to close)} {ps && ps.aheadCount > 0 && · {ps.aheadCount} ahead (${glMoney(ps.aheadTotal)})} {ps && (!(window.DMS && window.DMS.can) || window.DMS.can("period")) && setModal("period")}>{`Close ${ps.openLabel}…`}}
{tabBtn("journal", `Journal Entries (${periodJEs.length})`)} {tabBtn("ledger", "Chart of Accounts / Trial Balance")} {tabBtn("sched", "Schedule Detail")} {tabBtn("parts", "Parts")} {tabBtn("coa", "COA Manager")}
{(!(window.DMS && window.DMS.can) || window.DMS.can("glEdit")) ? + New Journal Entry : View only — your role can't change the ledger}
{tab === "ledger" && ( { setSchedAcct(acct); setTab("sched"); }} /> )} {tab === "coa" && glToast(m, "info")} />} {tab === "parts" && ( { setSchedAcct(acct); setTab("sched"); }} onJournal={() => setTab("journal")} go={go} /> )} {tab === "sched" && } {tab === "journal" && }
ESC-Close setModal("help")}>F1-Help {(!(window.DMS && window.DMS.can) || window.DMS.can("glEdit")) && F2-New Journal Entry} {(!(window.DMS && window.DMS.can) || window.DMS.can("period")) && setModal("period")}>F6-Period End} F7-Print Journal {drafts > 0 && setModal("post")}>{`F8-Post ${drafts} Draft${drafts === 1 ? "" : "s"}`}} go && go("docentry")}>F9-Doc Entry
force()} /> {modal === "help" && setModal(null)} />} {modal === "period" && setModal(null)} onChanged={(p) => { if (p) setVp(p); force(); }} />} {modal === "post" && setModal(null)} onPost={(n) => { setModal(null); glToast(n ? `${n} draft ${n === 1 ? "entry" : "entries"} posted to the GL.` : "No drafts could be posted (unbalanced or closed period).", n ? "ok" : "warn"); }} />}
); }; // ─── Chart of Accounts / trial balance for the viewed period ────────────── const LedgerTab = ({ vp, fig, sel, setSel, onOpenAcct, onOpenJe, onSchedule }) => { const [q, setQ] = React.useState(""); const [activeOnly, setActiveOnly] = React.useState(true); const chart = glChart(); const known = new Set(chart.map(a => String(a.acct))); // accounts that have activity but aren't in the chart still show (flagged) const extra = Object.keys(fig).filter(k => !known.has(k)).map(k => ({ acct: k, name: glAcctName(k) || "(not in chart)", type: "?", bal: 0, _orphan: true })); const rows = chart.concat(extra).map(a => { const f = fig[String(a.acct)] || { before: 0, dr: 0, cr: 0 }; const beg = (+a.bal || 0) + f.before; return { ...a, beg, dr: f.dr, cr: f.cr, end: beg + f.dr - f.cr }; }).filter(r => { if (activeOnly && !(r.dr || r.cr || Math.abs(r.beg) > 0.004)) return false; if (!q) return true; const s = q.toLowerCase(); return String(r.acct).toLowerCase().includes(s) || String(r.name || r.desc || "").toLowerCase().includes(s); }).sort((x, y) => String(x.acct).localeCompare(String(y.acct), undefined, { numeric: true })); const tot = rows.reduce((t, r) => ({ beg: t.beg + r.beg, dr: t.dr + r.dr, cr: t.cr + r.cr, end: t.end + r.end }), { beg: 0, dr: 0, cr: 0, end: 0 }); const allDr = Object.values(fig).reduce((s, f) => s + f.dr, 0), allCr = Object.values(fig).reduce((s, f) => s + f.cr, 0); const inBal = Math.abs(allDr - allCr) < 0.005; const cur = sel ? rows.find(r => String(r.acct) === String(sel)) || null : null; const curLines = []; if (cur) glJEsIn(vp).forEach(je => (je.lines || []).forEach(l => { if (String(l.acct) === String(cur.acct)) curLines.push({ je, l }); })); const hasSched = (acct) => typeof SCHEDULES !== "undefined" && !!SCHEDULES[acct]; const num = { textAlign: "right", whiteSpace: "nowrap" }; return (
Find: setQ(e.target.value)} placeholder="account # or name…" style={{ width: 220, background: "#FFFFE1", border: "1px solid #808080", padding: "1px 6px", fontFamily: "'Courier Prime', monospace" }} /> {glPerLabel(vp)} debits {glMoney(allDr)} · credits {glMoney(allCr)} · {inBal ? "IN BALANCE" : "OUT by " + glMoney(Math.abs(allDr - allCr))}
{rows.length === 0 && ( )} {rows.map(r => { const on = cur && String(cur.acct) === String(r.acct); return ( setSel(r.acct)} onDoubleClick={() => onOpenAcct(r.acct)} style={{ cursor: "pointer", background: on ? "#D0E2F0" : undefined, color: "var(--k2b-blue-data-dk)", outline: on ? "1px solid #1F4E8A" : "none" }}> ); })}
Acct #Account NameType BeginningDebitsCreditsEnding
{activeOnly ? `No account has a balance or activity through ${glPerLabel(vp)} yet. Untick "Only accounts with a balance or activity" to see the whole chart.` : "No accounts match."}
{r.acct} {r.name || r.desc}{r._orphan ? " ⚠" : ""} {r.type} {glMoney(r.beg)} {r.dr ? glMoney(r.dr) : ""} {r.cr ? glMoney(r.cr) : ""} {glMoney(r.end)}
Totals ({rows.length} accounts shown) {glMoney(tot.beg)} {glMoney(tot.dr)} {glMoney(tot.cr)} {glMoney(tot.end)}
{/* Detail panel */}
{!cur ? (
Click an account to see its {glPerLabel(vp)} activity here. Double-click (or "Account Ledger ▸") opens the full ledger with a running balance; every line opens its journal entry.
) : ( <>
{cur.acct} {cur.name || cur.desc}
Type:{cur.type} Beginning:{glMoney(cur.beg)} Debits:{glMoney(cur.dr)} Credits:{glMoney(cur.cr)} Ending:{glMoney(cur.end)}
{curLines.length === 0 && } {curLines.map((x, i) => ( onOpenJe(x.je.id)} style={{ cursor: "pointer" }} title={x.je.desc}> ))}
DateEntryDebitCredit
No activity in {glPerLabel(vp)}.
{x.je.date} {x.je.id} {+x.l.dr ? glMoney(x.l.dr) : ""} {+x.l.cr ? glMoney(x.l.cr) : ""}
onOpenAcct(cur.acct)}>Account Ledger ▸ {hasSched(cur.acct) && onSchedule(cur.acct)}>Schedule ▸}
)}
); }; // ─── Schedule Detail tab ────────────────────────────────────────────────── const ScheduleTab = ({ acct, setAcct, vp, onOpenAcct }) => { const grouped = (typeof SCHED_INDEX !== "undefined" ? SCHED_INDEX : []).reduce((m, s) => { (m[s.group] = m[s.group] || []).push(s); return m; }, {}); const exportCsv = () => { const sch = typeof SCHEDULES !== "undefined" ? SCHEDULES[acct] : null; if (!sch || !sch.rows || !sch.rows.length) { glToast(`Schedule ${acct} has no rows to export.`, "info"); return; } const cols = Object.keys(sch.rows[0]); const text = [cols.join(",")].concat(sch.rows.map(r => cols.map(c => `"${String(r[c] == null ? "" : r[c]).replace(/"/g, "'")}"`).join(","))).join("\n"); glDownload(`schedule_${acct}.csv`, text); }; return (
Schedule Index
{Object.entries(grouped).map(([group, items]) => (
{group}
{items.map(s => (
setAcct(s.acct)} style={{ padding: "2px 8px", cursor: "pointer", background: acct === s.acct ? "var(--k2b-navy)" : "transparent", color: acct === s.acct ? "#FFFFFF" : "var(--k2b-blue-data-dk)" }}>
{s.acct}
{s.title}
))}
))}
{typeof ScheduleView !== "undefined" ? :
Schedules module not loaded.
}
onOpenAcct(acct)}>{`GL ${acct} Ledger (${glPerShort(vp)}) ▸`} Export CSV
); }; // ─── Chart of Accounts Manager tab ──────────────────────────────────────── // Lets the user add new accounts (from the full GMSAM library or custom), // edit existing account numbers/names/balances, and delete accounts they // don't use. Persists to localStorage. const CoaManagerTab = ({ toast }) => { const [, force] = React.useReducer(n => n + 1, 0); const [search, setSearch] = React.useState(""); const [mode, setMode] = React.useState("working"); // "working" | "library" const [edit, setEdit] = React.useState(null); // {acct, name, type, bal, sched} or {new:true} // Live-update when another station edits the COA. React.useEffect(() => { if (typeof window === "undefined" || !window.DMS || !window.DMS.subscribe) return; return window.DMS.subscribe(() => { if (_syncFromDMS()) force(); }); }, []); const types = ["Asset", "Contra", "Liability", "Equity", "Sales", "Cost", "F&I", "Expense"]; const filtered = (mode === "working" ? GL_ACCOUNTS : (window.GM_COA || []).map(r => ({ acct: r[0], name: r[1], type: r[2] === "A" ? "Asset" : r[2] === "CA" ? "Contra" : r[2] === "L" ? "Liability" : r[2] === "E" ? "Equity" : r[2] === "R" ? "Sales" : r[2] === "COGS" ? "Cost" : r[2] === "X" ? "Expense" : r[2], bal: 0, sched: false, fromGm: true, group: r[4], }))).filter(r => { if (!search) return true; const q = search.toLowerCase(); return r.acct.toLowerCase().includes(q) || r.name.toLowerCase().includes(q); }); const _dms = () => (typeof window !== "undefined" ? window.DMS : null); const saveEdit = (row) => { if (!row.acct || !row.name) { toast("Account # and name are required."); return; } let saved = null; if (edit && edit.new) { // Adding — check for dup if (GL_ACCOUNTS.find(a => a.acct === row.acct)) { toast(`Account ${row.acct} already exists.`); return; } saved = { acct: row.acct, name: row.name, type: row.type || "Asset", bal: parseFloat(String(row.bal || "0").replace(/[^0-9.\-]/g, "")) || 0, sched: !!row.sched, }; GL_ACCOUNTS.push(saved); toast(`Account ${row.acct} added.`); } else if (edit) { // Updating existing const i = GL_ACCOUNTS.findIndex(a => a.acct === edit.acct); if (i >= 0) { saved = { acct: row.acct, name: row.name, type: row.type || GL_ACCOUNTS[i].type, bal: parseFloat(String(row.bal).replace(/[^0-9.\-]/g, "")) || 0, sched: !!row.sched, }; GL_ACCOUNTS[i] = saved; } toast(`Account ${row.acct} updated.`); } persistCoa(); if (saved && _dms() && _dms().upsertGlAccount) _dms().upsertGlAccount(saved); setEdit(null); force(); }; const removeAcct = (acct) => { if (!window.confirm(`Remove account ${acct} from working COA? (You can re-add it from the GM library.)`)) return; const i = GL_ACCOUNTS.findIndex(a => a.acct === acct); if (i >= 0) GL_ACCOUNTS.splice(i, 1); persistCoa(); if (_dms() && _dms().deleteGlAccount) _dms().deleteGlAccount(acct); force(); toast(`Account ${acct} removed.`); }; const addFromLibrary = (row) => { if (GL_ACCOUNTS.find(a => a.acct === row.acct)) { toast(`Account ${row.acct} is already in your working COA.`); return; } const rec = { acct: row.acct, name: row.name, type: row.type, bal: 0, sched: false }; GL_ACCOUNTS.push(rec); persistCoa(); if (_dms() && _dms().upsertGlAccount) _dms().upsertGlAccount(rec); force(); toast(`Account ${row.acct} added to working COA.`); }; const resetToDefault = () => { if (!window.confirm("Reset working chart of accounts to factory defaults? Your customizations will be lost.")) return; GL_ACCOUNTS.length = 0; GL_ACCOUNTS_DEFAULT.forEach(a => GL_ACCOUNTS.push({ ...a })); persistCoa(); // persistCoa already routes through DMS.resetGlAccounts when DMS is wired. force(); toast("Working COA reset to defaults."); }; return (
setMode("working")} style={{ background: mode === "working" ? "#D0E2F0" : undefined, fontWeight: mode === "working" ? 700 : 400 }}> 📒 Working COA ({GL_ACCOUNTS.length}) setMode("library")} style={{ background: mode === "library" ? "#D0E2F0" : undefined, fontWeight: mode === "library" ? 700 : 400 }}> 📚 GM Standard Library ({(window.GM_COA || []).length}) Find: setSearch(e.target.value)} placeholder="account # or name…" style={{ width: 240, background: "var(--k2b-yellow)", border: "1px solid #808080", color: "var(--k2b-navy)", padding: "1px 6px", fontFamily: "'Courier Prime', monospace" }} />
{mode === "working" && ( <> setEdit({ acct: "", name: "", type: "Asset", bal: 0, sched: false, new: true })}> + Add Account Reset to Default )}
{filtered.length === 0 && ( )} {filtered.map((r, i) => ( ))}
Acct # Account Name Type {mode === "working" ? "Balance" : "Section"} Sch
No matches.
{r.acct} {r.name} {r.type} {mode === "working" ? fmt$(r.bal) : (r.group || "")} {r.sched ? "●" : "·"} {mode === "working" ? ( <> ) : ( )}
Tip: Switch to the GM Standard Library view to pull any of the ~250 accounts from the official GM-US-AC v3.0 chart into your working COA. Edits persist on this workstation.
{edit && ( setEdit(null)} /> )}
); }; // COA add/edit dialog const CoaEditDialog = ({ row, types, onSave, onClose }) => { const [acct, setAcct] = React.useState(row.acct || ""); const [name, setName] = React.useState(row.name || ""); const [type, setType] = React.useState(row.type || "Asset"); const [bal, setBal] = React.useState(String(row.bal ?? 0)); const [sched, setSched] = React.useState(!!row.sched); return (
{ e.currentTarget.__k2bDownOnBackdrop = (e.target === e.currentTarget); }} onClick={(e) => { const d = e.currentTarget.__k2bDownOnBackdrop; e.currentTarget.__k2bDownOnBackdrop = false; if (d && e.target === e.currentTarget) onClose && onClose(); }}>
e.stopPropagation()} className="k2b-window" style={{ width: 460, background: "#C0C0C0" }}> {}} onMax={() => {}} />
Account #: setAcct(e.target.value.toUpperCase())} disabled={!row.new} style={{ background: row.new ? "var(--k2b-yellow)" : "#E8E8E8", border: "1px solid #808080", padding: "2px 6px", fontFamily: "'Courier Prime', monospace", fontWeight: 700, color: row.new ? "var(--k2b-navy)" : "#666" }} /> Name: setName(e.target.value)} style={{ background: "var(--k2b-yellow)", border: "1px solid #808080", padding: "2px 6px", fontFamily: "'Courier Prime', monospace", color: "var(--k2b-navy)" }} /> Type: Opening Balance: setBal(e.target.value)} style={{ background: "var(--k2b-yellow)", border: "1px solid #808080", padding: "2px 6px", fontFamily: "'Courier Prime', monospace", textAlign: "right", color: "var(--k2b-blue-data-dk)" }} /> Has Schedule:
ESC-Cancel onSave({ acct, name, type, bal, sched })} style={{ background: "var(--k2b-yellow)", fontWeight: 700 }}> F10-{row.new ? "Add" : "Save"} Account
); }; // ─── Journal Entries tab ────────────────────────────────────────────────── const JournalTab = ({ vp, entries, onOpenJe, onNewJE }) => { const [q, setQ] = React.useState(""); const [srcF, setSrcF] = React.useState(""); const srcs = Array.from(new Set(entries.map(j => j.src || "—"))).sort(); const list = entries.filter(j => { if (srcF && (j.src || "—") !== srcF) return false; if (!q) return true; const s = q.toLowerCase(); return [j.id, j.desc, j.ref, j.src, j.by, j.date].some(v => String(v || "").toLowerCase().includes(s)) || (j.lines || []).some(l => String(l.acct).toLowerCase() === s); }).slice().sort((a, b) => { const da = glParseDate(a.date) || 0, db = glParseDate(b.date) || 0; return db - da; // newest first (stable for same-day → DMS order) }); const total = list.reduce((s, j) => s + glJeTotal(j), 0); const num = { textAlign: "right", whiteSpace: "nowrap" }; return (
Find: setQ(e.target.value)} placeholder="entry, description, ref, account #…" style={{ width: 260, background: "#FFFFE1", border: "1px solid #808080", padding: "1px 6px", fontFamily: "'Courier Prime', monospace" }} /> {list.length} entr{list.length === 1 ? "y" : "ies"} · ${glMoney(total)}
{entries.length === 0 && ( )} {entries.length > 0 && list.length === 0 && ( )} {list.map(je => ( onOpenJe(je.id)} style={{ cursor: "pointer", opacity: je.reversedBy ? 0.7 : 1 }} title="Open this journal entry"> ))}
EntryDateSourceJrn RefDescriptionAmountStatus
No journal entries in {glPerLabel(vp)} yet. Posting a deal, repair order, parts ticket or payroll writes here automatically — or { e.preventDefault(); onNewJE(); }}>post a manual journal entry.
No entries match.
{je.id} {je.date} {je.src} {je.jrn} {je.ref || ""} {je.desc} {glMoney(glJeTotal(je))} {glJeChips(je).map(([t, c]) => )}
); }; // ─── Modals ─────────────────────────────────────────────────────────────── const AcctHelpDialog = ({ onClose }) => (
Keys
F1 This help. F2 New journal entry (balanced before it can post). F6 Period End — month-end checklist, close the open month, set / reopen periods (admin). F7 Print the journal for the period you're viewing. F9 Doc Entry — journal-by-journal document entry. ESC Close the top dialog / this window.
Tips
• ◀ ▶ or the month box changes the period you're VIEWING. Only F6 changes the OPEN period.
• Click any journal row to open it: lines with account names, Open Deal, Reverse, Edit / Correct, Print.
• Click an account number anywhere to open its ledger with a running balance.
• Posted entries are never changed in place: every correction is a reversing entry plus a corrected copy (the server enforces it).
); // F6 — month-end: live checklist, real close, admin period controls. const PeriodEndDialog = ({ onClose, onChanged }) => { useGlLive(); const D = glD(); const ps = D && D.periodStatus ? D.periodStatus() : { open: glNowPeriod(), openLabel: glPerLabel(glNowPeriod()), closed: [] }; const checks = D && D.monthEndChecklist ? D.monthEndChecklist() : []; const isOk = (c) => (c.ok != null ? c.ok : c.done); const exceptions = checks.filter(c => !isOk(c)).length; const r = glRange(ps.open); const admin = !!(D && ((D.can && D.can("period")) || (D.isAdmin && D.isAdmin()))); const [result, setResult] = React.useState(null); const [setTo, setSetTo] = React.useState(ps.open); const [reopen, setReopen] = React.useState(((ps.closed || []).slice().sort().pop()) || ""); const run = (fn, nextPer) => { const res = fn(); setResult(res); glToast(res.msg, res.ok ? "ok" : "warn", 5200); if (res.ok) onChanged && onChanged(nextPer ? nextPer() : null); }; const closeIt = () => { if (!window.confirm(`Close ${ps.openLabel}?\n\n${exceptions ? exceptions + " checklist exception(s) are still open — they will be noted on the close.\n\n" : ""}This locks ${ps.openLabel} (${r.startLabel} – ${r.endLabel}), makes ${glPerLabel(glPerShift(ps.open, 1))} the open period and releases post-ahead entries.`)) return; run(() => D.closeMonth(), () => glOpenPeriod()); }; const lab = { color: "var(--k2b-maroon)", fontWeight: 700 }; return ( Close {`Close ${ps.openLabel} →`} }>
Open period {ps.openLabel} — period beginning {r.startLabel} · ending {r.endLabel}
{(ps.closed || []).length > 0 &&
Closed {ps.closed.slice().sort().reverse().map(glPerShort).join(" · ")}
}
Month-end checklist
{checks.length === 0 &&
Checklist unavailable.
} {checks.map(c => (
{isOk(c) ? "✓" : "⚠"} {c.label} {c.detail}
))}
{exceptions ? `${exceptions} exception${exceptions === 1 ? "" : "s"} — you can still close; they are recorded in the audit log.` : "All clear — ready to close."}
{result && (
{result.msg}
)}
Period administration {admin ? "" : "(admin only)"}
Set open period… setSetTo(e.target.value)} style={{ fontFamily: "'Courier Prime', monospace", border: "1px solid #808080", background: "#FFFFE1" }} /> { if (!admin) { glToast("Only an administrator can change the open period.", "warn"); return; } if (!window.confirm(`Make ${glPerLabel(setTo)} the open period?\n\nAny closed month at or after it is reopened.`)) return; run(() => D.setAcctPeriod(setTo), () => setTo); }}>Set Open Period Reopen period { if (!admin) { glToast("Only an administrator can reopen a period.", "warn"); return; } if (!reopen) return; if (!window.confirm(`Reopen ${glPerLabel(reopen)}? Entries can be posted and edited there again.`)) return; run(() => D.reopenPeriod(reopen), () => reopen); }}>Reopen
); }; // Legacy draft entries (older builds parked unposted drafts in the JEStore). // Each one posts through DMS.postJournalEntry — balanced + period-checked. const PostJournalDialog = ({ onClose, onPost }) => { const store = useJEStore(); const pending = store.getSession(); return ( { if (window.confirm(`Discard ${pending.length} draft(s)?`)) { store.clearSession(); onClose(); } }}>Discard Drafts Cancel onPost(pending.length ? store.releaseAll() : 0)}>Post All }> {pending.map(p => ( ))}
RefDateDescriptionAmount
{p.id}{p.date}{p.desc}{glMoney(glJeTotal(p))}
); }; Object.assign(window, { Accounting, GL_ACCOUNTS, JOURNAL_ENTRIES, JEStore, JeDetailModal, JeEditorModal, AcctLedgerModal, GlModals, AcctPeriodPicker, PeriodEndDialog, glPerLabel, glRange, glOpenPeriod, glJEsIn, });