/* 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 — RC32 (lane ros): the technician's phone screen "My jobs" (owner Q2: view only now; letting techs update their own // jobs is decided later — T5). A technician signing in on a PHONE (T2, on by default) lands here; on a laptop the technician gets the // RO screen (view only). Nothing here writes: no buttons that change an RO, no prices or costs (a technician never sees cost). // • My jobs: Mine · Unassigned · All open, counters (to do, working, flagged today, done), cards sorted by promise time. // • One job per screen: the vehicle + VIN (Copy VIN), complaint / cause / correction / tech notes / parts as text, the inspection. // • Bottom bar: Service menu (the same presentation, read for the customer) · Print · Full RO (the RO screen, view only). // My jobs and the RO editor are never on the page together (exactly one data-svc-menu-btn and one data-tech-viewonly). // Script order: after ServiceCore.jsx.txt (DMS.svc* helpers), before app.jsx (app.jsx asks svcTechPhoneMode() for the landing route). // The ONE predicate for "a technician on a phone with My jobs turned on" — app.jsx (view-only bar, landing) and ServiceTickets use it. const svcTechPhoneMode = () => { try { const D = window.DMS; if (!D || !D.roleIs || !D.roleIs("Technician")) return false; const s = D.svcSettings ? D.svcSettings() : null; if (!s || !s.ro || !s.ro.techs || !s.ro.techs.myJobsOnPhone) return false; return (D.svcDeviceType ? D.svcDeviceType() : "laptop") === "phone"; } catch (e) { return false; } }; const TJ_STATUS_TONE = { ACTIVE: { fg: "#1456A0", bg: "#E5EFFA", bar: "#1B66A8" }, PEND: { fg: "#8A5A00", bg: "#FFF3D9", bar: "#C8862A" }, HOLD: { fg: "#B23A2E", bg: "#FBE9E7", bar: "#B23A2E" }, DONE: { fg: "#0E7A37", bg: "#E6F7EC", bar: "#16A34A" } }; const TJ_ORDER = { ACTIVE: 0, PEND: 1, HOLD: 2, DONE: 3 }; const tjBtn = { minHeight: 48, minWidth: 48, borderRadius: 10, border: "1px solid var(--jb-border-strong, #C3CAD6)", background: "var(--jb-surface, #fff)", color: "var(--jb-text)", fontSize: 15, fontWeight: 700, cursor: "pointer", fontFamily: "var(--jb-font)", padding: "0 12px" }; const tjWord = (cfg, code) => { const w = cfg && cfg.ro && cfg.ro.lists && cfg.ro.lists.jobStatus; return (w && w[code]) || code; }; const TechJobs = ({ ros, techs, cfg, go, onClose, who, onFullRo }) => { const [, force] = React.useReducer(n => n + 1, 0); React.useEffect(() => DMS.subscribe(force), []); const me = who || (typeof roMeIds === "function" ? roMeIds() : { techIds: [] }); const linked = me.techIds.length > 0; const [scope, setScope0] = React.useState(linked ? "mine" : "all"); // RC33 (part C, RC32 acceptance F1): on a fresh browser the technicians arrive AFTER the first draw — a linked sign-in still opens on "Mine" // (once the link is known), unless the technician already picked a list const picked = React.useRef(false); const setScope = (k) => { picked.current = true; setScope0(k); }; React.useEffect(() => { if (linked && !picked.current) setScope0("mine"); }, [linked]); const [find, setFind] = React.useState(false); const [q, setQ] = React.useState(""); const [open, setOpen] = React.useState(null); // { roId, jobId } const [insp, setInsp] = React.useState(false); const live = []; (ros || []).filter(r => r.status === "Open").forEach(r => (r.ops || []).forEach((o, i) => { if (o && o.approval !== "declined") live.push({ ro: r, job: o, n: i + 1 }); })); const mine = live.filter(x => me.techIds.includes(x.job.techId)); const unassigned = live.filter(x => !x.job.techId); const base = scope === "mine" ? mine : scope === "unassigned" ? unassigned : live; const ql = q.trim().toLowerCase(); const hit = (x) => !ql || [x.ro.id, x.ro.customer, x.ro.vehicle, x.ro.vin, x.job.desc, x.job.op].join(" ").toLowerCase().includes(ql) || (window.DMS && DMS.custQueryHit ? DMS.custQueryHit(q, x.ro.custNum, [x.ro.cust && x.ro.cust.phone]) : false); const promiseT = (r) => { const d = r.promise ? new Date(r.promise).getTime() : NaN; return isNaN(d) ? Infinity : d; }; const list = base.filter(hit).sort((a, b) => promiseT(a.ro) - promiseT(b.ro) || (TJ_ORDER[a.job.status || "PEND"] - TJ_ORDER[b.job.status || "PEND"]) || String(a.ro.id).localeCompare(String(b.ro.id)) || a.n - b.n); const counts = { todo: mine.filter(x => (x.job.status || "PEND") === "PEND" || x.job.status === "HOLD").length, working: mine.filter(x => x.job.status === "ACTIVE").length, done: mine.filter(x => x.job.status === "DONE").length }; const myTech = (techs || []).find(t => me.techIds.includes(t.id)); const showHours = !!(cfg && cfg.ro && cfg.ro.techs.showOwnHours) && !!myTech; const viewOnly = 👁 View only; const topBar = (left, title, right) => (
{left} {title} {right}
); const darkBtn = { ...tjBtn, background: "transparent", color: "#fff", border: "1px solid rgba(255,255,255,.35)" }; // ── one job per screen ── const cur = open ? live.find(x => x.ro.id === open.roId && String(x.job.id) === String(open.jobId)) : null; if (open && !cur) { setTimeout(() => setOpen(null), 0); } if (cur) { const r = cur.ro, j = cur.job; const tone = TJ_STATUS_TONE[j.status || "PEND"] || TJ_STATUS_TONE.PEND; const parts = (r.partsLines || []).filter(l => l.jobId != null && String(l.jobId) === String(j.id) && +l.qty > 0); const items = (cfg && cfg.ro && cfg.ro.lists.inspection.items) || (typeof RO_MPI_ITEMS !== "undefined" ? RO_MPI_ITEMS : []); const gr = (cfg && cfg.ro && cfg.ro.lists.inspection.grades) || ["OK", "Attn", "Now"]; const mpi = r.mpi || {}; const nG = items.filter(i => mpi[i] === "g").length, nY = items.filter(i => mpi[i] === "y").length, nR = items.filter(i => mpi[i] === "r").length; const veh = r.veh || {}; const vehTitle = [veh.yr, veh.mk, veh.mdl].filter(Boolean).join(" ") || r.vehicle || ""; const copyVin = () => { try { navigator.clipboard.writeText(r.vin || veh.vin || ""); window.k2bToast && window.k2bToast("VIN copied.", "ok", 1800); } catch (e) {} }; const box = (label, text, extra) => (
{label}{extra}
{text || "Not written yet."}
); return (
{topBar(, insp ? `Inspection · ${r.id}` : `${r.id} · Job ${cur.n}`, <>{viewOnly}{window.JBHelperButton ? : null})}
{insp ? ( <>
{nG + nY + nR} of {items.length} checked · {nR} {gr[2]} · {nY} {gr[1]}
{items.map(it => { const v = mpi[it]; const c = v === "r" ? ["#B23A2E", gr[2]] : v === "y" ? ["#C8862A", gr[1]] : v === "g" ? ["#16A34A", gr[0]] : ["#C7CDD4", "—"]; return (
{it} {c[1]}
); })} {r.mpiNotes ? box("Inspection notes", r.mpiNotes) : null} ) : ( <>
{vehTitle}
VIN {r.vin || veh.vin || "—"} {(r.vin || veh.vin) && }
{(r.miles || veh.miles) ? {Number(r.miles || veh.miles).toLocaleString()} mi : null} {typeof roPromiseText === "function" && roPromiseText(r) ? ⏰ {roPromiseText(r)} : null} {r.transport ? {r.transport} : null} ● {tjWord(cfg, j.status || "PEND")}{j.status === "HOLD" && j.holdReason ? " · " + j.holdReason : ""}
{j.desc || j.op} {j.op}
{box("Complaint", j.complaint)} {box("Cause", j.cause)} {box("Correction", j.correction)} {box("Tech notes", j.techNotes, never prints)}
Parts on this job
{parts.length === 0 &&
None.
} {parts.map(l =>
{l.desc || l.part} × {l.qty} {l.part}
)}
)}
{window.SvcMenuButton ? : }
); } // ── My jobs ── const seg = (k, label, n) => ( ); return (
{topBar(typeof window.k2bToggleAppMenu === "function" ? : null, "My jobs", <>{viewOnly}{window.JBHelperButton ? : null})}
{[["to do", counts.todo], ["working", counts.working], ...(showHours ? [["flagged today", (+myTech.flaggedToday || 0).toFixed(1) + " h"]] : []), ["done", counts.done]].map(([l, v], i) => (
{v}
{l}
))}
{!linked &&
Your sign-in is not linked to a technician yet, so "Mine" is empty — ask the office (Service Dept › Setup › Technicians).
}
{seg("mine", "Mine", mine.length)}{seg("unassigned", "Unassigned", unassigned.length)}{seg("all", "All open", live.length)}
{find &&
} {list.length === 0 && (q ? setQ("")} /> :
{scope === "mine" ? "No jobs for you right now." : "No jobs here."}
)} {list.map(x => { const st = x.job.status || "PEND", tone = TJ_STATUS_TONE[st] || TJ_STATUS_TONE.PEND; const veh = String(x.ro.vehicle || "").replace(/^(\d{4})\s+(CHEVROLET|CHEVY|GMC|BUICK|CADILLAC)\s+/i, "$1 "); const pr = typeof roPromiseText === "function" ? roPromiseText(x.ro) : ""; const late = typeof roPromiseLate === "function" ? roPromiseLate(x.ro) : false; return ( ); })}
Sorted by promise time. Tap a job to open it. View only — your service advisor types the status, cause, correction and inspection.
); }; Object.assign(window, { TechJobs, svcTechPhoneMode });