/* 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 — Shared registries: USERS (directory), LENDERS/BANKS, and F&I PRODUCTS. // Lightweight localStorage-backed stores so the data persists across reloads // and is shared by every screen that needs it: // • jbUsers is a name/role DIRECTORY only (CRM roster, presence). It holds // NO passwords and ships empty — sign-on is validated by the server. // • Settings → Users (Server) is the one place accounts are managed. // • NOTE (audit C-04): lenders / products / forms / fleet are still // per-workstation. Settings → Data & Backup has an export/import so the // book can be copied between stations until DMSCore gets shared // `lenders` / `products` / `forms` collections (see fix-handoff-ui.md). // • Desking, Finance Worksheet, Banks screen read jbLenders. // • Desking, Finance Worksheet, F&I Products screen read jbProducts. // Each store exposes load/save/list + a subscribe() so screens re-render live. (function () { // ── tiny pub/sub ─────────────────────────────────────────────── function makeStore(key, version, defaults, normalize) { const STORAGE = `jbrews-${key}-v${version}`; const listeners = new Set(); function load() { try { const raw = localStorage.getItem(STORAGE); if (raw) { const parsed = JSON.parse(raw); // An empty array is a DELIBERATE empty store (e.g. after Wipe // Workstation Data) — only seed defaults when the key is absent. if (Array.isArray(parsed)) return parsed.map(normalize); } } catch (e) {} // Fresh-start: after a data wipe, data stores seed EMPTY — never their // demo defaults. Users are exempt (sign-in must keep working). try { if (key !== "users" && localStorage.getItem("jbrews-fresh-start")) return []; } catch (e) {} return defaults.map(normalize); } function save(list) { try { localStorage.setItem(STORAGE, JSON.stringify(list)); } catch (e) {} listeners.forEach(fn => { try { fn(list); } catch (e) {} }); } // Cross-tab real-time: when another station saves this store, notify this // tab's subscribers so every open screen re-renders instantly. if (typeof window !== "undefined") { window.addEventListener("storage", (e) => { if (e.key !== STORAGE) return; listeners.forEach(fn => { try { fn(load()); } catch (err) {} }); }); } return { STORAGE, list: load, save, subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); }, upsert(rec) { const list = load(); const i = list.findIndex(x => x.id === rec.id); if (i >= 0) list[i] = normalize({ ...list[i], ...rec }); else list.unshift(normalize(rec)); save(list); return rec; }, remove(id) { save(load().filter(x => x.id !== id)); }, get(id) { return load().find(x => x.id === id) || null; }, reset() { try { localStorage.removeItem(STORAGE); } catch (e) {} listeners.forEach(fn => { try { fn(load()); } catch (e) {} }); }, }; } const uid = (p) => p + Math.random().toString(36).slice(2, 8) + Date.now().toString(36).slice(-3); // ════════════════════════ USERS ════════════════════════ const USER_ROLES = [ "Admin", "General Manager", "Sales Manager", "Salesperson", "F&I Manager", "Accounting", "Service Advisor", "Parts Counter", ]; // Per-user capability flags (mirrors the advisor/permission grids in // production DMS platforms). Shown as toggles in Settings → Users. const PERMISSIONS = [ { key: "editDeals", label: "Edit Deals", group: "Sales" }, { key: "fiProcessing", label: "F&I Processing", group: "Sales" }, { key: "processRefund", label: "Process Refund", group: "Sales" }, { key: "viewReports", label: "View Reports", group: "Sales" }, { key: "closeRO", label: "Close / Post RO", group: "Service" }, { key: "showEdr", label: "Show EDR", group: "Service" }, { key: "showRepairOrders", label: "Show Repair Orders", group: "Service" }, { key: "manualPayments", label: "Allow Manual Payments", group: "Accounting" }, { key: "postAccounting", label: "Post to Accounting", group: "Accounting" }, { key: "showDmsId", label: "Show DMS ID", group: "Admin" }, { key: "editGLFlow", label: "Edit GL Flow", group: "Admin" }, { key: "manageUsers", label: "Manage Users", group: "Admin" }, ]; const PERM_KEYS = PERMISSIONS.map(p => p.key); const ALL_ON = () => PERM_KEYS.reduce((m, k) => (m[k] = true, m), {}); // Sensible per-role defaults applied to new users. const ROLE_PERMS = { "Admin": ALL_ON(), "General Manager": ALL_ON(), "Sales Manager": { editDeals: true, fiProcessing: true, processRefund: true, viewReports: true, showDmsId: true }, "Salesperson": { editDeals: true, viewReports: false }, "F&I Manager": { editDeals: true, fiProcessing: true, processRefund: true, viewReports: true, showDmsId: true }, "Accounting": { viewReports: true, manualPayments: true, postAccounting: true, processRefund: true, showDmsId: true }, "Service Advisor": { closeRO: true, showEdr: true, showRepairOrders: true, viewReports: true }, "Parts Counter": { showRepairOrders: true }, }; const defaultPerms = (role) => ({ ...PERM_KEYS.reduce((m, k) => (m[k] = false, m), {}), ...(ROLE_PERMS[role] || {}) }); // Audit C-02c: this registry is a DIRECTORY only (names / roles / permission // flags for the CRM roster and presence panel). It ships EMPTY — no seeded // accounts — and it never stores a password. Sign-on is validated by the // server (DMS.login); accounts are managed in Settings → Users (Server). // Any password that reaches normUser (old local copies, stale server // mirrors) is dropped on the floor so it can't be re-mirrored anywhere. const LEGACY_SEED_IDS = ["U-489A", "U-310", "U-204", "U-415", "U-501"]; // ids of the retired shipped demo accounts const normUser = (u) => { const role = USER_ROLES.includes(u.role) ? u.role : "Salesperson"; return { id: u.id || uid("U-"), username: String(u.username || "").trim(), name: u.name || "", role, email: u.email || "", active: u.active !== false, mfa: !!u.mfa, updatedAt: +u.updatedAt || Date.now(), perms: { ...defaultPerms(role), ...(u.perms || {}) }, }; }; // v4: new key so stations that still hold the old seeded v3 list start clean. try { localStorage.removeItem("jbrews-users-v3"); } catch (e) {} const users = makeStore("users", 4, [], normUser); // Hide the retired demo directory entries if a stale server mirror carries them. const _usersList = users.list; users.list = () => _usersList().filter(u => !LEGACY_SEED_IDS.includes(u.id)); // Stamp edits so multi-station merges keep the NEWEST copy of each account. const _usersUpsert = users.upsert.bind(users); users.upsert = (rec) => { const safe = Object.assign({}, rec || {}); delete safe.password; /* no object-rest: Babel hoists a shared `_excluded` const that collides across files */ return _usersUpsert({ ...safe, updatedAt: Date.now() }); }; users.ROLES = USER_ROLES; // Credential policy — enforced wherever users are created/edited: // username: 9+ characters, letters AND numbers, no spaces // password: 10+ characters with letters, numbers, and a special character users.validateUsername = (u) => { const s = String(u || "").trim(); if (s.length < 9) return "Username must be at least 9 characters."; if (!/^[A-Za-z0-9]+$/.test(s)) return "Username may only contain letters and numbers."; if (!/[A-Za-z]/.test(s) || !/\d/.test(s)) return "Username must include both letters and numbers."; return ""; }; users.validatePassword = (p) => { const s = String(p || ""); if (s.length < 10) return "Password must be at least 10 characters."; if (!/[A-Za-z]/.test(s)) return "Password must include a letter."; if (!/\d/.test(s)) return "Password must include a number."; if (!/[^A-Za-z0-9]/.test(s)) return "Password must include a special character (!@#$…)."; return ""; }; // (C-02c) The old "multi-station login sync" bridge (pushToDMS / pullFromDMS // via DMS.syncUsersFromRegistry / DMS.get().users) is GONE: users is no // longer a server collection and the core call is an inert no-op. Server // accounts are read through DMS.roster() / DMS.teamList() instead. users.PERMISSIONS = PERMISSIONS; users.PERM_KEYS = PERM_KEYS; users.defaultPerms = defaultPerms; // No users.authenticate: there are no local passwords. Sign-on is server-only // (SignOn → DMS.login); "Work Offline Anyway" opens a restricted local session. // ════════════════════════ LENDERS / BANKS ════════════════════════ const LENDER_TYPES = ["Captive", "Prime Bank", "Credit Union", "Subprime", "Buy-Here Pay-Here"]; const lenderDefaults = [ { id: "L-GMF", name: "GM Financial", short: "GMF", type: "Captive", active: true, buyRate: 5.49, sellRateCap: 7.49, reserveSplit: "75/25", flat: 0, acqFee: 0, minFico: 620, maxAdvance: 135, maxTermMo: 84, phone: "(800) 284-2271", contact: "Dealer Desk", notes: "Captive lender — best subvented rates on new GM units." }, // ── GM Financial TITLING ENTITIES (from the dealer's own GMF documents) ── // On a GMF LEASE the vehicle is titled to ACAR Leasing Ltd. (the lessee is // the registrant); on a GMF retail FINANCE deal the customer is the owner // with AmeriCredit d/b/a GM Financial recorded as lienholder. The role + // parent fields drive auto-selection in Desking / Title App / printables. // Fully editable in the Banks screen (addresses change). { id: "L-GMF-RETAIL", name: "AmeriCredit Financial Services, Inc. d/b/a GM Financial", short: "GMF-RTL", type: "Captive", active: true, role: "retail-lienholder", parent: "GM Financial", addr: "4001 Embarcadero Drive", city: "Arlington", state: "TX", zip: "76014", buyRate: 0, sellRateCap: 0, reserveSplit: "—", flat: 0, acqFee: 0, minFico: 0, maxAdvance: 0, maxTermMo: 0, phone: "(800) 284-2271", contact: "Dealer Desk", notes: "Titling entity — retail finance lienholder for GM Financial contracts (customer is owner; lien recorded to this entity)." }, { id: "L-GMF-LEASE", name: "ACAR Leasing Ltd. d/b/a GM Financial Leasing", short: "GMF-LSE", type: "Captive", active: true, role: "lease-lessor", parent: "GM Financial", addr: "P.O. Box 183581", city: "Arlington", state: "TX", zip: "76096-3581", buyRate: 0, sellRateCap: 0, reserveSplit: "—", flat: 0, acqFee: 0, minFico: 0, maxAdvance: 0, maxTermMo: 0, phone: "(800) 284-2271", contact: "Dealer Desk", notes: "Titling entity — lessor / titled owner on GM Financial leases (lessee is the registrant)." }, { id: "L-ALLY", name: "Ally Financial", short: "ALLY", type: "Prime Bank", active: true, buyRate: 6.24, sellRateCap: 8.24, reserveSplit: "70/30", flat: 0, acqFee: 0, minFico: 640, maxAdvance: 130, maxTermMo: 84, phone: "(888) 925-2559", contact: "Buyer 14", notes: "Strong on used; flexible advance." }, { id: "L-CHASE", name: "Chase Auto", short: "CHASE", type: "Prime Bank", active: true, buyRate: 6.49, sellRateCap: 8.49, reserveSplit: "70/30", flat: 0, acqFee: 0, minFico: 660, maxAdvance: 125, maxTermMo: 75, phone: "(800) 336-6675", contact: "Regional Desk", notes: "Prime/super-prime only." }, { id: "L-CAP1", name: "Capital One Auto", short: "CAP1", type: "Prime Bank", active: true, buyRate: 6.99, sellRateCap: 9.99, reserveSplit: "65/35", flat: 0, acqFee: 0, minFico: 600, maxAdvance: 128, maxTermMo: 75, phone: "(800) 689-1789", contact: "Auto Navigator", notes: "Wide credit band; quick auto-decision." }, { id: "L-EXET", name: "Exeter Finance", short: "EXET", type: "Subprime", active: true, buyRate: 11.90, sellRateCap: 19.99, reserveSplit: "Flat", flat: 595, acqFee: 0, minFico: 520, maxAdvance: 120, maxTermMo: 72, phone: "(866) 957-9876", contact: "Subprime Desk", notes: "Tier C/D paper; flat reserve." }, { id: "L-WEST", name: "Westlake Financial", short: "WEST", type: "Subprime", active: true, buyRate: 12.90, sellRateCap: 21.99, reserveSplit: "Flat", flat: 695, acqFee: 0, minFico: 500, maxAdvance: 115, maxTermMo: 72, phone: "(888) 893-7937", contact: "Funding", notes: "Deep subprime; strong on older/high-mile used." }, { id: "L-BNM", name: "Bank of New Madrid", short: "BNM", type: "Prime Bank", active: true, buyRate: 6.75, sellRateCap: 8.75, reserveSplit: "Flat", flat: 250, acqFee: 0, minFico: 660, maxAdvance: 120, maxTermMo: 72, phone: "(573) 748-5531", contact: "Tom R.", notes: "Local relationship bank; also floorplans used units." }, { id: "L-SEMOCU", name: "SE Missouri Credit Union", short: "SEMOCU", type: "Credit Union", active: true, buyRate: 5.99, sellRateCap: 6.99, reserveSplit: "Flat", flat: 175, acqFee: 0, minFico: 640, maxAdvance: 125, maxTermMo: 84, phone: "(573) 471-2222", contact: "Member Lending", notes: "Member rates; thin reserve but great approvals." }, ]; const normLender = (l) => ({ id: l.id || uid("L-"), name: l.name || "", short: l.short || (l.name || "").slice(0, 6).toUpperCase(), type: LENDER_TYPES.includes(l.type) ? l.type : "Prime Bank", active: l.active !== false, buyRate: +l.buyRate || 0, sellRateCap: +l.sellRateCap || 0, reserveSplit: l.reserveSplit || "70/30", flat: +l.flat || 0, acqFee: +l.acqFee || 0, minFico: +l.minFico || 0, maxAdvance: +l.maxAdvance || 0, maxTermMo: +l.maxTermMo || 0, phone: l.phone || "", contact: l.contact || "", notes: l.notes || "", // Titling-entity fields (lienholder / lessor wiring — blank on plain lenders) addr: l.addr || "", city: l.city || "", state: l.state || "", zip: l.zip || "", role: ["retail-lienholder", "lease-lessor"].includes(l.role) ? l.role : "", parent: l.parent || "", }); // RC35 (brd): the first store's local bank is a default of the PRIMARY store only (a new store's lender list starts without it) const lenderDefaultsHere = (window.K2BStore && window.K2BStore.pageId() && !window.K2BStore.isPrimary()) ? lenderDefaults.filter((l) => l.id !== "L-BNM") : lenderDefaults; const lenders = makeStore("lenders", 1, lenderDefaultsHere, normLender); lenders.TYPES = LENDER_TYPES; lenders.ROLES = ["", "retail-lienholder", "lease-lessor"]; // Titling entities (role != "") are legal lien/lessor entities — kept OUT of // the desking/finance lender menus, but they drive owner & lienholder lines. lenders.activeNames = () => lenders.list().filter(l => l.active && !l.role).map(l => l.name); // ── GMF entity auto-selection ──────────────────────────────────── // lender + deal type → the legal titling entity: // LEASE → role "lease-lessor" (ACAR Leasing Ltd. d/b/a GM Financial Leasing) // FINANCE → role "retail-lienholder" (AmeriCredit Financial Services, Inc. d/b/a GM Financial) // Matches on the entity's parent (or its own name); returns null for lenders // with no titling entity on the book so callers fall back to the plain name. lenders.titlingEntityFor = (lenderName, dealType) => { const ln = String(lenderName || "").trim().toLowerCase(); if (!ln) return null; const role = /lease/i.test(String(dealType || "")) ? "lease-lessor" : "retail-lienholder"; return lenders.list().find(l => l.active !== false && l.role === role && (String(l.parent || "").trim().toLowerCase() === ln || String(l.name || "").trim().toLowerCase() === ln)) || null; }; // Deal-record shape: deal.lienholder = { name, address, city, state, zip, kind } lenders.lienholderFor = (lenderName, dealType) => { const e = lenders.titlingEntityFor(lenderName, dealType); return e ? { name: e.name, address: e.addr, city: e.city, state: e.state, zip: e.zip, kind: e.role } : null; }; // One-time seed of the GMF titling entities into stations whose lender book // predates them (store defaults only seed when the key is absent). Marker- // guarded so a user who deliberately deletes an entity is not re-seeded. try { const GMF_MARK = "jbrews-gmf-entities-v1"; if (typeof localStorage !== "undefined" && !localStorage.getItem(GMF_MARK)) { const cur = lenders.list(); if (cur.length) { ["retail-lienholder", "lease-lessor"].forEach(role => { if (!cur.some(l => l.role === role)) { const seed = lenderDefaults.find(l => l.role === role); if (seed) lenders.upsert({ ...seed }); } }); } localStorage.setItem(GMF_MARK, "1"); } } catch (e) {} // ════════════════════════ F&I PRODUCTS (incl. service contracts) ════════ const PRODUCT_CATEGORIES = [ "Service Contract", "GAP", "Maintenance", "Tire & Wheel", "Appearance Protection", "Key / Theft", "Other", ]; const productDefaults = [ { id: "P-VSC-PLAT", category: "Service Contract", name: "Platinum VSC — 7yr/100k", provider: "GM Protection Plan", retail: 3295, cost: 1810, taxable: false, termMo: 84, active: true, code: "VSC-PLAT", notes: "Exclusionary; 0 deductible. Best penetration on new." }, { id: "P-VSC-GOLD", category: "Service Contract", name: "Gold VSC — 6yr/75k", provider: "GM Protection Plan", retail: 2895, cost: 1545, taxable: false, termMo: 72, active: true, code: "VSC-GOLD", notes: "Stated-component; $100 deductible." }, { id: "P-VSC-USED", category: "Service Contract", name: "Certified Used VSC — 4yr/48k", provider: "Zurich", retail: 2195, cost: 1180, taxable: false, termMo: 48, active: true, code: "VSC-USED", notes: "Powertrain-plus for used inventory." }, { id: "P-GAP", category: "GAP", name: "GAP Waiver", provider: "GMF GAP", retail: 895, cost: 395, taxable: false, termMo: 0, active: true, code: "GAP", notes: "Caps to 150% LTV. Refundable on early payoff." }, { id: "P-MAINT", category: "Maintenance", name: "Prepaid Maintenance — 4yr", provider: "GM PMP", retail: 995, cost: 560, taxable: false, termMo: 48, active: true, code: "PMP", notes: "Covers scheduled oil/rotation/inspections." }, { id: "P-TW", category: "Tire & Wheel", name: "Tire & Wheel Protection", provider: "Zurich Road Hazard", retail: 795, cost: 360, taxable: false, termMo: 60, active: true, code: "TW", notes: "Road-hazard repair/replace; no deductible." }, { id: "P-APP", category: "Appearance Protection", name: "Paint & Fabric / Appearance", provider: "Simoniz", retail: 695, cost: 240, taxable: true, termMo: 60, active: true, code: "APP", notes: "Interior/exterior surface protection." }, { id: "P-KEY", category: "Key / Theft", name: "Key Replacement + Theft Etch", provider: "EasyCare", retail: 399, cost: 120, taxable: false, termMo: 60, active: true, code: "KEY", notes: "Smart-key replacement; VIN etch theft deterrent." }, ]; const normProduct = (p) => ({ id: p.id || uid("P-"), category: PRODUCT_CATEGORIES.includes(p.category) ? p.category : "Other", name: p.name || "", provider: p.provider || "", code: p.code || "", retail: +p.retail || 0, cost: +p.cost || 0, taxable: !!p.taxable, termMo: +p.termMo || 0, active: p.active !== false, notes: p.notes || "", }); const products = makeStore("products", 1, productDefaults, normProduct); products.CATEGORIES = PRODUCT_CATEGORIES; products.activeByCat = (cat) => products.list().filter(p => p.active && p.category === cat); // ════════════════════════ FORMS SEQUENCING ════════════════════════ // Document Group Assignment — which printed forms attach to a deal, and in // what order, based on deal type (New/Used/Lease), the funding bank, and the // buyer's state. Mirrors the Dealertrack "Forms Sequence / Document Group // Assignment" flow. resolve(ctx) returns the ordered, matching docs. const DEAL_TYPES = ["New", "Used", "Lease", "Wholesale", "Cash"]; // groups: which conditional bucket a form belongs to (drives auto-attach). const FORM_GROUPS = ["Always Required", "Finance", "Lease", "Trade", "Service Contracts", "Aftermarket", "Taxes", "Out-of-State"]; const formDefaults = [ { id: "F-BO", seq: 10, name: "Vehicle Buyer's Order", printKey: "buyersOrder", group: "Always Required", dealTypes: ["New", "Used", "Lease", "Cash"], banks: [], states: [], copies: 2, dest: "screen", active: true }, { id: "F-BOS", seq: 11, name: "Bill of Sale", printKey: "billOfSale", group: "Always Required", // RC37: seq 20 -> 11 (the dealer's packet order) dealTypes: ["New", "Used", "Cash"], banks: [], states: [], copies: 1, dest: "screen", active: true }, { id: "F-ODO", seq: 12, name: "Odometer Disclosure", printKey: "odometer", group: "Always Required", // RC37: seq 30 -> 12 dealTypes: ["New", "Used", "Lease"], banks: [], states: [], copies: 1, dest: "screen", active: true }, { id: "F-PRIV", seq: 40, name: "Privacy Notice", printKey: "privacy", group: "Always Required", dealTypes: ["New", "Used", "Lease", "Cash"], banks: [], states: [], copies: 1, dest: "screen", active: true }, { id: "F-RTL", seq: 50, name: "Retail Installment Contract", printKey: "buyersOrder", group: "Finance", dealTypes: ["New", "Used"], banks: [], states: [], copies: 2, dest: "printer", active: true }, { id: "F-BANK", seq: 60, name: "Bank Contract / Lender Agreement", printKey: "buyersOrder", group: "Finance", dealTypes: ["New", "Used"], banks: ["GM Financial", "Ally Financial"], states: [], copies: 1, dest: "printer", active: true }, { id: "F-LEASE",seq: 70, name: "Lease Agreement", printKey: "buyersOrder", group: "Lease", dealTypes: ["Lease"], banks: [], states: [], copies: 2, dest: "printer", active: true }, // Lease packet + retail contract prep (B-03) — print functions in printables.jsx, // wired through PrintSelect.printerFor. { id: "F-LWS", seq: 71, name: "Lease Worksheet (Dealer)", printKey: "leaseWorksheet", group: "Lease", dealTypes: ["Lease"], banks: [], states: [], copies: 1, dest: "screen", active: true }, { id: "F-LDISC", seq: 72, name: "Lease Disclosure (Customer)", printKey: "leaseDisclosure", group: "Lease", dealTypes: ["Lease"], banks: [], states: [], copies: 2, dest: "printer", active: true }, { id: "F-GMFODO",seq: 73, name: "Leased Vehicle Odometer Statement (GMF)", printKey: "gmfLeaseOdometer", group: "Lease", dealTypes: ["Lease"], banks: ["GM Financial"], states: [], copies: 1, dest: "printer", active: true }, { id: "F-553", seq: 55, name: "Contract Prep — 553-MO (Retail)", printKey: "contractPrep", group: "Finance", dealTypes: ["New", "Used"], banks: [], states: [], copies: 1, dest: "screen", active: true }, { id: "F-TRADE",seq: 80, name: "Release of Liability (Trade)", printKey: "releaseTrade", group: "Trade", dealTypes: ["New", "Used", "Lease"], banks: [], states: [], copies: 1, dest: "screen", active: true }, { id: "F-ASIS", seq: 21, name: "Sold \u201cAs-Is\u201d \u2014 No Warranty", printKey: "asIs", group: "Always Required", // RC37: seq 90 -> 21 dealTypes: ["Used"], banks: [], states: [], copies: 1, dest: "screen", active: true }, // RC37 (forms37): the dealership's own forms, in its deal packet's order (seq 10-24 with F-BO / F-BOS / F-ODO / F-ASIS; K2BForms.jsx.txt prints them) ...[["F37-BG", 13, "FTC Buyers Guide", "buyerGuide", "Always Required", ["Used"]], ["F37-POA", 14, "MO Secure Power of Attorney (DOR 5086)", "moSecurePoa", "Trade", ["New", "Used", "Lease"]], ["F37-DMG", 15, "Acknowledgement of Disclosure of Damage", "damageAck", "Always Required", ["New", "Used", "Cash"]], ["F37-FUND", 16, "Funding Notice", "fundingNotice", "Finance", ["New", "Used"]], ["F37-TRC", 17, "Title Received Confirmation", "titleReceived", "Always Required", ["New", "Used", "Cash"]], ["F37-TRF", 18, "Title Receipt Form", "titleReceipt", "Always Required", ["New", "Used", "Cash"]], ["F37-CA", 19, "Credit Authorization Form", "creditApp", "Finance", ["New", "Used", "Lease"]], ["F37-TLT", 20, "Authorization for Title and Lien Transfer", "titleLienAuth", "Trade", ["New", "Used", "Lease"]], ["F37-TIA", 22, "Trade-In Authorization and Assignment", "tradeAck", "Trade", ["New", "Used", "Lease"]], ["F37-INS", 23, "Agreement to Provide Insurance", "insuranceAgreement", "Finance", ["New", "Used", "Lease"]], ["F37-WO", 24, "We Owe", "weOweYouOwe", "Always Required", ["New", "Used", "Lease", "Cash"]]] .map(([id, seq, name, printKey, group, dealTypes]) => ({ id, seq, name, printKey, group, dealTypes, banks: [], states: [], copies: 1, dest: "screen", active: true })), { id: "F-VSC", seq: 100, name: "Service Contract Application", printKey: "buyersOrder", group: "Service Contracts", dealTypes: ["New", "Used"], banks: [], states: [], copies: 1, dest: "screen", active: true }, { id: "F-OOS", seq: 110, name: "Out-of-State Tax Affidavit", printKey: "buyersOrder", group: "Out-of-State", dealTypes: ["New", "Used"], banks: [], states: ["AR", "TN", "IL", "KY"], copies: 1, dest: "screen", active: true }, ]; const normForm = (f) => ({ id: f.id || uid("F-"), seq: +f.seq || 0, name: f.name || "", printKey: f.printKey || "buyersOrder", group: FORM_GROUPS.includes(f.group) ? f.group : "Always Required", dealTypes: Array.isArray(f.dealTypes) ? f.dealTypes : [], banks: Array.isArray(f.banks) ? f.banks : [], states: Array.isArray(f.states) ? f.states.map(s => String(s).toUpperCase().trim()).filter(Boolean) : [], copies: +f.copies || 1, dest: f.dest === "printer" ? "printer" : "screen", active: f.active !== false, }); const forms = makeStore("forms", 1, formDefaults, normForm); forms.DEAL_TYPES = DEAL_TYPES; forms.GROUPS = FORM_GROUPS; // One-time merge-by-id of the lease / contract-prep forms into stations // that already hold a saved forms list (the store only seeds defaults when // its key is absent). Marker-guarded so a deliberately deleted form stays // deleted. Skipped on a fresh-start (empty) book. try { const LEASE_MARK = "jbrews-forms-lease-v1"; if (typeof localStorage !== "undefined" && !localStorage.getItem(LEASE_MARK)) { const cur = forms.list(); if (cur.length) { ["F-LWS", "F-LDISC", "F-GMFODO", "F-553"].forEach(id => { if (!cur.some(f => f.id === id)) { const seed = formDefaults.find(f => f.id === id); if (seed) forms.upsert({ ...seed }); } }); } localStorage.setItem(LEASE_MARK, "1"); } } catch (e) {} // RC37 (forms37): once per store, a saved list gets the dealership's forms; F-BOS / F-ODO / F-ASIS move to the dealer's packet order only // while they still have the old default sequence numbers (a number the owner changed stays). Skipped on a fresh-start (empty) book. try { const M37 = window.K2BStore && window.K2BStore.storeKey ? window.K2BStore.storeKey("jbrews-forms-rc37-v1") : "jbrews-forms-rc37-v1"; if (typeof localStorage !== "undefined" && !localStorage.getItem(M37)) { const cur = forms.list(); if (cur.length) { formDefaults.filter(f => /^F37-/.test(f.id) && !cur.some(c => c.id === f.id)).forEach(f => forms.upsert({ ...f })); [["F-BOS", 20, 11], ["F-ODO", 30, 12], ["F-ASIS", 90, 21]].forEach(([id, was, now]) => { const c = forms.get(id); if (c && +c.seq === was) forms.upsert({ id, seq: now }); }); } localStorage.setItem(M37, "1"); } } catch (e) {} // Resolve the ordered packet for a deal context { dealType, bank, state }. forms.resolve = (ctx = {}) => { const dt = ctx.dealType || "Used"; const bank = ctx.bank || ""; const st = String(ctx.state || "").toUpperCase().trim(); return forms.list().filter(f => f.active) .filter(f => !f.dealTypes.length || f.dealTypes.includes(dt)) .filter(f => !f.banks.length || (bank && f.banks.includes(bank))) .filter(f => !f.states.length || (st && f.states.includes(st))) .sort((a, b) => a.seq - b.seq); }; // ════════════════════════ GM FLEET ACCOUNTS ════════════════════════ // Fleet customers + their GM Fleet Account Number (FAN) and incentive // eligibility. Types per the GM Fleet Policy manual. const FLEET_TYPES = ["Small Business", "Dealer Fleet", "Large Fleet", "Government", "Rental"]; // Common GM fleet incentive programs (representative). const FLEET_INCENTIVES = [ { code: "CPA", label: "Competitive Price Allowance" }, { code: "NFA", label: "National Fleet Account Incentive" }, { code: "FAR", label: "Fleet Account Retail (FAR)" }, { code: "GVT", label: "Government Allowance" }, { code: "RENT", label: "Daily Rental Allowance" }, { code: "BUS", label: "Small Business / Commercial" }, ]; // Audit P2-6: the fleet book ships EMPTY. These four fictional accounts // ($2.5M YTD) are DEMO rows, loaded only by Controls → Load Sample Data // (jbFleet.loadDemo()) — never seeded into a blank store. const fleetDemo = [ { id: "FL-1001", name: "CITY OF SIKESTON — PUBLIC WORKS", type: "Government", fan: "GF-204815", contact: "Fleet Office", phone: "573-555-0190", email: "fleet@sikeston.example", arAcct: "1001", incentives: ["GVT", "NFA"], ytdUnits: 11, ytdSpend: 486200, fanRequired: true, active: true, notes: "Tax-exempt municipal fleet. PPV/Tahoe + Silverado units." }, { id: "FL-1002", name: "DELTA LOGISTICS LLC", type: "Dealer Fleet", fan: "GF-118720", contact: "Ray Holcomb", phone: "573-555-7714", email: "purchasing@deltalog.example", arAcct: "1002", incentives: ["NFA", "FAR", "CPA"], ytdUnits: 24, ytdSpend: 1284000, fanRequired: true, active: true, notes: "Mixed Express/Silverado last-mile fleet, 24 units YTD." }, { id: "FL-1003", name: "RIVERBend HVAC & PLUMBING", type: "Small Business", fan: "", contact: "Owner", phone: "573-555-3320", email: "rb@riverbendhvac.example", arAcct: "", incentives: ["BUS"], ytdUnits: 3, ytdSpend: 142500, fanRequired: false, active: true, notes: "Owns 6 vans — under FAN threshold; small-business commercial only." }, { id: "FL-1004", name: "BOOTHEEL RENTAL CO.", type: "Rental", fan: "GF-330061", contact: "Fleet Desk", phone: "573-555-8890", email: "ops@bootheelrental.example", arAcct: "", incentives: ["RENT"], ytdUnits: 18, ytdSpend: 612000, fanRequired: true, active: true, notes: "Daily rental fleet — 60-day hold rules apply." }, ]; const normFleet = (f) => ({ id: f.id || uid("FL-"), name: f.name || "", type: FLEET_TYPES.includes(f.type) ? f.type : "Small Business", fan: (f.fan || "").trim().toUpperCase(), contact: f.contact || "", phone: f.phone || "", email: f.email || "", arAcct: f.arAcct || "", incentives: Array.isArray(f.incentives) ? f.incentives : [], ytdUnits: +f.ytdUnits || 0, ytdSpend: +f.ytdSpend || 0, fanRequired: f.type === "Small Business" ? false : (f.fanRequired !== false), active: f.active !== false, notes: f.notes || "", }); // v2 key: stations that were auto-seeded with the demo accounts under v1 // start from an empty book. try { localStorage.removeItem("jbrews-fleet-v1"); } catch (e) {} const fleet = makeStore("fleet", 2, [], normFleet); fleet.TYPES = FLEET_TYPES; fleet.INCENTIVES = FLEET_INCENTIVES; fleet.loadDemo = () => { if (window.K2BStore && window.K2BStore.pageId() && !window.K2BStore.isPrimary()) return 0; fleetDemo.forEach(f => fleet.upsert({ ...f })); return fleetDemo.length; }; // RC35: sample rows - the primary store only // FAN compliance: types other than Small Business require a FAN on file. fleet.fanCompliant = (rec) => !rec.fanRequired || !!rec.fan; window.jbUsers = users; window.jbLenders = lenders; window.jbProducts = products; window.jbForms = forms; window.jbFleet = fleet; })();