747977b368
Matches _payUpline (level > levelIndex, levelIndex = buyer.level-1). The old depth+1 rule told #21 (Culmen) it could not catch #49's Apex buy; #21 is #49's 4th matrix upline and does catch it. Fixed in the owner alert, coaching scan, dashboard pipeline, AI prompt, leader digest, chatbot and how-pay-works copy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
129 lines
6.0 KiB
JavaScript
129 lines
6.0 KiB
JavaScript
// Leader Ops client — renders the coaching radar for the signed-in leader's own
|
||
// organisation, then asks the engine for a written plan on request.
|
||
(function () {
|
||
'use strict';
|
||
var $ = function (id) { return document.getElementById(id); };
|
||
function esc(s) {
|
||
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
||
});
|
||
}
|
||
function n(v) { return Number(v || 0).toLocaleString(); }
|
||
|
||
function gate(msg) {
|
||
$('lGate').style.display = 'block';
|
||
$('lGate').innerHTML = msg;
|
||
}
|
||
|
||
function tile(value, label, cls) {
|
||
return '<div class="l-tile ' + (cls || '') + '"><div class="big">' + value + '</div>' +
|
||
'<div class="lab">' + label + '</div></div>';
|
||
}
|
||
|
||
function renderScan(scan) {
|
||
if (!scan || !scan.ready) {
|
||
gate('The on-chain index is still catching up. Give it a moment and refresh.');
|
||
return;
|
||
}
|
||
$('lBody').style.display = 'block';
|
||
var t = scan.totals || {};
|
||
|
||
$('lTiles').innerHTML =
|
||
tile(n(scan.scanned), 'positions below you', 'cool') +
|
||
tile(n(t.atRiskPol), 'POL forming that someone below cannot catch', t.atRiskPol > 0 ? 'hot' : '') +
|
||
tile(n(t.oneAwayCount), 'one direct short of qualifying', t.oneAwayCount > 0 ? 'hot' : '') +
|
||
tile(n(t.rollForwardCount), 'qualified but still on Scintilla', '');
|
||
|
||
var out = [];
|
||
|
||
if ((scan.atRisk || []).length) {
|
||
var rows = scan.atRisk.map(function (r) {
|
||
return '<tr><td><b>#' + esc(r.id) + '</b></td>' +
|
||
'<td>' + esc(r.levelName) + '</td>' +
|
||
'<td>' + (r.qualified
|
||
? '<span class="pill u">upgrade to ' + esc(r.neededLevelName || '') + '</span>'
|
||
: '<span class="pill q">' + esc(r.directCount) + ' of 2 directs</span>') + '</td>' +
|
||
'<td><b>' + n(r.atRiskPol) + '</b></td>' +
|
||
'<td>' + n(r.earnedPol) + '</td>' +
|
||
'<td>' + (r.upgradeCost ? n(r.upgradeCost) : '—') + '</td></tr>';
|
||
}).join('');
|
||
out.push('<div class="l-sec"><h2>Money forming they can\'t catch</h2>' +
|
||
'<div class="why">A position only catches an upgrade payment if it is qualified <em>and</em> already at or above the level the buyer is leaving. These people have activity underneath them that is passing them by — the fastest, kindest conversation you can have this week.</div>' +
|
||
'<div class="table-wrap"><table class="l-tbl"><thead><tr><th>Position</th><th>Level</th><th>What they need</th><th>POL passing them</th><th>Earned</th><th>Upgrade cost</th></tr></thead><tbody>' +
|
||
rows + '</tbody></table></div></div>');
|
||
}
|
||
|
||
if ((scan.rollForward || []).length) {
|
||
var rf = scan.rollForward.map(function (r) {
|
||
return '<tr><td><b>#' + esc(r.id) + '</b></td><td>' + n(r.earnedPol) + '</td><td>' + n(r.ascensusCost) + '</td></tr>';
|
||
}).join('');
|
||
out.push('<div class="l-sec"><h2>Already qualified, still on Scintilla</h2>' +
|
||
'<div class="why">They have their two and their entry rewards are sitting there. Ascensus is the upgrade that starts catching their directs\' payments — for several of these, what they have already earned covers it.</div>' +
|
||
'<div class="table-wrap"><table class="l-tbl"><thead><tr><th>Position</th><th>Earned (POL)</th><th>Ascensus costs</th></tr></thead><tbody>' +
|
||
rf + '</tbody></table></div></div>');
|
||
}
|
||
|
||
if ((scan.oneAway || []).length) {
|
||
out.push('<div class="l-sec"><h2>One direct short</h2>' +
|
||
'<div class="why">A single placement qualifies each of these. If you have anyone coming in, this is where to put them.</div>' +
|
||
'<div style="font-size:15px;line-height:1.9">' +
|
||
scan.oneAway.map(function (r) { return '<b>#' + esc(r.id) + '</b> <span style="color:var(--muted);font-size:13px">(' + esc(r.levelName) + ')</span>'; }).join(' · ') +
|
||
'</div></div>');
|
||
}
|
||
|
||
if (!out.length) {
|
||
out.push('<div class="l-sec"><h2>Nothing needs chasing</h2>' +
|
||
'<div class="why">Nobody below you is one direct short, sitting on uncatchable money, or qualified but stuck on Scintilla. That is the quiet week you spend working on your own two.</div></div>');
|
||
}
|
||
|
||
$('lSections').innerHTML = out.join('');
|
||
}
|
||
|
||
async function writePlan(btn) {
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<span class="spin"></span>Reading your organisation…';
|
||
$('lErr').style.display = 'none';
|
||
try {
|
||
var r = await fetch('/api/public/suite-leader', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
|
||
var d = await r.json();
|
||
if (!r.ok) {
|
||
$('lErr').textContent = d.error || 'Could not write the plan just now.';
|
||
$('lErr').style.display = 'block';
|
||
} else {
|
||
$('lDigest').textContent = d.text || '';
|
||
$('lDigest').style.display = 'block';
|
||
if (d.scan) renderScan(d.scan);
|
||
}
|
||
} catch (e) {
|
||
$('lErr').textContent = 'Connection hiccup — try again.';
|
||
$('lErr').style.display = 'block';
|
||
}
|
||
btn.disabled = false;
|
||
btn.innerHTML = '🧭 Write this week\'s coaching plan';
|
||
}
|
||
|
||
async function boot() {
|
||
try {
|
||
var r = await fetch('/api/public/suite-leader');
|
||
if (r.status === 401) { gate('You’re not signed in yet. <a href="/suite" style="color:var(--gold);font-weight:700">Open the Suite</a> and connect the wallet that holds your position, then come back.'); return; }
|
||
if (r.status === 403) {
|
||
var g = await r.json();
|
||
gate((g.error || 'Not open for this position yet.') + ' <a href="/suite" style="color:var(--gold);font-weight:700">See your Suite</a>.');
|
||
return;
|
||
}
|
||
if (!r.ok) return;
|
||
var d = await r.json();
|
||
renderScan(d.scan);
|
||
if (d.writerReady === false) {
|
||
$('lWrite').disabled = true;
|
||
$('lWrite').textContent = 'The writer is warming up';
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
boot();
|
||
$('lWrite').addEventListener('click', function () { writePlan(this); });
|
||
});
|
||
})();
|