/* 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 — Deal search (RC19). ONE search box for the long deal lists: // Desking "Pick Customer / Deal" (open deals) · Funding (posted deals) · Deal Pipeline. // Type and the list narrows: customer name (first, last, either order), co-buyer, phone (digits only — // any format), email, deal #, stock #, VIN (full, last 6 or last 8), salesperson, lender. // "12 of 240 deals", most recent first while searching; an empty box (and no chip) = the screen's own // list, unchanged. Keys: Ctrl+F or "/" focuses the box, ↑ ↓ move, Enter opens, Esc clears (Esc again // leaves the box). While the box has focus its keys never reach the screen's own shortcuts. // Enter opens ONLY the row the screen shows highlighted; with nothing highlighted (an empty box before any // ↑ ↓) Enter does nothing — it never falls back to "the first deal in the list". // It searches ONLY the deals this station already holds (DMS.deals()) — the server decides what a // login sees (a Sales login's other-people deals arrive without phone / email / amounts, so those // can never match on them); nothing new is asked of the server. // Every global name in this file starts with ds / Ds / DS_ (all screens share one global scope). const dsStr = (v) => (v == null ? "" : String(v)); const dsLow = (v) => dsStr(v).toLowerCase(); const dsDigitsOf = (v) => dsStr(v).replace(/\D/g, ""); const dsAlnumOf = (v) => dsStr(v).toUpperCase().replace(/[^A-Z0-9]/g, ""); // a phone as 10 digits (a leading US "1" dropped) — "(573) 555-0142", "573.555.0142", "+1 573 555 0142" are one number const dsPhoneKey = (v) => { const d = dsDigitsOf(v); return d.length === 11 && d[0] === "1" ? d.slice(1) : d; }; // the deal's date for "most recent first": delivered, else the deal date, else created (same rule as Classic Reports) const dsDealTime = (d) => { if (!d) return 0; const keys = ["delivered", "date", "created"]; for (let i = 0; i < keys.length; i++) { const t = Date.parse(d[keys[i]]); if (isFinite(t)) return t; } return 0; }; const dsCoBuyerNames = (d) => { const out = []; if (!d) return out; [d.cobuyer, d.coBuyerName, typeof d.coBuyer === "string" ? d.coBuyer : ""].forEach(x => { if (dsStr(x).trim()) out.push(dsStr(x)); }); const cbs = [d.coBuyer && typeof d.coBuyer === "object" ? d.coBuyer : null, d.creditApp && d.creditApp.coBuyer, d.credit && d.credit.coBuyer]; cbs.forEach(cb => { if (cb && typeof cb === "object") { const n = [cb.firstName || cb.first, cb.lastName || cb.last].filter(Boolean).join(" "); if (n.trim()) out.push(n); } }); return out; }; const dsCoBuyerContacts = (d) => { const ph = [], em = []; const cbs = [d && d.coBuyer && typeof d.coBuyer === "object" ? d.coBuyer : null, d && d.creditApp && d.creditApp.coBuyer, d && d.credit && d.credit.coBuyer]; cbs.forEach(cb => { if (cb && typeof cb === "object") { if (cb.phone) ph.push(cb.phone); if (cb.cell) ph.push(cb.cell); if (cb.email) em.push(cb.email); } }); if (d && d.coBuyerPhone) ph.push(d.coBuyerPhone); if (d && d.coBuyerEmail) em.push(d.coBuyerEmail); return { ph, em }; }; const DS_PHONE_KEYS = ["phone", "cell", "mobile", "homePhone", "workPhone", "cellPhone"]; // One searchable row per deal. `cust` = the station's customer file for the deal (its own deals only: // a deal the server sent in the limited view (_limited) is searched on what the row itself carries). function dsEntry(d, custs) { const cust = (!d._limited && d.custNum && custs) ? (custs[d.custNum] || null) : null; const custName = cust ? [cust.firstName, cust.lastName].filter(Boolean).join(" ") : ""; const cb = dsCoBuyerContacts(d); const phones = []; const addPh = (v) => { const k = dsPhoneKey(v); if (k.length >= 4 && phones.indexOf(k) === -1) phones.push(k); }; DS_PHONE_KEYS.forEach(k => { addPh(d[k]); if (cust) addPh(cust[k]); }); cb.ph.forEach(addPh); const emails = [d.email, cust && cust.email].concat(cb.em).filter(Boolean).map(dsStr); const cash = /cash/i.test(dsStr(d.dealType)) || (d.postedToGL && (+d.amtFinanced || 0) <= 0); // [label, text, alnum-compare?, shownInRow?] const fields = [ ["Buyer", [d.buyer, custName].filter(Boolean).join(" · "), false, true], ["Deal #", [d.id, d.invoiceNo, d.invNo].filter(Boolean).join(" · "), true, true], ["Co-buyer", dsCoBuyerNames(d).join(" · "), false, false], ["Email", emails.join(" · "), false, false], ["Stock #", d.stock, true, false], ["VIN", d.vin, true, false], ["Salesperson", d.salesperson, false, false], ["Lender", d.lender || (cash ? "Cash" : ""), false, false], ].filter(f => dsStr(f[1]).trim()); return { d, fields: fields.map(f => ({ label: f[0], text: dsStr(f[1]), low: dsLow(f[1]), al: f[2] ? dsAlnumOf(f[1]) : "", shown: f[3] })), phones, rawPhones: [d.phone, cust && cust.phone].concat(cb.ph).filter(Boolean), t: dsDealTime(d) }; } // The query as tokens. A query of digits only ("(573) 555-0142", "573", "2610134") is ONE number token: it matches // a phone (any format), or the digits of a deal #, stock # or VIN. function dsParse(q) { const s = dsStr(q).trim(); if (!s) return []; if (/^[\d\s().+\-]+$/.test(s) && dsDigitsOf(s).length >= 3) return [{ raw: s, low: dsLow(s), al: dsDigitsOf(s), ph: dsPhoneKey(s), phoneOnly: true }]; return s.split(/[\s,;]+/).filter(Boolean).map(t => { const isPh = /^[\d().+\-]+$/.test(t) && dsDigitsOf(t).length >= 3; return { raw: t, low: dsLow(t), al: dsAlnumOf(t), ph: isPh ? dsPhoneKey(t) : "", phoneOnly: false }; }); } // null = no match; else the label of the field that matched function dsMatchTok(e, tok) { if (tok.ph && e.phones.some(p => p.indexOf(tok.ph) !== -1)) return "Phone"; for (let i = 0; i < e.fields.length; i++) { const f = e.fields[i]; if (!tok.phoneOnly && f.low.indexOf(tok.low) !== -1) return f.label; if (f.al && tok.al.length >= 3 && f.al.indexOf(tok.al) !== -1) return f.label; } return null; } // Run one search: base = the screen's own list (its order), chips = [{key,label,group,test}], on = {group: key}. // Returns { list, total, filtering, why: Map(id -> "Phone (573) 555-0142") }. function dsSearchDeals(base, q, chips, on, custs) { const all = Array.isArray(base) ? base : []; const toks = dsParse(q); const active = (chips || []).filter(c => on && on[c.group] === c.key); const why = new Map(); if (!toks.length && !active.length) return { list: all, total: all.length, filtering: false, why }; const cmap = custs || ((window.DMS && window.DMS.customers && window.DMS.customers()) || {}); // RC31 (fx31, RC30 acceptance F6): an OLD 10-digit number / a phone in any format finds the deals of its account — DMS.custQueryHit, the one // reading every search box shares (a deal carries the account's customer #, often no phone of its own) const D0 = window.DMS; const custQ = D0 && D0.custNumsFor && D0.custNumsFor(q).length ? (d) => !d._limited && D0.custQueryHit(q, d.custNum) : null; const out = []; for (let i = 0; i < all.length; i++) { const d = all[i]; if (!d) continue; let ok = true; for (let c = 0; c < active.length && ok; c++) { try { ok = !!active[c].test(d); } catch (x) { ok = false; } } if (!ok) continue; if (toks.length) { const e = dsEntry(d, cmap); let hidden = null; for (let k = 0; k < toks.length && ok; k++) { const lab = dsMatchTok(e, toks[k]); if (!lab) ok = false; else if (!hidden) { const f = e.fields.find(x => x.label === lab); if (lab === "Phone" || (f && !f.shown)) hidden = lab; } } if (!ok && custQ && custQ(d)) { ok = true; why.set(d.id, "Customer # " + dsStr(d.custNum)); out.push({ d, t: e.t, i }); continue; } if (!ok) continue; if (hidden) { const f = e.fields.find(x => x.label === hidden); const val = hidden === "Phone" ? (e.rawPhones.find(p => toks.some(t => t.ph && dsPhoneKey(p).indexOf(t.ph) !== -1)) || e.rawPhones[0] || "") : (f ? f.text : ""); why.set(d.id, hidden + " " + val); } out.push({ d, t: e.t, i }); } else out.push({ d, t: dsDealTime(d), i }); } // most recent first; same date -> the screen's own order out.sort((a, b) => (b.t - a.t) || (a.i - b.i)); return { list: out.map(x => x.d), total: all.length, filtering: true, why }; } // Is this deal the signed-in person's own? (their CRM lead points at it, or the deal names them) — same rule as the desk's view-only screen function dsIsMine(d) { try { const D = window.DMS; if (!D || !d || d._limited) return false; const me = dsStr(D.userId && D.userId()); if (!me) return false; if (dsStr(d.spId) === me) return true; const leads = (D.crmLeads && D.crmLeads()) || []; if (leads.some(l => l && dsStr(l.dealId) === dsStr(d.id) && dsStr(l._owner) === me)) return true; const mine = ((D.roster && D.roster()) || []).find(u => u && dsStr(u.id) === me); const sp = dsStr(d.salesperson).split("/")[0].trim().toLowerCase(); return !!(mine && mine.name && sp && sp === dsStr(mine.name).trim().toLowerCase()); } catch (e) { return false; } } // May Ctrl+F / "/" move focus into this box right now? (it is on screen, and no other dialog is on top of it) function dsMayFocus(inp) { if (!inp || !inp.isConnected) return false; const r = inp.getBoundingClientRect(); if (!r.width && !r.height) return false; const all = document.querySelectorAll("[data-k2b-modal]"); if (all.length && !all[all.length - 1].contains(inp)) return false; return true; } const dsFieldTarget = (e) => { if (window.jbKeyFromField) return window.jbKeyFromField(e); const t = e && e.target; const tag = dsStr(t && t.tagName).toUpperCase(); return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || !!(t && t.isContentEditable); }; /* The hook a screen uses. opts: deals the screen's own list (in its own order) — what an empty search shows chips [{ key, label, group, test(deal) }] — one chip per group can be on selectedId / onSelect (optional) the screen's own selection: ↑ ↓ move it; it stays on the same deal while that deal is still in the list, else it moves to the first result onOpen(d) Enter (and the screen's click) — open / pick the deal returns { q, setQ, list, total, filtering, why(d), on, toggle(chip), count(chip), activeId, inputRef, rowProps(d), clear() } activeId = the ONE row Enter opens (null = none). The screen must draw exactly that row as highlighted: a controlled screen draws its own selection (= activeId); an uncontrolled one draws ds.activeId. While searching it is the selection if still listed, else the first result. With an empty box it is the screen's own selection (controlled), or — uncontrolled — nothing until ↑ ↓ is pressed (a box emptied again drops it), so an empty box looks and acts exactly like the screen's list before RC19. */ function dsUseDealSearch(opts) { const o = opts || {}; const base = Array.isArray(o.deals) ? o.deals : []; const chips = o.chips || []; const [q, setQState] = React.useState(""); const [on, setOn] = React.useState(() => Object.assign({}, o.initialOn || {})); const [ownActive, setOwnActive] = React.useState(null); const inputRef = React.useRef(null); const kbdMove = React.useRef(false); const controlled = typeof o.onSelect === "function"; const selId = controlled ? (o.selectedId == null ? null : o.selectedId) : ownActive; const setActive = (id) => { if (controlled) { if (id !== o.selectedId) o.onSelect(id); } else setOwnActive(id); }; const res = dsSearchDeals(base, q, chips, on); const list = res.list; const ids = res.filtering ? list.map(d => d.id).join("|") : ""; // the row Enter opens = the row drawn highlighted: the selection while it is listed; while searching, else the // first result (the effect below moves the selection there); with an empty box and no selection: NONE const selListed = selId != null && list.some(d => d.id === selId); const activeId = selListed ? selId : (res.filtering && list[0] ? list[0].id : null); // selection: kept while still visible; else the first result. Uncontrolled: the box emptied again = no highlight React.useEffect(() => { if (!res.filtering) { if (!controlled) setOwnActive(null); return; } if (selListed) return; if (list[0]) setActive(list[0].id); else if (!controlled) setOwnActive(null); }, [ids, res.filtering]); // keyboard moves keep the moved-to row in view React.useEffect(() => { if (!kbdMove.current || activeId == null) return; kbdMove.current = false; try { const sel = '[data-ds-row="' + (window.CSS && CSS.escape ? CSS.escape(String(activeId)) : String(activeId).replace(/"/g, '\\"')) + '"]'; const el = document.querySelector(sel); if (el && el.scrollIntoView) el.scrollIntoView({ block: "nearest" }); } catch (e) {} }, [activeId]); const stateRef = React.useRef({}); stateRef.current = { list, activeId, q, onOpen: o.onOpen, setActive, setQ: setQState }; React.useEffect(() => { const onKey = (e) => { const inp = inputRef.current; if (!inp || !inp.isConnected) return; const S = stateRef.current; if (e.target === inp) { const k = e.key; // Ctrl+S (save, anywhere) and Tab keep working; every other key stays in the box — the screen's shortcuts never see it if ((e.ctrlKey || e.metaKey) && dsLow(k) === "s") return; if (k === "Tab") return; e.stopImmediatePropagation(); e.stopPropagation(); if (k === "ArrowDown" || k === "ArrowUp" || k === "PageDown" || k === "PageUp" || ((e.ctrlKey || e.metaKey) && (k === "Home" || k === "End"))) { e.preventDefault(); const L = S.list; if (!L.length) return; let i = L.findIndex(d => d.id === S.activeId); const step = k === "PageDown" ? 10 : k === "PageUp" ? -10 : k === "ArrowDown" ? 1 : -1; if (k === "Home") i = 0; else if (k === "End") i = L.length - 1; else if (i < 0) i = step > 0 ? 0 : L.length - 1; else i = Math.max(0, Math.min(L.length - 1, i + step)); kbdMove.current = true; S.setActive(L[i].id); return; } if (k === "Enter") { e.preventDefault(); // only the highlighted row — never "the first deal" when nothing is highlighted (an empty box before ↑ ↓) const L = S.list; const d = S.activeId == null ? null : L.find(x => x.id === S.activeId); if (d && S.onOpen) S.onOpen(d); return; } if (k === "Escape") { e.preventDefault(); if (S.q) S.setQ(""); else inp.blur(); return; } if ((e.ctrlKey || e.metaKey) && !e.altKey && dsLow(k) === "f") { e.preventDefault(); inp.select(); } return; } const ctrlF = (e.ctrlKey || e.metaKey) && !e.altKey && !e.shiftKey && dsLow(e.key) === "f"; const slash = e.key === "/" && !e.ctrlKey && !e.metaKey && !e.altKey && !dsFieldTarget(e); if ((ctrlF || slash) && dsMayFocus(inp)) { e.preventDefault(); e.stopImmediatePropagation(); e.stopPropagation(); inp.focus(); try { inp.select(); } catch (x) {} } }; window.addEventListener("keydown", onKey, true); return () => window.removeEventListener("keydown", onKey, true); }, []); const toggle = (c) => setOn(cur => { const n = Object.assign({}, cur); n[c.group] = cur[c.group] === c.key ? null : c.key; return n; }); const count = (c) => { let n = 0; for (let i = 0; i < base.length; i++) { try { if (c.test(base[i])) n++; } catch (x) {} } return n; }; return { q, setQ: setQState, list, total: res.total, filtering: res.filtering, why: (d) => (d && res.why.get(d.id)) || "", on, toggle, count, chips, activeId, inputRef, clear: () => { setQState(""); setOn({}); }, rowProps: (d) => ({ "data-ds-row": d.id, "data-ds-active": d.id === activeId ? "1" : undefined }), }; } // The bar: box + chips + "12 of 240 deals". Put it at the top of the list (it sticks while the list scrolls). const DsSearchBar = ({ ds, placeholder, autoFocus, style, noun }) => { const word = noun || "deal"; const n = ds.total, shown = ds.list.length; const countTxt = ds.filtering ? `${shown} of ${n} ${word}${n === 1 ? "" : "s"}` : `${n} ${word}${n === 1 ? "" : "s"}`; const chipBtn = (c) => { const isOn = ds.on[c.group] === c.key; return ( ); }; return (