onChange(v.map(String))} disabled={disabled} testid={tid} />; }
if (s.k === "group") { const cur = value && typeof value === "object" ? value : {}; return (
{(s.f || []).map(([key, label, sub]) => (
{label && {label}}
onChange({ ...cur, [key]: v })} />
))}
); }
if (s.k === "steps") { const cur = Array.isArray(value) ? value : []; const D = window.DMS; const lockedOf = (st) => ["Write-Up", "Awaiting Approval", "In Progress", "Cashed Out"].includes(st);
return (
Stored stepOnName shownShows in
{cur.map((st, i) => (
{st.stage}
onChange(cur.map((x, n) => n === i ? { ...x, on: e.target.checked } : x))} title={lockedOf(st.stage) ? "Always on" : ""} />
onChange(cur.map((x, n) => n === i ? { ...x, label: e.target.value } : x))} style={svcSetupInp} autoComplete="off" />
))}
); }
if (s.k === "byRole") { const cur = value && typeof value === "object" ? value : {}; const names = { advisor: "Service advisor / Service", parts: "Parts", tech: "Technician", office: "Office (Admin / F&I, Office Manager, Accounting)", gm: "Sales Manager / GM", sales: "Sales" };
return {Object.keys(names).map(k => (
{names[k]}
onChange({ ...cur, [k]: v })} />))}
; }
if (s.k === "packets") { const cur = value && typeof value === "object" ? value : { open: {}, closed: {}, hide: [] }; const hide = Array.isArray(cur.hide) ? cur.hide : [];
const cell = (moment, d) => { const x = (cur[moment] || {})[d] || { on: false, copies: 1 }; const set = (patch) => onChange({ ...cur, [moment]: { ...(cur[moment] || {}), [d]: { ...x, ...patch } } });
return
set({ copies: e.target.value.replace(/[^0-9]/g, "") === "" ? "" : +e.target.value.replace(/[^0-9]/g, "") })} style={{ ...svcSetupInp, width: 44, textAlign: "right" }} title="copies" />; };
return (
DocumentWrite-up / open RO · copiesCash-out / closed RO · copiesHidden
{SVCSET_DOCS.map(([d, l]) => {l}{cell("open", d)}{cell("closed", d)} onChange({ ...cur, hide: e.target.checked ? SVCSET_DOCS.map(([k]) => k).filter(k => k === d || hide.includes(k)) : hide.filter(k => k !== d) })} />)}
); }
if (s.k === "ops") { const cur = Array.isArray(value) ? value : []; const set = (i, k, v) => onChange(cur.map((o, n) => n === i ? { ...o, [k]: v } : o));
return (
Op codeDescriptionHoursLabor $
{cur.map((o, i) => (
set(i, "op", e.target.value.toUpperCase())} style={svcSetupInp} autoComplete="off" />
set(i, "desc", e.target.value)} style={svcSetupInp} autoComplete="off" />
set(i, "hrs", e.target.value.replace(/[^0-9.]/g, ""))} style={{ ...svcSetupInp, textAlign: "right" }} autoComplete="off" />
set(i, "lab", e.target.value.replace(/[^0-9.]/g, ""))} style={{ ...svcSetupInp, textAlign: "right" }} autoComplete="off" />
{!disabled ? : }
))}
{!disabled &&
}
); }
if (s.k === "rateMap") { const cur = value && typeof value === "object" ? value : {}; const ids = rateIds || [];
return {["Customer", "Warranty", "Internal", "Service Contract", "Courtesy"].map(pt => (
{pt}))}
; }
return {JSON.stringify(value)};
};
// RC32 fix (r32-aud): the Service Manager's rights (owner Q5) reach only a sign-in whose role is "Service Manager" (or "Service Mgr") — the
// server's rule (rolematrix.isServiceManager). The office sees who that is, or that nobody is, so a service manager who signs in with the
// role "Service" is noticed before roll-out. Words only; nothing here changes a role.
const SvcSmNote = () => {
const D = window.DMS;
const roster = (D && D.roster ? D.roster() || [] : []).filter(u => u && u.active !== false);
if (!roster.length) return null;
const nm = (u) => String(u.name || u.id || "").trim();
const sms = roster.filter(u => /^service\s*(manager|mgr)$/i.test(String(u.role || "").trim())).map(nm).filter(Boolean);
const plain = roster.filter(u => /^service$/i.test(String(u.role || "").trim())).map(nm).filter(Boolean);
return (
{sms.length
? <>The Service Manager may also change the rows on this page that are not marked "Only the office": {sms.join(", ")}.>
: <>No sign-in has the role Service Manager yet, so only the office can change this page. To let your service manager change the RO screen, steps, lists and technicians rows, give his sign-in the role "Service Manager" in Settings › Users.>}
{plain.length > 0 && <> {plain.join(", ")} {plain.length === 1 ? "signs" : "sign"} in with the role "Service", which cannot change these settings.>}
);
};
// a made-up RO (never saved, never in the store) for "Preview an RO"
const svcSetPreviewRo = (rs) => {
const opened = new Date(Date.now() - 9 * 86400000).toISOString();
return { id: "RO-PREVIEW", customer: "SAMPLE, CUSTOMER", customerId: "", vehicle: "2021 CHEVROLET EQUINOX LT", vin: "2GNAXUEV0M6000000", miles: 41250, stage: "In Progress", status: "Open", opened, advisor: "", payType: "Customer",
promise: new Date(Date.now() + 3 * 3600000).toISOString(), transport: (rs.ro.lists.transport || [])[1] || "",
ops: [{ id: "P1", op: "OIL-LOF", desc: "Oil Change Lube Filter", lab: 65.5, hrs: 0.5, status: "DONE", approval: "approved", techId: "T1", payType: "Customer", complaint: "Maintenance due", cause: "Due", correction: "Changed oil" },
{ id: "P2", op: "BRK-PADS-F", desc: "Front Brake Pad Replacement", lab: 189, hrs: 1.5, status: "ACTIVE", approval: "approved", techId: "T1", payType: "Customer", complaint: "Brakes squeal" },
{ id: "P3", op: "ALIGN-4W", desc: "4-Wheel Alignment", lab: 129, hrs: 1, status: "HOLD", approval: "pending", payType: "Customer", holdReason: (rs.ro.lists.holdReasons || [])[0] || "" }],
partsLines: [], payments: [], mpi: {}, stageHist: {},
authorization: { method: "esign", by: "SAMPLE, CUSTOMER", at: opened, amount: 254.5 } };
};
// the RO screen lane's own preview (window.RoScreenPreview) can throw on a made-up RO it was not built for — fall back to the simple one, never break Setup
class SvcPreviewGuard extends React.Component {
constructor(p) { super(p); this.state = { failed: false }; }
static getDerivedStateFromError() { return { failed: true }; }
componentDidCatch(e) { try { console.warn("Preview an RO: the RO screen preview failed, showing the simple preview", e && e.message); } catch (x) {} try { this.props.onFail && this.props.onFail(); } catch (x) {} }
render() { return this.state.failed ? null : this.props.children; }
}
// "Preview an RO" — the real RO screen (lane ros, when loaded) for a made-up RO with the (unsaved) settings; else the simple preview below
const SvcSetPreview = ({ settings, onClose }) => {
const [role, setRole] = React.useState("Service Advisor");
const [lay, setLay] = React.useState("desk");
const [simple, setSimple] = React.useState(false);
// RC32 fix (r32-aud): the made-up RO is made readable (look only) BEFORE the RO screen draws it — its money / approval reads look the RO
// up by id (DMS.svcPreviewRO, DMSCore); forgotten again when the window closes. It is never saved and never listed.
const ro = React.useMemo(() => { const r = svcSetPreviewRo(settings); try { if (window.DMS && window.DMS.svcPreviewRO) window.DMS.svcPreviewRO(r); } catch (e) {} return r; }, [settings]);
React.useEffect(() => () => { try { if (window.DMS && window.DMS.svcPreviewRO) window.DMS.svcPreviewRO(null); } catch (e) {} }, []);
if (!window.RoScreenPreview || simple) return ;
const RSP = window.RoScreenPreview;
const roles = ["Service Advisor", "Service Manager", "Technician", "Parts", "Office Manager", "Sales"];
return (
Close>}>
Seen as
on a
Nothing here can be changed — it only shows how the RO screen will look.
setSimple(true)}>
);
};
// the simple preview (its own drawing of the made-up RO with the settings)
const SvcSetPreviewSimple = ({ settings, onClose }) => {
const D = window.DMS; const rs = settings;
const money = (n) => "$" + (Number(n) || 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const ro = svcSetPreviewRo(rs); const opened = ro.opened;
const est = { labor: 383.5, parts: 141.9, sublet: 0, tax: 18.04, total: 543.44 };
const steps = rs.ro.workflow.steps; const grp = D.svcStepGroup ? D.svcStepGroup(ro, { settings: rs }) : "progress";
const view = D.svcRoleView ? D.svcRoleView({ settings: rs }) : null;
const next = D.svcNextStep ? D.svcNextStep(ro, { settings: rs, readOnly: false, owes: 543.44, canClose: true }) : null;
const close = D.svcCloseChecklist ? D.svcCloseChecklist(ro, { settings: rs }) : { items: [] };
const words = rs.ro.lists.jobStatus; const aged = Math.floor((Date.now() - Date.parse(opened)) / 86400000) >= rs.ro.workflow.agedDays;
const tabName = { jobs: "Jobs (3)", cust: "Customer & vehicle", notes: "Notes", mpi: "Inspection", pay: "Payments · owes " + money(543.44) };
const hdr = rs.ro.layout.headerButtons; const more = [["carfax", "Carfax"], ["vin", "History by VIN"], ["cust", "Change customer"], ["flag", "Flag hours"]];
const chip = (t, bg, fg) => {t};
const store = rs.ro.texts.storeName || (D.storeProfile && D.storeProfile() && D.storeProfile().name) || "the store profile name";
return (
Close}>
RO-PREVIEW{chip((SVCSET_GROUPS.find(g => g[0] === grp) || ["", grp])[1])}{aged && chip(`${rs.ro.workflow.agedDays}+ days aged`, "#FDE2E1", "#9B1C1C")}
SAMPLE, CUSTOMER · #5550000
{more.filter(([k]) => hdr.includes(k)).map(([k, l]) =>
)}
2021 CHEVROLET EQUINOX LT · VIN 2GNAXUEV0M6000000 · 41,250 mi {rs.ro.workflow.promise.mode !== "off" && chip(`Promised today ${rs.ro.workflow.promise.suggestHrs} h after write-up`)} {chip((rs.ro.lists.transport || [])[1] || "Drop-off")}
{SVCSET_GROUPS.map(([g, l]) =>
{l}
{steps.filter(st => st.on && st.group === g).map(st => st.label).join(" · ") || "—"}
)}
{next && next.key &&
}
{chip(rs.ro.workflow.autoStep ? "Moves to the next step by itself" : "Moves by hand")}
{(view ? view.tabs : ["jobs", "cust", "notes", "pay"]).map((t, i) => {tabName[t]})}{view && view.acct === "tab" ? Accounting : ⋯ More (Accounting)}
Customer OK · signed by e-sign
The customer approves: {rs.ro.texts.approvalAmount === "whole" ? `${money(est.total)} — the whole estimate (labor ${money(est.labor)} + parts ${money(est.parts)} + tax ${money(est.tax)})` : `${money(254.5)} — labor only`}
E-sign page: {store}{rs.ro.texts.esignSend.via !== "show" ? ` · the link is ${rs.ro.texts.esignSend.via === "text" ? "texted" : "emailed"}` : " · the link is shown"}{rs.ro.texts.esignSend.fillMobile ? " · mobile filled in" : ""}
{rs.ro.layout.addWork.bulkOil !== "off" && }{rs.ro.layout.addWork.laborOp && rs.money.opCatalog.on && }{rs.ro.layout.addWork.blankJob && }
{ro.ops.map((j, i) =>
Job {i + 1}{j.op}{j.desc}{chip(words[j.status] || j.status)}{j.status === "HOLD" && j.holdReason && chip("On hold: " + j.holdReason, "#FFF4CE")}
{money(j.lab)}
{(rs.ro.layout.jobRows === "open" || j.status === "ACTIVE") &&
Complaint · Cause · Correction{rs.ro.layout.threeCs === "notes" ? " (typed on the Notes tab)" : ""} · Tech notes · Parts
}
)}
{close.items.length > 0 &&
Before you close ({close.mode === "must" ? "must be done" : "reminders"}){close.items.map(it =>
{it.done ? "✓" : "○"} {it.label}
)}
}
{rs.ro.layout.moneyLine === "full" && <>Labor {money(est.labor)}Parts {money(est.parts)}Sublet $0.00Fees $0.00>}
Tax {money(est.tax)}Total {money(est.total)}Paid $0.00Customer owes {money(est.total)}{rs.ro.layout.moneyLine === "short" && Details ▾}
Payment methods: {(rs.money.payMethods.offered || []).map(m => (SVCSET_PAYM.find(x => x[0] === m) || [m, m])[1] + (m === rs.money.payMethods.first ? " (first)" : "")).join(" · ")}
);
};
// one Setup page of rows (RO screen & steps / Lists / Technicians / Texts & printing / the new rows on Rates)
const SvcSettingsPage = ({ page }) => {
const D = window.DMS;
const [, force] = React.useReducer(n => n + 1, 0);
React.useEffect(() => (D && D.subscribe ? D.subscribe(force) : undefined), []);
const [draft, setDraft] = React.useState({});
const [adv, setAdv] = React.useState(false);
const [preview, setPreview] = React.useState(null);
const [gen, setGen] = React.useState(0);
// RC35 (brd): a store switch asks first when this page has unsaved rows (SPEC 4.4)
const skSaveRef = React.useRef(null);
if (window.useK2BUnsaved) window.useK2BUnsaved("svc-setup-" + page, { label: "Service Setup",
isDirty: () => { try { const c = D && D.svcSettings ? D.svcSettings() : {}; return (SVCSET_ROWS[page] || []).some(r => Object.prototype.hasOwnProperty.call(draft, r.p) && svcSetStable(draft[r.p]) !== svcSetStable(svcSetGet(c, r.p))); } catch (e) { return false; } },
save: () => (skSaveRef.current ? skSaveRef.current() : false), discard: () => { setDraft({}); setGen(g => g + 1); } });
if (!D || !D.svcSettings || !D.SVC_SETTING_LEAVES) return Service settings not loaded.
;
const cur = D.svcSettings();
const rows = SVCSET_ROWS[page] || [];
const valOf = (p) => (Object.prototype.hasOwnProperty.call(draft, p) ? draft[p] : svcSetGet(cur, p));
const canRow = (p) => !!(D.svcCanEditSetting && D.svcCanEditSetting(p));
const changed = rows.filter(r => Object.prototype.hasOwnProperty.call(draft, r.p) && svcSetStable(draft[r.p]) !== svcSetStable(svcSetGet(cur, r.p)));
const toast = (r) => { window.k2bToast && window.k2bToast(r.msg, r.ok ? "ok" : "warn", 4200); return r; };
const save = () => {
const patch = {};
changed.forEach(r => { if (canRow(r.p)) svcSetPut(patch, r.p, svcSetClone(draft[r.p])); });
if (!Object.keys(patch).length) return toast({ ok: true, msg: "Nothing to save." });
const r = toast(D.saveServiceSettings(patch));
if (r.ok) { setDraft({}); setGen(g => g + 1); }
return r;
};
skSaveRef.current = () => { if (!changed.length) return true; const r = save(); return !!(r && r.ok); };
const today = () => {
const T = D.SVC_RO_TODAY || {}; const next = { ...draft }; let n = 0;
rows.forEach(r => { const L = svcSetLeaf(r.p); if (!L || L.later || !canRow(r.p)) return; next[r.p] = svcSetClone(svcSetGet(T, r.p)); n++; });
setDraft(next); setGen(g => g + 1);
toast({ ok: true, msg: n ? `${n} row(s) filled with today's look — press Save to keep them.` : "Nothing here you may change." });
};
const doPreview = () => {
const raw = svcSetClone(D.serviceSettings ? D.serviceSettings() || {} : {});
Object.keys(draft).forEach(p => svcSetPut(raw, p, svcSetClone(draft[p])));
const rs = D.svcResolveSettings(raw, {});
if (rs.invalid && rs.invalid.length) toast({ ok: false, msg: `The preview uses the saved value for ${rs.invalid.length} row(s) that can't be read yet: ${rs.invalid.join(", ")}.` });
setPreview(rs);
};
const shownRows = rows.filter(r => !r.adv || adv);
const anyEdit = rows.some(r => canRow(r.p));
const rateIds = (cur.laborRates || []).map(r => ({ id: r.id, name: r.name, rate: r.rate }));
return (
{page !== "rates" &&
}
{anyEdit &&
}
{!anyEdit &&
View only — {page === "ro" || page === "lists" || page === "techs" ? "the office or the Service Manager" : "the office"} changes these.}
{changed.length > 0 &&
{changed.length} unsaved}
{changed.length > 0 &&
}
{anyEdit &&
}
{(page === "ro" || page === "lists" || page === "techs") && canRow("ro.layout.moneyLine") &&
}
{page === "techs" &&
}
{shownRows.map(r => {
const L = svcSetLeaf(r.p); if (!L) return null;
const may = canRow(r.p); const dirty = changed.includes(r);
return (
{L.id}
{L.label}
{L.later && Comes in a later release}
Today: {r.today}{!may && !L.later ? ` · Only ${svcSetWho[L.who] || L.who} can change this.` : ""}
setDraft(d => ({ ...d, [r.p]: v }))} />
);
})}
{rows.some(r => r.adv) &&
}
{preview &&
setPreview(null)} />}
);
};
// T1: link each technician to a sign-in (the office; one sign-in per technician) — "Mine" and My jobs know whose jobs are whose
const SvcTechLinks = () => {
const D = window.DMS;
const techs = (D.technicians ? D.technicians() : []) || [];
const roster = (D.roster ? D.roster() : []) || [];
const may = !!(D.isAdmin && D.isAdmin()) || !!(D.can && D.can("glEdit"));
const nameOf = (id) => { const u = roster.find(x => String(x.id) === String(id)); return u ? `${u.name || u.id} (${u.id}${u.role ? " · " + u.role : ""})` : String(id); };
const link = (t, uid) => { const r = D.linkTechnician(t.id, uid); window.k2bToast && window.k2bToast(r.msg, r.ok ? "ok" : "warn", 4200); };
return (
T1Link each technician to a sign-in
Today: technicians are names only. Needed so "Mine" and "My jobs" know whose jobs are whose.{!may ? " · Only the office (Admin / F&I, Office Manager, Accounting) can change this." : ""}
{techs.length === 0 &&
No technicians yet (Settings › Technicians).}
{techs.map(t => (
{t.name} {t.id}
{may ? (
) : {t.userId ? nameOf(t.userId) : "— not linked —"}}
))}
);
};
const ServiceSetupPanel = ({ embedded }) => {
const [, force] = React.useReducer(n => n + 1, 0);
React.useEffect(() => window.DMS && DMS.subscribe ? DMS.subscribe(force) : undefined, []);
const D = window.DMS;
// RC28 (svc) fix (svc-aud finding 5): the Service Manager keeps the maintenance schedule and the service menu — a login that may set up the
// service pack but not the standard jobs opens on the pack's tab
const [tab, setTab] = React.useState(() => (D && D.svcCanSetup && !D.svcCanSetup() && window.SvcSetupTab && D.svcPackCanSetup && D.svcPackCanSetup() ? "schedule" : "jobs"));
const [selId, setSelId] = React.useState(null);
const [f, setF] = React.useState(null);
const [q, setQ] = React.useState("");
const [sf, setSf] = React.useState(null);
if (!D || !D.svcSettings) return Service module not loaded.
;
const canEdit = D.svcCanSetup();
const jobs = D.stdJobs() || [];
const s = D.svcSettings();
const toast = (r) => { window.k2bToast && window.k2bToast(r.msg, r.ok ? "ok" : "warn", 3200); return r; };
const money = (n) => "$" + (Number(n) || 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
const pick = (j) => { setSelId(j ? j.id : "new"); setF(j ? JSON.parse(JSON.stringify({ ...SVCSETUP_BLANK, ...j })) : { ...SVCSETUP_BLANK, parts: [] }); };
const set = (k, v) => setF(x => ({ ...x, [k]: v }));
const setPart = (i, k, v) => setF(x => ({ ...x, parts: x.parts.map((p, n) => n === i ? { ...p, [k]: v } : p) }));
const rate = f ? (f.rateSource === "custom" ? +f.rate || 0 : D.svcRateFor(f.payType)) : 0;
const laborPreview = f ? (f.rateMode === "flat" ? +f.flat || 0 : (+f.estHours || 0) * rate) : 0;
const partsPreview = f ? f.parts.reduce((t, p) => t + (+p.qty || 0) * (+p.price || 0), 0) : 0;
const shown = jobs.filter(j => !q || (j.laborOp + " " + j.desc).toLowerCase().includes(q.toLowerCase()));
const save = () => { const r = toast(D.saveStdJob({ ...f, id: selId === "new" ? "" : f.id })); if (r.ok) { setSelId(r.id); setF(x => ({ ...x, id: r.id })); } };
const del = () => { if (!window.confirm(`Delete standard job ${f.laborOp}? ROs that already used it keep their copy.`)) return; const r = toast(D.deleteStdJob(f.id)); if (r.ok) { setSelId(null); setF(null); } };
const sfv = sf || { cardFeePct: s.cardFeePct, cardFeeOn: s.cardFeeOn, reconWarnPct: s.reconWarnPct, shopRate: s.shopRate, reconLaborRate: s.reconLaborRate == null ? "" : s.reconLaborRate, invoiceFooter: s.invoiceFooter, laborRates: s.laborRates.map(r => ({ ...r })) };
const setS = (k, v) => setSf({ ...sfv, [k]: v });
const saveSettings = () => { const r = toast(D.saveServiceSettings(sfv)); if (r.ok) setSf(null); };
const tabBtn = (k, label) => (
);
return (
{tabBtn("jobs", `Standard Jobs (${jobs.length})`)}
{tabBtn("rates", "Rates, Card Fee & Invoice")}
{SVCSET_TABS.map(([k, l]) => tabBtn(k, l))}{/* RC32 (rset) */}
{window.SvcSetupTab && D.svcCanPresent && D.svcCanPresent() && <>{tabBtn("schedule", "Maintenance schedule")}{tabBtn("menu", "Service menu")}{tabBtn("sunbit", "Sunbit & reminders")}>}{/* RC28 (svc) */}
{!canEdit && (tab === "jobs" || tab === "rates") &&
View only — an office manager or admin maintains these.}
{tab === "jobs" && (
setQ(e.target.value)} placeholder="Find op / description…" style={{ ...svcSetupInp, flex: 1 }} autoComplete="off" name="k2b-stdjob-filter" data-svcsetup-q="1" />{/* RC32 (rset, owner 10/26): the browser never fills a sign-in id into a search box */}
{canEdit && }
{shown.length === 0 &&
{jobs.length ? <>No standard jobs match '{q}' · > : "No standard jobs yet. Add the jobs your shop writes every day — oil change, brakes, alignment — so advisors can apply them in one click."}
}
{shown.map(j => (
))}
{!f ? (
Pick a standard job to edit{canEdit ? ", or + New to create one" : ""}. Applying a standard job to an RO copies it — changing the RO never changes the template, and changing the template never changes existing ROs.
) : (
)}
)}
{(tab === "schedule" || tab === "menu" || tab === "sunbit") && window.SvcSetupTab &&
}{/* RC28 (svc): kept on the server (/api/svc/settings/…) */}
{SVCSET_TABS.some(([k]) => k === tab) &&
}{/* RC32 (rset) */}
{tab === "rates" && (
)}
{tab === "rates" &&
}{/* RC32 (rset, M2): today's shared tax flags, office + Service Manager as today */}
{tab === "rates" &&
}{/* RC32 (rset): M3 / M4 / M5 (+ M1 shown locked, a later release) */}
);
};
// RC32 (rset, M2): tax on sublet / tax on fees are today's SHARED tax settings (serviceSettings.tax — the server already reads them); one copy
// of the truth, changed by the office or the Service Manager through DMS.fopsSetSharedTax as the gear's tax rate is. The RO fee account is shown.
const SvcTaxFlags = () => {
const D = window.DMS;
if (!D || !D.fopsTaxConfig) return null;
const c = D.fopsTaxConfig() || {}; const may = !!(D.fopsSeesShopCost && D.fopsSeesShopCost());
const set = (k, v) => { const r = D.fopsSetSharedTax({ [k]: v }); window.k2bToast && window.k2bToast(r.msg, r.ok ? "ok" : "warn", 4000); };
return (
);
};
Object.assign(window, { ServiceSetupPanel });