Auto-debit platform credits when purchase is approved

When a pending purchase is resolved as completed:
- Inserts a debit row into platform_credits for the matching platform
  (joins token_purchases.platform_id slug → platforms.id)
- Debit notes include purchase #, player name, username, token count, amount, method
- Total shown in credit modal now subtracts debits from credits (net balance)

Credit history table updates:
- CREDIT/DEBIT type badges, debit rows tinted red with − prefix
- Debit rows show "Purchase #X ↗" button that closes modal, jumps to
  the Purchases section (all tab), and highlights that purchase row
- Edit/delete buttons hidden on auto-generated debit rows

Also fixes: resolve_purchase was echoing $sent (undefined variable bug)
Also fixes: purchaseCard div now has id="pr-N" so jump-highlight works

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 21:38:03 +00:00
parent 1367fa334b
commit f54cdb11db
4 changed files with 85 additions and 17 deletions
+43 -12
View File
@@ -1228,7 +1228,7 @@ function purchaseCard(p, showActions=true) {
'<button class="btn btn-red" data-pid="'+p.id+'" onclick="resolvePurchase(this.dataset.pid,&quot;failed&quot;)" style="width:100%;font-size:15px;padding:10px">✗ Reject</button></div>'
: (p.admin_note ? '<div style="font-size:14px;color:var(--gold);padding:6px 8px;background:rgba(240,192,64,.07);border-radius:6px;margin-top:4px">📝 '+escHtmlA(p.admin_note)+'</div>' : '');
return '<div class="card" style="margin-bottom:10px;'+(isPending?'border-color:rgba(240,192,64,.25);background:rgba(240,192,64,.02)':'')+'">'+
return '<div class="card" id="pr-'+p.id+'" style="margin-bottom:10px;'+(isPending?'border-color:rgba(240,192,64,.25);background:rgba(240,192,64,.02)':'')+'">'+
'<div style="display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap">'+
'<div style="flex:1;min-width:180px">'+
'<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:4px">'+
@@ -3173,22 +3173,37 @@ async function loadCreditEntries() {
list.innerHTML = `<table style="width:100%;border-collapse:collapse;font-size:14px">
<thead><tr style="border-bottom:1px solid var(--border)">
<th style="padding:8px 6px;text-align:left;color:var(--text2);font-size:12px;font-weight:700;letter-spacing:.5px">DATE</th>
<th style="padding:8px 6px;text-align:right;color:var(--text2);font-size:12px;font-weight:700;letter-spacing:.5px">CREDITS</th>
<th style="padding:8px 6px;text-align:left;color:var(--text2);font-size:12px;font-weight:700;letter-spacing:.5px">TYPE</th>
<th style="padding:8px 6px;text-align:right;color:var(--text2);font-size:12px;font-weight:700;letter-spacing:.5px">AMOUNT</th>
<th style="padding:8px 6px;text-align:left;color:var(--text2);font-size:12px;font-weight:700;letter-spacing:.5px">METHOD</th>
<th style="padding:8px 6px;text-align:left;color:var(--text2);font-size:12px;font-weight:700;letter-spacing:.5px">NOTES</th>
<th style="padding:8px 6px;width:80px"></th>
</tr></thead>
<tbody>
${d.credits.map(c=>`<tr style="border-bottom:1px solid rgba(255,255,255,0.05)">
<td style="padding:9px 6px;color:var(--text)">${escHtmlA(c.credit_date)}</td>
<td style="padding:9px 6px;text-align:right;font-family:'Exo 2',sans-serif;font-weight:700;color:var(--cyan)">${parseFloat(c.credits_purchased).toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2})}</td>
<td style="padding:9px 6px;color:var(--text2)">${escHtmlA(c.payment_method||'—')}</td>
<td style="padding:9px 6px;color:var(--text2);font-size:13px;max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtmlA(c.notes||'')}</td>
<td style="padding:9px 6px;text-align:right;white-space:nowrap">
<button onclick="editCreditEntry(${c.id})" style="background:rgba(0,229,255,.08);border:1px solid rgba(0,229,255,.2);color:var(--cyan);border-radius:5px;padding:3px 8px;font-size:13px;cursor:pointer;margin-right:4px">✏️</button>
<button onclick="deleteCreditEntry(${c.id})" style="background:rgba(255,68,68,.08);border:1px solid rgba(255,68,68,.2);color:var(--red);border-radius:5px;padding:3px 8px;font-size:13px;cursor:pointer">🗑</button>
</td>
</tr>`).join('')}
${d.credits.map(c=>{
const isDebit = c.type === 'debit';
const amtColor = isDebit ? 'var(--red)' : 'var(--cyan)';
const amtPrefix = isDebit ? '' : '+';
const rowBg = isDebit ? 'background:rgba(255,68,68,.03)' : '';
const typeBadge = isDebit
? '<span style="background:rgba(255,68,68,.15);color:var(--red);font-size:11px;font-weight:700;padding:2px 7px;border-radius:10px;letter-spacing:.3px">DEBIT</span>'
: '<span style="background:rgba(0,229,255,.12);color:var(--cyan);font-size:11px;font-weight:700;padding:2px 7px;border-radius:10px;letter-spacing:.3px">CREDIT</span>';
const refLink = (isDebit && c.purchase_ref_id)
? `<button onclick="jumpToPurchase(${c.purchase_ref_id})" style="background:rgba(240,192,64,.1);border:1px solid rgba(240,192,64,.25);color:var(--gold);border-radius:5px;padding:3px 8px;font-size:12px;cursor:pointer;font-family:'Exo 2',sans-serif;font-weight:700;white-space:nowrap">Purchase #${c.purchase_ref_id} ↗</button>`
: '';
const editDel = !isDebit
? `<button onclick="editCreditEntry(${c.id})" style="background:rgba(0,229,255,.08);border:1px solid rgba(0,229,255,.2);color:var(--cyan);border-radius:5px;padding:3px 8px;font-size:13px;cursor:pointer;margin-right:4px">✏️</button>
<button onclick="deleteCreditEntry(${c.id})" style="background:rgba(255,68,68,.08);border:1px solid rgba(255,68,68,.2);color:var(--red);border-radius:5px;padding:3px 8px;font-size:13px;cursor:pointer">🗑</button>`
: '';
return `<tr style="border-bottom:1px solid rgba(255,255,255,0.05);${rowBg}">
<td style="padding:9px 6px;color:var(--text)">${escHtmlA(c.credit_date)}</td>
<td style="padding:9px 6px">${typeBadge}</td>
<td style="padding:9px 6px;text-align:right;font-family:'Exo 2',sans-serif;font-weight:700;color:${amtColor}">${amtPrefix}${parseFloat(c.credits_purchased).toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2})}</td>
<td style="padding:9px 6px;color:var(--text2)">${escHtmlA(c.payment_method||'—')}</td>
<td style="padding:9px 6px;color:var(--text2);font-size:13px;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escHtmlA(c.notes||'')}">${escHtmlA(c.notes||'')}</td>
<td style="padding:9px 6px;text-align:right;white-space:nowrap">${refLink}${editDel}</td>
</tr>`;
}).join('')}
</tbody></table>`;
}
@@ -3249,6 +3264,22 @@ async function deleteCreditEntry(id) {
else toast(d.error||'Error','err');
}
async function jumpToPurchase(purchaseId) {
closeCreditModal();
showSec('purchases');
document.querySelectorAll('#section-purchases .ftab').forEach(b=>b.classList.remove('active'));
const allTab = document.querySelector('#section-purchases .ftab[onclick*="\'all\'"]');
if (allTab) allTab.classList.add('active');
await loadPurchases('all');
const el = document.getElementById('pr-' + purchaseId);
if (el) {
el.scrollIntoView({behavior:'smooth', block:'center'});
el.style.outline = '2px solid var(--gold)';
el.style.borderRadius = '12px';
setTimeout(() => { el.style.outline = ''; el.style.borderRadius = ''; }, 2500);
}
}
// Sync hex input with color picker
document.addEventListener('DOMContentLoaded', function() {
const picker = document.getElementById('gf-color');