Add platform credit overview to dashboard

New section below pending purchases/cashouts: one square card per
active platform showing net credit balance, completed purchase count,
and sent cashout count. Loads on page load alongside other dashboard
data. Credits turn yellow below 100 and red at/below 0 with a warning.
Clicking a card jumps to Game Management and opens that platform's
credit modal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 22:03:38 +00:00
parent f54cdb11db
commit d8202427ae
2 changed files with 78 additions and 0 deletions
+61
View File
@@ -349,6 +349,15 @@ tr:hover td{background:rgba(255,255,255,.015)}
<div class="card-title">⚡ Pending Cashout Requests</div>
<div id="dash-cashouts"></div>
</div>
<!-- PLATFORM CREDIT OVERVIEW -->
<div style="margin-top:8px">
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:16px;color:var(--text);margin-bottom:14px;display:flex;align-items:center;gap:8px">
🕹️ Platform Credit Overview
<button onclick="loadPlatformStats()" style="background:none;border:none;color:var(--text2);font-size:13px;cursor:pointer;font-family:'Exo 2',sans-serif;font-weight:700;padding:0;margin-left:4px">↻</button>
</div>
<div id="dash-platform-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px"></div>
</div>
</div>
<!-- PURCHASES -->
@@ -1200,6 +1209,58 @@ async function loadStats() {
loadDashPurchases();
loadDashCashouts();
loadPendingSignups();
loadPlatformStats();
}
async function loadPlatformStats() {
const grid = document.getElementById('dash-platform-grid');
if (!grid) return;
grid.innerHTML = '<div style="color:var(--text2);font-size:14px;padding:8px 0">Loading...</div>';
const d = await apiFetch('platform_stats');
if (!d.success || !d.platforms.length) {
grid.innerHTML = '<div style="color:var(--text2);font-size:14px;padding:8px 0">No platforms found.</div>';
return;
}
grid.innerHTML = d.platforms.map(p => {
const bal = parseFloat(p.credits_balance);
const balFmt = bal % 1 === 0 ? bal.toLocaleString() : bal.toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2});
const color = p.color || '#00e5ff';
const balColor = bal <= 0 ? 'var(--red)' : bal < 100 ? 'var(--yellow)' : 'var(--cyan)';
const lowBadge = bal <= 0
? '<div style="font-size:11px;font-weight:700;color:var(--red);letter-spacing:.5px;margin-top:4px">⚠ LOW</div>'
: (bal < 100 ? '<div style="font-size:11px;font-weight:700;color:var(--yellow);letter-spacing:.5px;margin-top:4px">⚠ LOW</div>' : '');
return `<div onclick="openGameCredits('${escHtmlA(p.slug)}')" style="background:var(--card);border:1px solid var(--border);border-radius:12px;padding:16px 14px;cursor:pointer;transition:all .18s;position:relative;overflow:hidden"
onmouseover="this.style.borderColor='${color}44';this.style.transform='translateY(-2px)'" onmouseout="this.style.borderColor='';this.style.transform=''">
<div style="position:absolute;top:0;left:0;right:0;height:3px;background:${color};opacity:.7;border-radius:12px 12px 0 0"></div>
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:13px;color:var(--text);margin-bottom:10px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escHtmlA(p.name)}</div>
<div style="font-size:11px;font-weight:700;color:var(--text2);letter-spacing:1px;text-transform:uppercase;margin-bottom:3px">Credits</div>
<div style="font-family:'Exo 2',sans-serif;font-weight:900;font-size:22px;color:${balColor};line-height:1">${balFmt}</div>
${lowBadge}
<div style="display:flex;gap:10px;margin-top:12px;padding-top:10px;border-top:1px solid var(--border)">
<div style="flex:1;text-align:center">
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:17px;color:var(--gold)">${p.purchases}</div>
<div style="font-size:11px;color:var(--text2);font-weight:700;letter-spacing:.5px">PURCH</div>
</div>
<div style="flex:1;text-align:center;border-left:1px solid var(--border)">
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:17px;color:var(--green)">${p.cashouts}</div>
<div style="font-size:11px;color:var(--text2);font-weight:700;letter-spacing:.5px">CASH</div>
</div>
</div>
</div>`;
}).join('');
}
function openGameCredits(slug) {
// Switch to Games section and open the credit modal for this platform
showSec('games');
// Wait for games to load then find and click the matching game's edit button
const tryOpen = () => {
const games = window._gamesData || [];
const g = games.find(x => x.slug === slug);
if (g) { editGame(g.id); setTimeout(() => openCreditModal(), 150); }
};
if ((window._gamesData||[]).length) { tryOpen(); }
else { loadGames().then(() => tryOpen()); }
}
// ─── SECTION NAV ───────────────────────────────────────────
+17
View File
@@ -68,6 +68,23 @@ switch ($action) {
}
break;
// ─── PLATFORM STATS ──────────────────────────────────────
case 'platform_stats':
if (!$isAdmin) { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
$rows = db()->query("
SELECT p.id, p.name, p.slug, p.color,
COALESCE(SUM(CASE WHEN pc.type='debit' THEN -pc.credits_purchased ELSE pc.credits_purchased END),0) AS credits_balance,
(SELECT COUNT(*) FROM token_purchases tp WHERE tp.platform_id=p.slug AND tp.status='completed') AS purchases,
(SELECT COUNT(*) FROM cashout_requests cr WHERE cr.platform_id=p.slug AND cr.status IN ('sent','approved')) AS cashouts
FROM platforms p
LEFT JOIN platform_credits pc ON pc.platform_id=p.id
WHERE p.is_deleted=0 AND p.is_active=1
GROUP BY p.id, p.name, p.slug, p.color
ORDER BY p.sort_order, p.id
")->fetchAll();
echo json_encode(['success'=>true,'platforms'=>$rows]);
break;
// ─── PURCHASES ────────────────────────────────────────────
case 'purchases':
$status = $_GET['status'] ?? 'pending';