/* 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() &&
+ New Body RO }
{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.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 }) => (
);
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 && }
window.k2bPrintBodyRO && window.k2bPrintBodyRO(ro, "estimate")}>🖨 Estimate
window.k2bPrintBodyRO && window.k2bPrintBodyRO(ro, "ro")}>🖨 Repair Order
window.k2bPrintBodyRO && window.k2bPrintBodyRO(ro, "invoice")}>🖨 Invoice
{!closed && bsCanEdit() && (
setModal({ type: "import", ro })}>⇣ Import Estimate
setModal({ type: "addLabor", ro })}>+ Labor
setModal({ type: "addPart", ro })}>+ Reserve Part
setModal({ type: "addSublet", ro })}>+ Sublet
setModal({ type: "addSupp", ro })}>+ Supplement
{ro.status === "Estimate" && act(D.updateBodyRO(ro.id, { status: "Open" }))}>Convert to RO → }
)}
{/* Labor */}
{(ro.lines || []).length === 0 ? No labor lines yet.
: (
Type Description Hrs Rate Sale
{ro.lines.map(l => (
{typeBadge(l.type)}
{l.desc}
{(+l.hours).toFixed(1)}
{bsMoney(l.rate)}
{bsMoney(l.sale)}
{!closed && bsCanEdit() && act(D.removeBodyLine(ro.id, l.id))}>✕ }
))}
)}
{/* Parts */}
{(ro.partsLines || []).length === 0 ? No parts reserved.
: (
Part # Description Qty Sale
{ro.partsLines.map(p => (
{p.part}{p.backorder && ⚠B/O }
{p.desc}
{p.qty}
{bsMoney(p.sale)}
{!closed && bsCanEdit() && act(D.removeBodyPart(ro.id, p.id))}>✕ }
))}
)}
{/* Sublet */}
{(ro.sublets || []).length === 0 ? No sublet work (glass, alignment, towing, etc.).
: (
Vendor Description Cost Sale
{ro.sublets.map(s => (
{s.vendor}
{s.desc}
{s.cost == null ? "—" : bsMoney(s.cost)}
{bsMoney(s.sale)}
{!closed && bsCanEdit() && act(D.removeSublet(ro.id, s.id))}>✕ }
))}
)}
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" && (
act(D.setSupplementStatus(ro.id, s.id, "Approved"))}>Approve
act(D.setSupplementStatus(ro.id, s.id, "Denied"))}>Deny
)}
))}
)}
{/* 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]) => (
))}
Total{m.committed ? " (posted)" : " (preview)"}
{bsMoney(m.total)}
{!closed && bsCanEdit() &&
setModal({ type: "close", ro })}>Close & Post → }
{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)} }
setAcctOpen(o => !o)} style={{ border: 0, background: "transparent", color: "var(--jb-blue)", fontWeight: 700, cursor: "pointer", fontSize: 12.5 }}>{acctOpen ? "▾" : "▸"} Accounting detail
)}
{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
take(false)}>Take payment
{office && ro.payType === "Insurance" && take(true)}>Insurer's check }
{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" && { const why = ask(`Void ${p.receiptNo}? Reason:`); if (why) act(D.voidBodyShopPayment(ro.id, p.receiptNo, why)); }}>Void }
{office && !p.voided && !p.refundOf && ["cash", "card"].indexOf(p.method) !== -1 && { const a = ask(`Refund how much of ${p.receiptNo}?`, String(Math.abs(+p.amount || 0).toFixed(2))); if (!a) return; const how = (ask("Refund by cash / card / check?", p.method) || "").toLowerCase(); if (!how) return; const why = ask("Reason:"); if (why) act(D.refundBodyShopPayment(ro.id, p.receiptNo, a, how, why)); }}>Refund }
))}
);
};
/* 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 0)} onClick={go} style={{ background: "var(--jb-yellow)", color: "var(--jb-navy)", border: 0, fontWeight: 800, ...(busy || (m.problems && m.problems.length > 0) ? { opacity: 0.5, cursor: "not-allowed" } : {}) }}>{busy ? "Posting…" : "Close & Post"} >}>
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
Take {bsMoney(m.customer)} now
{pay.on &&
{[["cash", "💵 Cash"], ["card", "💳 Card"], ["check", "🧾 Check"]].concat(D.can && D.can("acct") ? [["invoice", "🧾 Invoice (A/R)"]] : []).map(([k, l]) => (
setPay({ ...pay, method: k })} style={{ background: pay.method === k ? "var(--jb-navy, #1B66A8)" : undefined, color: pay.method === k ? "#fff" : undefined }}>{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" &&
— pick account — {arAccts.map(a => {a.acct} — {a.name} )}}
}
{!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 }) => {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 (
Cancel Create >}>
{ 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("payType", e.target.value)} style={bsInp}>Insurance Customer
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("type", o.k)} style={f.type === o.k ? { background: o.color, color: "#fff", borderColor: o.color, fontWeight: 700 } : {}}>{o.label} )}
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)}
act(window.DMS.reserveBodyPart(ro.id, p.part, +qty || 1))}>Reserve
);
})}
);
};
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 Sample Cancel { 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
);
};
Object.assign(window, { BodyShop });