/* 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 — Document Entry (DocEntry).
// Faithful recreation of the Doc Entry screen + working journal-entry
// workflow:
// • Post-Ahead Y/N toggles
// • Document Date is editable
// • "Process Docs" opens a real journal-entry form for the selected
// document type (pre-loaded with the canonical distribution)
// • "Setup Journal" opens the per-journal distribution-rule editor
const DOC_TYPES = [
["CAR SALES", 10],
["SERVICE TICKETS", 30],
["SERVICE TICKETS", 31],
["BODY SHOP", 32],
["PARTS TICKETS", 35],
["Old Checking", 40],
["Current Checking", 41],
["CASH RECEIPTS", 50],
["DEPOSITS", 55],
["EFT DEPOSIT", 56],
["NEW VEHICLE PURCHASE", 70],
["USED VEHICLE PURCHASE",71],
["GENERAL PURCHASE", 75],
["GENERAL JOURNAL", 80],
["FC JOURNAL", 81],
];
// Canonical chart-of-accounts distribution per document type
const JOURNAL_RULES = {
10: { name: "CAR SALES", ref: "CS", lines: [
{ acct: "231", side: "CR", note: "New car inventory relief (cost)" },
{ acct: "205", side: "DR", note: "Contracts in Transit" },
{ acct: "400A",side: "CR", note: "Vehicle sales revenue" },
{ acct: "310", side: "DR", note: "Floorplan payoff" },
]},
30: { name: "SERVICE TICKETS", ref: "ST", lines: [
{ acct: "460A",side: "CR", note: "Customer labor revenue" },
{ acct: "660A",side: "DR", note: "Cost — customer labor" },
{ acct: "220", side: "DR", note: "A/R or cash collected" },
]},
31: { name: "SERVICE TICKETS", ref: "ST", lines: [
{ acct: "462", side: "CR", note: "Warranty labor revenue" },
{ acct: "263", side: "DR", note: "Warranty claims receivable" },
]},
32: { name: "BODY SHOP", ref: "BS", lines: [
{ acct: "470", side: "CR", note: "Body shop labor" },
{ acct: "220", side: "DR", note: "Insurance / customer A/R" },
]},
35: { name: "PARTS TICKETS", ref: "PT", lines: [
{ acct: "242", side: "CR", note: "Parts inventory" },
{ acct: "220", side: "DR", note: "Parts A/R or cash" },
]},
40: { name: "OLD CHECKING", ref: "CK", lines: [
{ acct: "202B",side: "CR", note: "Checking — BNM Used Cars" },
]},
41: { name: "CURRENT CHECKING", ref: "CK", lines: [
{ acct: "202", side: "DR", note: "Operating cash" },
]},
50: { name: "CASH RECEIPTS", ref: "CR", lines: [
{ acct: "202", side: "DR", note: "Operating cash" },
{ acct: "220", side: "CR", note: "A/R relieved" },
]},
55: { name: "DEPOSITS", ref: "DP", lines: [
{ acct: "202", side: "DR", note: "Operating cash" },
{ acct: "331", side: "CR", note: "Customer deposits" },
]},
56: { name: "EFT DEPOSIT", ref: "EFT",lines: [
{ acct: "202", side: "DR", note: "Operating cash (EFT)" },
{ acct: "205", side: "CR", note: "Contracts funded" },
]},
70: { name: "NEW VEHICLE PURCHASE", ref: "NP", lines: [
{ acct: "231", side: "DR", note: "New vehicle inventory" },
{ acct: "310", side: "CR", note: "Floorplan payable" },
]},
71: { name: "USED VEHICLE PURCHASE",ref: "UP", lines: [
{ acct: "240", side: "DR", note: "Used vehicle inventory" },
{ acct: "314", side: "CR", note: "Used floorplan — Bank of NM" },
]},
75: { name: "GENERAL PURCHASE", ref: "AP", lines: [
{ acct: "242", side: "DR", note: "Parts / supplies" },
{ acct: "300", side: "CR", note: "A/P trade" },
]},
80: { name: "GENERAL JOURNAL", ref: "JE", lines: [
{ acct: "", side: "DR", note: "" },
{ acct: "", side: "CR", note: "" },
]},
81: { name: "FC JOURNAL", ref: "FC", lines: [
{ acct: "310", side: "DR", note: "Floorplan curtailment" },
{ acct: "202", side: "CR", note: "Operating cash" },
]},
};
// Common recurring General-Journal templates. Shown only when journal = 80.
// Templates carry ACCOUNTS only — amounts are always keyed by the user.
const GJ_TEMPLATES = [
{ id: "depr", label: "Monthly depreciation accrual",
desc: "Monthly depreciation — fixed assets",
lines: [
{ acct: "0831",side: "DR", note: "Depr — buildings & improvements", amt: 0 },
{ acct: "351", side: "CR", note: "Accum depr — buildings", amt: 0 },
]},
{ id: "comm", label: "Sales commission accrual",
desc: "Sales commission accrual",
lines: [
{ acct: "855", side: "DR", note: "F&I commission expense", amt: 0 },
{ acct: "321", side: "CR", note: "Wages payable", amt: 0 },
]},
{ id: "vsc", label: "VSC / GAP income recognition",
desc: "VSC income recognition",
lines: [
{ acct: "220", side: "DR", note: "VSC receivable", amt: 0 },
{ acct: "806", side: "CR", note: "Finance income — new", amt: 0 },
]},
{ id: "recon", label: "Used recon — parts to inventory",
desc: "Reclass — used recon (parts to inventory)",
lines: [
{ acct: "240", side: "DR", note: "Used vehicle inventory", amt: 0 },
{ acct: "242", side: "CR", note: "Parts inventory", amt: 0 },
]},
{ id: "accrue", label: "Accrued expense (manual)",
desc: "Accrued expense",
lines: [
{ acct: "", side: "DR", note: "Expense account", amt: 0 },
{ acct: "332", side: "CR", note: "Accrued liabilities", amt: 0 },
]},
{ id: "blank", label: "Blank (2 lines)",
desc: "",
lines: [
{ acct: "", side: "DR", note: "", amt: 0 },
{ acct: "", side: "CR", note: "", amt: 0 },
]},
];
// ─── Persistence ─────────────────────────────────────────────────────────
// Distribution rules are editable per-journal and persist to localStorage.
const DOC_RULES_KEY = "jbrews-doc-rules-v1";
const _loadDocRules = () => {
try { const r = localStorage.getItem(DOC_RULES_KEY); return r ? JSON.parse(r) : null; } catch (e) { return null; }
};
const _saveDocRules = (rules) => {
try { localStorage.setItem(DOC_RULES_KEY, JSON.stringify(rules)); } catch (e) {}
};
// Hydrate JOURNAL_RULES from localStorage at load time if user has saved customizations
(function _hydrateRules() {
const saved = _loadDocRules();
if (saved && typeof saved === "object") {
Object.keys(saved).forEach(k => {
if (JOURNAL_RULES[k]) Object.assign(JOURNAL_RULES[k], saved[k]);
});
}
})();
// Next free document number for a journal prefix (CS-0001, JE-0002 …) based on
// what is actually in the ledger — never a canned number.
const docNextRef = (prefix) => {
const all = (window.DMS && window.DMS.journalEntries ? window.DMS.journalEntries() : []);
const re = new RegExp("^" + prefix + "-(\\d+)$");
const max = all.reduce((m, j) => { const x = String(j.id).match(re); return x ? Math.max(m, +x[1]) : m; }, 0);
return prefix + "-" + String(max + 1).padStart(4, "0");
};
const DocEntry = ({ onClose, go }) => {
const D = window.DMS;
const [, force] = React.useReducer(n => n + 1, 0);
React.useEffect(() => (D && D.subscribe) ? D.subscribe(force) : undefined, []);
const [postAhead, setPostAhead] = React.useState("N");
// Document date defaults to TODAY (or the last day of the open period when
// today falls in a closed month) — never a hard-coded demo date.
const [docDate, setDocDate] = React.useState(() => glDefaultEntryIso());
const [docType, setDocType] = React.useState(10);
const [modal, setModal] = React.useState(null); // 'process' | 'setup'
const [stack, setStack] = React.useState([]); // shared GL modal stack (JE detail / ledger / edit)
const [flash, setFlash] = React.useState("");
const [viewPer, setViewPer] = React.useState(() => glOpenPeriod());
const toast = (msg) => { setFlash(msg); setTimeout(() => setFlash(""), 3200); };
const openPer = glOpenPeriod();
const docPer = String(docDate || "").slice(0, 7);
const docSt = /^\d{4}-\d{2}$/.test(docPer) ? glPerState(docPer) : "bad";
// Every document posts through DMS.postJournalEntry — balanced, period
// checked, audited, synced to every station. No drafts, no raw upserts.
const handlePost = (entry) => {
if (docSt === "ahead" && postAhead !== "Y") return { ok: false, msg: `${glPerLabel(docPer)} is after the open period (${glPerLabel(openPer)}). Set "Post Ahead" to Y to post into it.` };
const r = D.postJournalEntry({ id: entry.id, date: glUsFromIso(docDate), desc: entry.desc, ref: entry.ref || entry.id, jrn: entry.jrn, lines: entry.lines, src: "DocEntry" });
if (!r.ok) return r;
setModal(null);
toast(r.msg);
setStack([{ t: "je", id: r.id }]);
return r;
};
const drafts = (typeof JEStore !== "undefined") ? JEStore.getSession().length : 0;
const releaseDrafts = () => {
const n = JEStore.releaseAll();
toast(n ? `${n} legacy draft(s) posted to the GL.` : "No draft could be posted (unbalanced or closed period).");
};
const docJEs = glJEsIn(viewPer).filter(j => j.src === "DocEntry");
return (
{flash && (
✓ {flash}
)}
{/* LEFT — fields + this period's Doc Entry postings */}
Open period: {glPerLabel(openPer)} ({glRange(openPer).startLabel} – {glRange(openPer).endLabel})
Do you wish to Post Ahead:
setPostAhead(p => p === "Y" ? "N" : "Y")}
className="k2b-active-field" data-gl="doc-postahead"
style={{ width: 32, textAlign: "center", cursor: "pointer",
background: postAhead === "Y" ? "#D0E2F0" : undefined,
border: "1px solid #808080", padding: 0,
fontFamily: "'Courier Prime', monospace", fontSize: 15 }}>
{postAhead}
Y allows dates after the open period
Default Document Date:
setDocDate(e.target.value)}
style={{ background: "#FFFFE1", border: "1px solid #808080",
padding: "0 4px", color: "var(--k2b-blue-data-dk)",
fontFamily: "'Courier Prime', monospace", fontSize: 15 }} />
{docSt === "bad" ? "pick a date" : docSt === "closed" ? `${glPerLabel(docPer)} is CLOSED` : docSt === "ahead" ? (postAhead === "Y" ? `POST-AHEAD into ${glPerLabel(docPer)}` : `${glPerLabel(docPer)} is ahead — Post Ahead = N`) : `→ ${glPerLabel(docPer)}`}
Document Type (Journal):
{docType}
{JOURNAL_RULES[docType]?.name}
{/* Real Doc Entry postings for the viewed period — each opens the JE */}
Doc Entry postings ({docJEs.length})
setViewPer(glPerShift(viewPer, -1))}>◀
{" "}{glPerLabel(viewPer)}{" "}
setViewPer(glPerShift(viewPer, 1))}>▶
{docJEs.length === 0 ? (
No journal entries in {glPerLabel(viewPer)} from Doc Entry yet. Pick a journal at right, then
Process Docs .
) : (
Ref
Date
Jrn
Description
Amount
Status
{docJEs.map(p => (
setStack([{ t: "je", id: p.id }])} style={{ cursor: "pointer" }} title="Open this journal entry">
{p.id}
{p.date}
{p.jrn}
{p.desc}
{glMoney(glJeTotal(p))}
{glJeChips(p).map(([t, c]) => )}
))}
)}
{/* MIDDLE — document-type list */}
{DOC_TYPES.map(([name, code]) => (
setDocType(code)}
onDoubleClick={() => { setDocType(code); setModal("process"); }}
style={{
display: "grid", gridTemplateColumns: "1fr auto",
padding: "2px 12px", height: 26,
fontFamily: "'Courier Prime', monospace",
fontSize: 14, fontWeight: 400,
background: docType === code ? "#FFFFFF" : "#F0F0F0",
color: "var(--k2b-blue-data-dk)",
outline: docType === code ? "1px solid var(--k2b-maroon)" : "none",
outlineOffset: -3,
}}>
{name}
{code}
))}
{/* RIGHT — action buttons */}
◀ Back To Menu
setModal("process")}>▶ Process Docs
setModal("setup")}>🔧 Setup Journal
go && go("accounting")}>📒 Open Ledger
{drafts > 0 && {`✓ Post ${drafts} Draft${drafts === 1 ? "" : "s"}`} }
{modal === "process" && (
setModal(null)}
onPost={handlePost}
/>
)}
{modal === "setup" && (
setModal(null)}
onSave={(updatedLines) => {
// Persist the new distribution rules
JOURNAL_RULES[docType].lines = updatedLines;
const all = {};
Object.keys(JOURNAL_RULES).forEach(k => { all[k] = { lines: JOURNAL_RULES[k].lines }; });
_saveDocRules(all);
setModal(null);
toast(`Journal ${docType} distribution rules saved. (Persisted.)`);
}}
/>
)}
{typeof GlModals !== "undefined" && }
);
};
// ─── Process Docs dialog ─────────────────────────────────────────────────
const ProcessDocsDialog = ({ journal, rule, date, postAhead, refNo, onClose, onPost }) => {
// Pre-load lines from the rule
const [lines, setLines] = React.useState(() =>
(rule?.lines || []).map(l => ({ acct: l.acct, dr: 0, cr: 0, note: l.note, hint: l.side }))
);
const [err, setErr] = React.useState("");
const [desc, setDesc] = React.useState(rule?.name + " — " + date);
const [vendor, setVendor] = React.useState("");
const [tmpl, setTmpl] = React.useState("");
const isGJ = journal === 80;
const applyTemplate = (id) => {
setTmpl(id);
if (!id) return;
const t = GJ_TEMPLATES.find(t => t.id === id);
if (!t) return;
if (t.desc) setDesc(t.desc);
setLines(t.lines.map(l => ({
acct: l.acct,
dr: l.side === "DR" && l.amt ? l.amt.toFixed(2) : 0,
cr: l.side === "CR" && l.amt ? l.amt.toFixed(2) : 0,
note: l.note, hint: l.side,
})));
};
const setLine = (i, k, v) => setLines(arr => arr.map((l, j) => j === i ? { ...l, [k]: v } : l));
const drTotal = lines.reduce((s, l) => s + (parseFloat(l.dr) || 0), 0);
const crTotal = lines.reduce((s, l) => s + (parseFloat(l.cr) || 0), 0);
const unknown = lines.filter(l => l.acct && ((parseFloat(l.dr) || 0) || (parseFloat(l.cr) || 0)) && !glAcctKnown(l.acct)).map(l => l.acct);
const noAcct = lines.some(l => !l.acct && ((parseFloat(l.dr) || 0) || (parseFloat(l.cr) || 0)));
const balanced = Math.abs(drTotal - crTotal) < 0.005 && drTotal > 0;
const canPost = balanced && !unknown.length && !noAcct;
const submit = () => {
if (!canPost) return;
const r = onPost({
id: refNo,
ref: vendor ? refNo + " · " + vendor : refNo,
jrn: journal,
desc: vendor ? `${desc} — ${vendor}` : desc,
lines: lines.map(l => ({ acct: String(l.acct).trim(), dr: parseFloat(l.dr) || 0, cr: parseFloat(l.cr) || 0, desc: l.note || "" })),
});
if (r && !r.ok) setErr(r.msg);
};
const acctOptions = (typeof glChart !== "undefined" ? glChart() : (typeof GL_ACCOUNTS !== "undefined" ? GL_ACCOUNTS : []));
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: 820, background: "#C0C0C0" }}>
{}} onMax={() => {}} />
Reference..:
{refNo}
Date.....:
{date}
{postAhead && POST-AHEAD }
Jrn.....:
{journal}
Description:
setDesc(e.target.value)}
style={{ flex: 1, background: "var(--k2b-yellow)", border: "1px solid #808080",
padding: "0 4px", fontFamily: "'Courier Prime', monospace",
color: "var(--k2b-navy)" }} />
{(journal === 70 || journal === 71 || journal === 75 || journal === 81) && (
Vendor/Stk:
setVendor(e.target.value)}
placeholder={journal === 75 ? "Vendor name" : "Stock number"}
style={{ width: 240, background: "var(--k2b-yellow)", border: "1px solid #808080",
padding: "0 4px", fontFamily: "'Courier Prime', monospace",
color: "var(--k2b-navy)" }} />
)}
{isGJ && (
Template..:
applyTemplate(e.target.value)}
style={{ flex: 1, background: "var(--k2b-yellow)", border: "1px solid #808080",
padding: "0 4px", fontFamily: "'Courier Prime', monospace",
color: "var(--k2b-navy)", fontWeight: 700 }}>
— Pick a recurring GJ template or build from scratch —
{GJ_TEMPLATES.map(t => (
{t.label}
))}
templates pre-fill the distribution; you can still edit before posting
)}
{/* Distribution */}
Acct
Description / Note
Debit
Credit
{lines.map((l, i) => (
setLine(i, "acct", e.target.value)}
list="acct-list"
style={{ width: "100%", background: "var(--k2b-yellow)", border: "1px solid #808080",
padding: "0 4px", fontFamily: "'Courier Prime', monospace",
color: "var(--k2b-navy)", fontWeight: 700 }} />
setLine(i, "note", e.target.value)}
placeholder={l.hint === "DR" ? "Debit line note…" : "Credit line note…"}
style={{ width: "100%", background: "#FFFFFF", border: "1px solid #C0C0C0",
padding: "0 4px", fontFamily: "'Courier Prime', monospace",
color: "var(--k2b-navy)" }} />
setLine(i, "dr", e.target.value.replace(/[^0-9.]/g, ""))}
style={{ width: "100%", textAlign: "right", background: l.hint === "DR" ? "var(--k2b-yellow)" : "#FFFFFF",
border: "1px solid #808080", padding: "0 4px",
fontFamily: "'Courier Prime', monospace", color: "var(--k2b-navy)" }} />
setLine(i, "cr", e.target.value.replace(/[^0-9.]/g, ""))}
style={{ width: "100%", textAlign: "right", background: l.hint === "CR" ? "var(--k2b-yellow)" : "#FFFFFF",
border: "1px solid #808080", padding: "0 4px",
fontFamily: "'Courier Prime', monospace", color: "var(--k2b-navy)" }} />
))}
{/* Totals row */}
—
Totals · {balanced ? BALANCED : {drTotal || crTotal ? `Out of balance by $${Math.abs(drTotal - crTotal).toFixed(2)}` : "enter amounts"} }
{drTotal.toFixed(2)}
{crTotal.toFixed(2)}
{(unknown.length > 0 || noAcct || err) && (
{noAcct &&
⚠ Every amount needs an account.
}
{unknown.length > 0 &&
⚠ Not in the chart of accounts: {unknown.join(", ")}
}
{err &&
⚠ {err}
}
)}
{acctOptions.map(a => {a.acct} — {a.name || a.desc} )}
ESC-Cancel
setLines(arr => [...arr, { acct: "", dr: 0, cr: 0, note: "" }])}>F3-Add Line
{
// Auto-balance: copy DR total into the first empty CR (or vice versa)
setLines(arr => {
const dr = arr.reduce((s, l) => s + (parseFloat(l.dr) || 0), 0);
const cr = arr.reduce((s, l) => s + (parseFloat(l.cr) || 0), 0);
const diff = dr - cr;
if (Math.abs(diff) < 0.005) return arr;
const idx = diff > 0
? arr.findIndex(l => !(parseFloat(l.cr) > 0))
: arr.findIndex(l => !(parseFloat(l.dr) > 0));
if (idx < 0) return arr;
const next = [...arr];
next[idx] = { ...next[idx], [diff > 0 ? "cr" : "dr"]: Math.abs(diff).toFixed(2) };
return next;
});
}}>F4-Balance
F10-Post Entry
);
};
// ─── Setup Journal dialog ─────────────────────────────────────────────────
const SetupJournalDialog = ({ journal, rule, onClose, onSave }) => {
const [lines, setLines] = React.useState(() => (rule?.lines || []).map(l => ({ ...l })));
const setLine = (i, k, v) => setLines(arr => arr.map((l, j) => j === i ? { ...l, [k]: v } : l));
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: 680, background: "#C0C0C0" }}>
{}} onMax={() => {}} />
{rule?.name} — default GL distribution
Account
Side
Default Note
{lines.map((l, i) => (
setLine(i, "acct", e.target.value)}
style={{ width: "100%", background: "var(--k2b-yellow)", border: "1px solid #808080",
padding: "0 4px", fontFamily: "'Courier Prime', monospace",
color: "var(--k2b-navy)", fontWeight: 700 }} />
setLine(i, "side", e.target.value)}
style={{ width: "100%", background: "var(--k2b-yellow)", border: "1px solid #808080",
fontFamily: "'Courier Prime', monospace",
color: "var(--k2b-navy)" }}>
DR (Debit)
CR (Credit)
setLine(i, "note", e.target.value)}
style={{ width: "100%", background: "#FFFFFF", border: "1px solid #C0C0C0",
padding: "0 4px", fontFamily: "'Courier Prime', monospace",
color: "var(--k2b-navy)" }} />
setLines(arr => arr.filter((_, j) => j !== i))}
style={{ background: "#F0F0F0", border: "1px solid #808080",
color: "var(--k2b-maroon)", fontFamily: "'Courier Prime', monospace",
cursor: "pointer" }}
title="Remove line">×
))}
setLines(arr => [...arr, { acct: "", side: "DR", note: "" }])}>F3-Add Line
ESC-Cancel
onSave(lines)}
style={{ background: "var(--k2b-yellow)", fontWeight: 700 }}>F10-Save Rules
);
};
Object.assign(window, { DocEntry, DOC_TYPES, JOURNAL_RULES, GJ_TEMPLATES });