Coaching Radar: live downline triage in admin + member dashboards
- chain.js getCoachingScan(rootId): classifies everyone below a root into atRisk (POL forming they can't catch - corrected bought-level rule), rollForward (qualified-at-Scintilla with entry rewards covering Ascensus), and oneAway (1/2 directs) - Admin: GET /api/admin/coaching?root= (name-decorated) + "Coaching Radar" panel with tiered who/what-to-say/POL-at-stake rows, auto-loaded - Member dashboards: memberPublic now returns .coach scoped to the member's own leg; new "Coach your team" card shows the same triage so every member coaches their own team - computed live, no snapshots or cron needed - Chatbot canned answer + AI system prompt updated to describe the panel Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -536,6 +536,7 @@ async function memberPublic(id) {
|
||||
cur = state && state.members[cur] ? state.members[cur].uplineId : 0;
|
||||
}
|
||||
out.uplineChain = chain;
|
||||
try { out.coach = getCoachingScan(id, 6); } catch (e) {}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -657,6 +658,43 @@ function getOrgShare(rootId) {
|
||||
};
|
||||
}
|
||||
|
||||
// Coaching radar: triage every member below `rootId` into actionable tiers.
|
||||
// - atRisk: money forming in their leg that they can't catch yet (corrected
|
||||
// rule: catcher must be qualified AND at the level being bought)
|
||||
// - rollForward: qualified but still Scintilla — entry rewards already cover
|
||||
// the Ascensus upgrade that catches their directs' first payments
|
||||
// - oneAway: one direct short of qualifying (a placement fixes them)
|
||||
function getCoachingScan(rootId, maxItems = 15) {
|
||||
if (!state || !state.snapshotAt || !costs) return { ready: false };
|
||||
const ids = [];
|
||||
{ const stack = [rootId]; const seen = new Set([rootId]);
|
||||
while (stack.length) { const x = stack.pop(); const m = state.members[x]; if (!m) continue;
|
||||
for (const c of [m.l, m.r]) if (c && !seen.has(c)) { seen.add(c); ids.push(c); stack.push(c); } } }
|
||||
const atRisk = [], rollForward = [], oneAway = [];
|
||||
for (const id of ids) {
|
||||
const m = state.members[id]; const lvl = m.level || 1, q = (m.directCount || 0) >= 2;
|
||||
let missing = 0, minDepth = 0, fromIds = [];
|
||||
(function walk(nid, depth) { const mm = state.members[nid]; if (!mm) return;
|
||||
if (depth >= 1 && (mm.level || 1) === depth && !(q && lvl >= depth + 1)) {
|
||||
const amt = (costs.up[mm.tier === 2 ? 2 : 1] || [])[depth - 1] || 0;
|
||||
if (amt) { missing += amt; fromIds.push(nid); if (!minDepth || depth < minDepth) minDepth = depth; }
|
||||
}
|
||||
if (depth < 16) { if (mm.l) walk(mm.l, depth + 1); if (mm.r) walk(mm.r, depth + 1); } })(id, 0);
|
||||
if (missing > 0) atRisk.push({ id, level: lvl, levelName: levelName(lvl), qualified: q,
|
||||
directCount: m.directCount || 0, atRiskPol: +missing.toFixed(2), fromIds: fromIds.slice(0, 6),
|
||||
need: q ? 'upgrade' : 'qualify', neededLevel: q ? minDepth + 1 : null,
|
||||
neededLevelName: q ? levelName(minDepth + 1) : null });
|
||||
if (q && lvl === 1) rollForward.push({ id, earnedPol: +(m.earnedPol || 0).toFixed(2),
|
||||
ascensusCost: +((costs.up[m.tier === 2 ? 2 : 1] || [])[0] || 0).toFixed(2) });
|
||||
if ((m.directCount || 0) === 1) oneAway.push({ id, levelName: levelName(lvl) });
|
||||
}
|
||||
atRisk.sort((a, b) => b.atRiskPol - a.atRiskPol);
|
||||
return { ready: true, rootId, scanned: ids.length,
|
||||
atRisk: atRisk.slice(0, maxItems), rollForward: rollForward.slice(0, maxItems), oneAway: oneAway.slice(0, maxItems),
|
||||
totals: { atRiskPol: +atRisk.reduce((s, r) => s + r.atRiskPol, 0).toFixed(2),
|
||||
atRiskCount: atRisk.length, rollForwardCount: rollForward.length, oneAwayCount: oneAway.length } };
|
||||
}
|
||||
|
||||
// focused income read for one position — for the admin "my positions" income view
|
||||
async function getIncome(id) {
|
||||
const m = await fetchMember(id);
|
||||
@@ -671,4 +709,4 @@ async function getIncome(id) {
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, memberPublic, getIncome, getOwnerUpgradeNeeds, getOrgRouting, getOrgShare, getMatrixTree, isInTeam, balanceOf, CONTRACT };
|
||||
module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, memberPublic, getIncome, getOwnerUpgradeNeeds, getOrgRouting, getOrgShare, getCoachingScan, getMatrixTree, isInTeam, balanceOf, CONTRACT };
|
||||
|
||||
Reference in New Issue
Block a user