/* 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. */ // K2B DMS β€” Sales alerts (sales features, lane 1). // β€’ SlsBell πŸ”” in the top bar: unread count, the alert list, Claim / Log contact / Open customer … // β€’ SalesAlertsPage route "salesalerts" (Workspace β†’ My Alerts): every alert, filters, dismissed ones // β€’ slsLogContact the "Log contact" dialog (call / text / email / visit keeps a claimed customer yours) // Alerts live on the server (/api/sales-alerts, one person's rows only), so an // alert made while someone is signed out is waiting at their next sign-in. // Live updates arrive over the WebSocket: DMSCore hands every message type it // does not know to the page as a window "k2b-ws" event (hunk H0). Because the // socket is best effort, the list is ALSO fetched on start, when the // connection comes back, when the window gets focus and every 60 seconds. // Feature detection: GET /sales-alerts/me. A server without the sales features // (404 / error) β†’ the bell renders nothing and My Alerts says so. // Every top-level name is prefixed sls / Sls and exported through window. /* ── role class: a copy of the server's salesroles.js (contract 5) ── */ const SLS_MANAGER_RE = /^((sales|general|gm)\s*)?(manager|mgr)$|^f\s*&\s*i(\s*manager)?$|^finance(\s*manager)?$|^sales\s*director$|^owner$/i; const slsRoleClass = (role) => { const r = String(role || "").trim(); if (!r) return "other"; if (/admin/i.test(r)) return "manager"; if (/^sales$/i.test(r)) return "sales"; if (SLS_MANAGER_RE.test(r)) return "manager"; return "other"; }; // Menus and screens use the client's own rule (matches the server for every role the app knows). const slsIsMgr = () => !!(window.DMS && ((typeof DMS.isAdmin === "function" && DMS.isAdmin()) || (typeof DMS.roleIs === "function" && DMS.roleIs("Sales Manager")))); /* ── formatting (plain English; date-only values are built from their parts) ── */ const SLS_DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const SLS_MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; // "2026-10-12" β†’ "Mon Oct 12" (never new Date("YYYY-MM-DD"): that is the day before in Missouri) const slsFmtDay = (day) => { const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(day || "")); if (!m) return ""; const d = new Date(Date.UTC(+m[1], +m[2] - 1, +m[3])); return SLS_DAYS[d.getUTCDay()] + " " + SLS_MONTHS[d.getUTCMonth()] + " " + d.getUTCDate(); }; // "08:30" β†’ "8:30 AM" const slsFmtTime = (hhmm) => { const m = /^(\d{1,2}):(\d{2})/.exec(String(hhmm || "")); if (!m) return ""; let h = +m[1]; const ap = h >= 12 ? "PM" : "AM"; h = h % 12 || 12; return h + ":" + m[2] + " " + ap; }; // a timestamp in this PC's local time: "Mon Nov 9" / "Mon Nov 9, 2:14 PM" const slsFmtLocalDay = (iso) => { const d = new Date(iso); if (!iso || isNaN(d)) return ""; return SLS_DAYS[d.getDay()] + " " + SLS_MONTHS[d.getMonth()] + " " + d.getDate(); }; const slsFmtStamp = (iso) => { const d = new Date(iso); if (!iso || isNaN(d)) return ""; const hh = String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0"); return slsFmtLocalDay(iso) + ", " + slsFmtTime(hh); }; const slsFmtWhen = (iso) => { const t = new Date(iso).getTime(); if (!iso || !isFinite(t)) return ""; const s = Math.round((Date.now() - t) / 1000); if (s < 60) return "just now"; if (s < 3600) return Math.floor(s / 60) + " min ago"; if (s < 6 * 3600) { const h = Math.floor(s / 3600); return h + (h === 1 ? " hour ago" : " hours ago"); } return slsFmtStamp(iso); }; const slsDigits = (p) => { let d = String(p || "").replace(/\D/g, ""); if (d.length === 11 && d[0] === "1") d = d.slice(1); return d; }; // "5735550101" β†’ "(573) 555-0101" const slsFmtPhone = (p) => { const d = slsDigits(p); if (d.length === 10) return "(" + d.slice(0, 3) + ") " + d.slice(3, 6) + "-" + d.slice(6); if (d.length === 7) return d.slice(0, 3) + "-" + d.slice(3); return String(p || ""); }; const slsMoney = (n) => { const v = Number(n); return isFinite(v) ? "$" + Math.round(v).toLocaleString("en-US") : ""; }; const slsPlural = (n, one, many) => n + " " + (n === 1 ? one : (many || one + "s")); /* ── server calls (the global apiFetch from DMSCore adds the sign-in token) ── */ const slsApi = (path, opts) => (typeof apiFetch === "function" ? apiFetch(path, opts || {}) : Promise.reject(Object.assign(new Error("Not connected to the server."), { status: 0 }))); const slsErrText = (e, fallback) => (e && e.body && e.body.error) || (e && e.status && e.message) || fallback; const slsToast = (msg, kind, ms) => { try { window.k2bToast && window.k2bToast(msg, kind || "info", ms); } catch (e) {} }; /* ── one shared store for the bell, My Alerts and the other sales screens ── */ const slsS = { tok: null, // the sign-in this state belongs to (a new sign-in starts clean) me: null, meStatus: "idle", mePromise: null, // meStatus: idle | loading | on | off alerts: [], unread: 0, loaded: false, error: false, mineP: null, seen: new Map(), // alert id β†’ readAt last seen (toast rule) toastAt: new Map(), // customer β†’ {at, more, timer} (one toast per customer per minute) takenOverride: new Map(), // alert id β†’ {claimedBy, until} (a lost claim shows "Claimed by …" at once) firstDone: false, autoOpen: false, listeners: new Set(), timers: null, version: 0, }; let slsGo = null; // the shell's go(routeKey), set by the bell / page const slsEmit = () => { slsS.version++; slsS.listeners.forEach((fn) => { try { fn(slsS.version); } catch (e) {} }); }; const slsTok = () => { try { return (window.DMS && typeof DMS.getAuthToken === "function" && DMS.getAuthToken()) || ""; } catch (e) { return ""; } }; const slsMyId = () => { try { const ti = window.DMS && DMS.tokenInfo && DMS.tokenInfo(); return ti && ti.sub ? String(ti.sub) : ""; } catch (e) { return ""; } }; // A new sign-in (or a sign-off) throws away the previous person's alerts. function slsCheckSession(silent) { const tok = slsTok(); if (tok !== slsS.tok) { slsS.tok = tok; slsS.me = null; slsS.meStatus = "idle"; slsS.mePromise = null; slsS.alerts = []; slsS.unread = 0; slsS.loaded = false; slsS.error = false; slsS.mineP = null; slsS.seen = new Map(); slsS.takenOverride = new Map(); slsS.firstDone = false; slsS.autoOpen = false; slsS.toastAt.forEach((t) => { if (t.timer) clearTimeout(t.timer); }); slsS.toastAt = new Map(); if (!silent) slsEmit(); } return tok; } // GET /sales-alerts/me β€” the ONLY source of the feature flags. Resolves to the // answer, or null when the features are off (old server, error, signed out). function slsGetMe(force) { const tok = slsCheckSession(); if (!tok) return Promise.resolve(null); if (!force && slsS.meStatus === "on") return Promise.resolve(slsS.me); if (!force && slsS.meStatus === "off") return Promise.resolve(null); if (slsS.mePromise) return slsS.mePromise; if (slsS.meStatus === "idle") slsS.meStatus = "loading"; const p = slsApi("/sales-alerts/me").then( (r) => { if (tok !== slsS.tok) return null; if (r && r.ok) { slsS.me = r; slsS.meStatus = "on"; } else { slsS.me = null; slsS.meStatus = "off"; } return slsS.me; }, () => { if (tok !== slsS.tok) return null; slsS.me = null; slsS.meStatus = "off"; return null; }); slsS.mePromise = p; p.then(() => { if (slsS.mePromise === p) slsS.mePromise = null; slsEmit(); }); return p; } // The customer an alert is about (toast grouping + plain-English names). const slsCustKeyOf = (a) => (a && a.customer && (a.customer.custNum || a.customer.name)) || (a && a.custKey) || (a && a.id) || ""; const slsCustName = (a) => (a && a.customer && a.customer.name) || ""; // Toast rule (contract 3.5): only for an alert id not seen before in this // session, or one whose readAt went from a value back to empty; at most one // toast per customer per minute (the rest become "3 new alerts for JONES, MARY"). function slsMaybeToast(a, prevReadAt, isNew) { if (!a || !a.unread) return; if (!isNew && !(prevReadAt && !a.readAt)) return; const key = slsCustKeyOf(a); const name = slsCustName(a); const now = Date.now(); const t = slsS.toastAt.get(key); if (t && now - t.at < 60000) { t.more++; if (!t.timer) { t.timer = setTimeout(() => { const n = t.more; t.more = 0; t.timer = null; t.at = Date.now(); if (n > 0) slsToast("πŸ”” " + slsPlural(n, "new alert") + (name ? " for " + name : ""), "info", 6000); }, Math.max(1000, 60000 - (now - t.at))); } return; } slsS.toastAt.set(key, { at: now, more: 0, timer: null }); slsToast("πŸ”” " + a.title, "info", 5000); } // A claim this station just lost shows "Claimed by …" even if a refresh still says open for a moment. function slsApplyOverride(a) { const o = slsS.takenOverride.get(a.id); if (!o) return a; if (Date.now() > o.until || a.state !== "open") { slsS.takenOverride.delete(a.id); return a; } return Object.assign({}, a, { state: "taken", claimedBy: o.claimedBy || a.claimedBy, actions: (a.actions || []).filter((x) => x !== "claim") }); } const slsSort = (list) => list.slice().sort((x, y) => String(y.updatedAt || "").localeCompare(String(x.updatedAt || "")) || String(y.id).localeCompare(String(x.id))); // A fresh list from GET /mine. function slsApplyList(list, unread) { const first = !slsS.firstDone; for (const a of list) { const had = slsS.seen.has(a.id); if (!first) slsMaybeToast(a, had ? slsS.seen.get(a.id) : null, !had); slsS.seen.set(a.id, a.readAt || null); } slsS.alerts = slsSort(list.map(slsApplyOverride)); slsS.unread = Number(unread) || 0; if (first) { slsS.firstDone = true; // Waiting alerts at sign-in: open the bell once and say how many (once per sign-in). if (slsS.unread > 0) { const mark = String(slsS.tok || "").slice(-24); let done = false; try { done = sessionStorage.getItem("jb_sls_welcomed") === mark; sessionStorage.setItem("jb_sls_welcomed", mark); } catch (e) {} if (!done) { slsS.autoOpen = true; slsToast("You have " + slsPlural(slsS.unread, "new alert") + ".", "info", 6000); } } } } function slsLoadMine() { const tok = slsCheckSession(); if (!tok) return Promise.resolve(); if (slsS.meStatus !== "on") return slsGetMe().then((me) => (me && tok === slsS.tok ? slsLoadMine() : null)); if (slsS.mineP) return slsS.mineP; const p = slsApi("/sales-alerts/mine?limit=100").then( (r) => { if (tok !== slsS.tok) return; slsApplyList((r && r.alerts) || [], r && r.unread); slsS.loaded = true; slsS.error = false; }, () => { if (tok !== slsS.tok) return; slsS.error = true; }); slsS.mineP = p; p.then(() => { if (slsS.mineP === p) slsS.mineP = null; slsEmit(); }); return p; } // A live message from the server (the "k2b-ws" event of hunk H0). function slsOnWs(ev) { const m = ev && ev.detail; if (!m || !m.type) return; if (m.type === "sales_alert") { if (!slsS.tok || slsS.meStatus !== "on") return; if (m.op === "upsert" && m.alert && m.alert.id) { const a = m.alert; const had = slsS.seen.has(a.id); if (slsS.firstDone) slsMaybeToast(a, had ? slsS.seen.get(a.id) : null, !had); slsS.seen.set(a.id, a.readAt || null); const rest = slsS.alerts.filter((x) => x.id !== a.id); slsS.alerts = slsSort([slsApplyOverride(a)].concat(rest)); if (m.unread != null) slsS.unread = Number(m.unread) || 0; slsEmit(); } else if (m.op === "remove" && m.id) { slsS.alerts = slsS.alerts.filter((x) => x.id !== m.id); if (m.unread != null) slsS.unread = Number(m.unread) || 0; slsEmit(); } else if (m.op === "resync") { if (m.unread != null) slsS.unread = Number(m.unread) || 0; slsEmit(); slsLoadMine(); } } } // Polling + listeners run while anything on screen uses the store. function slsStart() { if (slsS.timers) return; const onFocus = () => { slsLoadMine(); }; const offConn = (window.DMS && typeof DMS.onConn === "function") ? DMS.onConn((state) => { if (state === "online") slsGetMe(true).then((me) => { if (me) slsLoadMine(); }); }) : null; window.addEventListener("k2b-ws", slsOnWs); window.addEventListener("focus", onFocus); const iv = setInterval(() => { slsLoadMine(); }, 60000); slsS.timers = { iv, onFocus, offConn }; slsLoadMine(); } function slsStop() { const t = slsS.timers; if (!t) return; clearInterval(t.iv); window.removeEventListener("k2b-ws", slsOnWs); window.removeEventListener("focus", t.onFocus); if (t.offConn) { try { t.offConn(); } catch (e) {} } slsS.timers = null; } // React hook: re-render on store changes; keeps the store running while mounted. function slsUseStore() { const [, setV] = React.useState(0); React.useEffect(() => { const fn = (v) => setV(v); slsS.listeners.add(fn); slsStart(); return () => { slsS.listeners.delete(fn); if (slsS.listeners.size === 0) slsStop(); }; }, []); slsCheckSession(true); // during render: no emit (the other screens catch up on the next change) return slsS; } /* ── actions ── */ async function slsMarkRead(ids) { const list = (ids || []).filter(Boolean); if (!list.length) return; const now = new Date().toISOString(); slsS.alerts = slsS.alerts.map((a) => (list.indexOf(a.id) >= 0 && a.unread ? Object.assign({}, a, { unread: false, readAt: now }) : a)); list.forEach((id) => { if (slsS.seen.has(id)) slsS.seen.set(id, now); }); slsEmit(); try { const r = await slsApi("/sales-alerts/read", { method: "POST", body: { ids: list } }); if (r && r.unread != null) { slsS.unread = Number(r.unread) || 0; slsEmit(); } } catch (e) { slsLoadMine(); } } async function slsMarkAllRead() { const now = new Date().toISOString(); slsS.alerts = slsS.alerts.map((a) => (a.unread ? Object.assign({}, a, { unread: false, readAt: now }) : a)); slsS.alerts.forEach((a) => slsS.seen.set(a.id, a.readAt)); slsS.unread = 0; slsEmit(); try { await slsApi("/sales-alerts/read", { method: "POST", body: { all: true } }); } catch (e) { slsToast(slsErrText(e, "Could not mark the alerts as read. Try again."), "warn", 4000); } slsLoadMine(); } async function slsDismiss(a) { if (!a) return; slsS.alerts = slsS.alerts.filter((x) => x.id !== a.id); slsEmit(); try { const r = await slsApi("/sales-alerts/dismiss", { method: "POST", body: { ids: [a.id] } }); if (r && r.unread != null) { slsS.unread = Number(r.unread) || 0; slsEmit(); } } catch (e) { slsToast(slsErrText(e, "Could not dismiss that alert. Try again."), "warn", 4000); slsLoadMine(); } try { window.dispatchEvent(new CustomEvent("sls-alerts-changed")); } catch (e) {} } const slsOwnerChanged = (custKeys) => { try { window.dispatchEvent(new CustomEvent("sls-owner-changed", { detail: { custKeys: (custKeys || []).filter(Boolean) } })); } catch (e) {} }; async function slsClaim(a) { if (!a) return { ok: false }; const name = slsCustName(a) || "This customer"; try { await slsApi("/sales-alerts/" + encodeURIComponent(a.id) + "/claim", { method: "POST", body: {} }); const days = (slsS.me && slsS.me.claimDays) || 30; slsToast(name + " is your customer now. Log a call, text, email or visit within " + days + " days to keep them.", "ok", 7000); slsLoadMine(); slsOwnerChanged([a.custKey]); return { ok: true }; } catch (e) { const b = (e && e.body) || {}; if (e && e.status === 409 && b.code === "taken") { const who = (b.claimedBy && b.claimedBy.name) || ""; slsToast(b.error || ("Someone else claimed this customer first: " + (who || "another salesperson") + "."), "warn", 7000); slsS.takenOverride.set(a.id, { claimedBy: b.claimedBy || null, until: Date.now() + 20000 }); slsS.alerts = slsS.alerts.map(slsApplyOverride); slsEmit(); slsLoadMine(); slsOwnerChanged([a.custKey]); } else if (e && e.status === 409 && b.code === "closed") { // Lost by a hair: the winner's claim already marked this alert "taken" before ours was read. // Say who got the customer (same words as a lost race), not just "no longer open". await slsLoadMine(); const cur = slsS.alerts.find((x) => x.id === a.id); if (cur && cur.state === "taken" && cur.claimedBy && cur.claimedBy.name) slsToast("Someone else claimed this customer first: " + cur.claimedBy.name + ".", "warn", 7000); else slsToast(b.error || "This alert is no longer open.", "warn", 7000); slsOwnerChanged([a.custKey]); } else { slsToast(slsErrText(e, "Could not claim this customer. Try again."), "warn", 7000); slsLoadMine(); } return { ok: false, code: b.code || null }; } } function slsOpenCustomer(custNum) { if (!custNum) return; try { localStorage.setItem("jb_cust_focus", String(custNum)); } catch (e) {} try { window.dispatchEvent(new CustomEvent("sls-cust-focus", { detail: { custNum: String(custNum) } })); } catch (e) {} if (slsGo) slsGo("customers"); } function slsOpenTrade(tradeId) { if (!tradeId) return; if (typeof window.TradeEval !== "function") { slsToast("Trade-In Requests is not on this PC yet. Ask your administrator.", "warn", 5000); return; } try { localStorage.setItem("jb_trade_focus", String(tradeId)); } catch (e) {} if (slsGo) slsGo("trades"); } function slsOpenAliases() { try { localStorage.setItem("jb_co_tab", "aliases"); } catch (e) {} try { window.dispatchEvent(new CustomEvent("sls-co-tab", { detail: { tab: "aliases" } })); } catch (e) {} if (slsGo) slsGo("custowners"); } /* ── Log contact dialog (opened from the bell, Customers, or any screen via window.slsLogContact) ── */ const SLS_CONTACT_KINDS = [["call", "Call"], ["text", "Text"], ["email", "Email"], ["visit", "Visit"]]; const SlsContactModal = ({ custKey, name, presetKind, leadId, onClose, onDone }) => { const [kind, setKind] = React.useState(SLS_CONTACT_KINDS.some((k) => k[0] === presetKind) ? presetKind : "call"); const [note, setNote] = React.useState(""); const [busy, setBusy] = React.useState(false); const [err, setErr] = React.useState(""); const save = async () => { if (busy) return; setBusy(true); setErr(""); try { const body = { custKey, kind }; if (note.trim()) body.note = note.trim().slice(0, 500); if (leadId) body.leadId = leadId; const r = await slsApi("/customer-owners/contact", { method: "POST", body }); const who = name || "This customer"; if (r && r.claim && r.claim.expiresAt) slsToast("Contact logged. " + who + " stays yours until " + slsFmtLocalDay(r.claim.expiresAt) + ".", "ok", 6000); else slsToast("Contact logged for " + who + ".", "ok", 4000); slsOwnerChanged([custKey]); slsLoadMine(); onDone && onDone(r); onClose(); } catch (e) { setErr(slsErrText(e, "Could not log the contact. Try again.")); setBusy(false); } }; // the dialog's own buttons sit in the K2BModal footer (without them K2BModal adds an "OK" there that closes WITHOUT saving) const buttons = ( <> ); return (
{name ? {name} : null}
A call, text, email or visit keeps a customer you claimed yours for another {(slsS.me && slsS.me.claimDays) || 30} days.