/* 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 (