/* 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. */
// JBREWS DMS — Check Writing (vendor payments). Manage a VENDOR MASTER, set
// CHECK SEQUENCING (next #, bank account), write checks against open payables or
// any vendor/payee, and keep a check register. Every check posts to the GL
// (DR target acct / CR bank cash) so the ledger always ties out.
// (ldg, RC16) The SERVER writes and voids a check in one transaction (DMS.writeCheck / DMS.voidCheck → POST /api/checks/post
// and /void): the number, the check, its entry, the invoice and the bank rec commit together or not at all; printing is
// offered only after the server confirmed. Register rows are keyed by the check's id (bank + number) — two bank accounts
// may use the same numbers — and a reprint uses the print data stored with the check.
const ckId = (c) => (c && c.id != null && c.id !== "" ? String(c.id) : String(c && c.num));
const ckMoney = (n) => "$" + (Number(n) || 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const CkField = ({ label, children }) => {label} {children} ;
const CK_TONE = { Outstanding: { bg: "var(--jb-blue-tint,#E5EFFA)", fg: "var(--jb-blue,#1B66A8)" }, Cleared: { bg: "var(--jb-green-tint,#E6F7EC)", fg: "#0E7A37" }, Void: { bg: "#FBE9E7", fg: "#B23A2E" } };
const ckInp = { width: "100%", boxSizing: "border-box", padding: "7px 9px", border: "1px solid #C8CCD2", borderRadius: 4, fontSize: 13 };
const ckAcctName = (a) => { const coa = (window.GM_COA || []); const h = coa.find(e => String(Array.isArray(e) ? e[0] : e.acct) === String(a)); return h ? (Array.isArray(h) ? h[1] : h.name) : ""; };
const CK_VENDOR_TYPES = ["Trade", "OEM / Parts", "Floorplan", "Advertising", "Utilities", "Sublet", "Tax / Gov", "Other"];
const CK_STARTER_VENDORS = [
{ name: "GM PARTS (PDC)", type: "OEM / Parts", terms: "Net 30", defaultAcct: "242", phone: "800-555-0100" },
{ name: "ACDELCO", type: "OEM / Parts", terms: "Net 30", defaultAcct: "242" },
{ name: "GM FINANCIAL — FLOORPLAN", type: "Floorplan", terms: "Due on Demand", defaultAcct: "310" },
{ name: "RIVERBEND ADVERTISING", type: "Advertising", terms: "Net 15", defaultAcct: "800" },
{ name: "AMEREN MISSOURI", type: "Utilities", terms: "Net 21", defaultAcct: "810" },
{ name: "MO DEPT OF REVENUE", type: "Tax / Gov", terms: "On Receipt", defaultAcct: "324A" },
];
const CheckWriting = ({ onClose, go }) => {
const D = window.DMS;
const [, force] = React.useReducer(n => n + 1, 0);
React.useEffect(() => (D && D.subscribe) ? D.subscribe(force) : undefined, []);
const act = (r) => { window.k2bToast && window.k2bToast(r.msg, r.ok ? "ok" : "warn", 3600); return r; };
const payables = (D && D.payables) ? D.payables().filter(p => p.status === "Open") : [];
const checks = (D && D.checks) ? D.checks() : [];
const vendors = (D && D.vendors) ? D.vendors() : [];
const cfg = (D && D.checkConfig) ? D.checkConfig() : { nextNum: 48300, bankAcct: "202", bankName: "Operating" };
const [tab, setTab] = React.useState("pay"); // pay | vendors
const [modal, setModal] = React.useState(null);
const [sel, setSel] = React.useState({});
const [startPos, setStartPos] = React.useState(1);
const canSetup = !!(D && ((D.isAdmin && D.isAdmin()) || (D.can && D.can("acct"))));
const defBank = (cfg.accounts || []).find(a => a.id === cfg.defaultId) || (cfg.accounts || [])[0] || {};
const selIds = Object.keys(sel).filter(k => sel[k]);
const selChecks = checks.filter(c => selIds.includes(ckId(c)) && c.status !== "Void");
const printList = (list) => {
const again = list.filter(c => c.printCount);
if (again.length && !window.confirm(`Check #${again.map(c => c.num).join(", #")} ${again.length === 1 ? "was" : "were"} already printed. A reprint uses the same check data and makes no new payment, but it is a second paper check with the same number — destroy the first copy. Print anyway?`)) return;
const banks = new Set(list.map(c => c.bankId || cfg.defaultId));
const stock = banks.size === 1 ? (((cfg.accounts || []).find(a => a.id === Array.from(banks)[0]) || defBank).stock || "top") : "top";
window.k2bPrintChecks && window.k2bPrintChecks(list, { stock, startPos });
setSel({});
};
const openAP = payables.reduce((s, p) => s + (+p.amount || 0), 0);
const outstanding = checks.filter(c => c.status === "Outstanding").reduce((s, c) => s + c.amount, 0);
const cashBal = (D && D.bankRec) ? (D.bankRec().bookBalance || 0) : 0;
const TabBtn = ({ id, children }) => (
setTab(id)}
style={tab === id ? { background: "var(--jb-navy, #1B66A8)", color: "#fff", fontWeight: 700 } : {}}>{children}
);
return (
{[["Open Payables", ckMoney(openAP), "var(--jb-navy)"], ["Outstanding Checks", ckMoney(outstanding), "var(--jb-blue)"],
["Book Cash (" + cfg.bankAcct + ")", ckMoney(cashBal), "var(--jb-green)"], ["Next Check # · " + (defBank.label || ""), cfg.nextNum, "var(--jb-navy)"]].map(([l, v, c], i) => (
))}
setModal({ type: "write" })}>✎ Write Check
setModal({ type: "setup" })}>⚙ Check Setup{defBank.routing && defBank.account ? "" : " ⚠"}
{/* tab strip */}
Pay & Register
Vendors ({vendors.length})
go && go("vendoraccts")}>🏪 Vendor Accounts (history, limits) →
{tab === "vendors" &&
setModal({ type: "vendor" })}>+ Add Vendor }
{tab === "pay" ? (
{/* open payables */}
Open Payables — click to pay
{payables.length === 0 ?
No open payables. Use “Write Check” for any vendor.
: (
{payables.map((p, i) => (
{p.vendor}
{p.inv} · acct {p.acct}{p.bodyRO ? " · " + p.bodyRO : ""}
{ckMoney(p.amount)}
setModal({ type: "write", payable: p })}>Pay
))}
)}
{/* check register */}
Check Register
{defBank.stock === "3up" &&
Start at check setStartPos(+e.target.value)} style={{ fontSize: 11.5 }}>1 (top) 2 (middle) 3 (bottom) on the first sheet }
printList(selChecks)}>🖨 Print selected ({selChecks.length})
{ const n = {}; checks.filter(c => c.status === "Outstanding" && !c.printCount).forEach(c => { n[ckId(c)] = true; }); setSel(n); }}>Select unprinted
{checks.length === 0 ?
No checks written yet.
: (
# Payee Acct Amount Status
{checks.map((c, i) => { const t = CK_TONE[c.status] || CK_TONE.Outstanding; return (
{c.status !== "Void" && setSel(x => ({ ...x, [ckId(c)]: e.target.checked }))} />}
{c.num}{(cfg.accounts || []).length > 1 ? {((cfg.accounts || []).find(a => a.id === (c.bankId || cfg.defaultId)) || {}).label || c.bankId}
: null}{c.printCount ? printed{c.printCount > 1 ? " ×" + c.printCount + " (reprint)" : ""}
: null}
{c.payee}{c.memo ? {c.memo}
: null}
{c.acct}
{ckMoney(c.amount)}
{c.status}
{c.status !== "Void" && !c.imported && printList([c])}>🖨{c.printCount ? " again" : ""} }
{c.status !== "Void" && c.imported && old register }
{c.status === "Outstanding" && act(D.clearCheck(ckId(c)))}>Clear }
{c.status === "Outstanding" && { const why = window.prompt("Void check #" + c.num + " to " + c.payee + "? The server reverses the GL posting and undoes what the check paid (it reopens the invoice; a refund to a customer is held for them again) and keeps the number as void — all at once.\n\nReason:", "Printed wrong / misaligned"); if (why !== null && String(why).trim()) Promise.resolve(D.voidCheck(ckId(c), why)).then(act); else if (why !== null) act({ ok: false, msg: "A reason is required to void a check." }); }}>Void }
{/* RC28 (fx, finding 2): Reissue (= void & reissue; labelled "Reissue" so the register's "Void" button stays the only one named Void) — the replacement check (next number, same payee, amount and distribution) in the SAME step; what the first check paid stays paid */}
{c.status === "Outstanding" && { const why = window.prompt("Void check #" + c.num + " to " + c.payee + " AND write a replacement check for the same amount (the next number)? What the check paid stays paid — by the new check.\n\nReason:", "Check lost — reissued"); if (why !== null && String(why).trim()) Promise.resolve(D.voidCheck(ckId(c), why, { reissue: true })).then(act); else if (why !== null) act({ ok: false, msg: "A reason is required to void a check." }); }}>Reissue }
); })}
)}
) : (
setModal({ type: "vendor", vendor: v })} onPay={(v) => setModal({ type: "write", vendor: v })} onSeed={() => { CK_STARTER_VENDORS.forEach(v => D.upsertVendor(v)); act({ ok: true, msg: "Added 6 common dealership vendors." }); }} />
)}
ESC-Close
go && go("acctoffice")}>Acct Office (A/P)
go && go("accounting")}>General Ledger
go && go("fixedopsgl")}>GL Posting
{modal && modal.type === "write" && setModal(null)} />}
{modal && modal.type === "setup" && setModal(null)} />}
{modal && modal.type === "vendor" && setModal(null)} />}
);
};
/* ── Vendors pane ── */
const VendorsPane = ({ vendors, act, onEdit, onPay, onSeed }) => {
const D = window.DMS;
if (!vendors.length) return (
No vendors yet. Add your dealership's suppliers, lenders, and service providers here.
+ Load 6 common vendors to start
);
return (
{["Vendor", "Type", "Terms", "Default GL", "Open Balance", ""].map((h, i) => {h} )}
{vendors.map((v, i) => {
const bal = D.vendorBalance ? D.vendorBalance(v.id) : 0;
return (
onEdit(v)}>{v.name}
{v.contact}{v.phone ? " · " + v.phone : ""}{v.acctNo ? " · acct " + v.acctNo : ""}
{v.type}
{v.terms}
{v.defaultAcct} {ckAcctName(v.defaultAcct)}
0 ? "var(--jb-navy)" : "#aaa" }}>{ckMoney(bal)}
onEdit(v)}>Edit
onPay(v)}>Pay
);
})}
);
};
/* ── Vendor add/edit modal ── */
const VendorModal = ({ rec, act, onClose }) => {
const [f, setF] = React.useState(rec || { name: "", type: "Trade", terms: "Net 30", defaultAcct: "300", contact: "", phone: "", email: "", address: "", acctNo: "", active: true, notes: "" });
const set = (k, v) => setF(s => ({ ...s, [k]: v }));
const save = () => { const r = window.DMS.upsertVendor(f); act(r); if (r.ok) onClose(); };
return (
{rec && { if (window.confirm("Remove vendor " + f.name + "?")) { act(window.DMS.removeVendor(rec.id)); onClose(); } }}>Delete }
Cancel Save
>}>
set("name", e.target.value.toUpperCase())} style={ckInp} />
set("type", e.target.value)} style={ckInp}>{CK_VENDOR_TYPES.map(t => {t} )}
set("terms", e.target.value)} style={ckInp}>{["Net 15", "Net 30", "Net 45", "Net 60", "On Receipt", "Due on Demand"].map(t => {t} )}
set("defaultAcct", e.target.value)} style={{ ...ckInp, fontFamily: "var(--jb-mono)" }} />
Account name {ckAcctName(f.defaultAcct) || "—"}
set("contact", e.target.value)} style={ckInp} />
set("phone", e.target.value)} style={ckInp} />
set("email", e.target.value)} style={ckInp} />
set("acctNo", e.target.value)} style={ckInp} />
set("address", e.target.value)} style={ckInp} />
set("active", e.target.checked)} style={{ width: 16, height: 16 }} />
Active
);
};
/* ── Check setup: company lines, bank accounts (routing / account / stock / next #), MICR font ── */
const ckMask = (v) => { const x = String(v || ""); return x.length > 4 ? "•".repeat(Math.min(6, x.length - 4)) + x.slice(-4) : x; };
const CkBankForm = ({ rec, canEdit, act, onSaved, onCancel }) => {
const D = window.DMS;
const [f, setF] = React.useState(() => ({ label: "Operating", glAcct: "202", bankName: "", bankLine2: "", fractional: "", routing: "", account: "", nextNum: 1, stock: "top", micrFormat: "business", sigLine: true, sigLines: 1, sigOver: "", offsetX: 0, offsetY: 0, micrOffsetY: 0, voidAfterDays: 90, ...(rec || {}) }));
const set = (k, v) => setF(x => ({ ...x, [k]: v }));
const rOk = !f.routing || (D.abaValid && D.abaValid(f.routing));
const [saving, setSaving] = React.useState(false);
// The next check # is kept on the server (shared by every station): a changed number moves the server counter first
// and is refused when the server is unreachable or the number is already in use.
const save = async () => {
if (saving) return; setSaving(true);
try { const r = D.saveCheckAccountShared ? await D.saveCheckAccountShared(f) : D.saveCheckAccount(f); act(r); if (r.ok) onSaved && onSaved(r.id); }
finally { setSaving(false); }
};
const inp = (k, label, extra) => set(k, (extra && extra.clean) ? extra.clean(e.target.value) : e.target.value)} style={{ ...ckInp, ...((extra && extra.style) || {}) }} placeholder={(extra && extra.ph) || ""} /> ;
return (
{inp("label", "Account name (shows in K2B)", { ph: "Operating" })}
{inp("glAcct", "GL cash account", { style: { fontFamily: "var(--jb-mono)" }, ph: "202" })}
{inp("nextNum", "Next check # (shared — kept on the server)", { clean: (v) => v.replace(/\D/g, ""), style: { fontFamily: "var(--jb-mono)", fontWeight: 700 } })}
{inp("bankName", "Bank name (prints on check)", { ph: "Your bank's name" })}
{inp("bankLine2", "Bank city / state (prints)", { ph: "City, ST 00000" })}
{inp("fractional", "Fractional routing # (optional)", { ph: "80-123/815" })}
set("routing", e.target.value.replace(/\D/g, "").slice(0, 9))} style={{ ...ckInp, fontFamily: "var(--jb-mono)", borderColor: f.routing && !rOk ? "#B23A2E" : "#C8CCD2" }} />
{inp("account", "Account number", { clean: (v) => v.replace(/[^0-9-]/g, "").slice(0, 17), style: { fontFamily: "var(--jb-mono)" } })}
set("micrFormat", e.target.value)} style={ckInp}>Business (check # on the left) Personal (check # after account)
{ set("stock", e.target.value); if (e.target.value === "3up") set("sigLine", false); }} style={ckInp}>Check on top + 2 stubs (1 per page) 3 checks per page
{ set("sigLine", e.target.value !== "0"); set("sigLines", +e.target.value || 1); }} style={ckInp}>No — stock has one printed One line Two lines (two signers)
{inp("voidAfterDays", "Void after (days, 0 = none)", { clean: (v) => v.replace(/\D/g, "").slice(0, 3) })}
{inp("offsetX", "Nudge right (+) / left (−), inches", { clean: (v) => v.replace(/[^0-9.-]/g, "").slice(0, 6) })}
{inp("offsetY", "Nudge down (+) / up (−), inches", { clean: (v) => v.replace(/[^0-9.-]/g, "").slice(0, 6) })}
{inp("micrOffsetY", "Bank line only: down (+) / up (−)", { clean: (v) => v.replace(/[^0-9.-]/g, "").slice(0, 6) })}
{canEdit &&
window.k2bPrintChecks && window.k2bPrintChecks([], { alignment: true, bankId: f.id, stock: f.stock })}>Print alignment test
{onCancel &&
Cancel }
{saving ? "Saving…" : "Save bank account"}
}
);
};
const CheckSetupModal = ({ cfg, checks, act, canEdit, onClose }) => {
const D = window.DMS;
const [, force] = React.useReducer(n => n + 1, 0);
const live = D.checkConfig ? D.checkConfig() : cfg;
const [edit, setEdit] = React.useState(null); // account id | "new" | null
const dealer = window.K2BPrint ? window.K2BPrint.checkCompany({}) : {}; // RC35 (brd): the store's check header (Owner Panel > Stores), else its print name + address
const [co, setCo] = React.useState(() => ({ name: dealer.name || "", addr1: dealer.addr1 || "", addr2: dealer.addr2 || "", phone: dealer.phone || "", ...(live.company || {}) }));
const fs = window.ckpFontStatus ? window.ckpFontStatus() : { ok: false };
const highest = (id) => checks.filter(c => (c.bankId || live.defaultId) === id).reduce((m, c) => Math.max(m, parseInt(c.num, 10) || 0), 0);
const upload = (file) => {
if (!file) return;
if (file.size > 300 * 1024) { act({ ok: false, msg: "That font file is over 300 KB — use the .ttf/.otf MICR font file." }); return; }
const rd = new FileReader();
rd.onload = () => { act(D.setCheckConfig({ micrFont: rd.result, micrFontName: file.name })); force(); };
rd.readAsDataURL(file);
};
return (
Close >}>
{!canEdit && Only an administrator or the office can change bank accounts. Account numbers are shown masked.
}
Bank accounts
Account GL Bank Routing Account # Stock Next #
{(live.accounts || []).map(a => (
{a.label} {a.id === live.defaultId ? DEFAULT : null}
{a.glAcct} {a.bankName || not set }
{a.routing || not set }
{a.account ? (canEdit ? a.account : ckMask(a.account)) : not set }
{a.stock === "3up" ? "3 per page" : "Check on top"}
{a.nextNum}{highest(a.id) >= a.nextNum ? ≤ #{highest(a.id)} used
: null}
{canEdit && setEdit(a.id)}>Edit }
{canEdit && a.id !== live.defaultId && { act(D.setCheckConfig({ defaultId: a.id })); force(); }}>Make default }
{canEdit && (live.accounts || []).length > 1 && { if (window.confirm("Remove " + a.label + "?")) { act(D.removeCheckAccount(a.id)); force(); } }}>Remove }
))}
{edit && a.id === edit)} canEdit={canEdit} act={act} onSaved={() => { setEdit(null); force(); }} onCancel={() => setEdit(null)} />}
{!edit && canEdit && setEdit("new")}>+ Add bank account }
MICR font (bank line)
{fs.ok ? <>✓ Using {fs.name} ({fs.source}).> : <>✕ No MICR font on this PC. Checks will print a test bank line that banks will not accept.>}
Install an E-13B MICR font on the printing PC (for example the free GnuMICR, or the font that came with your check software), or upload the .ttf/.otf here once and every station uses it.
For bank-grade checks, print with a MICR toner cartridge.
{canEdit &&
upload(e.target.files && e.target.files[0])} />}
{canEdit && live.micrFont &&
{ act(D.setCheckConfig({ micrFont: null })); force(); }}>Remove uploaded font }
);
};
/* ── Write check modal (vendor-linked) ── */
const WriteCheckModal = ({ payable, vendor, vendors, cfg, act, onClose }) => {
const init = () => {
const bankId = cfg.defaultId || "";
const vAddr = (id) => { const v = (vendors || []).find(x => x.id === id); return v ? (v.address || "") : ""; };
// RC33 (mpx): a bill the server entered (Enter Bill) is on the books — its expense is booked and A/P 300 credited, so its check debits 300
if (payable) return { bankId, payee: payable.vendor, vendorId: payable.vendorId || "", payeeAddr: vAddr(payable.vendorId), amount: String(payable.amount), acct: payable.billJe ? "300" : payable.acct, memo: "Pay invoice " + payable.inv, inv: payable.inv };
if (vendor) return { bankId, payee: vendor.name, vendorId: vendor.id, payeeAddr: vendor.address || "", amount: "", acct: vendor.defaultAcct || "300", memo: "", inv: "" };
return { bankId, payee: "", vendorId: "", payeeAddr: "", amount: "", acct: "300", memo: "", inv: "" };
};
const [f, setF] = React.useState(init);
const set = (k, v) => setF(s => ({ ...s, [k]: v }));
const pickVendor = (id) => {
const v = (vendors || []).find(x => x.id === id);
if (!v) { set("vendorId", ""); return; }
setF(s => ({ ...s, vendorId: v.id, payee: v.name, payeeAddr: v.address || s.payeeAddr, acct: v.defaultAcct || s.acct }));
};
const amt = parseFloat(String(f.amount).replace(/[^0-9.]/g, "")) || 0;
const banks = cfg.accounts || [];
const bank = banks.find(b => b.id === f.bankId) || banks[0] || { glAcct: cfg.bankAcct || "202", nextNum: cfg.nextNum, label: cfg.bankName };
const bankAcct = bank.glAcct || "202";
// The number shown is a PREVIEW (the server's next number when it answers, else this station's copy). The real number
// is reserved on the server when you post; if it differs from the preview you are told before anything prints.
const [serverNext, setServerNext] = React.useState(null);
React.useEffect(() => {
let on = true; setServerNext(null);
if (window.DMS.peekServerCheckNum) window.DMS.peekServerCheckNum(bank.id).then(r => { if (on && r.ok) setServerNext(r.num); });
return () => { on = false; };
}, [bank.id]);
const nextNum = serverNext || (window.DMS.peekNextCheckNum ? window.DMS.peekNextCheckNum(bank.id) : cfg.nextNum);
const [busy, setBusy] = React.useState(false);
// (ldg round 2) paying less than the open amount of a bill is a PARTIAL payment the person says so on purpose (the server refuses
// a smaller check otherwise, and always a bigger one); the bill stays open for the rest
const [partial, setPartial] = React.useState(false);
const billOpen = payable ? +(+(payable.amount || 0) - (+payable.paidAmount || 0)).toFixed(2) : 0;
const submit = async () => {
if (busy) return;
const D = window.DMS;
const draft = { ...f, amount: amt, partial: !!(payable && partial && amt < billOpen) };
const pre = D.checkDraftProblem ? D.checkDraftProblem(draft) : null;
if (pre) { act({ ok: false, msg: pre }); return; }
setBusy(true);
try {
const res = await D.reserveCheckNum(bank.id); // the check number comes from the server — never this station's guess
if (!res.ok) { act(res); return; }
if (String(res.num) !== String(nextNum) && !window.confirm(`Another station used check #${nextNum} while this screen was open. The server reserved #${res.num} for this check. Post it as #${res.num}?`)) { D.releaseCheckNum(res.bankId, res.num); return; }
// ONE server transaction: number, check, entry, invoice, bank rec — printing is offered only after it committed
const r = await D.writeCheck({ ...draft, num: res.num, cid: res.cid });
act(r);
if (!r.ok) { if (!r.unknown) D.releaseCheckNum(res.bankId, res.num); return; }
onClose(); if (window.confirm("Check #" + r.num + " posted on the server. Print it now?")) window.k2bPrintChecks && window.k2bPrintChecks([r.check], { stock: bank.stock });
} finally { setBusy(false); }
};
return (
Cancel {busy ? "Posting on the server…" : `Post Check #${nextNum} (DR ${f.acct} / CR ${bankAcct})`} >}>
{(window.K2BPrint ? window.K2BPrint.checkCompany(window.DMS && window.DMS.checkConfig ? window.DMS.checkConfig() : {}).name : "")} — {bank.label} ({bankAcct}) CK #{nextNum} · {new Date().toLocaleDateString()}
PAY TO THE ORDER OF
{f.payee || "\u00a0"}
{ckMoney(amt)}
{amt > 0 && window.ckpWords ? window.ckpWords(amt) + " Dollars" : "\u00a0"}
The server posts the check, its journal entry (DR {f.acct} {ckAcctName(f.acct)} · CR {bankAcct} bank cash), the paid invoice and the bank-rec row together — all of it or none of it — and only then offers printing. Check #{nextNum} is a preview: the number is reserved on the server when you post, so two stations never print the same one — if another station takes it first you are told the new number before anything prints.
);
};
Object.assign(window, { CheckWriting, WriteCheckModal });