/* 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 — Controls (spec §8). // Role-based permissions, audit trail, and required-field policy. // Permissions + policy persist to localStorage; the audit trail reads the // live engine log (every GL-affecting action is recorded automatically). const PERMS_KEY = "jbrews-perms-v1"; const POLICY_KEY = "jbrews-fieldpolicy-v1"; // Modules that can be gated, grouped by department. const PERM_MODULES = [ { group: "Sales & F&I", items: [ { key: "deal", label: "Deals" }, { key: "desking", label: "Desking" }, { key: "buyersorder", label: "Buyer's Order" }, { key: "creditapp", label: "Credit App" }, { key: "finance", label: "Finance Worksheet" }, ]}, { group: "Inventory", items: [ { key: "inventory", label: "Inventory" }, { key: "stockin", label: "Stock-In" }, ]}, { group: "Operations", items: [ { key: "servicedept", label: "Service Dept" }, { key: "partsdept", label: "Parts Dept" }, ]}, { group: "Back Office", items: [ { key: "accounting", label: "Accounting / GL" }, { key: "acctoffice", label: "Acct Office (AR/AP)" }, { key: "docentry", label: "Doc Entry / Post JE" }, { key: "payroll", label: "Payroll" }, { key: "gmacct", label: "GM Statement" }, ]}, { group: "Administration", items: [ { key: "settings", label: "Settings / Security" }, { key: "controls", label: "Controls (this screen)" }, ]}, ]; const ROLES = ["Admin", "Sales", "F&I", "Accounting", "Service", "Parts"]; // Default permission matrix — what each role may access. const DEFAULT_PERMS = (() => { const all = {}; const allow = (role, keys) => { all[role] = {}; PERM_MODULES.forEach(g => g.items.forEach(i => all[role][i.key] = keys === "*" || keys.includes(i.key))); }; allow("Admin", "*"); allow("Sales", ["deal", "desking", "buyersorder", "creditapp", "finance", "inventory", "stockin"]); allow("F&I", ["deal", "desking", "buyersorder", "creditapp", "finance", "inventory"]); allow("Accounting", ["accounting", "acctoffice", "docentry", "payroll", "gmacct", "deal", "inventory"]); allow("Service", ["servicedept", "partsdept", "inventory"]); allow("Parts", ["partsdept", "servicedept", "inventory"]); return all; })(); const DEFAULT_POLICY = { vinRequired: true, mileageRequired: true, custSigRequired: true, payTypeRequired: true, acctRequired: true, costRequired: true, }; const loadPerms = () => { try { const r = localStorage.getItem(PERMS_KEY); return r ? JSON.parse(r) : JSON.parse(JSON.stringify(DEFAULT_PERMS)); } catch (e) { return JSON.parse(JSON.stringify(DEFAULT_PERMS)); } }; const savePerms = (p) => { try { localStorage.setItem(PERMS_KEY, JSON.stringify(p)); } catch (e) {} }; const loadPolicy = () => { try { const r = localStorage.getItem(POLICY_KEY); return r ? { ...DEFAULT_POLICY, ...JSON.parse(r) } : { ...DEFAULT_POLICY }; } catch (e) { return { ...DEFAULT_POLICY }; } }; const savePolicy = (p) => { try { localStorage.setItem(POLICY_KEY, JSON.stringify(p)); } catch (e) {} }; const Controls = ({ onClose, go }) => { const [, force] = React.useReducer(n => n + 1, 0); React.useEffect(() => DMS.subscribe(force), []); const [tab, setTab] = React.useState("perms"); const [perms, setPerms] = React.useState(loadPerms); const [policy, setPolicy] = React.useState(loadPolicy); const [dirty, setDirty] = React.useState(false); const [filter, setFilter] = React.useState(""); const audit = DMS.audit(); const toggle = (role, key) => { if (role === "Admin") return; // Admin always full access setPerms(p => { const next = { ...p, [role]: { ...p[role], [key]: !p[role][key] } }; return next; }); setDirty(true); }; const savePermissions = () => { savePerms(perms); setDirty(false); DMS.logAudit("Permissions Changed", "Role access matrix updated"); window.k2bToast && window.k2bToast("Permissions saved & logged to audit trail.", "ok", 2800); }; const setPol = (k, v) => { setPolicy(p => ({ ...p, [k]: v })); savePolicy({ ...policy, [k]: v }); DMS.logAudit("Field Policy Changed", `${k} = ${v}`); }; const fmtTs = (iso) => new Date(iso).toLocaleString("en-US", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }); const auditFiltered = audit.filter(a => !filter || (a.user + a.role + a.action + a.detail).toLowerCase().includes(filter.toLowerCase())); const TABS = [ { k: "perms", label: "Roles & Permissions" }, { k: "audit", label: `Audit Trail (${audit.length})` }, { k: "policy", label: "Required Fields" }, { k: "data", label: "Data & Sync" }, ]; return (
{TABS.map(t => ( ))}
{/* ── ROLES & PERMISSIONS ── */} {tab === "perms" && ( <>
Toggle which modules each role can open. Admin always has full access. Changes are logged to the audit trail.
💾 Save Permissions {dirty && Unsaved}
{ROLES.map(r => )} {PERM_MODULES.map(g => ( {g.items.map(item => ( {ROLES.map(role => { const on = perms[role] && perms[role][item.key]; const locked = role === "Admin"; return ( ); })} ))} ))}
Module{r}
{g.group}
{item.label}
)} {/* ── AUDIT TRAIL ── */} {tab === "audit" && ( <>
Filter: setFilter(e.target.value)} placeholder="user, action, or detail" style={{ flex: 1, maxWidth: 320, background: "var(--k2b-yellow, #FFFFE1)", border: "1px solid #808080", padding: "2px 6px", fontFamily: "'Courier Prime', monospace" }} /> {auditFiltered.length} of {audit.length} entries · newest first { const blob = new Blob([JSON.stringify(audit, null, 2)], { type: "application/json" }); const u = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = u; a.download = "jbrews-audit-" + new Date().toISOString().slice(0,10) + ".json"; a.click(); URL.revokeObjectURL(u); }}>⬇ Export
{auditFiltered.length === 0 && } {auditFiltered.map((a, i) => ( ))}
WhenUser RoleActionDetail
No matching audit entries.
{fmtTs(a.ts)} {a.user} {a.role} {a.action} {a.detail}
)} {/* ── REQUIRED FIELDS ── */} {tab === "policy" && (
Enforce that critical fields are completed before a document can be saved or posted (spec §8 — required fields).
{[ { k: "vinRequired", label: "VIN required", detail: "Block stock-in / deal posting without a VIN" }, { k: "mileageRequired", label: "Mileage / odometer required", detail: "Block RO close and deal posting without mileage" }, { k: "custSigRequired", label: "Customer signature required", detail: "Block delivery without a signed buyer's order" }, { k: "payTypeRequired", label: "RO pay type required", detail: "Block RO close without Customer / Warranty / Internal" }, { k: "acctRequired", label: "GL account required on JE", detail: "Block journal posting with a blank account line" }, { k: "costRequired", label: "Vehicle cost required", detail: "Block deal posting without recorded cost (protects gross)" }, ].map(row => (
{row.label}
{row.detail}
setPol(row.k, v)} />
))}
)} {/* ── DATA & SYNC ── */} {tab === "data" && }
ESC-Close go && go("settings")}>F2-Settings setTab("audit")}>F3-Audit Trail go && go("home")}>F5-Dashboard
); }; const CtlToggle = ({ value, onChange }) => ( ); /* ── Data & Sync: live stations, encryption, backups, reset/wipe ── */ const DataSyncPanel = ({ go }) => { const [, force] = React.useReducer(n => n + 1, 0); React.useEffect(() => { if (!window.DMS) return; const offP = window.DMS.onPresence(force); const id = setInterval(force, 4000); return () => { offP(); clearInterval(id); }; }, []); const live = window.DMS ? window.DMS.presence() : []; const mySession = window.DMS ? window.DMS.session() : { station: "—" }; const vault = window.JBVault; const encOn = vault && vault.isEnabled(); const [confirm, setConfirm] = React.useState(null); // 'reset' | 'wipe' const [encForm, setEncForm] = React.useState({ open: false, p1: "", p2: "" }); const [pwForm, setPwForm] = React.useState({ open: false, old: "", n1: "" }); const card = { background: "#FFF", border: "1px solid var(--jb-border, #C0C0C0)", borderRadius: 8, padding: "14px 16px", marginBottom: 14 }; const head = { fontSize: 13, fontWeight: 700, color: "var(--jb-navy, #1B66A8)", marginBottom: 8, display: "flex", alignItems: "center", gap: 8 }; const doEnable = async () => { if (encForm.p1.length < 6) { window.k2bToast && window.k2bToast("Passphrase must be at least 6 characters.", "warn"); return; } if (encForm.p1 !== encForm.p2) { window.k2bToast && window.k2bToast("Passphrases do not match.", "warn"); return; } const r = await window.DMS.enableEncryption(encForm.p1, "browser"); window.k2bToast && window.k2bToast(r.msg, r.ok ? "ok" : "warn", 3400); setEncForm({ open: false, p1: "", p2: "" }); force(); }; const doChangePw = async () => { // (blocker 4) DMS re-encrypts this PC's copy with the new key at once const r = window.DMS && window.DMS.changePassphrase ? await window.DMS.changePassphrase(pwForm.old, pwForm.n1, "browser") : await window.JBVault.changePassphrase(pwForm.old, pwForm.n1, "browser"); window.k2bToast && window.k2bToast(r.ok ? "Passphrase changed." : r.msg, r.ok ? "ok" : "warn", 3200); if (r.ok) setPwForm({ open: false, old: "", n1: "" }); force(); }; const doDisable = async () => { const r = await window.DMS.disableEncryption(); window.k2bToast && window.k2bToast(r.msg, r.ok ? "ok" : "warn", r.ok ? undefined : 6000); force(); }; const vaultLocked = !!(encOn && vault && vault.isUnlocked && !vault.isUnlocked()); return (
{/* Live stations */}
Live Stations · {live.length} online
Every signed-in window on this workstation shares one ledger and syncs in real time. You are {mySession.station}. Open this URL in another window to add a station.
{live.map(s => ( ))} {live.length === 0 && }
StationUserScreenHeartbeat
{s.station}{s.sessionId === mySession.sessionId ? " (you)" : ""} {s.user} {(window.ROUTE_LABELS && window.ROUTE_LABELS[s.route]) || s.route || "—"} ● {Math.max(0, Math.round((Date.now() - (s.ts || 0)) / 1000))}s ago
Connecting…
{/* Encryption */}
🔐 Encryption at Rest {encOn ? {vaultLocked ? "ON · LOCKED — changes are not saved on this PC until it is unlocked" : "ON · AES-256-GCM"} : OFF}
{encOn ? "Dealership data is stored as AES-256-GCM ciphertext. A passphrase is required to decrypt it when this browser is opened fresh." : "Data is currently stored in readable form. Turn on encryption to protect it at rest with a dealership passphrase."}
{!vault || !vault.supported() ?
This browser does not support Web Crypto — encryption unavailable.
: !encOn ? ( !encForm.open ? setEncForm({ open: true, p1: "", p2: "" })}>Enable Encryption… : (
New passphrase: setEncForm({ ...encForm, p1: e.target.value })} /> Confirm: setEncForm({ ...encForm, p2: e.target.value })} />
Encrypt Data setEncForm({ open: false, p1: "", p2: "" })}>Cancel
⚠ Store this passphrase safely — encrypted data cannot be recovered without it.
) ) : ( !pwForm.open ?
setPwForm({ open: true, old: "", n1: "" })}>Change Passphrase… { window.JBVault.lock(); window.k2bToast && window.k2bToast("Vault locked. Changes are no longer saved on this PC until you reload and enter the passphrase (they still go to the server).", "info", 6000); force(); }}>Lock Now Turn Off Encryption
: (
Current passphrase: setPwForm({ ...pwForm, old: e.target.value })} /> New passphrase: setPwForm({ ...pwForm, n1: e.target.value })} />
Save setPwForm({ open: false, old: "", n1: "" })}>Cancel
) )}
{/* Backup */}
💾 Backup & Restore
Download a full backup of the ledger, or restore one. Encrypted backups (.jbz) require their passphrase to import.
{ const payload = window.DMS.exportBackup(); const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `jbrews-${(window.K2BStore && window.K2BStore.pageId()) || "store"}-backup-${new Date().toISOString().slice(0,10)}.json`; a.click(); URL.revokeObjectURL(url); window.DMS.logAudit("Data Backup", "Full ledger downloaded"); window.k2bToast && window.k2bToast("Backup downloaded.", "ok"); }}>⬇ Download Backup { localStorage.setItem("jbrews-upload-mode", "restore"); go && go("uploadtool"); }}>⬆ Restore from File…
{/* Reset / Wipe — audit C-01 / C-05: admin-only. These replace or erase the SHARED ledger for every station. */} {!(window.jbIsAdmin ? window.jbIsAdmin() : false) ? (
⚠ Data Lifecycle
Wipe, sample-data and legacy-import actions are available to Admin users only.
) : (
⚠ Data Lifecycle (admin)
The dealership starts blank — you enter your own records and they persist until you change them here. Nothing is ever auto-discarded. These actions replace the SHARED ledger and broadcast to every station instantly.
Load Sample Data replaces everything with the fictional demo dealership (deals, ROs, parts, GL balances and demo journal entries) — only for training before real work starts.
Import Legacy Data merges the real parts master + service history recovered from the old Btrieve files (PTL/SSM) — it adds records without wiping what's there.
{((window.DMS && ((window.DMS.deals && window.DMS.deals().some(d => d.postedToGL)) || (window.DMS.journalEntries && window.DMS.journalEntries().length > 0) || (window.DMS.customers && Object.keys(window.DMS.customers()).length > 0))) && !(window.DMS.isDemo && window.DMS.isDemo())) ? null : confirm !== "blank" ? setConfirm("blank")}>Start Blank / Wipe All Data : Erase all records on EVERY station and start empty? { window.DMS.startBlank(); setConfirm(null); window.k2bToast && window.k2bToast("Started blank — all records cleared.", "ok"); }}>Confirm setConfirm(null)}>Cancel} {((window.DMS && ((window.DMS.deals && window.DMS.deals().some(d => d.postedToGL)) || (window.DMS.journalEntries && window.DMS.journalEntries().length > 0) || (window.DMS.customers && Object.keys(window.DMS.customers()).length > 0))) && !(window.DMS.isDemo && window.DMS.isDemo())) ? 🔒 Sample data & wipe are locked — this dealership has live records. : confirm !== "sample" ? setConfirm("sample")}>Load Sample Data : Replace the SHARED ledger (all stations) with the demo dataset? { const typed = window.prompt("Type LOAD to replace the shared dealership ledger with the demo dataset:"); if (String(typed || "").trim() !== "LOAD") { window.k2bToast && window.k2bToast("Not loaded — you must type LOAD exactly.", "warn"); return; } const r = window.DMS.loadSampleData(); // Demo GL journal and demo fleet accounts are explicit too // (Accounting / Registry no longer seed them on their own). if (window.__k2bJEStore && window.__k2bJEStore.loadDemoJournal) window.__k2bJEStore.loadDemoJournal(); if (window.jbFleet && window.jbFleet.loadDemo) window.jbFleet.loadDemo(); setConfirm(null); window.k2bToast && window.k2bToast((r && r.msg) || "Sample data loaded.", "ok", 5000); }}>Confirm setConfirm(null)}>Cancel} {confirm !== "legacy" ? setConfirm("legacy")}>Import Legacy Data (PTL/SSM) : Merge real parts + service history from the old Btrieve files? { const run = () => { const r = window.DMS.importLegacy(); setConfirm(null); window.k2bToast && window.k2bToast(r.msg, r.ok ? "ok" : "err"); }; // legacy_seed.js is no longer in the boot load order — fetch on demand. window.jbLoadLegacySeed ? window.jbLoadLegacySeed().then(run) : run(); }}>Confirm setConfirm(null)}>Cancel}
)}
); }; const ctlTable = { width: "100%", borderCollapse: "collapse", fontFamily: "'Courier Prime', monospace", fontSize: 12 }; const ctlHead = { background: "#C0C0C0", color: "var(--k2b-maroon, #8a1c1c)", borderBottom: "1px solid #808080", position: "sticky", top: 0 }; const cTh = { padding: "3px 8px", textAlign: "left", whiteSpace: "nowrap" }; const cTd = { padding: "3px 8px", color: "var(--k2b-navy, #11337A)" }; const cTdMono = { ...cTd, fontFamily: "'Courier Prime', monospace", fontWeight: 700, color: "var(--k2b-blue-data-dk, #11337A)" }; // Expose a permission checker other modules / the shell can use. window.jbCan = function(role, moduleKey) { try { const p = loadPerms(); if (!p[role]) return true; return !!p[role][moduleKey]; } catch (e) { return true; } }; Object.assign(window, { Controls });