/* 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 — Body Shop (collision repair). Master–detail like Service ROs, // but with estimate import, labor by type (body/paint/frame/mechanical), parts // reserved from inventory, sublet, supplement approvals, and insurance-vs- // customer billing. Closing posts to the GL (labor/parts/sublet/tax) and // relieves reserved parts. Easy print at every step. const bsMoney = (n) => "$" + (Number(n) || 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const BS_LABOR = [ { k: "body", label: "Body", color: "#1B66A8" }, { k: "paint", label: "Paint", color: "#7C3AED" }, { k: "frame", label: "Frame", color: "#B23A2E" }, { k: "mechanical", label: "Mechanical", color: "#0E7A37" }, ]; const BS_STATUS_TONE = { Estimate: { bg: "#EDEFF3", fg: "#5A6472" }, Open: { bg: "var(--jb-blue-tint,#E5EFFA)", fg: "var(--jb-blue,#1B66A8)" }, Closed: { bg: "var(--jb-green-tint,#E6F7EC)", fg: "#0E7A37" } }; // RC32 (lane ros): who may change a body-shop job (the server's rule — service or the office); M5 labor rates, M4 sublet markup const bsCanEdit = () => { const D = window.DMS; return !!(D && D.can && (D.can("service") || D.can("acct"))); }; const bsCfg = () => { try { const s = window.DMS && window.DMS.svcSettings ? window.DMS.svcSettings() : null; return s && s.money ? s : null; } catch (e) { return null; } }; const bsRateFor = (type) => { const c = bsCfg(); const r = c && c.money.bodyRates && c.money.bodyRates[type]; return r != null ? +r : ((window.DMS && window.DMS.BODY_RATES && window.DMS.BODY_RATES[type]) || 0); }; const BSBadge = ({ s }) => { const t = BS_STATUS_TONE[s] || BS_STATUS_TONE.Estimate; return {s}; }; const BodyShop = ({ 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) => Promise.resolve(r).then(x => { window.k2bToast && window.k2bToast(x.msg, x.ok ? "ok" : "warn", 3600); return x; }); // RC33 (mpx): money after the close is a SERVER command (a promise) const ros = (D && D.bodyShopROs) ? D.bodyShopROs() : []; const [selId, setSelId] = React.useState(null); const [modal, setModal] = React.useState(null); React.useEffect(() => { if (selId && ros.find(r => r.id === selId)) return; if (ros[0]) setSelId(ros[0].id); }, [ros.length]); // RC26 (inv) fix round: a hand-off from another screen (the Recon Tracker's "Serviced: BR-…" link) opens that job once React.useEffect(() => { let id = null; try { id = localStorage.getItem("jb_body_focus"); } catch (e) {} id = id || window.jbBodyFocus || null; if (!id) return; try { localStorage.removeItem("jb_body_focus"); } catch (e) {} window.jbBodyFocus = null; if (ros.find(r => r.id === id)) setSelId(id); }, []); const sel = ros.find(r => r.id === selId) || null; const newRO = () => setModal({ type: "new" }); const openWip = ros.filter(r => r.status !== "Closed").length; const wipDollars = ros.filter(r => r.status !== "Closed").reduce((s, r) => s + (+r.total || 0), 0); return (
{openWip} open · WIP {bsMoney(wipDollars)}
{bsCanEdit() && }
{ros.length === 0 &&
No body shop ROs. Click “+ New Body RO” to start, then import an estimate.
} {ros.map(r => { const on = r.id === selId; return (
setSelId(r.id)} style={{ padding: "10px 14px", borderBottom: "1px solid var(--jb-border)", cursor: "pointer", background: on ? "var(--jb-blue-tint, #E5EFFA)" : "transparent", borderLeft: on ? "3px solid var(--jb-blue)" : "3px solid transparent" }}>
{r.id}
{r.customer || "—"}
{r.vehicle || ""}
{bsMoney(r.total)} · {r.payType} {r.insurer && · {r.insurer}}
); })}
{sel ? :
Select or create a body shop RO.
}
ESC-Close go && go("service")}>Service ROs go && go("partsdept")}>Parts Dept go && go("checkwriting")}>Check Writing
{modal && modal.type === "new" && setModal(null)} onCreated={(id) => setSelId(id)} />} {modal && modal.type === "import" && setModal(null)} />} {modal && modal.type === "addLabor" && setModal(null)} />} {modal && modal.type === "addPart" && setModal(null)} />} {modal && modal.type === "addSublet" && setModal(null)} />} {modal && modal.type === "addSupp" && setModal(null)} />} {modal && modal.type === "close" && r.id === modal.ro.id) || modal.ro} act={act} onClose={() => setModal(null)} />}
); }; // Hoisted — an inline section wrapper remounts everything inside it on each // render, which drops the cursor out of any input after one character. const BsSect = ({ title, right, children }) => (
{title}
{right}
{children}
); const BsEditor = ({ ro, act, go, setModal }) => { const D = window.DMS; // RC32: the office sees the account numbers; everyone else reads plain words (the account lines one click away, still in the page) const bsOffice = !!(D && D.fopsIsOffice && D.fopsIsOffice()); const [acctOpen, setAcctOpen] = React.useState(false); const closed = ro.status === "Closed"; const pendingSupp = (ro.supplements || []).filter(s => s.status === "Pending").length; const Sect = BsSect; const typeBadge = (t) => { const o = BS_LABOR.find(x => x.k === t) || BS_LABOR[0]; return {o.label}; }; return (
{/* header */}
{ro.id} {ro.payType}{ro.insurer ? " · " + ro.insurer : ""}{ro.claimNo ? " · claim " + ro.claimNo : ""}
{ro.customer || "—"}
{ro.vehicle || ""}{ro.vin ? " · VIN " + ro.vin : ""}
{ro.vin && window.CarfaxButton && }
{!closed && bsCanEdit() && (
{ro.status === "Estimate" && }
)} {/* Labor */} {(ro.lines || []).length === 0 ?
No labor lines yet.
: ( {ro.lines.map(l => ( ))}
TypeDescriptionHrsRateSale
{typeBadge(l.type)} {l.desc} {(+l.hours).toFixed(1)} {bsMoney(l.rate)} {bsMoney(l.sale)} {!closed && bsCanEdit() && }
)}
{/* Parts */} {(ro.partsLines || []).length === 0 ?
No parts reserved.
: ( {ro.partsLines.map(p => ( ))}
Part #DescriptionQtySale
{p.part}{p.backorder && ⚠B/O} {p.desc} {p.qty} {bsMoney(p.sale)} {!closed && bsCanEdit() && }
)}
{/* Sublet */} {(ro.sublets || []).length === 0 ?
No sublet work (glass, alignment, towing, etc.).
: ( {ro.sublets.map(s => ( ))}
VendorDescriptionCostSale
{s.vendor} {s.desc} {s.cost == null ? "—" : bsMoney(s.cost)} {bsMoney(s.sale)} {!closed && bsCanEdit() && }
)}
Sublet vendor costs become A/P payables on close — pay them in Check Writing.
{/* Supplements */} {(ro.supplements || []).length === 0 ?
No supplements. Add one when the insurer approves extra repairs.
: (
{ro.supplements.map(s => (
{s.desc} {bsMoney(s.amount)} {s.status} {!closed && bsCanEdit() && s.status === "Pending" && ( )}
))}
)}
{/* Totals + close — RC24 (svcm): the SAME calculation the server commits (DMS.bodyMoney / SvcmCalc); once closed, the server's invoice */} {(() => { const m = (D.bodyMoney && D.bodyMoney(ro)) || { total: ro.total, labor: ro.laborSale, parts: ro.partsSale, sublet: ro.subletSale, supplements: ro.supplementTotal || 0, tax: ro.tax }; return (<>
{[["Labor", m.labor], ["Parts", m.parts], ["Sublet", m.sublet], ["Supplements", m.supplements], ["Tax", m.tax]].map(([l, v]) => (
{l}
{bsMoney(v)}
))}
Total{m.committed ? " (posted)" : " (preview)"}
{bsMoney(m.total)}
{!closed && bsCanEdit() && }
{m.problems && m.problems.length > 0 &&
{m.problems[0]}
} {!bsOffice && (
{ro.payType === "Insurance" ? Insurance pays {bsMoney(m.insurer)} · deductible {bsMoney(m.deductible)} · customer owes {bsMoney(closed ? (m.customerOwed || 0) : m.deductible)} : Customer owes {bsMoney(closed ? (m.customerOwed || 0) : m.customer)}}
)}
{closed ? "Posted" : "At close"}: {ro.payType === "Insurance" && {bsMoney(m.insurer)} → {ro.insurer || "insurer"} (262 insurance receivable)} {ro.payType === "Insurance" && {bsMoney(m.deductible)} deductible from the customer} {ro.payType !== "Insurance" && {bsMoney(m.customer)} from the customer} {closed && paid at close {bsMoney(m.paid || 0)}{m.customerOwed > 0 ? <> · {bsMoney(m.customerOwed)} on the customer's A/R (220) : " · nothing owed"}} {!closed && cash / card taken at close; the rest goes on the customer's A/R (220). Sublet costs become A/P bills; parts leave stock at the job's cost.} {closed && ro.closeJeId && {ro.closeJeId}}
{closed && } ); })()}
); }; /* RC33 (mpx): money on a CLOSED body-shop job — a later payment (the customer's share, or the insurer's check for its share), a void or a refund. Each is a SERVER command (POST /api/fixedops/bodyshop/payment | void | refund: the entry, the job's payment list, the customer's A/R line together). */ const BsAfterClose = ({ ro, act }) => { const D = window.DMS; const office = !!(D.can && (D.can("acct") || D.can("glEdit"))); const pays = (ro.payments || []).filter(p => p && p.receiptNo); const ask = (q, d) => { const v = window.prompt(q, d || ""); return v === null ? null : String(v).trim(); }; const take = (insurer) => { const a = ask(insurer ? `The insurer's check for ${ro.id} — amount:` : `Customer payment on ${ro.id} — amount:`); if (!a) return; if (insurer) { const ref = ask("The insurer's check number:"); if (!ref) return; act(D.bodyShopPayment(ro.id, { method: "insurance", amount: a, ref })); return; } const how = (ask("How was it paid? cash / card / check", "card") || "").toLowerCase(); if (!how) return; const ref = how === "cash" ? "" : ask(how === "card" ? "Processor reference / auth code:" : "The customer's check number:"); if (how !== "cash" && !ref) return; const drawerRef = how === "cash" ? ask("Cash drawer / deposit reference:") : ""; if (how === "cash" && !drawerRef) return; act(D.bodyShopPayment(ro.id, { method: how, amount: a, ref, drawerRef })); }; return (
Payments {office && ro.payType === "Insurance" && }
{pays.length === 0 &&
No payments recorded.
} {pays.map(p => (
{p.receiptNo}{p.method === "insurance" ? "insurer" : (p.tender || p.method)} {bsMoney(Math.abs(+p.amount || 0))}{+p.amount < 0 ? " refund" : ""} {p.voided && void} {office && !p.voided && !p.refundOf && p.method !== "invoice" && } {office && !p.voided && !p.refundOf && ["cash", "card"].indexOf(p.method) !== -1 && }
))}
); }; /* RC24 (svcm): the confirm step before Close & Post — every amount the server will commit (the same calculation), the insurer / customer split, and the customer's payment taken now (cash / card / an invoice to an A/R account for the office). The result shown is the server's. */ const BsCloseModal = ({ ro, act, onClose }) => { const D = window.DMS; const m = (D.bodyMoney && D.bodyMoney(ro)) || {}; const [pay, setPay] = React.useState({ on: m.customer > 0, method: "cash", amount: m.customer > 0 ? String((+m.customer).toFixed(2)) : "", tendered: "", drawerRef: "", ref: "", last4: "", account: "" }); const [busy, setBusy] = React.useState(false); const set = (k) => (e) => setPay({ ...pay, [k]: e && e.target ? (e.target.type === "checkbox" ? e.target.checked : e.target.value) : e }); const arAccts = D.arCustomers ? D.arCustomers() : []; const fee = pay.method === "card" && D.roCardFee ? D.roCardFee(+pay.amount || 0) : { fee: 0 }; const row = (l, v, strong) =>
{l}{bsMoney(v)}
; const go = async () => { setBusy(true); try { const payments = pay.on && +pay.amount > 0 ? [{ method: pay.method, amount: pay.amount, tendered: pay.tendered || undefined, drawerRef: pay.drawerRef, ref: pay.ref, last4: pay.last4, account: pay.account }] : undefined; const r = await D.closeBodyRO(ro.id, { payments }); act(r); if (r.ok) onClose(); } finally { setBusy(false); } }; return ( Cancel}>
What the server will post
{row("Labor (460A)", m.labor)}{row("Parts (467)", m.parts)}{row("Sublet (468)", m.sublet)}{m.supplements > 0 && row("Approved supplements (460A)", m.supplements)}{row("Sales tax on parts (324A)", m.tax)}
{row("Total", m.total, true)}
{ro.payType === "Insurance" ? <>{row(`Billed to ${ro.insurer || "insurer"} (262)`, m.insurer)}{row("Deductible — customer", m.deductible)} : row("Customer's share", m.customer)}
Parts leave stock at the job's cost; sublet costs become A/P bills for the vendors; what the customer does not pay now goes on their A/R account (220).
{m.problems && m.problems.length > 0 &&
{m.problems[0]}
}
Customer's payment now
{pay.on &&
{[["cash", "💵 Cash"], ["card", "💳 Card"], ["check", "🧾 Check"]].concat(D.can && D.can("acct") ? [["invoice", "🧾 Invoice (A/R)"]] : []).map(([k, l]) => ( ))}
setPay({ ...pay, amount: e.target.value.replace(/[^0-9.]/g, "") })} style={{ ...bsInp, fontFamily: "var(--jb-mono)" }} /> {pay.method === "cash" && <> setPay({ ...pay, tendered: e.target.value.replace(/[^0-9.]/g, "") })} style={bsInp} />} {pay.method === "check" && <>} {pay.method === "card" && <> setPay({ ...pay, last4: e.target.value.replace(/\D/g, "").slice(0, 4) })} style={bsInp} />{fee.fee > 0 &&
Card fee {bsMoney(fee.fee)} — charge the card {bsMoney((+pay.amount || 0) + fee.fee)}
}} {pay.method === "invoice" && }
} {!pay.on &&
{bsMoney(m.customer)} goes on the customer's A/R account (220) — collect it later in A/R.
}
); }; /* ── modals ── */ const bsInp = { width: "100%", boxSizing: "border-box", padding: "6px 8px", border: "1px solid #C8CCD2", borderRadius: 4, fontSize: 13 }; const BsField = ({ label, children }) => ; const BsNewModal = ({ act, onClose, onCreated }) => { const [f, setF] = React.useState({ customer: "", custNum: "", vehicle: "", vin: "", payType: "Insurance", insurer: "", claimNo: "", deductible: "" }); // RC30 (cn): the job carries the customer's Customer # (the phone without the area code) so the body-shop close finds the customer's own A/R // account and the customer file shows the job — picked from the customer file (name / phone / Customer #), or typed as a walk-in name const [cq, setCq] = React.useState(""); const custRows = React.useMemo(() => (cq.trim().length >= 2 && window.DMS && window.DMS.customerSearch ? window.DMS.customerSearch(cq).slice(0, 8) : []), [cq]); const pickCust = (c) => { setF(s => ({ ...s, customer: String(c.name || "").toUpperCase(), custNum: c.custNum || "" })); setCq(""); }; // A 17-character VIN typed / pasted here fills the Vehicle box ("2022 CHEVROLET SILVERADO 1500") from our server (window.DMS.vin: // MarketCheck -> NHTSA -> the VIN itself) when it is still blank. A typed vehicle stays; Undo puts it back. const bsTyped = React.useRef(new Set()); const set = (k, v) => { bsTyped.current.add(k); setF(s => ({ ...s, [k]: v })); }; const vinFill = (typeof VlUseVinFill === "function") ? VlUseVinFill({ vin: f.vin, keys: { vehicle: "ymm" }, read: () => f, write: (p) => setF(s => ({ ...s, ...p })), typed: () => bsTyped.current, }) : null; const submit = () => { const r = window.DMS.createBodyRO(f); act(r); if (r.ok) { onCreated(r.id); onClose(); } }; return ( CancelCreate}>
{ if (f.custNum) setF(s => ({ ...s, custNum: "", customer: "" })); setCq(e.target.value); setF(s => ({ ...s, customer: e.target.value.toUpperCase() })); }} style={bsInp} /> {custRows.length > 0 && !f.custNum && (
{custRows.map((c) =>
pickCust(c)} style={{ padding: "6px 9px", cursor: "pointer", fontSize: 12, borderTop: "1px solid #F0F1F4" }}>{c.name} #{c.custNum} {c.phone ? · {c.phone} : null}
)}
)}
set("vehicle", e.target.value)} style={bsInp} /> set("vin", e.target.value.toUpperCase())} onBlur={() => vinFill && vinFill.onBlur()} spellCheck={false} style={{ ...bsInp, fontFamily: "var(--jb-mono)" }} /> {vinFill && typeof VlVinNote === "function" &&
} set("insurer", e.target.value)} placeholder="e.g. State Farm" style={bsInp} /> set("claimNo", e.target.value)} style={bsInp} /> set("deductible", e.target.value.replace(/[^0-9.]/g, ""))} style={bsInp} />
); }; const BsLaborModal = ({ ro, act, onClose }) => { const [f, setF] = React.useState({ type: "body", desc: "", hours: "", rate: "" }); const set = (k, v) => setF(s => ({ ...s, [k]: v })); const rate = f.rate || bsRateFor(f.type); // RC32: M5 (default = today's 58 / 62 / 75 / 145) return ( Cancel { act(window.DMS.addBodyLine(ro.id, { ...f, rate: f.rate || rate })); onClose(); }}>Add}>
{BS_LABOR.map(o => )}
set("desc", e.target.value)} placeholder="e.g. R&R front bumper cover" style={bsInp} /> set("hours", e.target.value.replace(/[^0-9.]/g, ""))} style={bsInp} /> set("rate", e.target.value.replace(/[^0-9.]/g, ""))} placeholder={String(rate)} style={bsInp} />
); }; const BsPartModal = ({ ro, act, onClose }) => { const parts = window.DMS ? window.DMS.parts() : []; const [q, setQ] = React.useState(""); const [qty, setQty] = React.useState("1"); const matches = q.length >= 2 ? parts.filter(p => p.part.toLowerCase().includes(q.toLowerCase()) || (p.desc || "").toLowerCase().includes(q.toLowerCase())).slice(0, 8) : []; return ( Close}>
setQ(e.target.value)} placeholder="part # or description…" style={{ ...bsInp, flex: 1 }} /> setQty(e.target.value.replace(/[^0-9]/g, ""))} style={{ ...bsInp, width: 70 }} />
{matches.length === 0 ?
Type to search the parts master.
: matches.map(p => { const avail = p.onHand - p.committed; return (
{p.part}
{p.desc} · avail {avail} · {bsMoney(p.list)}
); })}
); }; const BsSubletModal = ({ ro, act, onClose }) => { const [f, setF] = React.useState({ vendor: "", desc: "", cost: "", sale: "" }); const mk = (bsCfg() && bsCfg().money.subletMarkup) || { on: false, factor: 1.25 }; // M4: fill the sale in from the cost (off = today) const typed = React.useRef(false); const set = (k, v) => setF(s => { const n = { ...s, [k]: v }; if (k === "sale") typed.current = true; if (k === "cost" && mk.on && !typed.current) n.sale = v ? (Math.round(+v * mk.factor * 100) / 100).toFixed(2) : ""; return n; }); return ( Cancel { act(window.DMS.addSublet(ro.id, f)); onClose(); }}>Add}>
set("vendor", e.target.value)} placeholder="e.g. Courtois Glass" style={bsInp} /> set("desc", e.target.value)} placeholder="windshield R&R" style={bsInp} /> set("cost", e.target.value.replace(/[^0-9.]/g, ""))} style={bsInp} /> set("sale", e.target.value.replace(/[^0-9.]/g, ""))} style={bsInp} />
); }; const BsSuppModal = ({ ro, act, onClose }) => { const [f, setF] = React.useState({ desc: "", amount: "" }); const set = (k, v) => setF(s => ({ ...s, [k]: v })); return ( Cancel { act(window.DMS.addSupplement(ro.id, f)); onClose(); }}>Submit}> set("desc", e.target.value)} placeholder="hidden damage found on teardown" style={bsInp} />
set("amount", e.target.value.replace(/[^0-9.]/g, ""))} style={bsInp} />
Pending supplements must be approved or denied before the RO can be closed. Approved amounts add to the invoice total.
); }; const BsImportModal = ({ ro, act, onClose }) => { const [text, setText] = React.useState(""); const sample = "body, R&R front bumper cover, 2.5\npaint, refinish bumper, 3.0\nframe, pull LH rail, 4.0\n19260218, , 1\nbody, blend LH fender, 1.5"; const parse = () => text.split(/\r?\n/).map(l => l.trim()).filter(Boolean).map(l => { const c = l.split(",").map(x => x.trim()); // part rows: first cell looks like a part number (alnum, no space) + qty in 3rd if (/^[A-Z0-9\-]{6,}$/i.test(c[0]) && !BS_LABOR.find(b => b.k === c[0].toLowerCase())) return { part: c[0], qty: c[2] || c[1] || 1 }; return { type: c[0], desc: c[1], hours: c[2] }; }); const rows = text ? parse() : []; return ( setText(sample)}>Load SampleCancel { act(window.DMS.importBodyEstimate(ro.id, parse())); onClose(); }}>Import {rows.length ? "(" + rows.length + ")" : ""}}>
Paste estimate lines (from CCC / Mitchell / Audatex export), one per line:
labor: type, description, hours  ·  part: part#, , qty