/* 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 (