Pipeline: count linked positions' buyers on member cards, the way My line and badges do; show the main/positions split on the card
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+24
-7
@@ -9,7 +9,21 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const db = require('./db');
|
const db = require('./db');
|
||||||
|
|
||||||
let DATA_DIR = null, accounts = null, coach = null;
|
let DATA_DIR = null, accounts = null, coach = null, chain = null;
|
||||||
|
// linked positions (Qualified Start) belong to the member: their buyers count toward the account, the same way
|
||||||
|
// My line chips and the badges count them (Jim's card showed 1 buyer while his badge said Circuit, 2026-09-15)
|
||||||
|
const posCache = new Map();
|
||||||
|
async function positionBuyers(email) {
|
||||||
|
let ids = []; try { ids = (await accounts.positions(email)).map(p => p.memberId).filter(Boolean); } catch (e) {}
|
||||||
|
let n = 0;
|
||||||
|
for (const id of ids) {
|
||||||
|
const c = posCache.get(id); let v = c && Date.now() - c.t < 60000 ? c.v : null;
|
||||||
|
if (v == null) { try { v = (await chain.member(id)).buyerCount || 0; } catch (e) { v = 0; } posCache.set(id, { t: Date.now(), v }); }
|
||||||
|
n += v;
|
||||||
|
}
|
||||||
|
return { count: n, positions: ids.length };
|
||||||
|
}
|
||||||
|
function rungForTotal(rung, total) { if (rung < 3) return rung; return total === 0 ? 3 : total === 1 ? 4 : total < 5 ? 5 : 6; }
|
||||||
const DAY = 86400000;
|
const DAY = 86400000;
|
||||||
|
|
||||||
// columns, in order. Prospects live in the first and last; directs land by coaching rung.
|
// columns, in order. Prospects live in the first and last; directs land by coaching rung.
|
||||||
@@ -46,7 +60,7 @@ const D = {
|
|||||||
};
|
};
|
||||||
const impl = () => db.enabled() ? D : J;
|
const impl = () => db.enabled() ? D : J;
|
||||||
|
|
||||||
function init(opts) { DATA_DIR = opts.dataDir; accounts = opts.accounts; coach = opts.coach; }
|
function init(opts) { DATA_DIR = opts.dataDir; accounts = opts.accounts; coach = opts.coach; chain = opts.chain; }
|
||||||
|
|
||||||
// who may see the live board: mode on = everyone; preview = the admin account only
|
// who may see the live board: mode on = everyone; preview = the admin account only
|
||||||
function visible(mode, email, adminEmail) {
|
function visible(mode, email, adminEmail) {
|
||||||
@@ -79,13 +93,16 @@ async function board(email) {
|
|||||||
for (const m of (levels[0] ? levels[0].members : [])) {
|
for (const m of (levels[0] ? levels[0].members : [])) {
|
||||||
let c = null; try { c = await coach.describe(m, now); } catch (e) {}
|
let c = null; try { c = await coach.describe(m, now); } catch (e) {}
|
||||||
const n = notes[m.email] || {};
|
const n = notes[m.email] || {};
|
||||||
const rung = c ? c.rung : 0;
|
const pb = c && c.rung >= 3 ? await positionBuyers(m.email) : { count: 0, positions: 0 };
|
||||||
|
const buyersMain = c ? c.buyerCount || 0 : 0, buyersAll = buyersMain + pb.count;
|
||||||
|
const rung = c ? rungForTotal(c.rung, buyersAll) : 0;
|
||||||
|
const RR = coach.RUNGS[rung];
|
||||||
const stage = PARKED.has(n.tag) && rung < 3 ? 'later' : RUNG_STAGE[rung] || 'joined';
|
const stage = PARKED.has(n.tag) && rung < 3 ? 'later' : RUNG_STAGE[rung] || 'joined';
|
||||||
cards.push({ key: m.email, kind: 'member', email: m.email, name: m.username ? '@' + m.username : (m.memberId ? 'member #' + m.memberId : 'member'),
|
cards.push({ key: m.email, kind: 'member', email: m.email, name: m.username ? '@' + m.username : (m.memberId ? 'member #' + m.memberId : 'member'),
|
||||||
memberId: m.memberId || 0, stage, rung, rungLabel: c ? c.label : 'Joined', since: m.created || now,
|
memberId: m.memberId || 0, stage, rung, rungLabel: RR.label, since: m.created || now,
|
||||||
lastSeen: m.lastSeen || 0, quietDays: c ? c.quietDays : 0, stalled: !!(c && c.stalled),
|
lastSeen: m.lastSeen || 0, quietDays: c ? c.quietDays : 0, stalled: !!(c && c.stalled && rung < 6),
|
||||||
buyers: c ? c.buyerCount || 0 : 0, bought: !!(c && c.counted),
|
buyers: buyersAll, buyersMain, buyersPositions: pb.count, positions: pb.positions, bought: !!(c && c.counted),
|
||||||
next: c ? c.next : 'Link a wallet', say: c ? c.say.replace('{{name}}', m.username || 'there') : '',
|
next: RR.next, say: RR.say.replace('{{name}}', m.username || 'there'),
|
||||||
note: n.note || '', followUp: n.followUp || null, tag: n.tag || '' });
|
note: n.note || '', followUp: n.followUp || null, tag: n.tag || '' });
|
||||||
}
|
}
|
||||||
const due = cards.filter(c => c.followUp && c.followUp <= eod.getTime()).sort((a, b) => a.followUp - b.followUp);
|
const due = cards.filter(c => c.followUp && c.followUp <= eod.getTime()).sort((a, b) => a.followUp - b.followUp);
|
||||||
|
|||||||
+1
-1
@@ -1383,7 +1383,7 @@
|
|||||||
function fillPipeCard(c) {
|
function fillPipeCard(c) {
|
||||||
$('pipeCardName').textContent = c.name;
|
$('pipeCardName').textContent = c.name;
|
||||||
const stage = (PIPE.stages.find(s => s.key === c.stage) || {}).label || '';
|
const stage = (PIPE.stages.find(s => s.key === c.stage) || {}).label || '';
|
||||||
$('pipeCardMeta').textContent = stage + (c.kind === 'member' ? ' · joined ' + new Date(c.since).toLocaleDateString() + (c.lastSeen ? ' · last seen ' + pipeAgo(c.lastSeen) : ' · never signed in') + (c.bought ? ' · your qualifying buyer' : '') : ' · prospect' + (c.contact ? ' · ' + c.contact : ''));
|
$('pipeCardMeta').textContent = stage + (c.kind === 'member' ? ' · joined ' + new Date(c.since).toLocaleDateString() + (c.lastSeen ? ' · last seen ' + pipeAgo(c.lastSeen) : ' · never signed in') + (c.bought ? ' · your qualifying buyer' : '') + (c.buyersPositions ? ' · ' + c.buyers + ' buyers: ' + c.buyersMain + ' on the main wallet, ' + c.buyersPositions + ' on linked positions' : '') : ' · prospect' + (c.contact ? ' · ' + c.contact : ''));
|
||||||
$('pipeCardNext').innerHTML = '<b>Next for them:</b> ' + esc(c.next) + (c.stalled ? ' <span class="chip warm">quiet ' + c.quietDays + ' days</span>' : '');
|
$('pipeCardNext').innerHTML = '<b>Next for them:</b> ' + esc(c.next) + (c.stalled ? ' <span class="chip warm">quiet ' + c.quietDays + ' days</span>' : '');
|
||||||
const say = (c.say || '').replace('{{link}}', pipeInvite());
|
const say = (c.say || '').replace('{{link}}', pipeInvite());
|
||||||
$('pipeSayBlock').hidden = !say; $('pipeSay').textContent = say;
|
$('pipeSayBlock').hidden = !say; $('pipeSay').textContent = say;
|
||||||
|
|||||||
+1
-1
@@ -1021,7 +1021,7 @@
|
|||||||
<script src="/assets/common.js?v=20260914a"></script>
|
<script src="/assets/common.js?v=20260914a"></script>
|
||||||
<script src="/assets/wallet.js?v=20260911a"></script>
|
<script src="/assets/wallet.js?v=20260911a"></script>
|
||||||
<script src="/assets/promo.js?v=20260911a"></script>
|
<script src="/assets/promo.js?v=20260911a"></script>
|
||||||
<script src="/assets/my.js?v=20260915c"></script>
|
<script src="/assets/my.js?v=20260915d"></script>
|
||||||
<script src="/assets/chat.js?v=20260907l"></script>
|
<script src="/assets/chat.js?v=20260907l"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ async function boot() {
|
|||||||
setInterval(() => ads.scheduleSweep().catch(e => console.error('schedule sweep', e.message)), 5 * 60 * 1000); // scheduled starts/ends
|
setInterval(() => ads.scheduleSweep().catch(e => console.error('schedule sweep', e.message)), 5 * 60 * 1000); // scheduled starts/ends
|
||||||
// follow-up email sequence: send whatever came due (every 10 min, first pass shortly after boot)
|
// follow-up email sequence: send whatever came due (every 10 min, first pass shortly after boot)
|
||||||
coach.init({ dataDir: DATA_DIR, chain, accounts, mailer, ads, tank, lb: () => leaderboard, siteConfig });
|
coach.init({ dataDir: DATA_DIR, chain, accounts, mailer, ads, tank, lb: () => leaderboard, siteConfig });
|
||||||
pipeline.init({ dataDir: DATA_DIR, accounts, coach });
|
pipeline.init({ dataDir: DATA_DIR, accounts, coach, chain });
|
||||||
tank.init({ dataDir: DATA_DIR, chain, accounts, messages, mailer, site: 'https://instantadpay.com' });
|
tank.init({ dataDir: DATA_DIR, chain, accounts, messages, mailer, site: 'https://instantadpay.com' });
|
||||||
legacy.init({ dataDir: DATA_DIR });
|
legacy.init({ dataDir: DATA_DIR });
|
||||||
traffic.init({ dataDir: DATA_DIR });
|
traffic.init({ dataDir: DATA_DIR });
|
||||||
|
|||||||
Reference in New Issue
Block a user