/* 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 — Parts Counter (modern master–detail workspace). // Left: searchable / filterable parts catalog straight from the shared DMSCore // master file, with live on-hand availability. Right: the working ticket. // RC20 (fops): Cash Out sends the WHOLE ticket to the server once (POST /api/parts/checkout). The server relieves the stock, // posts ONE journal entry and stores the invoice — or does nothing and says why; the ticket stays on the screen until the // server says "committed", and the receipt is printed from the server's stored invoice (never from this preview). // The preview below uses the same calculation as the server (FopsCalc): integer cents, ONE tax on the taxable subtotal, // the shared tax rate (Parts gear → shared by every station). const PT_COUNTERMEN = ["Ray M.", "Tina K.", "Emilio V.", "Audrey F."]; const PartsTickets = ({ onClose, go }) => { const [, force] = React.useReducer(n => n + 1, 0); React.useEffect(() => DMS.subscribe(force), []); const parts = DMS.parts(); const act = (r) => { window.k2bToast && window.k2bToast(r.msg, r.ok ? "ok" : "warn", 3200); return r; }; const [q, setQ] = React.useState(""); const [filter, setFilter] = React.useState("all"); const [counter, setCounter] = React.useState(PT_COUNTERMEN[0]); const [kind, setKind] = React.useState("retail"); // retail | wholesale const [custName, setCustName] = React.useState(""); const [lines, setLines] = React.useState([]); // [{part, qty}] const [modal, setModal] = React.useState(null); const TENDER0 = { method: "cash", tendered: "", drawerRef: "", ref: "", last4: "", account: "" }; const [tender, setTender] = React.useState(TENDER0); const [busy, setBusy] = React.useState(false); const avail = (p) => p.onHand - (p.committed || 0); const FILTERS = [ { k: "all", label: "All", test: () => true }, { k: "stock", label: "In Stock", test: (p) => avail(p) > 4 }, { k: "low", label: "Low", test: (p) => avail(p) > 0 && avail(p) <= 4, tone: "amber" }, { k: "out", label: "Out", test: (p) => avail(p) <= 0, tone: "red" }, ]; const curFilter = FILTERS.find(f => f.k === filter) || FILTERS[0]; const matchQ = (p) => !q || (p.part + " " + (p.desc || "") + " " + (p.bin || "")).toLowerCase().includes(q.toLowerCase()); const catalog = parts.filter(p => matchQ(p) && curFilter.test(p)).slice(0, 200); // ── ticket math: the server's calculation (integer cents, one tax on the taxable subtotal, the shared rate) ── const pv = DMS.partsTicketPreview ? DMS.partsTicketPreview(lines, kind) : { lines: [], subtotalCents: 0, taxCents: 0, totalCents: 0, taxExempt: kind === "wholesale", taxRatePct: "0" }; const decorated = lines.map((l, i) => { const p = parts.find(x => x.part === l.part) || {}; const pl = pv.lines[i] || { unitCents: 0, extCents: 0 }; return { ...l, desc: p.desc, bin: p.bin, cost: p.cost || 0, list: p.list || 0, trade: p.trade || 0, onHand: p.onHand || 0, committed: p.committed || 0, unit: pl.unitCents / 100, ext: pl.extCents / 100, short: l.qty > (p.onHand - (p.committed || 0)), // round 2: no list / trade price on file → never sold for $0.00 from the counter ticket (the server refuses it too) noPrice: !(window.FopsCalc && window.FopsCalc.toCents(kind === "wholesale" ? p.trade : p.list) > 0) }; }); const sub = pv.subtotalCents / 100; const taxRate = pv.taxExempt ? 0 : +pv.taxRatePct; const taxAmt = pv.taxCents / 100; const total = pv.totalCents / 100; const anyShort = decorated.some(l => l.short); const noPriceLine = decorated.find(l => l.noPrice); const addPart = (p) => setLines(prev => { const ex = prev.find(l => l.part === p.part); if (ex) return prev.map(l => l.part === p.part ? { ...l, qty: l.qty + 1 } : l); return [...prev, { part: p.part, qty: 1 }]; }); const setQty = (part, qty) => setLines(prev => prev.map(l => l.part === part ? { ...l, qty: Math.max(0, qty) } : l).filter(l => l.qty > 0)); const removeLine = (part) => setLines(prev => prev.filter(l => l.part !== part)); // ── cash out → ONE server command for the whole ticket; success only from the committed answer ── const tenderBody = () => { const t = tender; if (t.method === "card") return { method: "card", ref: t.ref, last4: t.last4 || undefined }; if (t.method === "charge") return { method: "charge", account: t.account }; return { method: "cash", tendered: t.tendered !== "" ? +t.tendered : undefined, drawerRef: t.drawerRef || undefined }; }; const cashOut = async () => { if (busy) return; if (!decorated.length) return act({ ok: false, msg: "Ticket is empty." }); if (anyShort) return act({ ok: false, msg: "One or more lines exceed available stock. Reduce qty or special-order." }); if (noPriceLine) return act({ ok: false, msg: `Part ${noPriceLine.part} has no ${kind === "wholesale" ? "trade" : "list"} price in the parts file, so it can't be sold for $0.00. Put the price in the parts file (Parts Dept), or sell it from Parts Dept › Sell with a one-time price and a reason. The ticket is kept.` }); setBusy(true); let r; try { r = await DMS.partsCheckout({ lines: lines.map(l => ({ part: l.part, qty: l.qty })), kind, customer: custName ? { name: custName } : undefined, tender: tenderBody() }); } finally { setBusy(false); } if (!r || !r.ok) { // nothing was sold: the ticket stays exactly as it is, and the reason is shown setModal({ type: "refused", msg: (r && r.msg) || "The server refused this ticket.", unknown: !!(r && r.unknown), part: r && r.part }); return; } const je = (r.records || []).find(x => x.collection === "journalEntries"); const inv = r.invoice || {}; setModal({ type: "done", invoice: inv, je: je ? je.record : null, effects: (inv.lines || []).map(l => ({ label: `${l.qty}× ${l.part} relieved from inventory`, detail: `sold at ${jbm(l.unitCents / 100)} each (acct 242)` })), title: `Ticket ${r.saleId} cashed out — posted to the books` }); setLines([]); setCustName(""); setTender(TENDER0); window.k2bToast && window.k2bToast(r.msg, "ok", 4200); }; const printQuote = () => { if (!decorated.length) return act({ ok: false, msg: "Ticket is empty — add parts first." }); window.k2bPrintPartsTicket && window.k2bPrintPartsTicket({ quote: true, customer: custName, kind, clerk: (window.DMS && window.DMS.session && (window.DMS.session().user || "")) || "", lines: decorated, sub, tax: taxAmt, total, taxRatePct: pv.taxRatePct, taxExempt: pv.taxExempt }); }; const printInvoice = (inv, reprint) => window.k2bPrintPartsTicket && window.k2bPrintPartsTicket({ invoice: inv, reprint: !!reprint }); // ── KPIs ── const invValue = parts.reduce((s, p) => s + Math.max(0, p.onHand) * p.cost, 0); const outCount = parts.filter(p => avail(p) <= 0).length; return (
{/* KPI strip */}
setModal({ type: "recent" })}>🧾 Recent tickets go && go("partsdept")}>📦 Parts Dept
{/* Body: catalog | ticket */}
{/* ── LEFT: catalog ── */}
{FILTERS.map(f => ( setFilter(f.k)} count={parts.filter(p => matchQ(p) && f.test(p)).length}>{f.label} ))}
Part #DescriptionAvail{kind === "wholesale" ? "Trade" : "List"}Cost
{catalog.map(p => { const a = avail(p); return (
{p.part} {p.desc}{p.bin ? · {p.bin} : null} {a} {jbm(kind === "wholesale" ? p.trade : p.list)} {jbm(p.cost)}
); })} {catalog.length === 0 && (
{q && setQ("")} />} go && go("partsdept")} />
)}
{/* ── RIGHT: ticket ── */}
Working Ticket
{/* sale-type segmented toggle */}
{[["retail", "Counter Retail"], ["wholesale", "Wholesale"]].map(([k, lbl]) => ( ))}
{decorated.length === 0 ? (
🛒
Add parts from the catalog to start a ticket.
) : decorated.map(l => (
{l.part}
{l.desc}
setQty(l.part, parseInt(e.target.value.replace(/[^0-9]/g, "")) || 0)} style={{ width: 44, textAlign: "center", padding: "3px 4px", fontFamily: "var(--jb-mono)" }} /> @ {jbm(l.unit)}
{jbm(l.ext)}
{l.short &&
⚠ Only {l.onHand - l.committed} available — special-order the rest.
} {l.noPrice &&
⚠ No {kind === "wholesale" ? "trade" : "list"} price on file — sell it from Parts Dept › Sell with a one-time price and a reason.
}
))}
{/* totals + cash out */}
setCustName(e.target.value.toUpperCase())} placeholder="Customer name (prints on ticket)" style={{ width: "100%", boxSizing: "border-box", fontSize: 12.5, padding: "6px 8px", border: "1px solid #C8CCD2", borderRadius: 4, marginBottom: 8 }} />
Subtotal{jbm(sub)}
Tax{pv.taxExempt ? " — exempt" : ` (${pv.taxRatePct}%)`}{jbm(taxAmt)}
{/* RC28 (fx, gate 2c): the core deposit is collected on the ticket — held for the customer, refunded when the old unit comes back */} {pv.coreDepositCents ?
Core deposit (refundable){jbm(pv.coreDepositCents / 100)}
: null}
Total {jbm(total)}
{/* tender — how the customer pays (cash 201 · card 202 · charge to an A/R account 220) */}
{tender.method === "cash" && setTender({ ...tender, tendered: e.target.value.replace(/[^0-9.]/g, "") })} placeholder={"received " + total.toFixed(2)} style={{ width: 110, fontSize: 12, padding: "4px 6px" }} />} {tender.method === "cash" && tender.tendered !== "" && +tender.tendered >= total && change {jbm(+tender.tendered - total)}} {tender.method === "card" && setTender({ ...tender, ref: e.target.value })} placeholder="terminal ref / auth" style={{ width: 130, fontSize: 12, padding: "4px 6px" }} />} {tender.method === "card" && setTender({ ...tender, last4: e.target.value.replace(/[^0-9]/g, "").slice(0, 4) })} placeholder="last 4" style={{ width: 60, fontSize: 12, padding: "4px 6px" }} />} {tender.method === "charge" && }
{busy ? "Posting…" : "Cash Out & Post to GL →"} 🖨 Print Quote
{modal && modal.type === "done" && ( setModal(null)} width={600} buttons={<> printInvoice(modal.invoice)}>🖨 Print receipt setModal(null)}>Done}>
{modal.invoice.saleId} · {modal.invoice.kind === "wholesale" ? "Wholesale" : "Counter retail"}{modal.invoice.customer && modal.invoice.customer.name ? " · " + modal.invoice.customer.name : ""}
Subtotal {jbm(modal.invoice.subtotalCents / 100)} · Tax{modal.invoice.taxExempt ? " (exempt)" : ` (${modal.invoice.taxRatePct}%)`} {jbm(modal.invoice.taxCents / 100)} · Total {jbm(modal.invoice.totalCents / 100)}
{modal.invoice.tender && modal.invoice.tender.method === "cash" && modal.invoice.tender.changeCents ?
Cash received {jbm(modal.invoice.tender.tenderedCents / 100)} · change {jbm(modal.invoice.tender.changeCents / 100)}
: null}
{modal.je ? :
Posted to the books as {modal.invoice.journalId} (the office sees the entry in the General Ledger). {modal.effects.map(e => e.label).join(" · ")}
}
)} {modal && modal.type === "refused" && ( setModal(null)} width={520} buttons={ setModal(null)}>OK}>
{modal.msg}
Nothing was sold, no stock moved and nothing was posted. Your ticket is still on the screen.
)} {modal && modal.type === "recent" && setModal(null)} onPrint={(inv) => printInvoice(inv, true)} />}
); }; const iconBtnP = { border: "none", background: "transparent", color: "var(--jb-text-muted)", cursor: "pointer", fontSize: 17, lineHeight: 1, width: 22, height: 22, borderRadius: 4 }; const stepBtn = { width: 26, padding: "2px 0", fontWeight: 700 }; Object.assign(window, { PartsTickets });