Add admin "My Positions — Income" panel
Aggregates every on-chain payment received by a set of owned IDs (defaults to 21,24,25, saved to config.ownerIds and prefilled/auto-loaded next time). Shows grand total + per-position totals and a merged, newest-first ledger (to / from / entry-or-upgrade level / amount) via a focused chain.getIncome read and an admin-gated /api/admin/income endpoint. Verified: 5,064.43 POL across 21/24/25, 11 payments. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -34,6 +34,8 @@ function render(){
|
||||
renderAnalytics();
|
||||
const ai=state.aiChat||{};document.getElementById('aiModel').textContent=ai.model||'OpenRouter';document.getElementById('aiStatus').innerHTML=ai.configured?'<span style="color:var(--ok)">● AI chat is ON</span> — key is set':'<span style="color:var(--muted)">○ AI chat is OFF</span> — using built-in answers';
|
||||
const em=state.email||{};document.getElementById('emailStatus').innerHTML=em.configured?'<span style="color:var(--ok)">● Payment emails are ON</span> — key is set':'<span style="color:var(--muted)">○ Payment emails are OFF</span> — no SendGrid key';
|
||||
const incEl=document.getElementById('incomeIds');if(incEl&&!incEl.value)incEl.value=state.config.ownerIds||'21,24,25';
|
||||
if(!incomeAutoLoaded&&incEl&&incEl.value){incomeAutoLoaded=true;loadIncome();}
|
||||
const efi=document.getElementById('emailFromInput');if(efi&&!efi.value)efi.value=state.config.emailFrom||em.from||'';
|
||||
const f=document.getElementById('configForm'),c=state.config;for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel','bemobPostbackUrl','telegramBotToken','telegramChatId','telegramTopicId','teamRootId','teamAlertEmail'])if(f.elements[k])f.elements[k].value=c[k]??'';f.elements.showSponsorName.checked=!!c.showSponsorName;f.elements.showQueueProgress.checked=!!c.showQueueProgress;
|
||||
}
|
||||
@@ -48,6 +50,23 @@ rows.addEventListener('change',async e=>{const sel=e.target.closest('select[data
|
||||
document.getElementById('sgKeyForm').addEventListener('submit',async e=>{e.preventDefault();const inp=e.currentTarget.elements.key,key=inp.value.trim();if(!key){showToast('Paste a key first');return}try{const d=await api('/api/admin/sendgrid-key',{method:'POST',body:JSON.stringify({key})});state.email={...(state.email||{}),configured:d.configured};inp.value='';render();showToast('Payment emails enabled')}catch(x){showToast(x.message)}});
|
||||
document.getElementById('sgKeyClear').addEventListener('click',async()=>{if(!confirm('Disable "you\'ve been paid" emails?'))return;try{const d=await api('/api/admin/sendgrid-key',{method:'POST',body:JSON.stringify({key:''})});state.email={...(state.email||{}),configured:d.configured};render();showToast('Payment emails disabled')}catch(x){showToast(x.message)}});
|
||||
document.getElementById('emailFromSave').addEventListener('click',async()=>{const v=document.getElementById('emailFromInput').value.trim();try{const d=await api('/api/admin/config',{method:'PATCH',body:JSON.stringify({emailFrom:v})});state.config=d.config;showToast('From address saved')}catch(x){showToast(x.message)}});
|
||||
let incomeAutoLoaded=false;
|
||||
async function loadIncome(){
|
||||
const ids=(document.getElementById('incomeIds').value||'').trim();
|
||||
const out=document.getElementById('incomeResult'),sum=document.getElementById('incomeSummary');
|
||||
if(!ids||!/^[\d,\s]+$/.test(ids)){out.innerHTML='<div class="empty">Enter comma-separated numeric IDs (e.g. 21,24,25).</div>';return}
|
||||
out.innerHTML='<div class="empty">Reading the blockchain…</div>';sum.innerHTML='';
|
||||
try{
|
||||
try{const c=await api('/api/admin/config',{method:'PATCH',body:JSON.stringify({ownerIds:ids})});state.config=c.config;}catch(_){}
|
||||
const d=await api('/api/admin/income?ids='+encodeURIComponent(ids));
|
||||
const fmt=n=>Number(n).toLocaleString(undefined,{maximumFractionDigits:2});
|
||||
const date=ts=>ts?new Date(ts*1000).toISOString().replace('T',' ').slice(0,16):'—';
|
||||
sum.innerHTML=`<div class="fact" style="border-color:rgba(123,224,161,.4)"><small>Total received (all)</small><strong style="color:var(--ok);font-size:18px">${fmt(d.grandEarnedPol)} POL</strong></div>`+
|
||||
d.ids.map(id=>{const p=d.perId[id]||{};return `<div class="fact"><small>#${id}${p.registered&&p.levelName?' · '+esc(p.levelName):''}</small><strong>${p.registered?fmt(p.totalEarnedPol)+' POL':'<span style=\"color:var(--danger)\">not registered</span>'}</strong>${p.registered?`<div class="micro" style="margin-top:2px">${p.count} payment${p.count===1?'':'s'}</div>`:''}</div>`}).join('');
|
||||
out.innerHTML=d.rows.length?`<div class="table-wrap"><table class="table" style="min-width:640px"><thead><tr><th>When (UTC)</th><th>To</th><th>From</th><th>For</th><th>Amount</th></tr></thead><tbody>${d.rows.map(r=>`<tr><td>${date(r.ts)}</td><td><strong class="mt-id">#${r.toId}</strong></td><td>#${r.fromId}</td><td>${esc(r.desc||'')}</td><td><strong>${fmt(r.pol)} POL</strong></td></tr>`).join('')}</tbody></table></div><p class="micro" style="margin:8px 0 0">${d.rows.length} payment${d.rows.length===1?'':'s'} across your positions, newest first.</p>`:'<div class="empty">No payments recorded to these positions yet.</div>';
|
||||
}catch(x){out.innerHTML=`<div class="empty" style="color:var(--danger)">${esc(x.message)}</div>`}
|
||||
}
|
||||
document.getElementById('incomeForm').addEventListener('submit',e=>{e.preventDefault();loadIncome()});
|
||||
document.getElementById('lookupForm').addEventListener('submit',async e=>{
|
||||
e.preventDefault();
|
||||
const id=document.getElementById('lookupId').value.trim(),out=document.getElementById('lookupResult');
|
||||
|
||||
Reference in New Issue
Block a user