/* 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 }) => ; 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 }) => ( ); 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) => (
{l}
{v}
))}
{/* tab strip */}
Pay & Register Vendors ({vendors.length})
{tab === "vendors" && }
{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)}
)}
{/* check register */}
Check Register
{defBank.stock === "3up" && }
{checks.length === 0 ?
No checks written yet.
: ( {checks.map((c, i) => { const t = CK_TONE[c.status] || CK_TONE.Outstanding; return ( ); })}
#PayeeAcctAmountStatus
{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 && } {c.status !== "Void" && c.imported && old register} {c.status === "Outstanding" && } {c.status === "Outstanding" && } {/* 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" && }
)}
) : ( 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.
); return (
{["Vendor", "Type", "Terms", "Default GL", "Open Balance", ""].map((h, i) => )} {vendors.map((v, i) => { const bal = D.vendorBalance ? D.vendorBalance(v.id) : 0; return ( ); })}
{h}
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)}
); }; /* ── 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} CancelSave }>
set("name", e.target.value.toUpperCase())} style={ckInp} />
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} />
); }; /* ── 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)" } })} {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 &&
{onCancel && }
}
); }; 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
{(live.accounts || []).map(a => ( ))}
AccountGLBankRoutingAccount #StockNext #
{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 && } {canEdit && a.id !== live.defaultId && } {canEdit && (live.accounts || []).length > 1 && }
{edit && a.id === edit)} canEdit={canEdit} act={act} onSaved={() => { setEdit(null); force(); }} onCancel={() => setEdit(null)} />} {!edit && canEdit && }
Company name on the check
{["name", "addr1", "addr2", "phone"].map(k => setCo(x => ({ ...x, [k]: e.target.value }))} style={ckInp} />)} {canEdit && }
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 && }
); }; /* ── 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"}
{vendors && vendors.length > 0 && (
)} {banks.length > 1 &&
} set("payee", e.target.value)} style={ckInp} /> set("amount", e.target.value.replace(/[^0-9.]/g, ""))} style={ckInp} /> {payable && amt > 0 && amt < billOpen && } {payable && amt > billOpen &&
More than the ${billOpen.toFixed(2)} open on invoice {payable.inv} — the server will refuse it.
}
set("acct", e.target.value)} style={{ ...ckInp, fontFamily: "var(--jb-mono)" }} />
Account name
{ckAcctName(f.acct) || "—"}
set("memo", e.target.value)} style={ckInp} /> set("inv", e.target.value)} style={{ ...ckInp, fontFamily: "var(--jb-mono)" }} />