/* © 2026 K2B NETWORK SOLUTIONS LLC. All rights reserved. */ /* eslint-disable */ // ServiceSetup — window.ServiceSetupPanel // Standard jobs (reusable templates applied to ROs as COPIES), shop labor // rates, card fee % + on/off, recon warning threshold, invoice footer. // Mounted as Settings → "Service Setup" (Settings builder) and as the // "Setup" tab in Service Department. Writes go through DMS.saveStdJob / // DMS.saveServiceSettings (ServiceCore) → DMS._mutate + DMS._audit. // RC32 (rset): + RO screen & steps, Lists, Technicians, Texts & printing (the RO screens' store settings) and new rows on Rates. const SVCSETUP_BLANK = { id: "", desc: "", laborOp: "", estHours: 1, rateMode: "rate", rateSource: "shop", rate: "", flat: "", payType: "Customer", parts: [], custDesc: "", techNotes: "", active: true }; const svcSetupLbl = { fontSize: 10.5, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.04em", color: "var(--jb-text-muted, #667)" }; const svcSetupInp = { width: "100%", boxSizing: "border-box", fontSize: 13, padding: "6px 8px", border: "1px solid var(--jb-border, #C8CCD2)", borderRadius: 4, fontFamily: "var(--jb-font)", background: "var(--jb-surface, #fff)", color: "var(--jb-text, #222)" }; const SvcSetupField = ({ label, children, hint, span }) => ( ); // ══ RC32 (lane rset) — Service Dept › Setup: the four new pages (RO screen & steps, Lists, Technicians, Texts & printing) and the new // rows on Rates. Every row is one store setting (DMS.SVC_SETTING_LEAVES — ServiceCore; work/r32fd/INTERFACE.md section 3): plain words, // today's value shown, "Advanced ▸" for the rarely changed rows. Edit gate per row (DMS.svcCanEditSetting: the office; the office + the // Service Manager for the screen, steps, lists and technicians) — read-only for everyone else; the server decides again (fixedops-guard). // "Preview an RO" opens a made-up RO drawn with the unsaved choices; "Back to today's look" fills every row you may change with today's // value (DMS.SVC_RO_TODAY) — saved only on Save. Saves go through DMS.saveServiceSettings (each leaf checked, saved whole, one audit row). const SVCSET_TABS = [["ro", "RO screen & steps"], ["lists", "Lists"], ["techs", "Technicians"], ["texts", "Texts & printing"]]; const SVCSET_GROUPS = [["writeup", "1 Write-up"], ["waiting", "2 Waiting"], ["progress", "3 In progress"], ["ready", "4 Ready"], ["closed", "5 Closed"]]; const SVCSET_VIEW = { k: "group", f: [["chip", "List starts on", { k: "enum", o: [["open", "Open"], ["mine", "Mine"], ["all", "All"], ["aged", "Aged"], ["warranty", "Warranty"], ["internal", "Internal"], ["closed", "Closed"]] }], ["counters", "Counters", { k: "enum", o: [["tiles", "Tiles"], ["line", "One line"], ["off", "Off"]] }], ["acct", "Accounting", { k: "enum", o: [["tab", "Its own tab"], ["more", "Under More"]] }], ["acctDetail", "Accounting detail", { k: "enum", o: [["open", "Open"], ["folded", "Folded"]] }], ["tabs", "Tabs, in order", { k: "words", o: ["jobs", "cust", "notes", "mpi", "pay"] }]] }; const SVCSET_DOCS = [["workorder", "Work order"], ["estimate", "Estimate"], ["invoice", "Invoice"], ["warranty", "Warranty invoice"], ["lube", "Quick lube checklist"], ["history", "Vehicle history"], ["wo2", "Work order (tech copy)"], ["rental", "Car rental agreement"]]; const SVCSET_DOC_ON = [[true, "Ticked"], [false, "Not ticked"], ["warranty", "Warranty ROs only"], ["notWarranty", "Not on warranty ROs"]]; const SVCSET_PAYM = [["cash", "Cash"], ["card", "Credit card"], ["invoice", "Invoice (A/R) — office only"], ["warranty", "Warranty"], ["contract", "Service contract"], ["courtesy", "Courtesy"], ["internal", "Internal"], ["sunbit", "Sunbit"]]; // rows per page: p = leaf path, adv = under "Advanced ▸", today = how the program works without the setting (SPEC 5 "Today"), s = the editor const SVCSET_ROWS = { ro: [ { p: "ro.workflow.steps", today: "12 fixed steps, all on", s: { k: "steps" } }, { p: "ro.workflow.okGate", today: "One check at Awaiting Approval", s: { k: "enum", o: [["work", "Before work starts (the Advance check — always)"], ["work+close", "Before work starts and before close (a screen check only)"]] } }, { p: "ro.workflow.autoStep", today: "Advance by hand", s: { k: "bool", l: "Move the RO to the next step by itself (one stored step at a time, never past the customer's OK)" } }, { p: "ro.workflow.closeList", today: "No list (money checks only)", s: { k: "group", f: [["mode", "The list", { k: "enum", o: [["off", "Off"], ["remind", "Remind only"], ["must", "Must be done (holds back the screen button only)"]] }], ["items", "Reminders", { k: "group", f: [["miles", "", { k: "bool", l: "Mileage in" }], ["ok", "", { k: "bool", l: "Customer OK" }], ["tech", "", { k: "bool", l: "A technician on every job" }], ["done", "", { k: "bool", l: "Every approved job Done" }], ["ccc", "", { k: "bool", l: "Cause and correction typed" }]] }]] } }, { p: "ro.workflow.agedDays", today: "7 days, fixed in code", s: { k: "num", min: 1, max: 365, step: 1, unit: "days" } }, { p: "ro.workflow.promise", today: "No promise time on the RO", s: { k: "group", f: [["mode", "Promise time", { k: "enum", o: [["off", "Off"], ["optional", "Optional"]] }], ["suggestHrs", "Suggest (hours ahead)", { k: "num", min: 0.25, max: 72, step: 0.25 }], ["waiterHrs", "Waiter (hours ahead)", { k: "num", min: 0.25, max: 72, step: 0.25 }]] } }, { p: "ro.lists.transport", today: "Only on appointments", s: { k: "lines" } }, { p: "ro.lists.jobStatus", today: "PEND / ACTIVE / DONE / HOLD", s: { k: "group", f: [["PEND", "PEND", { k: "text", max: 24 }], ["ACTIVE", "ACTIVE", { k: "text", max: 24 }], ["DONE", "DONE", { k: "text", max: 24 }], ["HOLD", "HOLD", { k: "text", max: 24 }]] } }, { p: "ro.lists.holdReasons", today: "None", s: { k: "lines" } }, { p: "ro.layout.jobRows", today: "Every job fully open", s: { k: "enum", o: [["fold", "Fold each job to one line (the one being priced or worked opens by itself)"], ["open", "Keep every job open"]] } }, { p: "ro.layout.threeCs", today: "Typed twice, both print", s: { k: "enum", o: [["both", "Both: on each job and on the Notes tab"], ["job", "On each job only"], ["notes", "On the Notes tab only"]] } }, { p: "ro.layout.headerButtons", today: "Carfax, History by VIN and Customer… in the header", s: { k: "checks", o: [["carfax", "Carfax"], ["vin", "History by VIN"], ["cust", "Change customer"], ["flag", "Flag hours"]] } }, { p: "ro.layout.byRole", adv: true, today: "Same screen for everyone (tiles, Accounting tab, list on Open)", s: { k: "byRole" } }, { p: "ro.layout.queue", adv: true, today: "Cards, 4 visible", s: { k: "group", f: [["style", "Style", { k: "enum", o: [["rows", "Rows"], ["cards", "Cards (today)"]] }], ["groupBySteps", "", { k: "bool", l: "Group by step" }], ["columns", "Columns", { k: "checks", o: [["ro", "RO"], ["cust", "Customer · vehicle"], ["waiting", "Waiting on"], ["tech", "Technician"], ["promised", "Promised"], ["total", "Total"], ["pay", "Pay type"]] }], ["fold", "Long groups", { k: "enum", o: [["never", "Never fold"], ["long", "Fold long groups"]] }]] } }, { p: "ro.layout.moneyLine", adv: true, today: "Up to 9 figures + 4 buttons", s: { k: "enum", o: [["short", "Short: Tax, Total, Paid, Customer owes + Details"], ["full", "Every figure"]] } }, { p: "ro.layout.afterClose", adv: true, today: "Posting window", s: { k: "enum", o: [["window", "The posting window"], ["note", "A short note with Print invoice?"]] } }, { p: "ro.layout.addWork", adv: true, today: "All four always shown", s: { k: "group", f: [["bulkOil", "Bulk oil", { k: "enum", o: [["auto", "Only when the store has a bulk tank"], ["on", "Always"], ["off", "Off"]] }], ["laborOp", "", { k: "bool", l: "+ Labor op" }], ["blankJob", "", { k: "bool", l: "+ Blank job" }]] } }, ], lists: [ { p: "ro.lists.advisorsFromUsers", today: "4 names fixed in code; a new RO starts on the first name", s: { k: "bool", l: "Advisors come from the sign-ins (Service Advisor / Service Manager); a new RO starts on the person signed in" } }, { p: "ro.lists.newJobTech", today: "The RO's default technician", s: { k: "enum", o: [["ro", "The RO's \"New jobs go to\" pick"], ["none", "Nobody (pick each time)"], ["next", "Next tech up"]] } }, { p: "ro.lists.inspection", today: "14 items on screen, 10 on the quick-lube print", s: { k: "group", f: [["items", "Inspection items (one per line)", { k: "lines" }], ["grades", "The 3 grades", { k: "lines" }], ["printItems", "Quick-lube print list (one per line)", { k: "lines" }]] } }, { p: "money.opCatalog", today: "16 ops with fixed prices", s: { k: "group", f: [["on", "", { k: "bool", l: "Show the + Labor op button" }], ["ops", "Labor ops (a new job's starting price only)", { k: "ops" }]] } }, { p: "money.payMethods", today: "All 8 always; Card picked first", s: { k: "group", f: [["offered", "Methods offered", { k: "checks", o: SVCSET_PAYM }], ["first", "Picked first", { k: "enum", o: SVCSET_PAYM }], ["cardBrands", "Card brands (one per line)", { k: "lines" }]] } }, { p: "money.drawerRemember", today: "Typed every time", s: { k: "bool", l: "Remember the last cash drawer on each computer (the drawer reference stays required)" } }, { p: "money.defaultPay", today: "Customer (Internal on recon ROs)", s: { k: "enum", o: [["Customer", "Customer"], ["Warranty", "Warranty"], ["Service Contract", "Service Contract"], ["Internal", "Internal"], ["Courtesy", "Courtesy"]] } }, { p: "ro.lists.bulkOilQts", adv: true, today: "5 / 6 / 8 qt, 6 picked", s: { k: "group", f: [["quick", "Quick amounts (quarts)", { k: "numlist" }], ["pick", "Picked first", { k: "num", min: 0.5, max: 40, step: 0.5 }]] } }, { p: "ro.lists.flagStart", adv: true, today: "1.0 h", s: { k: "num", min: 0, max: 24, step: 0.1, unit: "hours" } }, { p: "ro.lists.blankJob", adv: true, today: "LABOR, \"New job\", 1.0 h", s: { k: "group", f: [["op", "Op code", { k: "text", max: 16 }], ["desc", "Description", { k: "text", max: 80 }], ["hrs", "Hours", { k: "num", min: 0, max: 99, step: 0.1 }]] } }, { p: "ro.lists.apptTypes", adv: true, today: "Today's 10", s: { k: "lines" } }, ], techs: [ { p: "ro.techs.myJobsOnPhone", today: "A phone shows the advisor screen", s: { k: "bool", l: "Technician phones open on My jobs (view only; laptops open the RO screen)" } }, { p: "ro.techs.showOwnHours", today: "Not shown", s: { k: "bool", l: "Techs see their own flagged hours for today" } }, { p: "ro.techs.rateViewers", today: "Every role", s: { k: "enum", o: [["all", "Everyone (today)"], ["office+sm", "The office and the Service Manager only"]] } }, { p: "ro.techs.selfUpdate", today: "The advisor types it", s: { k: "bool", l: "Techs update their own jobs" } }, { p: "ro.techs.partRequests", today: "Not possible", s: { k: "bool", l: "Techs ask for a part from the job" } }, { p: "ro.techs.timeClock", today: "No time clock", s: { k: "bool", l: "Time clock on jobs" } }, ], texts: [ { p: "ro.texts.storeName", today: "The store profile name (Owner Panel > Stores)", s: { k: "text", max: 80, ph: "Blank = the store profile name" } }, { p: "ro.texts.authText", today: "Wording in code; the e-sign page ignores per-RO edits", s: { k: "group", f: [["text", "Wording (blank = the standard wording)", { k: "area", max: 4000, rows: 5 }], ["perRoEdit", "", { k: "bool", l: "An advisor may edit it on one RO" }]] } }, { p: "ro.texts.approvalAmount", today: "Labor only", s: { k: "enum", o: [["whole", "The whole estimate: labor + parts + sublet + tax"], ["labor", "Labor only"]] } }, { p: "ro.texts.packets", today: "Fixed 8-document packet", s: { k: "packets" } }, { p: "ro.texts.esignSend", today: "Link shown only; number typed", s: { k: "group", f: [["via", "E-sign link", { k: "enum", o: [["show", "Show it"], ["text", "Text it"], ["email", "Email it"]] }], ["fillMobile", "", { k: "bool", l: "Fill in the customer's mobile number" }]] } }, { p: "ro.texts.estimateLine", adv: true, today: "Today's line", s: { k: "area", max: 400, rows: 2, ph: "Blank = today's line" } }, { p: "ro.texts.newRo", adv: true, today: "Walk-in yes; customers added in the CRM", s: { k: "group", f: [["walkIn", "", { k: "bool", l: "Allow walk-in ROs" }], ["addCustomer", "", { k: "bool", l: "Add a new customer from the RO" }]] } }, { p: "money.roNumber", adv: true, today: "\"RO-\" and the next number (never below the store's first RO number)", s: { k: "group", f: [["prefix", "Starts with", { k: "text", max: 8 }], ["floor", "Never below", { k: "num", min: 1, max: 99999999, step: 1 }]] } }, ], rates: [ { p: "money.laborRates", today: "Warranty → Warranty rate, Internal → Internal rate, everything else → Shop rate", s: { k: "rateMap" } }, { p: "money.subletMarkup", today: "\"x1.25\" is only a label", s: { k: "group", f: [["on", "", { k: "bool", l: "Fill in the sublet sale price from the cost" }], ["factor", "Cost ×", { k: "num", min: 1, max: 5, step: 0.05 }]] } }, { p: "money.bodyRates", today: "Body 58, Paint 62, Frame 75, Mechanical 145", s: { k: "group", f: [["body", "Body $/h", { k: "num", min: 0.01, max: 1000, step: 1 }], ["paint", "Paint $/h", { k: "num", min: 0.01, max: 1000, step: 1 }], ["frame", "Frame $/h", { k: "num", min: 0.01, max: 1000, step: 1 }], ["mechanical", "Mechanical $/h", { k: "num", min: 0.01, max: 1000, step: 1 }]] } }, { p: "money.shopSupplies", today: "No fee line anywhere", s: { k: "group", f: [["on", "", { k: "bool", l: "Charge a shop supplies / hazmat fee" }]] } }, ], }; const svcSetStable = (v) => { if (v === null || typeof v !== "object") return JSON.stringify(v === undefined ? null : v); if (Array.isArray(v)) return "[" + v.map(svcSetStable).join(",") + "]"; return "{" + Object.keys(v).filter(k => v[k] !== undefined).sort().map(k => JSON.stringify(k) + ":" + svcSetStable(v[k])).join(",") + "}"; }; const svcSetGet = (o, path) => String(path).split(".").reduce((a, k) => (a && typeof a === "object" && !Array.isArray(a) ? a[k] : undefined), o); const svcSetClone = (v) => (v === undefined ? undefined : JSON.parse(JSON.stringify(v))); const svcSetPut = (o, path, v) => { const ks = path.split("."); let a = o; for (let i = 0; i < ks.length - 1; i++) { if (!a[ks[i]] || typeof a[ks[i]] !== "object" || Array.isArray(a[ks[i]])) a[ks[i]] = {}; a = a[ks[i]]; } a[ks[ks.length - 1]] = v; }; const svcSetWho = { office: "the office (Admin / F&I, Office Manager, Accounting)", "office+sm": "the office or the Service Manager", admin: "Admin / F&I" }; const svcSetLeaf = (path) => ((window.DMS && window.DMS.SVC_SETTING_LEAVES) || []).find(L => L.path === path) || null; // a list of words / numbers in a textarea (one per line, or "5, 6, 8"): keeps what is typed, hands up the parsed list const SvcSetLines = ({ value, onChange, disabled, nums, testid }) => { const join = (v) => (Array.isArray(v) ? v.join(nums ? ", " : "\n") : ""); const [t, setT] = React.useState(() => join(value)); const last = React.useRef(svcSetStable(value)); React.useEffect(() => { if (svcSetStable(value) !== last.current) { last.current = svcSetStable(value); setT(join(value)); } }, [svcSetStable(value)]); const parse = (s) => (nums ? s.split(/[,;\s]+/).filter(Boolean).map(x => (isFinite(+x) ? +x : x)) : s.split("\n").map(x => x.trim()).filter(Boolean)); const set = (s) => { setT(s); const v = parse(s); last.current = svcSetStable(v); onChange(v); }; return nums ? set(e.target.value)} style={{ ...svcSetupInp, maxWidth: 220 }} autoComplete="off" /> :