/* © 2026 K2B NETWORK SOLUTIONS LLC. All rights reserved. */ /* eslint-disable */ // ReconCore — recon BUDGET per used vehicle (logic only; UI lives in ReconTracker.jsx.txt). // // WHERE IT LIVES: on the inventory unit as `unit.reconBudget` (an object). `unit.recon` stays the legacy // NUMBER (recon $ capitalized by closeRO, read by Schedules / Inventory / stock-in / dashboard inventory value), // so it is never touched here. // unit.reconBudget = { budget, approvedBy, approvedAt, // history: [{ id, at, by, type: "set"|"increase-request"|"approved"|"denied"|"alert"|"ack", from, to, amount, reason, roId }], // requests: [{ id, amount (requested NEW budget), from, reason, roId, requestedBy, at, status: "pending"|"approved"|"denied", decidedBy, decidedAt, note }], // alerts: [{ id, at, by, roId, kind: "warn"|"over-projected"|"over-actual", projected, actual, budget, over, pct, reason, lines, ack, ackBy, ackAt, ackNote }] } // // TOTALS ARE COMPUTED, NEVER ACCUMULATED. Every read rebuilds them from the CURRENT lines of the recon ROs for the unit, // keyed by RO id + line id (duplicates collapse), so editing / re-saving an RO can never double-count. // line $ : labor = op.lab (flat) or op.hrs × recon labor rate; parts = sale × qty; outside/sublet = sale (or amount/cost) // declined lines (approval === "declined") are excluded. // actual = Σ lines of CLOSED recon ROs (what was charged to the vehicle) // estimated = Σ lines of OPEN recon ROs // projected = actual + estimated; remaining = budget − projected; pct = projected / budget // status = none (no budget) | ok | approaching (pct ≥ warn %) | over (projected > budget OR actual > budget) // Which ROs count: repairOrders + internal bodyShopROs whose internalStock/stock = unit stock, or VIN = unit VIN, AND that are // recon/internal (payType Internal/Recon, recon:true, or type/payer mentioning recon/internal/make-ready). (function () { const D = window.DMS; if (!D) return; const R2 = (n) => +(+n || 0).toFixed(2); const U = (s) => String(s || "").toUpperCase().trim(); const nowIso = () => new Date().toISOString(); const uid = (p) => (D._uid ? D._uid(p) : p + "-" + Date.now().toString(36).toUpperCase() + Math.random().toString(36).slice(2, 5).toUpperCase()); const who = () => { const id = D.userId ? D.userId() : ""; try { const r = (D.roster ? D.roster() : []) || []; const m = r.find(x => String(x.id) === String(id)); if (m && m.name) return m.name + " (" + id + ")"; } catch (e) {} return id || "unknown"; }; const settings = () => (D.serviceSettings ? D.serviceSettings() : {}) || {}; const warnPct = () => { const v = +settings().reconWarnPct; return v > 0 && v <= 100 ? v : 85; }; const laborRate = () => { const s = settings(); return +(s.reconLaborRate || s.internalLaborRate || s.internalRate || s.laborRate || s.doorRate) || 145; }; const isReconRO = (r) => !!r && (r.recon === true || /^(internal|recon)$/i.test(String(r.payType || "")) || /internal|recon|recondition|make-ready|makeready/i.test((r.type || "") + " " + (r.payer || "") + " " + (r.payType || ""))); const isClosed = (r) => /^(closed|posted|cashed out|invoiced)$/i.test(String(r.status || "")) || (!!r.postedToGL && !!r.closed); const tsKey = (id, fallback) => { const n = +id; return isFinite(n) && n > 1e11 ? n : fallback; }; // ── Pure: one RO → its budget lines (deduped by kind + line id) ── function roLines(ro, rate) { rate = +rate || 145; const out = [], seen = new Set(); const opened = Date.parse(ro.opened || ro.created || "") || 0; const push = (kind, l, i, amount, desc, qty) => { const lid = l && l.id != null ? String(l.id) : kind + "#" + i; const key = ro.id + "|" + kind + "|" + lid; if (seen.has(key)) return; // same line saved twice → counted once seen.add(key); out.push({ key, roId: ro.id, lineId: lid, kind, desc: desc || "", qty: qty == null ? 1 : qty, amount: R2(amount), declined: !!(l && l.approval === "declined"), order: tsKey(l && l.id, opened + i) }); }; const hasOps = Array.isArray(ro.ops), hasParts = Array.isArray(ro.partsLines); const subArr = Array.isArray(ro.sublets) ? ro.sublets : Array.isArray(ro.subletLines) ? ro.subletLines : Array.isArray(ro.outsideLines) ? ro.outsideLines : null; if (hasOps) ro.ops.forEach((o, i) => push("labor", o, i, (+o.lab > 0 ? +o.lab : (+o.hrs || 0) * rate), (o.op ? o.op + " — " : "") + (o.desc || o.complaint || "Labor"), +o.hrs || 0)); else if (+ro.laborSale) push("labor", null, 0, +ro.laborSale, ro.concern || "Labor (summary)"); if (hasParts) ro.partsLines.forEach((p, i) => push("parts", p, i, (+p.sale || +p.price || 0) * (p.qty == null ? 1 : +p.qty || 0), (p.part ? p.part + " " : "") + (p.desc || ""), p.qty == null ? 1 : +p.qty)); else if (+ro.partsSale) push("parts", null, 0, +ro.partsSale, "Parts (summary)"); if (subArr) subArr.forEach((s, i) => push("sublet", s, i, (+s.sale || +s.amount || +s.cost || 0) * (s.qty == null ? 1 : +s.qty || 0), (s.vendor ? s.vendor + " — " : "") + (s.desc || "Outside work"), s.qty == null ? 1 : +s.qty)); else if (+ro.subletSale || +ro.subletCost) push("sublet", null, 0, +ro.subletSale || +ro.subletCost, "Sublet (summary)"); return out; } function roTotals(ro, rate) { const lines = roLines(ro, rate).filter(l => !l.declined); const by = (k) => R2(lines.filter(l => l.kind === k).reduce((s, l) => s + l.amount, 0)); const labor = by("labor"), parts = by("parts"), sublet = by("sublet"); return { roId: ro.id, labor, parts, sublet, total: R2(labor + parts + sublet), lines, closed: isClosed(ro) }; } // ── Pure: summarize a unit given its ROs ── function summarize(unit, ros, opts) { opts = opts || {}; const rate = opts.rate || 145, wp = opts.warnPct || 85; const rb = unit.reconBudget || {}; const budget = rb.budget != null && rb.budget !== "" ? R2(rb.budget) : null; const per = ros.map(r => ({ ro: r, t: roTotals(r, rate) })); const actual = R2(per.filter(x => x.t.closed).reduce((s, x) => s + x.t.total, 0)); const estimated = R2(per.filter(x => !x.t.closed).reduce((s, x) => s + x.t.total, 0)); const projected = R2(actual + estimated); const hasBudget = budget != null && budget > 0; const pct = hasBudget ? +(projected / budget * 100).toFixed(1) : null; const actualOver = hasBudget && actual > budget + 0.004; const projOver = hasBudget && projected > budget + 0.004; const status = !hasBudget ? "none" : (projOver || actualOver) ? "over" : pct >= wp ? "approaching" : "ok"; return { stock: unit.stock, vin: unit.vin || "", vehicle: [unit.yr, unit.mk, unit.mdl].filter(Boolean).join(" "), budget, hasBudget, warnPct: wp, actual, estimated, projected, remaining: hasBudget ? R2(budget - projected) : null, pct, status, overBy: hasBudget ? R2(Math.max(0, projected - budget, actual - budget)) : 0, projOver, actualOver, ros: per.map(x => ({ id: x.ro.id, status: x.ro.status || "", closed: x.t.closed, labor: x.t.labor, parts: x.t.parts, sublet: x.t.sublet, total: x.t.total, opened: x.ro.opened || x.ro.created || "", closedAt: x.ro.closed || "", concern: x.ro.concern || "", body: !!x.ro._body, lines: x.t.lines })), approvedBy: rb.approvedBy || "", approvedAt: rb.approvedAt || "", history: rb.history || [], requests: rb.requests || [], alerts: rb.alerts || [], pending: (rb.requests || []).filter(q => q.status === "pending"), openAlerts: (rb.alerts || []).filter(a => a.kind !== "warn" && !a.ack) }; } // Walk lines chronologically (closed ROs by close date, then open ROs' lines by line time) to find which RO / line(s) // cross a limit. Returns { hit:{roId}, roIds:Set, lines:[…] } — lines at/after the crossing point. function crossing(sum, limit, onlyClosed) { let cum = 0, hit = null; const after = []; const closed = sum.ros.filter(r => r.closed).sort((a, b) => (Date.parse(a.closedAt) || 0) - (Date.parse(b.closedAt) || 0)); for (const r of closed) { cum = R2(cum + r.total); if (!hit && cum > limit + 0.004) hit = { roId: r.id }; if (hit) after.push(...r.lines.filter(l => !l.declined)); } if (!onlyClosed) { const open = []; sum.ros.filter(r => !r.closed).forEach(r => r.lines.filter(l => !l.declined).forEach(l => open.push(l))); open.sort((a, b) => a.order - b.order); for (const l of open) { cum = R2(cum + l.amount); if (!hit && cum > limit + 0.004) hit = { roId: l.roId }; if (hit) after.push(l); } } return { hit, roIds: new Set(after.map(l => l.roId)), lines: after }; } // ── State helpers (writes go through DMS._mutate only) ── function findUnit(st, q) { const k = U(q); if (!k) return null; return (st.inventory || []).find(u => U(u.stock) === k || (u.vin && U(u.vin) === k)) || null; } function rosFor(st, unit) { const s = U(unit.stock), v = U(unit.vin); const m = new Map(); const match = (r) => isReconRO(r) && ((s && (U(r.internalStock) === s || U(r.stock) === s)) || (v && v.length >= 11 && U(r.vin) === v)); (st.repairOrders || []).forEach(r => { if (r && r.id && match(r) && !m.has(r.id)) m.set(r.id, r); }); (st.bodyShopROs || []).forEach(r => { if (r && r.id && match(r) && !m.has(r.id)) m.set(r.id, { ...r, _body: true, ops: undefined, lines: undefined }); }); return Array.from(m.values()); } function summaryOf(st, unit) { return summarize(unit, rosFor(st, unit), { rate: laborRate(), warnPct: warnPct() }); } function unitForRO(st, ro) { if (!ro) return null; for (const c of [ro.internalStock, ro.stock].filter(Boolean)) { const u = findUnit(st, c); if (u) return u; } if (ro.vin && U(ro.vin).length >= 11) return (st.inventory || []).find(u => u.vin && U(u.vin) === U(ro.vin)) || null; return null; } const ensureRB = (u) => { if (!u.reconBudget || typeof u.reconBudget !== "object") u.reconBudget = { budget: null, history: [], requests: [], alerts: [] }; const rb = u.reconBudget; rb.history = rb.history || []; rb.requests = rb.requests || []; rb.alerts = rb.alerts || []; return rb; }; // Budget approvals: Admin, Office Manager and Sales Manager only (not the legacy // department roles that happen to have GL rights). const canApprove = () => !!(D.isAdmin && D.isAdmin()) || !!(D.roleIs && (D.roleIs("Office Manager") || D.roleIs("Sales Manager"))); const canRequest = () => canApprove() || !!(D.can && D.can("service")); const money = (n) => "$" + (+n || 0).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); const alertId = (stock, roId, kind, budget) => "RA-" + stock + "-" + roId + "-" + kind + "-" + Math.round(budget); // Which alert records the current numbers call for (ids are deterministic → dedupe per unit + RO + kind + budget level). function wanted(sum) { const w = []; if (!sum.hasBudget) return w; if (sum.actualOver) { const c = crossing(sum, sum.budget, true); if (c.hit) w.push({ kind: "over-actual", roId: c.hit.roId, lines: c.lines, over: sum.actual - sum.budget }); } if (sum.projOver) { const c = crossing(sum, sum.budget, false); c.roIds.forEach(id => { const r = sum.ros.find(x => x.id === id); if (r && !r.closed) w.push({ kind: "over-projected", roId: id, lines: c.lines, over: sum.projected - sum.budget }); }); } else if (sum.status === "approaching") { const c = crossing(sum, sum.budget * sum.warnPct / 100, false); if (c.hit) { const r = sum.ros.find(x => x.id === c.hit.roId); if (r && !r.closed) w.push({ kind: "warn", roId: c.hit.roId, lines: c.lines, over: 0 }); } } return w.map(x => ({ ...x, id: alertId(sum.stock, x.roId, x.kind, sum.budget) })); } // Evaluate one unit inside a mutation; returns newly created alerts. function evaluate(st, unit, ctx) { ctx = ctx || {}; const sum = summaryOf(st, unit); const want = wanted(sum); if (!want.length) return { sum, created: [] }; const rb = ensureRB(unit); const created = []; const lastReq = rb.requests.slice().sort((a, b) => String(b.at).localeCompare(String(a.at)))[0]; want.forEach(({ id, kind, roId, lines, over }) => { if (rb.alerts.some(a => a.id === id)) return; const ro = sum.ros.find(r => r.id === roId); const mine = (lines || []).filter(l => l.roId === roId && Math.abs(l.amount) > 0.004); const lineTxt = mine.slice(0, 4).map(l => l.desc.trim() + " " + money(l.amount)).join("; ") + (mine.length > 4 ? "; +" + (mine.length - 4) + " more" : ""); const parts = []; if (lineTxt) parts.push((kind === "over-actual" ? "Closed " + roId + " charged: " : "Work on " + roId + ": ") + lineTxt); if (ctx.reason && (!ctx.roId || ctx.roId === roId)) parts.push("Service note: " + ctx.reason); else if (ro && ro.concern) parts.push("RO concern: " + ro.concern); if (lastReq && lastReq.reason) parts.push("Last budget request (" + lastReq.status + ", " + lastReq.requestedBy + "): " + lastReq.reason); const a = { id, at: nowIso(), by: who(), roId, kind, projected: sum.projected, actual: sum.actual, budget: sum.budget, over: R2(over), pct: sum.pct, reason: parts.join(" · ") || "Recon exceeds the approved budget.", lines: mine.slice(0, 8).map(l => ({ desc: l.desc, amount: l.amount, kind: l.kind })), ack: false }; rb.alerts.unshift(a); rb.history.unshift({ id: uid("RH"), at: a.at, by: a.by, type: "alert", from: sum.budget, to: sum.budget, amount: a.over, reason: kind + " · " + (kind === "warn" ? sum.pct + "% of budget (warn at " + sum.warnPct + "%)" : money(a.over) + " over"), roId }); if (kind !== "warn" && D._audit) D._audit("Recon Budget Alert", `${unit.stock} ${roId} ${kind} ${money(a.over)} over budget ${money(sum.budget)}`); created.push(a); }); return { sum: created.length ? summaryOf(st, unit) : sum, created }; } const pickRO = (st, roOrId) => typeof roOrId === "string" ? (st.repairOrders || []).find(r => r.id === roOrId) : roOrId; const api = { reconWarnPct: warnPct, reconLaborRate: laborRate, reconIsRecon: isReconRO, reconCanApprove: canApprove, reconCanRequest: canRequest, reconUnitForRO(roOrId) { return D._read(st => unitForRO(st, pickRO(st, roOrId))); }, reconROsFor(stockOrVin) { return D._read(st => { const u = findUnit(st, stockOrVin); return u ? rosFor(st, u) : []; }); }, reconRoTotals(roOrId) { return D._read(st => { const ro = pickRO(st, roOrId); return ro ? roTotals(ro, laborRate()) : null; }); }, // Summary for one unit (stock or VIN) — null when not in inventory. reconBudget(stockOrVin) { return D._read(st => { const u = findUnit(st, stockOrVin); return u ? summaryOf(st, u) : null; }); }, // Summary for the unit an RO belongs to (RO object or id) + that RO's own totals. reconBudgetForRO(roOrId) { return D._read(st => { const ro = pickRO(st, roOrId); if (!ro) return null; const u = unitForRO(st, ro); if (!u) return { unit: null, stock: "", ro: roTotals(ro, laborRate()), isRecon: isReconRO(ro) }; return { ...summaryOf(st, u), unit: true, ro: roTotals(ro, laborRate()), isRecon: isReconRO(ro) }; }); }, // Used units (not new, not sold) with their summaries. reconUnits() { return D._read(st => (st.inventory || []).filter(u => !/new/i.test(u.type || "") && window.DealCalc.unitOnLot(u)).map(u => summaryOf(st, u))); }, // Manager/admin: set or change the approved budget directly (explicit approval, reason required). reconSetBudget(stock, amount, reason) { if (!canApprove()) return { ok: false, denied: true, msg: "Only a manager or admin can set a recon budget — send a budget request instead." }; const amt = R2(amount); if (!(amt > 0)) return { ok: false, msg: "Enter a budget greater than $0." }; if (!String(reason || "").trim()) return { ok: false, msg: "A reason is required to set or change the budget." }; return D._mutate(st => { const u = findUnit(st, stock); if (!u) return { ok: false, msg: "Unit not found." }; const rb = ensureRB(u); const from = rb.budget; const at = nowIso(), by = who(); if (from != null && Math.abs(from - amt) < 0.005) return { ok: false, msg: `Budget is already ${money(amt)}.` }; rb.budget = amt; rb.approvedBy = by; rb.approvedAt = at; rb.history.unshift({ id: uid("RH"), at, by, type: "set", from: from == null ? null : from, to: amt, amount: R2(amt - (+from || 0)), reason: String(reason).trim() }); if (D._audit) D._audit("Recon Budget Set", `${u.stock}: ${from == null ? "none" : money(from)} → ${money(amt)} — ${reason}`); evaluate(st, u, {}); return { ok: true, msg: `${u.stock} recon budget ${from == null ? "set to" : "changed to"} ${money(amt)}.` }; }); }, // Anyone in service (or a manager) asks for a higher budget. NEVER changes the budget. reconRequestIncrease(stock, req) { req = req || {}; if (!canRequest()) return { ok: false, denied: true, msg: "Your role can't request recon budget changes." }; const amt = R2(req.amount); if (!String(req.reason || "").trim()) return { ok: false, msg: "Give a reason for the increase." }; return D._mutate(st => { const u = findUnit(st, stock); if (!u) return { ok: false, msg: "Unit not found." }; const rb = ensureRB(u); if (!(amt > 0) || (rb.budget != null && amt <= rb.budget)) return { ok: false, msg: `Enter the NEW budget total — more than the current ${money(rb.budget)}.` }; if (rb.requests.some(q => q.status === "pending" && Math.abs(q.amount - amt) < 0.01)) return { ok: false, msg: "That request is already pending." }; const q = { id: uid("RBR"), amount: amt, from: rb.budget, reason: String(req.reason).trim(), roId: req.roId || "", requestedBy: who(), requestedById: D.userId ? D.userId() : "", at: nowIso(), status: "pending" }; rb.requests.unshift(q); rb.history.unshift({ id: uid("RH"), at: q.at, by: q.requestedBy, type: "increase-request", from: rb.budget, to: amt, amount: R2(amt - (+rb.budget || 0)), reason: q.reason, roId: q.roId }); if (D._audit) D._audit("Recon Budget Request", `${u.stock}: ${money(rb.budget)} → ${money(amt)} requested — ${q.reason}`); return { ok: true, id: q.id, msg: `Budget increase to ${money(amt)} requested for ${u.stock} — the approved budget stays ${rb.budget == null ? "unset" : money(rb.budget)} until a manager approves.` }; }); }, // Manager/admin approves or denies a pending request. Approval writes the new budget + history + audit. reconDecideRequest(stock, reqId, decision, note) { if (!canApprove()) return { ok: false, denied: true, msg: "Only a manager or admin can approve recon budget increases." }; if (decision !== "approved" && decision !== "denied") return { ok: false, msg: "Decision must be approved or denied." }; return D._mutate(st => { const u = findUnit(st, stock); if (!u) return { ok: false, msg: "Unit not found." }; const rb = ensureRB(u); const q = rb.requests.find(x => x.id === reqId); if (!q) return { ok: false, msg: "Request not found." }; if (q.status !== "pending") return { ok: false, msg: `That request was already ${q.status}.` }; const me = D.userId ? D.userId() : ""; if (q.requestedById && q.requestedById === me && !(D.isAdmin && D.isAdmin())) return { ok: false, msg: "You can't decide your own request — another manager must." }; const at = nowIso(), by = who(); const from = rb.budget; q.status = decision; q.decidedBy = by; q.decidedAt = at; q.note = String(note || "").trim(); if (decision === "approved") { rb.budget = q.amount; rb.approvedBy = by; rb.approvedAt = at; } rb.history.unshift({ id: uid("RH"), at, by, type: decision, from, to: decision === "approved" ? q.amount : from, amount: R2(q.amount - (+from || 0)), reason: q.reason + (q.note ? " — " + q.note : ""), roId: q.roId, requestId: q.id }); if (D._audit) D._audit(decision === "approved" ? "Recon Budget Approved" : "Recon Budget Denied", `${u.stock}: ${money(from)} → ${money(q.amount)} (requested by ${q.requestedBy}) — ${q.reason}${q.note ? " / " + q.note : ""}`); evaluate(st, u, {}); return { ok: true, msg: decision === "approved" ? `${u.stock} recon budget approved at ${money(q.amount)}.` : `Budget request for ${u.stock} denied — budget stays ${money(from)}.` }; }); }, // Re-evaluate after work is added/edited on an RO (RO id, or stock/VIN). Creates deduped alert records; never changes the budget. // ctx: { reason } — optional advisor note that goes on any alert this call creates. reconCheck(key, ctx) { const found = D._read(st => { const ro = (st.repairOrders || []).find(r => r.id === key); const u = ro ? unitForRO(st, ro) : findUnit(st, key); return { ro: ro ? ro.id : "", has: !!u }; }); if (!found.has) return { ok: false, msg: "No inventory unit for that RO.", created: [] }; return D._mutate(st => { const ro = (st.repairOrders || []).find(r => r.id === key); const u = ro ? unitForRO(st, ro) : findUnit(st, key); const r = evaluate(st, u, { ...(ctx || {}), roId: found.ro }); return { ok: true, status: r.sum.status, summary: r.sum, created: r.created }; }); }, reconAckAlert(stock, alertIdArg, note) { if (!canApprove()) return { ok: false, denied: true, msg: "Only a manager or admin can acknowledge recon alerts." }; return D._mutate(st => { const u = findUnit(st, stock); if (!u) return { ok: false, msg: "Unit not found." }; const rb = ensureRB(u); const a = rb.alerts.find(x => x.id === alertIdArg); if (!a) return { ok: false, msg: "Alert not found." }; if (a.ack) return { ok: true, msg: "Already acknowledged." }; a.ack = true; a.ackBy = who(); a.ackAt = nowIso(); a.ackNote = String(note || "").trim(); rb.history.unshift({ id: uid("RH"), at: a.ackAt, by: a.ackBy, type: "ack", from: rb.budget, to: rb.budget, amount: a.over, reason: "Acknowledged " + a.kind + (a.ackNote ? " — " + a.ackNote : ""), roId: a.roId }); if (D._audit) D._audit("Recon Alert Ack", `${u.stock} ${a.roId} ${a.kind}${a.ackNote ? " — " + a.ackNote : ""}`); return { ok: true, msg: "Alert acknowledged." }; }); }, // All alert records across units (newest first) with vehicle info. opts: { open: true } → unacknowledged over-budget only. reconAlerts(opts) { opts = opts || {}; return D._read(st => { const out = []; (st.inventory || []).forEach(u => ((u.reconBudget && u.reconBudget.alerts) || []).forEach(a => { if (opts.open && (a.ack || a.kind === "warn")) return; if (opts.overOnly && a.kind === "warn") return; out.push({ ...a, stock: u.stock, vin: u.vin || "", vehicle: [u.yr, u.mk, u.mdl].filter(Boolean).join(" ") }); })); return out.sort((a, b) => String(b.at).localeCompare(String(a.at))); }); }, reconPendingRequests() { return D._read(st => { const out = []; (st.inventory || []).forEach(u => ((u.reconBudget && u.reconBudget.requests) || []).forEach(q => { if (q.status === "pending") out.push({ ...q, stock: u.stock, vin: u.vin || "", vehicle: [u.yr, u.mk, u.mdl].filter(Boolean).join(" "), budget: u.reconBudget.budget }); })); return out; }); }, // Manager/admin: warning threshold (% of budget). Stored in serviceSettings.reconWarnPct (Service Setup edits the same key). reconSetWarnPct(pct) { if (!canApprove()) return { ok: false, denied: true, msg: "Only a manager or admin can change the recon warning threshold." }; const v = Math.round(+pct); if (!(v >= 50 && v <= 100)) return { ok: false, msg: "Threshold must be between 50% and 100%." }; const from = warnPct(); return D._mutate(st => { st.serviceSettings = { ...(st.serviceSettings || {}), reconWarnPct: v }; if (D._audit) D._audit("Recon Warn Threshold", `${from}% → ${v}%`); return { ok: true, msg: `Recon warning threshold set to ${v}% of budget.` }; }); }, }; Object.assign(D, api); // Owner badge count: unacknowledged over-budget alerts. window.jbReconAlertCount = () => { try { return D.reconAlerts({ open: true }).length; } catch (e) { return 0; } }; // Pure helpers exposed for tests. window.jbReconCalc = { roLines, roTotals, summarize, crossing, wanted, isReconRO }; // Safety net: whenever shared state changes, re-evaluate budgeted units so alert records exist even if the screen that // added the work didn't call reconCheck. Deterministic ids → idempotent; it only writes when a new record is due. let queued = false; const sweep = () => { queued = false; if (!D.userId || !D.userId()) return; try { const need = D._read(st => (st.inventory || []).some(u => { if (!u.reconBudget || !(+u.reconBudget.budget > 0)) return false; const have = new Set((u.reconBudget.alerts || []).map(a => a.id)); return wanted(summaryOf(st, u)).some(w => !have.has(w.id)); })); if (!need) return; D._mutate(st => { (st.inventory || []).forEach(u => { if (u.reconBudget && +u.reconBudget.budget > 0) evaluate(st, u, {}); }); }); } catch (e) { /* never break other screens */ } }; if (D.subscribe) D.subscribe(() => { if (!queued) { queued = true; setTimeout(sweep, 0); } }); })();