Files
tomtomgames/admin/index.php
T
myron 8d27290831 Fix 6 code review findings: auth, mysqldump stderr, dead code, audit logs
- backup.php: replace manual admin check with requireAdmin(); suppress
  mysqldump password warning (2>&1 → 2>/dev/null) to prevent corrupt dumps
- ttg-backup.sh: same mysqldump stderr fix
- admin.php toggle_user: fix undefined $adminId/$userId in logAdminAction
  call — use $_SESSION['user_id'] and $uid instead
- admin.php chat_clear_all: wrap in try/catch and add logAdminAction audit
- admin.php: delete unreachable broadcast query block after break statement
- admin/index.php: fix cashouts_total formatted as currency — use parseInt
  (tokens are whole numbers, not dollars)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-06 10:02:07 +00:00

3861 lines
238 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
ob_start();
require_once __DIR__ . '/../../includes/auth.php';
// Redirect to admin login if not logged in or not admin
if (!isLoggedIn() || empty($_SESSION['is_admin'])) {
ob_end_clean();
header('Location: /admin/login.php'); exit;
}
ob_end_clean();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet">
<meta name="googlebot" content="noindex, nofollow">
<title>TomTomGames Admin</title>
<link href="https://fonts.googleapis.com/css2?family=Exo+2:wght@400;600;700;900&family=Rajdhani:wght@400;500;600&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
:root{
/* ── Typography Scale ── */
--fs-xs:12px;--fs-sm:14px;--fs-base:16px;--fs-md:17px;
--fs-lg:19px;--fs-xl:21px;--fs-2xl:25px;--lh:1.6;--bg:#0a0a12;--bg2:#10101e;--bg3:#181828;--card:#1a1a2e;--border:rgba(255,255,255,0.08);--gold:#f0c040;--gold2:#ffdc73;--cyan:#00e5ff;--purple:#9b5de5;--green:#00e676;--red:#ff4444;--yellow:#ffd60a;--text:#e8e8f0;--text2:#8888aa;--r:12px;--rs:8px}
body{background:var(--bg);color:var(--text);font-family:'Rajdhani',sans-serif;font-size:17px;line-height:1.6;min-height:100vh;display:flex}
aside{width:210px;min-height:100vh;background:var(--bg2);border-right:1px solid var(--border);padding:0;display:flex;flex-direction:column;position:fixed;left:0;top:0;bottom:0}
.sidebar-head{padding:20px 18px 16px;border-bottom:1px solid var(--border)}
.sidebar-logo{font-family:'Exo 2',sans-serif;font-weight:900;font-size:15px;display:flex;align-items:center;gap:8px}.sidebar-logo span{background:linear-gradient(135deg,var(--gold),var(--cyan));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.sidebar-sub{font-size:12px;color:var(--text2);letter-spacing:1px;margin-top:2px;font-weight:700}
.nav-section{padding:12px 12px 4px;font-size:12px;font-weight:700;color:var(--text2);letter-spacing:1.5px;text-transform:uppercase}
.nav-item{display:flex;align-items:center;gap:9px;padding:10px 16px;color:var(--text2);cursor:pointer;transition:all .15s;font-weight:600;font-size:15px;border:none;background:none;width:100%;text-align:left;border-left:3px solid transparent}
.nav-item:hover{color:var(--text);background:rgba(255,255,255,.03)}
.nav-item.active{color:var(--gold);border-left-color:var(--gold);background:rgba(240,192,64,.05)}
.nav-item .badge-count{margin-left:auto;background:var(--red);color:#fff;font-size:12px;font-weight:700;padding:1px 6px;border-radius:10px;min-width:18px;text-align:center}
.sidebar-footer{margin-top:auto;padding:12px}
.logout-btn{width:100%;padding:10px;border:1px solid rgba(255,68,68,.3);border-radius:var(--rs);background:rgba(255,68,68,.06);color:var(--red);cursor:pointer;font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px;letter-spacing:.5px}
main{margin-left:210px;flex:1;padding:28px 32px;min-height:100vh}
.page-title{font-family:'Exo 2',sans-serif;font-weight:900;font-size:26px;margin-bottom:20px;background:linear-gradient(135deg,var(--gold),var(--cyan));-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.section{display:none}
.section.active{display:block}
/* STATS */
.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:14px;margin-bottom:28px}
.stat-card{background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:18px 16px}
.stat-label{font-size:12px;font-weight:700;color:var(--text2);letter-spacing:1.5px;text-transform:uppercase;margin-bottom:6px}
.stat-value{font-family:'Exo 2',sans-serif;font-weight:900;font-size:30px;color:var(--gold);line-height:1}
.stat-sub{font-size:13px;color:var(--text2);margin-top:4px}
/* CARD */
.card{background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:18px;margin-bottom:18px}
.card-title{font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;margin-bottom:14px;color:var(--text);display:flex;align-items:center;gap:8px}
/* TABLE */
.tbl-wrap{overflow-x:auto}
table{width:100%;border-collapse:collapse}
th{text-align:left;font-size:12px;font-weight:700;color:var(--text2);letter-spacing:1px;text-transform:uppercase;padding:0 10px 10px;border-bottom:1px solid var(--border);white-space:nowrap}
td{padding:10px;border-bottom:1px solid rgba(255,255,255,.03);font-size:15px;vertical-align:middle}
tr:last-child td{border-bottom:none}
tr:hover td{background:rgba(255,255,255,.015)}
/* BADGES */
.badge{display:inline-block;font-size:12px;font-weight:700;padding:2px 8px;border-radius:20px;letter-spacing:.3px;white-space:nowrap}
.b-pending {background:rgba(255,214,10,.15);color:var(--yellow)}
.b-approved,.b-completed{background:rgba(0,230,118,.15);color:var(--green)}
.b-rejected,.b-failed {background:rgba(255,68,68,.15);color:var(--red)}
.b-active {background:rgba(0,230,118,.15);color:var(--green)}
.b-suspended{background:rgba(255,68,68,.15);color:var(--red)}
.b-card {background:rgba(0,229,255,.12);color:var(--cyan)}
.b-venmo {background:rgba(59,89,200,.2);color:#7b9fff}
.b-chime {background:rgba(0,200,100,.12);color:#00e676}
.b-cashapp {background:rgba(0,180,50,.12);color:#4caf50}
.b-zelle {background:rgba(160,32,240,.15);color:#c77dff}
/* BUTTONS */
.btn{padding:7px 13px;border:none;border-radius:var(--rs);font-family:'Exo 2',sans-serif;font-weight:700;font-size:13px;letter-spacing:.5px;cursor:pointer;transition:all .15s;text-transform:uppercase;white-space:nowrap}
.btn:active{transform:scale(.97)}
.btn-green{background:rgba(0,230,118,.12);color:var(--green);border:1px solid rgba(0,230,118,.3)}
.btn-green:hover{background:rgba(0,230,118,.22)}
.btn-red{background:rgba(255,68,68,.12);color:var(--red);border:1px solid rgba(255,68,68,.3)}
.btn-red:hover{background:rgba(255,68,68,.22)}
.btn-gold{background:rgba(240,192,64,.12);color:var(--gold);border:1px solid rgba(240,192,64,.3)}
.btn-gold:hover{background:rgba(240,192,64,.22)}
.btn-cyan{background:rgba(0,229,255,.1);color:var(--cyan);border:1px solid rgba(0,229,255,.3)}
/* FILTER TABS */
.filter-bar{display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap}
.ftab{padding:6px 14px;border:1px solid var(--border);border-radius:20px;background:none;color:var(--text2);cursor:pointer;font-size:14px;font-weight:600;transition:all .15s}
.ftab.active{border-color:var(--gold);color:var(--gold);background:rgba(240,192,64,.06)}
/* FORM inline */
.fi-sm{background:var(--bg3);border:1px solid var(--border);border-radius:var(--rs);padding:7px 10px;color:var(--text);font-family:'Rajdhani',sans-serif;font-size:17px;line-height:1.6;outline:none}
.fi-sm:focus{border-color:var(--cyan)}
/* SEARCH */
.search-row{margin-bottom:14px;display:flex;gap:10px;align-items:center}
.search-row input{background:var(--bg3);border:1px solid var(--border);border-radius:var(--rs);padding:9px 13px;color:var(--text);font-family:'Rajdhani',sans-serif;font-size:17px;line-height:1.6;outline:none;width:260px}
.search-row input:focus{border-color:var(--cyan)}
/* PURCHASE CARD (milkyswipe style) */
.purchase-row{background:var(--bg3);border:1px solid var(--border);border-radius:var(--r);padding:14px;margin-bottom:10px}
.purchase-row.urgent{border-color:rgba(255,214,10,.3);background:rgba(255,214,10,.03)}
.pr-top{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;margin-bottom:10px}
.pr-info{}
.pr-user{font-weight:700;font-size:15px;color:var(--text)}
.pr-ref{font-size:13px;color:var(--text2);margin-top:1px}
.pr-amount{text-align:right}
.pr-tokens{font-family:'Exo 2',sans-serif;font-weight:900;font-size:22px;color:var(--gold);line-height:1}
.pr-dollars{font-size:14px;color:var(--green);font-weight:700}
.pr-meta{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:10px;align-items:center}
.pr-actions{display:flex;gap:8px;align-items:center}
.pr-note{flex:1}
.pr-note input{width:100%;background:var(--bg);border:1px solid var(--border);border-radius:var(--rs);padding:7px 10px;color:var(--text);font-family:'Rajdhani',sans-serif;font-size:17px;line-height:1.6;outline:none}
.pr-note input:focus{border-color:var(--cyan)}
/* TOAST */
#toast{position:fixed;top:18px;right:18px;padding:11px 18px;border-radius:var(--rs);font-weight:700;font-size:15px;z-index:999;display:none;box-shadow:0 4px 20px rgba(0,0,0,.4)}
#toast.show{display:block}
#toast.ok{background:#00e676;color:#000}
#toast.err{background:#ff4444;color:#fff}
#toast.info{background:var(--card);color:var(--cyan);border:1px solid rgba(0,229,255,.3)}
.empty{text-align:center;padding:36px;color:var(--text2);font-size:15px}
/* ─── ADMIN SAVED CARD ──────────────────────────────── */
.admin-saved-card{background:linear-gradient(135deg,#1c1c3a,#2d1b69);border-radius:14px;padding:18px;position:relative;overflow:hidden;border:1px solid rgba(255,255,255,.1)}
.admin-saved-card::before{content:'';position:absolute;top:-30px;right:-30px;width:110px;height:110px;border-radius:50%;background:rgba(255,255,255,.04)}
/* ─── GAME MANAGEMENT ────────────────────────────────── */
.game-row{display:flex;align-items:center;gap:14px;padding:14px 18px;border-bottom:1px solid var(--border);transition:background .15s}
.game-row:last-child{border-bottom:none}
.game-row:hover{background:rgba(255,255,255,.02)}
.game-color-dot{width:14px;height:14px;border-radius:50%;flex-shrink:0;box-shadow:0 0 8px currentColor}
.game-info{flex:1;min-width:0}
.game-name{font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;color:var(--text)}
.game-slug{font-size:13px;color:var(--text2);font-family:monospace;margin-top:1px}
.game-urls{font-size:13px;color:var(--text2);margin-top:3px;display:flex;flex-direction:column;gap:1px}
.game-url-row{display:flex;align-items:center;gap:5px;overflow:hidden}
.game-url-label{font-weight:700;color:var(--cyan);flex-shrink:0;font-size:12px;letter-spacing:.5px}
.game-url-val{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text2)}
.game-url-link{color:var(--cyan);text-decoration:none;font-size:12px;flex-shrink:0}
.game-url-link:hover{text-decoration:underline}
.game-actions{display:flex;gap:6px;flex-shrink:0}
.game-edit-btn{padding:6px 12px;border-radius:6px;border:none;cursor:pointer;font-family:'Exo 2',sans-serif;font-weight:700;font-size:13px;transition:all .15s}
.inactive-overlay{opacity:.5}
/* ─── ADMIN CHAT ─────────────────────────────────── */
.chat-inbox-row{display:flex;align-items:center;gap:14px;padding:14px 18px;border-bottom:1px solid var(--border);cursor:pointer;transition:background .15s}
.chat-inbox-row:last-child{border-bottom:none}
.chat-inbox-row:hover{background:rgba(255,255,255,.03)}
.chat-inbox-row.unread{background:rgba(240,192,64,.04)}
.ci-avatar{width:44px;height:44px;border-radius:50%;background:linear-gradient(135deg,var(--purple),var(--cyan));display:flex;align-items:center;justify-content:center;font-family:'Exo 2',sans-serif;font-weight:900;font-size:18px;color:#fff;flex-shrink:0}
.ci-body{flex:1;min-width:0}
.ci-top{display:flex;justify-content:space-between;align-items:baseline;gap:8px}
.ci-name{font-weight:700;font-size:15px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.ci-time{font-size:13px;color:var(--text2);white-space:nowrap;flex-shrink:0}
.ci-preview{font-size:14px;color:var(--text2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:2px}
.ci-preview.unread-preview{color:var(--text);font-weight:600}
.ci-badge{background:var(--red);color:#fff;font-size:12px;font-weight:700;min-width:18px;height:18px;border-radius:9px;display:flex;align-items:center;justify-content:center;padding:0 4px;flex-shrink:0}
.admin-chat-wrap{background:var(--card);border:1px solid var(--border);border-radius:var(--r);overflow:hidden;display:flex;flex-direction:column;height:calc(100vh - 180px)}
.admin-chat-header{display:flex;align-items:center;gap:12px;padding:14px 18px;background:var(--bg2);border-bottom:1px solid var(--border);flex-shrink:0}
.admin-chat-avatar{width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,var(--purple),var(--cyan));display:flex;align-items:center;justify-content:center;font-family:'Exo 2',sans-serif;font-weight:900;font-size:17px;color:#fff;flex-shrink:0}
.admin-chat-name{font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;color:var(--text)}
.admin-chat-meta{font-size:14px;color:var(--text2);margin-top:2px}
.admin-chat-messages{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:8px}
.admin-bubble-wrap{display:flex;flex-direction:column;max-width:75%}
.admin-bubble-wrap.from-user{align-self:flex-start;align-items:flex-start}
.admin-bubble-wrap.from-admin{align-self:flex-end;align-items:flex-end}
.admin-bubble{padding:9px 14px;border-radius:18px;font-size:15px;line-height:1.45;word-break:break-word}
.admin-bubble.from-user{background:var(--bg3);color:var(--text);border:1px solid var(--border);border-bottom-left-radius:4px}
.admin-bubble.from-admin{background:linear-gradient(135deg,var(--gold),#d4a017);color:#000;font-weight:500;border-bottom-right-radius:4px}
.admin-bubble-time{font-size:12px;color:var(--text2);margin-top:3px;padding:0 4px}
.admin-chat-input-bar{display:flex;gap:8px;padding:12px 14px;border-top:1px solid var(--border);background:var(--bg2);flex-shrink:0}
.admin-chat-input{flex:1;background:var(--bg3);border:1.5px solid var(--border);border-radius:24px;padding:9px 16px;color:var(--text);font-family:'Rajdhani',sans-serif;font-size:17px;line-height:1.6;outline:none;transition:border-color .15s}
.admin-chat-input:focus{border-color:var(--cyan)}
.admin-send-btn{width:38px;height:38px;border-radius:50%;background:linear-gradient(135deg,var(--gold),#d4a017);border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;color:#000}
.admin-send-btn svg{stroke:#000}
.admin-date-div{text-align:center;font-size:13px;color:var(--text2);font-weight:700;margin:8px 0;display:flex;align-items:center;gap:8px}
.admin-date-div::before,.admin-date-div::after{content:'';flex:1;height:1px;background:var(--border)}
/* ─── GAMER MANAGEMENT ───────────────────────────────── */
.gm-toolbar{display:flex;align-items:center;gap:12px;margin-bottom:20px;flex-wrap:wrap}
.gm-search-wrap{position:relative;flex:1;min-width:200px}
.gm-search-icon{position:absolute;left:12px;top:50%;transform:translateY(-50%);width:16px;height:16px;color:var(--text2)}
.gm-search{width:100%;background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:10px 14px 10px 36px;color:var(--text);font-family:'Rajdhani',sans-serif;font-size:17px;line-height:1.6;outline:none}
.gm-search:focus{border-color:var(--cyan)}
.gm-filters{display:flex;gap:6px;flex-shrink:0}
.gm-filter{padding:7px 14px;border:1px solid var(--border);border-radius:20px;background:none;color:var(--text2);cursor:pointer;font-size:14px;font-weight:600;transition:all .15s;white-space:nowrap}
.gm-filter.active{border-color:var(--gold);color:var(--gold);background:rgba(240,192,64,.07)}
.gm-stats-mini{font-size:14px;color:var(--text2);white-space:nowrap}
.gm-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:14px}
.gm-card{background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:18px;cursor:pointer;transition:all .18s;position:relative;overflow:hidden}
.gm-card:hover{border-color:rgba(240,192,64,.3);transform:translateY(-2px);box-shadow:0 6px 24px rgba(0,0,0,.3)}
.gm-card.suspended{border-color:rgba(255,68,68,.2);background:rgba(255,30,30,.03)}
.gm-card.unverified{border-color:rgba(255,214,10,.15);background:rgba(255,214,10,.02)}
.gm-card-top{display:flex;align-items:center;gap:12px;margin-bottom:14px}
.gm-card-avatar{width:46px;height:46px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-family:'Exo 2',sans-serif;font-weight:900;font-size:20px;color:#fff;flex-shrink:0}
.gm-card-info{flex:1;min-width:0}
.gm-card-name{font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.gm-card-alias{font-size:14px;color:var(--cyan);margin-top:2px}
.gm-card-badges{display:flex;gap:5px;flex-wrap:wrap;margin-top:2px}
.gm-card-stats{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:14px}
.gm-stat{background:var(--bg3);border-radius:8px;padding:8px 10px;text-align:center}
.gm-stat-val{font-family:'Exo 2',sans-serif;font-weight:700;font-size:17px;color:var(--gold)}
.gm-stat-lbl{font-size:12px;color:var(--text2);font-weight:600;letter-spacing:.5px;margin-top:1px}
.gm-card-email{font-size:13px;color:var(--text2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.gm-card-joined{font-size:12px;color:var(--text2);margin-top:3px}
.gm-card-hover-cta{position:absolute;bottom:0;left:0;right:0;background:linear-gradient(to top,rgba(240,192,64,.12),transparent);padding:8px 16px;font-size:13px;font-weight:700;color:var(--gold);letter-spacing:.5px;opacity:0;transition:opacity .2s;text-align:center}
.gm-card:hover .gm-card-hover-cta{opacity:1}
.gm-back-btn{display:flex;align-items:center;gap:7px;background:none;border:1px solid var(--border);border-radius:8px;color:var(--text2);cursor:pointer;font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;padding:8px 14px;margin-bottom:18px;transition:all .15s}
.gm-back-btn:hover{color:var(--cyan);border-color:var(--cyan)}
.gm-profile-card{background:linear-gradient(135deg,#1a1228,#0d1a2e);border:1px solid rgba(240,192,64,.2);border-radius:var(--r);padding:22px;margin-bottom:18px;display:flex;align-items:center;gap:18px;flex-wrap:wrap}
.gm-profile-avatar{width:72px;height:72px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-family:'Exo 2',sans-serif;font-weight:900;font-size:30px;color:#fff;flex-shrink:0;border:3px solid var(--gold);box-shadow:0 0 20px rgba(240,192,64,.3)}
.gm-profile-info{flex:1;min-width:0}
.gm-profile-name{font-family:'Exo 2',sans-serif;font-weight:900;font-size:22px;color:var(--text)}
.gm-profile-alias{font-size:15px;color:var(--cyan);margin-top:2px}
.gm-profile-email{font-size:15px;color:var(--text2);margin-top:4px}
.gm-profile-right{text-align:right;flex-shrink:0}
.gm-profile-tokens{font-family:'Exo 2',sans-serif;font-weight:900;font-size:32px;color:var(--gold);line-height:1}
.gm-profile-tok-lbl{font-size:13px;color:var(--text2);margin-top:2px;letter-spacing:1px;text-transform:uppercase}
.gm-profile-badges{display:flex;gap:6px;margin-top:8px;justify-content:flex-end}
.gm-tabs{display:flex;gap:0;background:var(--bg3);border-radius:var(--r);padding:4px;margin-bottom:16px;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none}
.gm-tabs::-webkit-scrollbar{display:none}
.gm-tab{flex:1;padding:9px 12px;border:none;background:none;color:var(--text2);font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px;letter-spacing:.3px;border-radius:8px;cursor:pointer;transition:all .15s;white-space:nowrap}
.gm-tab.active{background:linear-gradient(135deg,rgba(240,192,64,.15),rgba(0,229,255,.08));color:var(--gold)}
.gm-tab-panel{display:none}
.gm-tab-panel.active{display:block;animation:fadeUp .2s ease}
.gm-info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin-bottom:16px}
.gm-info-item{background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:14px}
.gm-info-label{font-size:12px;font-weight:700;color:var(--text2);letter-spacing:1.5px;text-transform:uppercase;margin-bottom:5px}
.gm-info-val{font-size:15px;font-weight:600;color:var(--text)}
.gm-actions-panel{background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:16px}
.gm-actions-title{font-size:13px;font-weight:700;color:var(--text2);letter-spacing:1.5px;text-transform:uppercase;margin-bottom:12px}
.gm-action-btns{display:flex;gap:8px;flex-wrap:wrap}
.gm-token-display{font-family:'Exo 2',sans-serif;font-weight:900;font-size:48px;color:var(--gold);text-align:center;padding:16px 0}
.gm-edit-label{display:block;font-size:13px;font-weight:700;color:var(--text2);letter-spacing:1px;text-transform:uppercase;margin-bottom:6px}
.gm-empty{text-align:center;padding:28px;color:var(--text2);font-size:15px}
/* ─── PAYMENT LEDGER ─────────────────────────────────── */
.pm-summary{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-bottom:16px}
.pm-sum-item{background:var(--bg3);border:1px solid var(--border);border-radius:var(--rs);padding:10px;text-align:center}
.pm-sum-val{font-family:'Exo 2',sans-serif;font-weight:900;font-size:18px;line-height:1}
.pm-sum-lbl{font-size:12px;color:var(--text2);font-weight:700;letter-spacing:.5px;text-transform:uppercase;margin-top:3px}
.pm-card{background:var(--bg3);border:1px solid var(--border);border-radius:var(--r);padding:14px;margin-bottom:10px;transition:border-color .15s}
.pm-card.pm-completed{border-left:3px solid var(--green)}
.pm-card.pm-pending{border-left:3px solid var(--yellow)}
.pm-card.pm-failed{border-left:3px solid var(--red);opacity:.85}
.pm-card-top{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:10px}
.pm-order-id{font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;color:var(--text)}
.pm-custom-badge{background:rgba(155,93,229,.2);color:#c77dff;font-size:12px;font-weight:700;padding:1px 7px;border-radius:10px;margin-left:6px;letter-spacing:.3px}
.pm-date{font-size:13px;color:var(--text2);margin-top:2px}
.pm-card-right{text-align:right}
.pm-amount{font-family:'Exo 2',sans-serif;font-weight:900;font-size:22px;color:var(--green);line-height:1}
.pm-tokens{font-size:14px;color:var(--gold);font-weight:700;margin-top:2px}
.pm-card-mid{}
.pm-detail-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap;font-size:14px}
.pm-lbl{color:var(--text2);font-weight:700}
.pm-val{color:var(--text)}
.pm-card-info{font-size:13px;color:var(--text2);background:var(--bg);border-radius:6px;padding:2px 8px}
.pm-failure{background:rgba(255,68,68,.08);border:1px solid rgba(255,68,68,.2);border-radius:6px;padding:7px 10px;margin-top:8px;font-size:14px;color:var(--red)}
.pm-admin-note{background:rgba(240,192,64,.05);border:1px solid rgba(240,192,64,.15);border-radius:6px;padding:7px 10px;margin-top:6px;font-size:14px;color:var(--gold2)}
.pm-receipt-link{font-size:14px;color:var(--cyan);text-decoration:none;font-weight:600}
.pm-receipt-link:hover{text-decoration:underline}
.pm-actions{display:flex;gap:8px;align-items:center;margin-top:12px;padding-top:12px;border-top:1px solid var(--border)}
.btn-sm{padding:6px 12px;font-size:13px}
/* ── Responsive Typography ─────────────────────────────── */
@media (min-width: 768px) {
body { font-size: 17px; }
.card { font-size: 16px !important; padding: 20px !important; }
.page-title { font-size: 26px !important; }
.nav-item { font-size: 15px !important; }
}
@media (min-width: 1200px) {
body { font-size: 18px; }
.card { font-size: 17px !important; }
.page-title { font-size: 28px !important; }
}
</style>
</head>
<body>
<div id="toast"></div>
<aside>
<div class="sidebar-head">
<div class="sidebar-logo" style="display:flex;align-items:center;gap:8px"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="28" height="28" style="display:inline-block;vertical-align:middle;flex-shrink:0"><defs><linearGradient id="li1" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#f0c040"/><stop offset="100%" stop-color="#ff6b35"/></linearGradient><linearGradient id="li2" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" stop-color="#00e5ff"/><stop offset="100%" stop-color="#7b2fbe"/></linearGradient></defs><rect x="6" y="16" width="36" height="22" rx="11" fill="url(#li1)"/><rect x="12" y="23" width="8" height="3" rx="1.5" fill="rgba(0,0,0,0.45)"/><rect x="15" y="20" width="3" height="8" rx="1.5" fill="rgba(0,0,0,0.45)"/><circle cx="32" cy="22" r="2.2" fill="#e63946" opacity=".9"/><circle cx="36" cy="25" r="2.2" fill="#2ec4b6" opacity=".9"/><circle cx="32" cy="28" r="2.2" fill="#7b2fbe" opacity=".9"/><circle cx="28" cy="25" r="2.2" fill="#f4a261" opacity=".9"/><rect x="21" y="24" width="6" height="3" rx="1.5" fill="rgba(0,0,0,0.3)"/><rect x="8" y="30" width="8" height="7" rx="4" fill="url(#li2)" opacity=".7"/><rect x="32" y="30" width="8" height="7" rx="4" fill="url(#li2)" opacity=".7"/><rect x="14" y="13" width="8" height="5" rx="2.5" fill="url(#li1)" opacity=".8"/><rect x="26" y="13" width="8" height="5" rx="2.5" fill="url(#li1)" opacity=".8"/></svg><span>TomTomGames</span></div>
<div class="sidebar-sub" style="-webkit-text-fill-color:var(--text2)">ADMIN PANEL</div>
</div>
<div class="nav-section">Main</div>
<button class="nav-item active" onclick="showSec('dashboard')">📊 Dashboard</button>
<button class="nav-item" onclick="showSec('users')">🎮 Players</button>
<button class="nav-item" onclick="showSec('pending')">⏳ Pending Signups <span class="badge-count" id="badge-pending">0</span></button>
<button class="nav-item" onclick="showSec('purchases')">🧾 Purchases <span class="badge-count" id="badge-purchases">0</span></button>
<button class="nav-item" onclick="showSec('cashouts')">💸 Cashouts <span class="badge-count" id="badge-cashouts">0</span></button>
<button class="nav-item" onclick="showSec('chat')">💬 Live Chat <span class="badge-count" id="badge-chat">0</span></button>
<button class="nav-item" onclick="showSec('broadcasts')">📢 Broadcasts</button>
<div class="nav-section">Manage</div>
<button class="nav-item" onclick="showSec('games')">🕹️ Game Management</button>
<button class="nav-item" onclick="showSec('referrals')">🎁 Referrals</button>
<button class="nav-item" onclick="showSec('platform-accounts')">🔑 Platform Accounts</button>
<button class="nav-item" onclick="showSec('payments')">💳 Payment Settings</button>
<button class="nav-item" onclick="showSec('cashout-methods')">💸 Cashout Methods</button>
<button class="nav-item" onclick="showSec('payout-settings')">💰 Payout Settings</button>
<button class="nav-item" onclick="showSec('history')">📋 All History</button>
<div class="nav-section">System</div>
<button class="nav-item" onclick="showSec('backups')">💾 Backups</button>
<div class="sidebar-footer">
<button class="logout-btn" onclick="fetch('/api/logout.php').then(()=>location='/admin/login.php')">🚪 LOGOUT</button>
</div>
</aside>
<main>
<!-- DASHBOARD -->
<div class="section active" id="section-dashboard">
<div class="page-title">📊 Dashboard</div>
<!-- Unread messages alert -->
<div id="unread-chat-alert" style="display:none;background:linear-gradient(135deg,rgba(240,192,64,.12),rgba(0,229,255,.06));border:1px solid rgba(240,192,64,.3);border-radius:12px;padding:14px 18px;margin-bottom:16px;display:none;align-items:center;gap:14px;cursor:pointer" onclick="showSec('chat')">
<div style="font-size:28px">💬</div>
<div style="flex:1">
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px;color:var(--gold)">New Messages from Players</div>
<div id="unread-chat-alert-text" style="font-size:14px;color:var(--text2);margin-top:2px">Click to view and respond</div>
</div>
<div style="font-family:'Exo 2',sans-serif;font-weight:900;font-size:24px;color:var(--gold)" id="unread-chat-alert-count">0</div>
</div>
<div class="stats-grid">
<div class="stat-card"><div class="stat-label">Total Users</div><div class="stat-value" id="s-users">—</div></div>
<div class="stat-card"><div class="stat-label">Total Revenue</div><div class="stat-value" id="s-rev" style="color:var(--green)">—</div></div>
<div class="stat-card"><div class="stat-label">Tokens Sold</div><div class="stat-value" id="s-toks">—</div></div>
<div class="stat-card"><div class="stat-label">Pending Purchases</div><div class="stat-value" id="s-ppurch" style="color:var(--yellow)">—</div></div>
<div class="stat-card"><div class="stat-label">Pending Cashouts</div><div class="stat-value" id="s-pcash" style="color:var(--yellow)">—</div></div>
</div>
<div class="card">
<div class="card-title">⚡ Pending Purchase Approvals</div>
<div id="dash-purchases"></div>
</div>
<div class="card">
<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 -->
<div class="section" id="section-purchases">
<div class="page-title">🧾 Token Purchases</div>
<div class="filter-bar">
<button class="ftab active" onclick="loadPurchases('pending',this)">⏳ Pending Approval</button>
<button class="ftab" onclick="loadPurchases('completed',this)">✅ Completed</button>
<button class="ftab" onclick="loadPurchases('failed',this)">❌ Failed</button>
<button class="ftab" onclick="loadPurchases('all',this)">📋 All</button>
</div>
<div id="purchases-list"></div>
</div>
<!-- CASHOUTS -->
<div class="section" id="section-cashouts">
<div class="page-title">💸 Cashout Requests</div>
<div class="filter-bar">
<button class="ftab active" onclick="loadCashouts('pending',this)">⏳ Pending</button>
<button class="ftab" onclick="loadCashouts('sent',this)">✅ Sent</button>
<button class="ftab" onclick="loadCashouts('rejected',this)">❌ Denied</button>
<button class="ftab" onclick="loadCashouts('deleted',this)">🗑 Deleted</button>
</div>
<div id="cashouts-table"></div>
</div>
<!-- USERS -->
<!-- GAMER MANAGEMENT -->
<div class="section" id="section-users">
<!-- LIST VIEW -->
<div id="gm-list-view">
<div class="page-title">🎮 Gamer Management</div>
<div class="gm-toolbar">
<div class="gm-search-wrap">
<svg class="gm-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
<input class="gm-search" id="gm-search" type="text" placeholder="Search username, alias, email..." oninput="filterGamers(this.value)">
</div>
<div class="gm-filters">
<button class="gm-filter active" onclick="setGamerFilter('all',this)">All</button>
<button class="gm-filter" onclick="setGamerFilter('active',this)">Active</button>
<button class="gm-filter" onclick="setGamerFilter('suspended',this)">Suspended</button>
<button class="gm-filter" onclick="setGamerFilter('unverified',this)">Unverified</button>
</div>
<div class="gm-stats-mini" id="gm-stats-mini"></div>
</div>
<div class="gm-grid" id="gm-grid"></div>
</div>
<!-- PLAYER DETAIL VIEW -->
<div id="gm-detail-view" style="display:none">
<button class="gm-back-btn" onclick="showGamerList()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="16" height="16"><polyline points="15 18 9 12 15 6"/></svg>
Back to Players
</button>
<div class="gm-profile-card" id="gm-profile-card"></div>
<div class="gm-tabs">
<button class="gm-tab active" onclick="switchGamerTab('overview',this)">Overview</button>
<button class="gm-tab" onclick="switchGamerTab('aliases',this)">🎮 Aliases</button>
<button class="gm-tab" onclick="switchGamerTab('payout',this)">💸 Payout</button>
<button class="gm-tab" onclick="switchGamerTab('platforms',this)">🔑 Accounts</button>
<button class="gm-tab" onclick="switchGamerTab('tokens',this)">Tokens</button>
<button class="gm-tab" onclick="switchGamerTab('purchases',this)">Purchases</button>
<button class="gm-tab" onclick="switchGamerTab('cashouts',this)">Cashouts</button>
<button class="gm-tab" onclick="switchGamerTab('billing',this)">Billing</button>
<button class="gm-tab" onclick="switchGamerTab('chat',this)">Chat</button>
<button class="gm-tab" onclick="switchGamerTab('edit',this)">Edit</button>
</div>
<!-- TAB: OVERVIEW -->
<div class="gm-tab-panel active" id="gtab-overview">
<div class="gm-info-grid" id="gm-info-grid"></div>
<div class="gm-actions-panel">
<div class="gm-actions-title">Quick Actions</div>
<div class="gm-action-btns" id="gm-action-btns"></div>
</div>
</div>
<!-- TAB: PLATFORM ACCOUNTS -->
<div class="gm-tab-panel" id="gtab-platforms">
<div class="card" style="margin-bottom:12px">
<div class="card-title">🔑 Platform Accounts</div>
<div style="font-size:14px;color:var(--text2);margin-bottom:12px">Manage game platform logins for this player. When you approve a request and set a username/password, it will automatically update their alias.</div>
<div id="gm-platform-accounts-list"><div style="color:var(--text2);text-align:center;padding:16px">Loading...</div></div>
</div>
</div>
<!-- TAB: PAYOUT METHODS -->
<div class="gm-tab-panel" id="gtab-payout">
<div class="card">
<div class="card-title">💸 Payout Methods</div>
<div style="font-size:14px;color:var(--text2);margin-bottom:12px">Where this player receives their cashout payments.</div>
<div id="gm-payout-list"><div style="color:var(--text2);text-align:center;padding:16px">Loading...</div></div>
</div>
</div>
<!-- TAB: PLATFORM ACCOUNTS -->
<div class="gm-tab-panel" id="gtab-platforms">
<div class="card">
<div class="card-title">🔑 Platform Accounts</div>
<div style="font-size:14px;color:var(--text2);margin-bottom:12px">Game platform logins created for this player.</div>
<div id="gm-platforms-list"><div style="color:var(--text2);text-align:center;padding:16px">Loading...</div></div>
</div>
</div>
<!-- TAB: ALIASES -->
<div class="gm-tab-panel" id="gtab-aliases">
<div class="card" style="margin-bottom:12px">
<div class="card-title">🎮 Game Aliases</div>
<div style="font-size:14px;color:var(--text2);margin-bottom:14px">Player's in-game usernames per platform. Edit and save below.</div>
<div id="gm-aliases-list"><div style="color:var(--text2);text-align:center;padding:16px;font-size:15px">Loading...</div></div>
<div id="gm-aliases-alert" class="alert" style="margin-top:10px"></div>
<button class="btn btn-gold" onclick="gmSaveAllAliases()" style="margin-top:12px">💾 SAVE ALL ALIASES</button>
</div>
</div>
<!-- TAB: TOKENS -->
<div class="gm-tab-panel" id="gtab-tokens">
<div class="card" style="margin-bottom:14px">
<div class="card-title">💰 Token Balance</div>
<div class="gm-token-display" id="gm-token-display">—</div>
</div>
<div class="card" style="margin-bottom:14px">
<div class="card-title">Adjust Tokens</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:12px">
<div><label class="gm-edit-label">Amount</label><input class="fi-sm" type="number" id="gm-tok-amount" placeholder="e.g. 50" style="width:100%;padding:10px 12px;font-size:15px"></div>
<div><label class="gm-edit-label">Reason</label><input class="fi-sm" type="text" id="gm-tok-reason" placeholder="e.g. Bonus" style="width:100%;padding:10px 12px;font-size:15px"></div>
</div>
<div style="display:flex;gap:8px">
<button class="btn btn-green" style="flex:1" onclick="gmAdjustTokens(1)"> ADD</button>
<button class="btn btn-red" style="flex:1" onclick="gmAdjustTokens(-1)"> REMOVE</button>
</div>
<div id="gm-tok-msg" class="alert" style="margin-top:12px"></div>
</div>
<div class="card">
<div class="card-title">Set Exact Balance</div>
<div style="display:flex;gap:8px">
<input class="fi-sm" type="number" id="gm-tok-set" placeholder="New balance" style="flex:1;padding:10px 12px;font-size:15px">
<button class="btn btn-gold" style="width:100px" onclick="gmSetTokens()">SET</button>
</div>
</div>
</div>
<!-- TAB: PURCHASES -->
<div class="gm-tab-panel" id="gtab-purchases">
<div id="gm-purchases-table"></div>
</div>
<!-- TAB: CASHOUTS -->
<div class="gm-tab-panel" id="gtab-cashouts">
<div class="card"><div class="tbl-wrap"><div id="gm-cashouts-table"></div></div></div>
</div>
<!-- TAB: BILLING -->
<div class="gm-tab-panel" id="gtab-billing">
<div class="card" style="margin-bottom:14px">
<div class="card-title">💳 Saved Payment Info</div>
<div id="gm-billing-card-display" style="display:none;margin-bottom:16px">
<div class="admin-saved-card" id="gm-billing-card"></div>
<div style="display:flex;gap:8px;margin-top:10px">
<button class="btn btn-red" style="flex:1" onclick="gmClearCard()">🗑 Remove Card</button>
</div>
</div>
<div id="gm-billing-no-card" style="display:none;padding:12px;background:var(--bg3);border-radius:var(--rs);font-size:15px;color:var(--text2);text-align:center;margin-bottom:12px">No card on file</div>
<div id="gm-billing-alert" class="alert" style="margin-bottom:12px"></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px">
<div><label class="gm-edit-label">First Name</label><input class="fi-sm" id="gm-b-first" type="text" style="width:100%;padding:10px 12px"></div>
<div><label class="gm-edit-label">Last Name</label><input class="fi-sm" id="gm-b-last" type="text" style="width:100%;padding:10px 12px"></div>
<div><label class="gm-edit-label">Email</label><input class="fi-sm" id="gm-b-email" type="email" style="width:100%;padding:10px 12px"></div>
<div><label class="gm-edit-label">ZIP Code</label><input class="fi-sm" id="gm-b-zip" type="text" style="width:100%;padding:10px 12px"></div>
</div>
<div style="margin-bottom:10px"><label class="gm-edit-label">Street Address</label><input class="fi-sm" id="gm-b-address" type="text" style="width:100%;padding:10px 12px"></div>
<div style="display:grid;grid-template-columns:1fr 70px;gap:8px;margin-bottom:14px">
<div><label class="gm-edit-label">City</label><input class="fi-sm" id="gm-b-city" type="text" style="width:100%;padding:10px 12px"></div>
<div><label class="gm-edit-label">State</label><input class="fi-sm" id="gm-b-state" type="text" maxlength="2" style="width:100%;padding:10px 12px;text-transform:uppercase"></div>
</div>
<div style="display:flex;gap:8px">
<button class="btn btn-gold" style="flex:1" onclick="gmSaveBilling()">💾 SAVE BILLING INFO</button>
<button class="btn btn-red" style="width:120px" onclick="gmClearAllBilling()">🗑 Clear All</button>
</div>
</div>
</div>
<!-- TAB: CHAT -->
<div class="gm-tab-panel" id="gtab-chat">
<div class="admin-chat-wrap" style="height:400px">
<div class="admin-chat-messages" id="gm-chat-messages"></div>
<div class="admin-chat-input-bar">
<input class="admin-chat-input" id="gm-chat-input" type="text" placeholder="Message this player..." onkeydown="if(event.key==='Enter')gmSendMsg()">
<button class="admin-send-btn" onclick="gmSendMsg()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
</button>
</div>
</div>
<button onclick="gmClearThread()" style="margin-top:10px;width:100%;background:rgba(255,68,68,.08);border:1px solid rgba(255,68,68,.2);color:var(--red);border-radius:8px;padding:10px;font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px;cursor:pointer">
🗑 Clear This Conversation
</button>
</div>
<!-- TAB: EDIT -->
<div class="gm-tab-panel" id="gtab-edit">
<div class="card" style="margin-bottom:14px">
<div class="card-title">✏️ Edit Player Info</div>
<div id="gm-edit-alert" class="alert" style="margin-bottom:12px"></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:14px">
<div><label class="gm-edit-label">Username</label><input class="fi-sm" id="gm-edit-username" type="text" style="width:100%;padding:10px 12px"></div>
<div><label class="gm-edit-label">Alias / Nickname</label><input class="fi-sm" id="gm-edit-alias" type="text" style="width:100%;padding:10px 12px"></div>
<div><label class="gm-edit-label">Email Address</label><input class="fi-sm" id="gm-edit-email" type="email" style="width:100%;padding:10px 12px"></div>
<div><label class="gm-edit-label">New Password <span style="color:var(--text2);font-weight:400">(blank = keep)</span></label><input class="fi-sm" id="gm-edit-password" type="password" placeholder="Leave blank to keep" style="width:100%;padding:10px 12px"></div>
</div>
<button class="btn btn-gold" onclick="gmSaveEdit()">💾 SAVE CHANGES</button>
</div>
<div class="card" style="border-color:rgba(255,68,68,.25)">
<div class="card-title" style="color:var(--red)">⚠️ Danger Zone</div>
<div style="display:flex;flex-direction:column;gap:8px">
<button class="btn btn-red" id="gm-suspend-btn" onclick="gmToggleSuspend()">Loading...</button>
<button class="btn" onclick="gmSendPasswordReset()" style="background:rgba(155,93,229,.12);color:#c77dff;border:1px solid rgba(155,93,229,.3)">🔑 Send Password Reset Email</button>
<button class="btn" id="gm-admin-toggle-btn" onclick="gmToggleAdmin()">Admin Toggle</button>
<button class="btn" onclick="gmDeleteAccount()" style="background:rgba(255,30,30,.18);color:#ff6666;border:1px solid rgba(255,30,30,.35)">🗑️ DELETE ACCOUNT PERMANENTLY</button>
</div>
<p style="font-size:15px;color:var(--text2);margin-top:12px;line-height:1.6">Deleting is permanent and removes all purchases, cashouts, and chat history.</p>
</div>
</div>
</div><!-- /gm-detail-view -->
</div><!-- /section-users -->
<!-- ── PAYMENT SETTINGS ─────────────────────────────────── -->
<div class="section" id="section-payments">
<div class="page-title">💳 Payment Settings</div>
<div style="background:rgba(0,229,255,.06);border:1px solid rgba(0,229,255,.15);border-radius:10px;padding:12px 16px;margin-bottom:16px;font-size:15px;color:var(--text2);line-height:1.6">
Enable or disable each payment method. Disabled methods are hidden from players instantly. Card payments run through Square — your Square account must be active for card to work.
</div>
<div id="payment-methods-list"></div>
</div>
<!-- ── PLATFORM ACCOUNTS ─────────────────────────────── -->
<!-- ── REFERRALS ──────────────────────────────────────── -->
<div class="section" id="section-referrals">
<div class="page-title">🎁 Referral Management</div>
<div style="display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap">
<button class="ftab active" onclick="loadAdminReferrals('pending',this)">⏳ Pending</button>
<button class="ftab" onclick="loadAdminReferrals('verified',this)">✅ Verified</button>
<button class="ftab" onclick="loadAdminReferrals('denied',this)">❌ Denied</button>
<button class="ftab" onclick="showRefSection('tiers')">⚙️ Manage Tiers</button>
<button class="ftab" onclick="showRefSection('shares')">📱 Social Shares</button>
</div>
<div id="ref-admin-list"></div>
<div id="ref-tiers-section" style="display:none">
<div class="card" style="margin-bottom:14px;border-color:rgba(240,192,64,.2)">
<div class="card-title" id="ref-tier-form-title">Add Referral Tier</div>
<div id="ref-tier-alert" class="alert" style="margin-bottom:8px"></div>
<input type="hidden" id="rt-id">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px">
<div><label class="gm-edit-label">Tier Name</label><input class="fi-sm" id="rt-name" type="text" placeholder="e.g. Gold Referrer" style="width:100%;padding:8px 10px"></div>
<div><label class="gm-edit-label">Min Referrals</label><input class="fi-sm" id="rt-min" type="number" min="1" value="1" style="width:100%;padding:8px 10px"></div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;margin-bottom:8px">
<div><label class="gm-edit-label">Tokens per Referral</label><input class="fi-sm" id="rt-per" type="number" min="0" step="0.5" value="5" style="width:100%;padding:8px 10px"></div>
<div><label class="gm-edit-label">Milestone Bonus</label><input class="fi-sm" id="rt-bonus" type="number" min="0" value="0" style="width:100%;padding:8px 10px"></div>
<div><label class="gm-edit-label">Sort Order</label><input class="fi-sm" id="rt-sort" type="number" value="0" style="width:100%;padding:8px 10px"></div>
</div>
<div style="margin-bottom:10px"><label class="gm-edit-label">Description</label><input class="fi-sm" id="rt-desc" type="text" placeholder="Shown to players" style="width:100%;padding:8px 10px"></div>
<div style="display:flex;gap:8px;margin-bottom:12px">
<div style="flex:1"><label class="gm-edit-label">Status</label>
<select class="fi-sm" id="rt-active" style="width:100%;padding:8px 10px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:var(--rs)">
<option value="1">Active</option><option value="0">Inactive</option>
</select></div>
</div>
<div style="display:flex;gap:8px">
<button class="btn btn-gold" onclick="saveRefTier()" style="flex:1">Save Tier</button>
<button class="btn btn-outline" onclick="resetRefTierForm()" style="width:100px">Clear</button>
</div>
</div>
<div class="card" style="padding:0;overflow:hidden">
<div style="padding:14px 18px;border-bottom:1px solid var(--border);font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px">All Tiers</div>
<div id="ref-tiers-admin-list"></div>
</div>
</div>
<div id="ref-shares-section" style="display:none">
<div style="display:flex;gap:8px;margin-bottom:12px">
<button class="ftab active" onclick="loadAdminShares('pending',this)">Pending</button>
<button class="ftab" onclick="loadAdminShares('approved',this)">Approved</button>
<button class="ftab" onclick="loadAdminShares('denied',this)">Denied</button>
</div>
<div id="ref-shares-admin-list"></div>
</div>
</div>
<div class="section" id="section-platform-accounts">
<div class="page-title">🔑 Platform Account Requests</div>
<div style="display:flex;gap:8px;margin-bottom:14px">
<button class="ftab active" onclick="loadPlatformAccountRequests('pending',this)">⏳ Pending</button>
<button class="ftab" onclick="loadPlatformAccountRequests('approved',this)">✅ Approved</button>
<button class="ftab" onclick="loadPlatformAccountRequests('denied',this)">❌ Denied</button>
</div>
<div id="pa-requests-list"></div>
</div>
<!-- ── BROADCASTS ──────────────────────────────────────── -->
<div class="section" id="section-broadcasts">
<div class="page-title">📢 Broadcast Messages</div>
<!-- Compose panel -->
<div class="card" style="margin-bottom:16px;border-color:rgba(240,192,64,.2)">
<div class="card-title" style="color:var(--gold)">✉️ Send Broadcast</div>
<div id="bc-alert" class="alert" style="margin-bottom:10px"></div>
<div class="fg" style="margin-bottom:10px">
<label class="gm-edit-label">Subject *</label>
<input class="fi-sm" id="bc-subject" type="text" placeholder="e.g. Important Update" style="width:100%;padding:10px 12px">
</div>
<div class="fg" style="margin-bottom:10px">
<label class="gm-edit-label">Message *</label>
<textarea id="bc-message" rows="4" placeholder="Write your message..." style="width:100%;box-sizing:border-box;background:var(--bg3);border:1px solid var(--border);border-radius:var(--rs);padding:10px 12px;color:var(--text);font-family:'Rajdhani',sans-serif;font-size:14px;resize:vertical;outline:none"></textarea>
</div>
<div style="display:grid;grid-template-columns:1fr auto;gap:10px;align-items:end">
<div>
<label class="gm-edit-label">Send To</label>
<select class="fi-sm" id="bc-target" style="width:100%;padding:10px 12px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:var(--rs)">
<option value="all">👥 All Active Players</option>
<option value="verified">✅ Verified Players Only</option>
<option value="unverified">⏳ Unverified Players Only</option>
<option value="admins">🔑 Admins Only</option>
</select>
</div>
<button class="btn btn-gold" style="padding:11px 20px;white-space:nowrap" onclick="sendBroadcast()">📢 SEND BROADCAST</button>
</div>
</div>
<!-- Sent broadcasts list -->
<div class="card" style="padding:0;overflow:hidden">
<div style="padding:14px 18px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center">
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px">Sent Broadcasts</div>
<button onclick="loadBroadcasts()" style="background:none;border:none;color:var(--cyan);cursor:pointer;font-size:14px;font-weight:700">↻ Refresh</button>
</div>
<div id="bc-list"></div>
</div>
<!-- Replies / Reads drawer -->
<div id="bc-drawer" style="display:none;margin-top:12px">
<div class="card" style="border-color:rgba(0,229,255,.2)">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">
<div class="card-title" id="bc-drawer-title" style="margin-bottom:0">Details</div>
<button onclick="closeBcDrawer()" style="background:none;border:none;color:var(--text2);cursor:pointer;font-size:18px">✕</button>
</div>
<div style="display:flex;gap:8px;margin-bottom:14px">
<button class="ftab active" onclick="switchBcTab('replies',this)" id="bc-tab-replies">💬 Replies</button>
<button class="ftab" onclick="switchBcTab('reads',this)" id="bc-tab-reads">👁 Read By</button>
</div>
<div id="bc-drawer-content"></div>
<!-- Admin reply -->
<div style="margin-top:14px;display:flex;gap:8px">
<input class="fi-sm" id="bc-reply-input" type="text" placeholder="Reply to this broadcast..." style="flex:1;padding:10px 12px" onkeydown="if(event.key==='Enter')adminBroadcastReply()">
<button class="btn btn-gold" onclick="adminBroadcastReply()" style="padding:10px 16px">Send</button>
</div>
</div>
</div>
</div>
<!-- ── PAYOUT SETTINGS ─────────────────────────────── -->
<div class="section" id="section-payout-settings">
<div class="page-title">💰 Payout Settings</div>
<div style="background:rgba(0,229,255,.06);border:1px solid rgba(0,229,255,.15);border-radius:10px;padding:12px 16px;margin-bottom:16px;font-size:15px;color:var(--text2);line-height:1.7">
Configure how you send cashout payments to players. <strong style="color:var(--cyan)">Square Gift Card</strong> sends instantly. <strong style="color:var(--gold)">Manual methods</strong> show the player handle so you send from the app and mark done.
</div>
<div id="payout-settings-list"></div>
</div>
<!-- ── CASHOUT METHOD TYPES ──────────────────────────────── -->
<div class="section" id="section-cashout-methods">
<div class="page-title">💸 Cashout Methods</div>
<div style="background:rgba(0,229,255,.06);border:1px solid rgba(0,229,255,.15);border-radius:10px;padding:12px 16px;margin-bottom:16px;font-size:15px;color:var(--text2);line-height:1.6">
💸 Manage the payout method types available to players when they cash out. Active methods appear in the player's payout method dropdown.
</div>
<!-- Add / Edit form -->
<div class="card" id="cmt-form-card" style="margin-bottom:16px">
<div class="card-title" id="cmt-form-title"> Add Cashout Method</div>
<div id="cmt-form-alert" class="alert" style="margin-bottom:10px"></div>
<input type="hidden" id="cmt-id" value="">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px">
<div>
<label class="gm-edit-label">Label *</label>
<input class="fi-sm" id="cmt-label" type="text" placeholder="e.g. Venmo" style="width:100%;padding:10px 12px">
</div>
<div>
<label class="gm-edit-label">Slug * <span style="font-weight:400;color:var(--text2)">lowercase, no spaces</span></label>
<input class="fi-sm" id="cmt-slug" type="text" placeholder="e.g. venmo" style="width:100%;padding:10px 12px" oninput="this.value=this.value.toLowerCase().replace(/[^a-z0-9_]/g,'')">
</div>
</div>
<div style="display:grid;grid-template-columns:80px 1fr;gap:10px;margin-bottom:10px">
<div>
<label class="gm-edit-label">Icon</label>
<input class="fi-sm" id="cmt-icon" type="text" value="💰" maxlength="4" style="width:100%;padding:10px 12px;font-size:20px;text-align:center">
</div>
<div>
<label class="gm-edit-label">Description <span style="font-weight:400;color:var(--text2)">shown to players</span></label>
<input class="fi-sm" id="cmt-desc" type="text" placeholder="e.g. Send via Venmo username" style="width:100%;padding:10px 12px">
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:14px">
<div>
<label class="gm-edit-label">Sort Order</label>
<input class="fi-sm" id="cmt-sort" type="number" value="99" min="0" style="width:100%;padding:10px 12px">
</div>
<div>
<label class="gm-edit-label">Status</label>
<select class="fi-sm" id="cmt-active" style="width:100%;padding:10px 12px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:var(--rs)">
<option value="1">✅ Active</option>
<option value="0">⏸ Inactive</option>
</select>
</div>
</div>
<div style="display:flex;gap:8px">
<button class="btn btn-gold" style="flex:1" onclick="saveCashoutMethod()">💾 SAVE</button>
<button class="btn btn-outline" onclick="resetCmtForm()" style="width:100px">CLEAR</button>
</div>
</div>
<!-- Methods list -->
<div class="card" style="padding:0;overflow:hidden">
<div style="padding:14px 18px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center">
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px">Active &amp; Inactive Methods</div>
<div style="font-size:14px;color:var(--text2)" id="cmt-count">Loading...</div>
</div>
<div id="cmt-list"></div>
</div>
</div>
<!-- ── GAME MANAGEMENT ─────────────────────────────────── -->
<div class="section" id="section-games">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;flex-wrap:wrap;gap:10px">
<div class="page-title" style="margin-bottom:0">🕹️ Game Management</div>
<?php if ((int)$_SESSION['user_id'] === MASTER_ADMIN_ID): ?>
<button class="btn btn-gold" onclick="resetGameForm();document.getElementById('game-form-card').scrollIntoView({behavior:'smooth'})" style="padding:10px 20px;font-size:15px">
Add New Game
</button>
<?php endif; ?>
</div>
<!-- Add / Edit form — master admin always sees this; others see it only when editing -->
<div class="card" id="game-form-card" style="margin-bottom:16px;<?= (int)$_SESSION['user_id'] !== MASTER_ADMIN_ID ? 'display:none' : '' ?>">
<div class="card-title" id="game-form-title"> Add New Game</div>
<div id="game-form-alert" class="alert" style="margin-bottom:10px"></div>
<input type="hidden" id="gf-id" value="">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px">
<div>
<label class="gm-edit-label">Game Name *</label>
<input class="fi-sm" id="gf-name" type="text" placeholder="e.g. VBlink 777" style="width:100%;padding:10px 12px">
</div>
<div>
<label class="gm-edit-label">Slug (ID) * <span style="font-weight:400;color:var(--text2)">lowercase, no spaces</span></label>
<input class="fi-sm" id="gf-slug" type="text" placeholder="e.g. vblink777" style="width:100%;padding:10px 12px" oninput="this.value=this.value.toLowerCase().replace(/[^a-z0-9_]/g,'')">
</div>
</div>
<div style="margin-bottom:10px">
<label class="gm-edit-label">Player URL * <span style="font-weight:400;color:var(--text2)">shown to players — opens the game</span></label>
<input class="fi-sm" id="gf-player-url" type="url" placeholder="https://game.example.com/play" style="width:100%;padding:10px 12px">
</div>
<!-- Agent Fields — EDIT mode (master admin only) -->
<div id="gf-agent-edit" style="background:rgba(155,93,229,0.06);border:1px solid rgba(155,93,229,0.2);border-radius:8px;padding:12px 14px;margin-bottom:10px;display:none">
<div style="font-size:12px;font-weight:700;color:var(--purple);letter-spacing:1px;text-transform:uppercase;margin-bottom:10px">🔐 Agent Info — Admin Only</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px">
<div>
<label class="gm-edit-label">Agent Login</label>
<input class="fi-sm" id="gf-agent-login" type="text" placeholder="Username or email" style="width:100%;padding:10px 12px">
</div>
<div>
<label class="gm-edit-label">Agent Password</label>
<input class="fi-sm" id="gf-agent-password" type="text" placeholder="Password" style="width:100%;padding:10px 12px">
</div>
</div>
<div style="margin-bottom:10px">
<label class="gm-edit-label">Agent Link <span style="font-weight:400;color:var(--text2)">admin/console URL</span></label>
<div style="display:flex;gap:6px;align-items:center">
<input class="fi-sm" id="gf-agent-link" type="url" placeholder="https://admin.game.example.com" style="flex:1;padding:10px 12px">
<a id="gf-agent-link-open" href="#" target="_blank" rel="noopener" onclick="return openFieldUrl('gf-agent-link')"
style="background:rgba(0,229,255,0.1);border:1px solid rgba(0,229,255,0.25);color:var(--cyan);border-radius:6px;padding:9px 12px;font-size:13px;font-weight:700;text-decoration:none;white-space:nowrap">↗</a>
</div>
</div>
<div style="margin-bottom:10px">
<label class="gm-edit-label">Games Link <span style="font-weight:400;color:var(--text2)">game listing or lobby URL</span></label>
<div style="display:flex;gap:6px;align-items:center">
<input class="fi-sm" id="gf-games-link" type="url" placeholder="https://game.example.com/lobby" style="flex:1;padding:10px 12px">
<a id="gf-games-link-open" href="#" target="_blank" rel="noopener" onclick="return openFieldUrl('gf-games-link')"
style="background:rgba(0,229,255,0.1);border:1px solid rgba(0,229,255,0.25);color:var(--cyan);border-radius:6px;padding:9px 12px;font-size:13px;font-weight:700;text-decoration:none;white-space:nowrap">↗</a>
</div>
</div>
<div>
<label class="gm-edit-label">Agent Guide <span style="font-weight:400;color:var(--text2)">notes, instructions, tips</span></label>
<textarea class="fi-sm" id="gf-agent-guide" rows="3" placeholder="Step-by-step agent instructions, notes, or tips..." style="width:100%;padding:10px 12px;resize:vertical;font-family:inherit;font-size:14px"></textarea>
</div>
<div style="border-top:1px solid rgba(155,93,229,0.2);margin-top:12px;padding-top:12px">
<div style="font-size:11px;font-weight:700;color:var(--purple);letter-spacing:1px;text-transform:uppercase;margin-bottom:8px">Sub-Account</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div>
<label class="gm-edit-label">Sub-Account Agent Login</label>
<input class="fi-sm" id="gf-sub-agent-login" type="text" placeholder="Username or email" style="width:100%;padding:10px 12px">
</div>
<div>
<label class="gm-edit-label">Sub-Account Agent Password</label>
<input class="fi-sm" id="gf-sub-agent-password" type="text" placeholder="Password" style="width:100%;padding:10px 12px">
</div>
</div>
</div>
<div style="border-top:1px solid rgba(155,93,229,0.2);margin-top:12px;padding-top:12px">
<div style="font-size:11px;font-weight:700;color:var(--purple);letter-spacing:1px;text-transform:uppercase;margin-bottom:8px">Cashier</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div>
<label class="gm-edit-label">Cashier Login</label>
<input class="fi-sm" id="gf-cashier-login" type="text" placeholder="Username or email" style="width:100%;padding:10px 12px">
</div>
<div>
<label class="gm-edit-label">Cashier Password</label>
<input class="fi-sm" id="gf-cashier-password" type="text" placeholder="Password" style="width:100%;padding:10px 12px">
</div>
</div>
</div>
</div>
<!-- Agent Fields — VIEW mode (non-master admins) -->
<div id="gf-agent-view" style="background:rgba(155,93,229,0.06);border:1px solid rgba(155,93,229,0.2);border-radius:8px;padding:12px 14px;margin-bottom:10px;display:none">
<div style="font-size:12px;font-weight:700;color:var(--purple);letter-spacing:1px;text-transform:uppercase;margin-bottom:10px">🔐 Agent Info — View Only</div>
<div id="gf-agent-view-content" style="display:flex;flex-direction:column;gap:6px"></div>
</div>
<!-- Credit Accounting — admin only -->
<div style="background:rgba(0,229,255,0.04);border:1px solid rgba(0,229,255,0.2);border-radius:8px;padding:12px 14px;margin-bottom:10px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
<div style="font-size:12px;font-weight:700;color:var(--cyan);letter-spacing:1px;text-transform:uppercase">💳 Credit Accounting — Admin Only</div>
</div>
<div style="display:flex;align-items:center;gap:12px;margin-top:8px">
<div style="flex:1">
<div style="font-size:12px;color:var(--text2);margin-bottom:3px;font-weight:600;text-transform:uppercase;letter-spacing:.5px">Available Credits</div>
<div id="gf-credit-total" style="font-family:'Exo 2',sans-serif;font-weight:900;font-size:26px;color:var(--cyan)">—</div>
</div>
<button type="button" onclick="openCreditModal()" id="gf-credit-btn"
style="background:rgba(0,229,255,0.1);border:1px solid rgba(0,229,255,0.3);color:var(--cyan);border-radius:8px;padding:10px 18px;font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px;cursor:pointer;white-space:nowrap"
disabled>📋 Manage Credits</button>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:14px">
<div>
<label class="gm-edit-label">Brand Color</label>
<div style="display:flex;align-items:center;gap:8px">
<input type="color" id="gf-color" value="#f0c040" style="width:44px;height:38px;padding:2px;border:1px solid var(--border);border-radius:6px;background:var(--bg3);cursor:pointer">
<input class="fi-sm" id="gf-color-hex" type="text" value="#f0c040" maxlength="7" style="flex:1;padding:10px 10px" oninput="syncColorPicker(this.value)">
</div>
</div>
<div>
<label class="gm-edit-label">Sort Order</label>
<input class="fi-sm" id="gf-sort" type="number" value="99" min="0" style="width:100%;padding:10px 12px">
</div>
<div>
<label class="gm-edit-label">Status</label>
<select class="fi-sm" id="gf-active" style="width:100%;padding:10px 12px;background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:var(--rs)">
<option value="1">✅ Active</option>
<option value="0">⏸ Inactive</option>
</select>
</div>
</div>
<div style="display:flex;gap:8px">
<button class="btn btn-gold" style="flex:1" onclick="saveGame()">💾 SAVE GAME</button>
<button class="btn btn-outline" onclick="resetGameForm()" style="width:100px">CLEAR</button>
</div>
</div>
<!-- Games list -->
<div class="card" style="padding:0;overflow:hidden">
<div style="padding:14px 18px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center">
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px">Active & Inactive Games</div>
<div style="font-size:14px;color:var(--text2)" id="games-count">Loading...</div>
</div>
<div id="games-list"></div>
</div>
<?php if ((int)$_SESSION['user_id'] === MASTER_ADMIN_ID): ?>
<!-- Archived Games — master admin only -->
<div class="card" style="padding:0;overflow:hidden;margin-top:16px">
<div style="padding:14px 18px;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center">
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px;color:var(--text2)">🗄️ Archived Games</div>
<button onclick="loadArchivedGames()" style="background:none;border:none;color:var(--cyan);font-size:13px;font-weight:700;cursor:pointer">↻ Refresh</button>
</div>
<div id="archived-games-list"><div style="padding:16px 18px;color:var(--text2);font-size:14px">Loading...</div></div>
</div>
<?php endif; ?>
</div>
<!-- ── HISTORY ─────────────────────────────────── -->
<div class="section" id="section-history">
<div class="page-title">📋 Full Audit Log <span style="font-size:15px;font-weight:400;color:var(--text2);margin-left:8px">90-day rolling · 20 per page</span></div>
<!-- Filters row -->
<div style="display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap;align-items:center">
<select id="hist-category" onchange="loadHistory(1)" style="background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px;cursor:pointer">
<option value="">📂 All Categories</option>
<option value="auth">🔐 Auth (Login/Register)</option>
<option value="player">👤 Player Actions</option>
<option value="admin">🛡️ Admin Actions</option>
<option value="security">⚠️ Security Events</option>
<option value="general">📋 General</option>
</select>
<select id="hist-severity" onchange="loadHistory(1)" style="background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px;cursor:pointer">
<option value="">🔍 All Severity</option>
<option value="critical">🔴 Critical</option>
<option value="warning">🟡 Warning</option>
<option value="info">🟢 Info</option>
</select>
<input type="text" id="hist-search" placeholder="Search user, action, detail..." onkeydown="if(event.key==='Enter')loadHistory(1)"
style="background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-size:14px;width:200px">
<input type="date" id="hist-date" onchange="loadHistory(1)"
style="background:var(--bg3);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:8px 12px;font-size:14px">
<button onclick="loadHistory(1)" style="background:rgba(0,229,255,.08);border:1px solid rgba(0,229,255,.2);color:var(--cyan);border-radius:8px;padding:8px 14px;font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px;cursor:pointer">↻ Refresh</button>
<button onclick="exportHistory()" style="background:rgba(240,192,64,.08);border:1px solid rgba(240,192,64,.2);color:var(--gold);border-radius:8px;padding:8px 14px;font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px;cursor:pointer">⬇ Export CSV</button>
<span id="hist-count" style="font-size:14px;color:var(--text2);margin-left:4px"></span>
</div>
<!-- Summary stats bar -->
<div id="hist-stats" style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:14px"></div>
<!-- Log table -->
<div id="history-table"></div>
<!-- Pagination -->
<div id="hist-pagination" style="display:flex;gap:8px;justify-content:center;margin-top:16px;flex-wrap:wrap"></div>
</div>
<!-- ── BACKUPS ──────────────────────────────────────────── -->
<div class="section" id="section-backups">
<div class="page-title">💾 Backup System</div>
<div style="background:rgba(0,229,255,.05);border:1px solid rgba(0,229,255,.15);border-radius:10px;padding:12px 18px;margin-bottom:18px;display:flex;align-items:center;gap:14px;flex-wrap:wrap">
<div style="flex:1;min-width:200px">
<div style="font-size:14px;font-weight:700;color:var(--cyan);margin-bottom:2px">🕐 Automated Schedule</div>
<div style="font-size:14px;color:var(--text2)">Daily at 2:00 AM · 7 rolling backups · Files + full database export</div>
</div>
<div style="display:flex;gap:8px">
<button class="btn btn-gold" id="backup-create-btn" onclick="createBackup()" style="padding:10px 20px;font-size:15px">📦 Create Backup Now</button>
<button onclick="loadBackups()" style="background:none;border:1px solid var(--border);color:var(--text2);border-radius:var(--rs);padding:10px 14px;cursor:pointer;font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px">↻</button>
</div>
</div>
<div id="backup-alert" class="alert" style="margin-bottom:12px"></div>
<div class="card" style="padding:0;overflow:hidden">
<div style="padding:14px 18px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between">
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px">Available Backups <span style="color:var(--text2);font-weight:400;font-size:13px">(last 7 days)</span></div>
<div id="backup-count" style="font-size:13px;color:var(--text2)"></div>
</div>
<div id="backup-list"><div style="padding:24px;text-align:center;color:var(--text2);font-size:15px">Loading...</div></div>
</div>
</div>
<!-- PENDING SIGNUPS -->
<div class="section" id="section-pending">
<div class="page-title">⏳ Pending Signups</div>
<div style="background:rgba(0,229,255,.06);border:1px solid rgba(0,229,255,.15);border-radius:10px;padding:12px 16px;margin-bottom:14px;font-size:15px;color:var(--text2);line-height:1.6">
⏳ Players who registered but haven't verified their email yet. <strong style="color:var(--gold)">Approve</strong> to create their account immediately, or <strong style="color:var(--red)">Delete</strong> to remove the request.
</div>
<div id="pending-list"></div>
</div>
<!-- CHAT INBOX -->
<div class="section" id="section-chat">
<div class="page-title">Live Chat
<span style="font-size:15px;font-weight:400;color:var(--green);margin-left:10px;letter-spacing:.5px">
● Auto-refreshing every 5s
</span>
</div>
<!-- INBOX VIEW -->
<div id="chat-inbox-view">
<!-- NEW MESSAGE COMPOSE PANEL -->
<div class="card" style="margin-bottom:14px;border-color:rgba(240,192,64,.2);background:linear-gradient(135deg,#1a1228,#0d1820)">
<div class="card-title" style="color:var(--gold);margin-bottom:12px">✉️ Send Message to Player</div>
<!-- Player search -->
<div style="position:relative;margin-bottom:10px">
<input class="fi-sm" id="compose-search" type="text" placeholder="Search by name, alias or email..."
style="width:100%;padding:10px 12px;font-size:14px"
oninput="searchComposePlayers(this.value)" autocomplete="off">
<div id="compose-dropdown" style="display:none;position:absolute;top:100%;left:0;right:0;background:var(--bg3);border:1px solid var(--border);border-radius:8px;z-index:50;max-height:200px;overflow-y:auto;margin-top:4px;box-shadow:0 8px 24px rgba(0,0,0,.4)"></div>
</div>
<!-- Selected player chip -->
<div id="compose-selected" style="display:none;background:rgba(0,229,255,.08);border:1px solid rgba(0,229,255,.2);border-radius:8px;padding:8px 12px;margin-bottom:10px;display:none;align-items:center;gap:10px">
<div id="compose-player-avatar" style="width:32px;height:32px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px;color:#fff;flex-shrink:0;background:#7b2fbe">?</div>
<div style="flex:1;min-width:0">
<div id="compose-player-name" style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;color:var(--text)">—</div>
<div id="compose-player-info" style="font-size:15px;color:var(--text2)">—</div>
</div>
<button onclick="clearComposePicker()" style="background:none;border:none;color:var(--text2);cursor:pointer;font-size:18px;line-height:1;padding:0 4px">×</button>
</div>
<!-- Message box -->
<textarea id="compose-msg" rows="3" placeholder="Type your message..."
style="width:100%;box-sizing:border-box;background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:10px 12px;color:var(--text);font-family:'Rajdhani',sans-serif;font-size:14px;resize:vertical;outline:none;margin-bottom:10px"
onkeydown="if(event.key==='Enter'&&event.ctrlKey)adminComposeSend()"></textarea>
<div style="display:flex;justify-content:space-between;align-items:center">
<div style="font-size:15px;color:var(--text2)">Ctrl+Enter to send · Player will see it in their Support chat</div>
<button class="btn btn-gold" style="width:120px" onclick="adminComposeSend()">
📨 SEND
</button>
</div>
<div id="compose-alert" class="alert" style="margin-top:10px"></div>
</div>
<!-- Inbox list -->
<div style="background:rgba(0,229,255,.06);border:1px solid rgba(0,229,255,.15);border-radius:10px;padding:12px 16px;margin-bottom:14px;font-size:15px;color:var(--text2);line-height:1.6;display:flex;align-items:center;gap:10px">
<span style="flex:1">📨 Messages from players appear below. Click any conversation to open it and reply.</span>
<button onclick="adminClearAllChats()" style="background:rgba(255,68,68,.1);border:1px solid rgba(255,68,68,.2);color:var(--red);border-radius:8px;padding:7px 12px;font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;cursor:pointer;white-space:nowrap;flex-shrink:0">
🗑 Clear All
</button>
</div>
<div class="card" style="padding:0;overflow:hidden">
<div id="chat-inbox-list"></div>
</div>
</div>
<!-- THREAD VIEW -->
<div id="chat-thread-view" style="display:none">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:14px">
<button onclick="showChatInbox()" style="background:none;border:none;color:var(--cyan);cursor:pointer;font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;display:flex;align-items:center;gap:6px;padding:0;flex:1">
← Back to Inbox
</button>
<button onclick="adminClearThread()" style="background:rgba(255,68,68,.1);border:1px solid rgba(255,68,68,.2);color:var(--red);border-radius:8px;padding:6px 12px;font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;cursor:pointer">
🗑 Clear Thread
</button>
</div>
<div class="admin-chat-wrap">
<div class="admin-chat-header" id="admin-chat-header">
<div class="admin-chat-avatar" id="admin-chat-avatar">?</div>
<div>
<div class="admin-chat-name" id="admin-chat-name">—</div>
<div class="admin-chat-meta" id="admin-chat-meta">—</div>
</div>
</div>
<div class="admin-chat-messages" id="admin-chat-messages"></div>
<div class="admin-chat-input-bar">
<input class="admin-chat-input" id="admin-chat-input" type="text" placeholder="Reply to player..."
onkeydown="if(event.key==='Enter')adminSendMsg()">
<button class="admin-send-btn" onclick="adminSendMsg()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
</button>
</div>
</div>
</div>
</div>
<!-- PROCESS PAYOUT MODAL -->
<!-- ── CREDIT ACCOUNTING MODAL ───────────────────────── -->
<div id="credit-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.88);z-index:950;align-items:flex-start;justify-content:center;padding:20px;overflow-y:auto">
<div style="background:var(--bg3);border:1px solid rgba(0,229,255,.25);border-radius:16px;padding:0;max-width:640px;width:100%;margin:auto">
<!-- Header -->
<div style="padding:20px 24px 16px;border-bottom:1px solid var(--border)">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
<div style="font-family:'Exo 2',sans-serif;font-weight:900;font-size:18px;color:var(--cyan)">💳 Credit Accounting</div>
<button onclick="closeCreditModal()" style="background:none;border:none;color:var(--text2);font-size:20px;cursor:pointer;line-height:1">✕</button>
</div>
<div id="cm-platform-name" style="font-size:13px;color:var(--text2);margin-bottom:10px"></div>
<div style="background:rgba(0,229,255,0.07);border:1px solid rgba(0,229,255,0.2);border-radius:10px;padding:14px 18px;display:flex;align-items:center;gap:16px">
<div>
<div style="font-size:11px;font-weight:700;color:var(--cyan);letter-spacing:1px;text-transform:uppercase;margin-bottom:2px">Total Credits This Platform</div>
<div id="cm-total" style="font-family:'Exo 2',sans-serif;font-weight:900;font-size:32px;color:var(--cyan)">0</div>
</div>
</div>
</div>
<!-- Add / Edit form -->
<div style="padding:16px 24px;border-bottom:1px solid var(--border)">
<div id="cm-form-title" style="font-size:13px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:1px;margin-bottom:10px"> Add Credit Entry</div>
<input type="hidden" id="cm-entry-id" value="">
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:10px">
<div>
<label class="gm-edit-label">Credits Purchased *</label>
<input class="fi-sm" id="cm-credits" type="number" min="0" step="0.01" placeholder="0.00" style="width:100%;padding:9px 11px">
</div>
<div>
<label class="gm-edit-label">Date *</label>
<input class="fi-sm" id="cm-date" type="date" style="width:100%;padding:9px 11px">
</div>
<div>
<label class="gm-edit-label">Payment Method</label>
<input class="fi-sm" id="cm-method" type="text" placeholder="e.g. Venmo, Cash" style="width:100%;padding:9px 11px">
</div>
</div>
<div style="margin-bottom:10px">
<label class="gm-edit-label">Notes <span style="font-weight:400;color:var(--text2)">(optional)</span></label>
<input class="fi-sm" id="cm-notes" type="text" placeholder="Any additional notes..." style="width:100%;padding:9px 11px">
</div>
<div id="cm-form-alert" class="alert" style="margin-bottom:8px"></div>
<div style="display:flex;gap:8px">
<button class="btn btn-gold" style="flex:1" onclick="saveCreditEntry()">💾 SAVE ENTRY</button>
<button class="btn btn-outline" onclick="resetCreditForm()" style="width:80px">CLEAR</button>
</div>
</div>
<!-- Entries list -->
<div style="padding:16px 24px">
<div style="font-size:13px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:1px;margin-bottom:10px">Credit History</div>
<div id="cm-list"><div style="color:var(--text2);text-align:center;padding:20px">Loading...</div></div>
</div>
</div>
</div>
<div id="process-payout-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.85);z-index:900;align-items:center;justify-content:center;padding:20px">
<div style="background:var(--bg3);border:1px solid rgba(240,192,64,.3);border-radius:16px;padding:28px 24px;max-width:480px;width:100%;max-height:90vh;overflow-y:auto">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px">
<div style="font-family:'Exo 2',sans-serif;font-weight:900;font-size:20px;color:var(--gold)">Process Payout</div>
<button onclick="closePayoutModal()" style="background:none;border:none;color:var(--text2);font-size:20px;cursor:pointer">X</button>
</div>
<div id="ppm-player-info" style="background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:14px;margin-bottom:16px"></div>
<div id="ppm-saved-method" style="margin-bottom:16px"></div>
<div class="fg" style="margin-bottom:12px">
<label style="font-size:15px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:1px;display:block;margin-bottom:6px">Send Payment Via</label>
<select id="ppm-method-select" class="fi fi-select" style="width:100%" onchange="onPayoutMethodChange()">
<option value="">-- Select payout method --</option>
</select>
</div>
<div id="ppm-method-detail" style="margin-bottom:12px"></div>
<div class="fg" style="margin-bottom:16px">
<label style="font-size:15px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:1px;display:block;margin-bottom:6px">Note (optional)</label>
<input class="fi-sm" type="text" id="ppm-note" placeholder="e.g. Sent via Venmo 3pm" style="width:100%;padding:10px 12px">
</div>
<div id="ppm-alert" class="alert" style="margin-bottom:10px"></div>
<button id="ppm-submit-btn" class="btn btn-gold" onclick="submitPayout()" style="width:100%;font-size:15px;padding:14px">Process Payout</button>
<div style="font-size:15px;color:var(--text2);text-align:center;margin-top:10px">This marks the cashout as Sent</div>
</div>
</div>
</main>
<script>
const PLATS = <?= PLATFORMS ?>; // fallback static list
let _dynamicPlats = null;
const pname = id => {
const src = _dynamicPlats || PLATS;
const p = src.find(p=>(p.id||p.slug)===id);
return p ? p.name : (id || '—');
};
// Load dynamic platforms list from DB
fetch('/api/platforms.php?action=admin_list').then(r=>r.json()).then(d=>{
if (d.success) _dynamicPlats = d.platforms.map(p=>({id:p.slug,name:p.name}));
}).catch(()=>{});
const CURRENT_ADMIN_ID = <?= (int)$_SESSION['user_id'] ?>;
const MASTER_ADMIN_ID = <?= MASTER_ADMIN_ID ?>;
const IS_MASTER_ADMIN = CURRENT_ADMIN_ID === MASTER_ADMIN_ID;
const methodLabel = m => ({ card:'💳 Card', venmo:'💙 Venmo', chime:'🟢 Chime', cashapp:'💚 Cash App', zelle:'💜 Zelle' }[m] || m);
const methodClass = m => ({ card:'b-card', venmo:'b-venmo', chime:'b-chime', cashapp:'b-cashapp', zelle:'b-zelle' }[m] || '');
let allUsers = [];
async function apiFetch(action, method='GET', body=null) {
const opts={method,headers:{'Content-Type':'application/json'}};
if(body)opts.body=JSON.stringify(body);
const r = await fetch(`/api/admin.php?action=${action}`,opts);
return r.json();
}
// ─── INIT ──────────────────────────────────────────────────
loadStats();
loadPurchases('pending');
loadCashouts('pending');
loadUsers();
// Initialize game form once DOM is ready
document.addEventListener('DOMContentLoaded', () => resetGameForm());
async function loadStats() {
const d = await apiFetch('stats');
if (!d.success) return;
const s = d.stats;
document.getElementById('s-users').textContent = s.total_users;
document.getElementById('s-rev').textContent = '$'+parseFloat(s.total_revenue).toFixed(2);
document.getElementById('s-toks').textContent = s.total_tokens_sold;
document.getElementById('s-ppurch').textContent= s.pending_purchases;
document.getElementById('s-pcash').textContent = s.pending_cashouts;
document.getElementById('badge-purchases').textContent = s.pending_purchases;
document.getElementById('badge-cashouts').textContent = s.pending_cashouts;
const ps = s.pending_signups || 0;
document.getElementById('badge-pending').textContent = ps;
document.getElementById('badge-pending').style.display = ps > 0 ? 'inline-block' : 'none';
// Dashboard quick lists
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:15px;color:var(--gold)">$${parseFloat(p.purchases_total).toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2})}</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:15px;color:var(--green)">${parseInt(p.cashouts_total).toLocaleString()} 🪙</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 ───────────────────────────────────────────
// ─── PURCHASE CARD RENDERER ──────────────────────────────
function purchaseCard(p, showActions=true) {
const isPending = p.status === 'pending';
const isManual = p.payment_method !== 'card';
const amt = (p.amount_cents/100).toFixed(2);
const date = (p.created_at||'').substring(0,16);
const statusColors = { pending:'var(--gold)', completed:'var(--green)', failed:'var(--red)' };
const statusLabels = { pending:'⏳ Pending Approval', completed:'✅ Completed', failed:'❌ Failed' };
const statusColor = statusColors[p.status] || 'var(--text2)';
const statusLabel = statusLabels[p.status] || p.status;
const mIcons = { card:'💳', venmo:'💙', cashapp:'💚', zelle:'💜', chime:'🟢', manual:'💵' };
const mLabels = { card:'Square Card', venmo:'Venmo', cashapp:'Cash App', zelle:'Zelle', chime:'Chime', manual:'Manual' };
const mIcon = mIcons[p.payment_method] || '💰';
const mLabel = mLabels[p.payment_method] || p.payment_method;
const platformRow = p.platform_id
? '<div style="font-size:14px;color:var(--text2);margin-top:4px">Platform: <strong style="color:var(--cyan)">'+pname(p.platform_id)+'</strong> · Alias: <strong style="color:var(--gold2)">'+escHtmlA(p.game_alias||'—')+'</strong></div>'
: '';
const actions = showActions && isPending
? '<div style="display:flex;flex-direction:column;gap:8px;flex-shrink:0;min-width:160px">'+
'<input class="fi-sm" type="text" id="pnote-'+p.id+'" placeholder="Note to player (optional)" style="width:100%;font-size:14px;padding:8px 10px">'+
'<button class="btn btn-green" data-pid="'+p.id+'" onclick="resolvePurchase(this.dataset.pid,&quot;completed&quot;)" style="width:100%;font-size:15px;padding:10px;font-weight:700">✓ Approve &amp; Credit</button>'+
'<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" 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">'+
'<span style="font-family:\'Exo 2\',sans-serif;font-weight:700;font-size:15px">'+escHtmlA(p.username)+'</span>'+
'<span style="font-size:15px;color:var(--text2)">#'+p.id+'</span>'+
'<span style="font-size:15px;font-weight:700;color:'+statusColor+';background:rgba(0,0,0,.25);border:1px solid '+statusColor+';border-radius:6px;padding:2px 8px">'+statusLabel+'</span></div>'+
'<div style="font-size:14px;color:var(--text2)">'+escHtmlA(p.alias||'')+' · '+mIcon+' '+mLabel+' · '+date+'</div>'+
platformRow+
'<div style="font-family:\'Exo 2\',sans-serif;font-weight:900;font-size:24px;color:var(--gold);margin-top:6px">'+p.tokens+' 🪙</div>'+
'<div style="font-size:14px;font-weight:700;color:var(--green)">$'+amt+'</div>'+
(isManual?'<div style="margin-top:8px;background:rgba(0,229,255,.06);border:1px solid rgba(0,229,255,.15);border-radius:6px;padding:6px 10px;font-size:14px"><span style="color:var(--cyan);font-weight:700">Manual Payment</span> — verify before approving</div>':'')+
'</div>'+actions+'</div></div>';
}
async function loadPurchases(status, btn=null) {
if (btn) { document.querySelectorAll('#section-purchases .ftab').forEach(b=>b.classList.remove('active')); btn.classList.add('active'); }
const d = await apiFetch('purchases&status='+status);
const el = document.getElementById('purchases-list');
if (!d.success || !d.purchases.length) { el.innerHTML='<div class="empty">No purchases found.</div>'; return; }
el.innerHTML = d.purchases.map(p => purchaseCard(p, true)).join('');
}
async function loadDashPurchases() {
const d = await apiFetch('purchases&status=pending');
const el = document.getElementById('dash-purchases');
if (!d.success || !d.purchases.length) { el.innerHTML='<div class="empty">No pending purchases 🎉</div>'; return; }
el.innerHTML = d.purchases.map(p => purchaseCard(p, true)).join('');
}
async function resolvePurchase(id, status) {
const note = (document.getElementById('pnote-'+id)?.value||'').trim();
const d = await apiFetch('resolve_purchase','POST',{id,status,note});
if (d.success) {
toast(status==='completed' ? '✓ Tokens credited!' : '✗ Purchase rejected', status==='completed'?'ok':'err');
document.getElementById('pr-'+id)?.remove();
loadStats();
} else toast(d.error||'Error','err');
}
// ─── CASHOUTS ──────────────────────────────────────────────
const PAYOUT_ICONS_A = { venmo:'💙', cashapp:'💚', zelle:'💜', chime:'🟢', bank:'🏦', other:'💰' };
const PAYOUT_LABELS_A = { venmo:'Venmo', cashapp:'Cash App', zelle:'Zelle', chime:'Chime', bank:'Bank Transfer', other:'Other' };
function cashoutRow(c, showActions=true) {
const payoutInfo = c.payout_handle
? `<div style="margin-top:6px;background:rgba(0,229,255,.06);border:1px solid rgba(0,229,255,.15);border-radius:6px;padding:6px 10px;font-size:14px">
<span style="font-weight:700;color:var(--cyan)">${PAYOUT_ICONS_A[c.payout_method_type]||'💰'} ${PAYOUT_LABELS_A[c.payout_method_type]||c.payout_method_type}:</span>
<span style="color:var(--text);margin-left:4px">${escHtmlA(c.payout_handle)}</span>
</div>`
: '<div style="font-size:15px;color:var(--red);margin-top:4px">⚠️ No payout method on file</div>';
const statusColors = { pending:'var(--text2)', locked:'var(--cyan)', approved:'var(--green)', sent:'var(--green)', rejected:'var(--red)', deleted:'var(--text2)' };
const statusLabels = { pending:'⏳ Draft', locked:'🔒 Ready to Process', sent:'✅ Sent', approved:'✅ Sent', rejected:'❌ Denied', deleted:'🗑 Deleted' };
const statusColor = statusColors[c.status] || 'var(--text2)';
const canAct = showActions && (c.status === 'pending' || c.status === 'locked');
const isLocked = c.status === 'locked';
const playerAlias = c.user_alias || c.alias || c.username || '';
return `<div class="card" style="margin-bottom:10px;${isLocked?'border-color:rgba(0,229,255,.25);background:rgba(0,229,255,.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">
<span style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px">${escHtmlA(c.username)}</span>
<span style="font-size:15px;color:var(--text2)">#${c.id}</span>
<span style="font-size:15px;font-weight:700;color:${statusColor};background:rgba(0,0,0,.25);border:1px solid ${statusColor};border-radius:6px;padding:2px 8px">${statusLabels[c.status]||c.status}</span>
</div>
<div style="font-size:14px;color:var(--text2)">${pname(c.platform_id)} · <span style="color:var(--cyan)">${escHtmlA(c.alias||'')}</span> · <span style="color:var(--text2)">${escHtmlA(playerAlias)}</span></div>
<div style="font-family:'Exo 2',sans-serif;font-weight:900;font-size:24px;color:var(--gold);margin-top:4px">${c.tokens} 🪙</div>
<div style="font-size:15px;color:var(--text2);margin-top:2px">${(c.created_at||'').substring(0,16)}</div>
${payoutInfo}
${c.admin_note ? `<div style="font-size:14px;color:var(--gold);margin-top:6px;padding:6px 8px;background:rgba(240,192,64,.07);border-radius:6px">📝 ${escHtmlA(c.admin_note)}</div>` : ''}
</div>
${canAct ? `
<div style="display:flex;flex-direction:column;gap:8px;flex-shrink:0;min-width:160px">
<button class="btn btn-gold" onclick="openPayoutModal(${c.id})" style="width:100%;font-size:15px;padding:10px">
💰 Process Payout
</button>
<button class="btn btn-red" onclick="resolveCashout(${c.id},'rejected')" style="width:100%;font-size:14px;padding:8px">
✗ Deny
</button>
<button onclick="resolveCashout(${c.id},'deleted')" style="width:100%;background:rgba(255,255,255,.04);border:1px solid var(--border);color:var(--text2);border-radius:8px;padding:8px;font-size:14px;cursor:pointer;font-family:'Exo 2',sans-serif;font-weight:700">
🗑 Delete
</button>
</div>` : ''}
</div>
</div>`;
}
async function loadCashouts(status, btn=null) {
if (btn) { document.querySelectorAll('#section-cashouts .ftab').forEach(b=>b.classList.remove('active')); btn.classList.add('active'); }
const d = await apiFetch('cashouts&status=' + status);
const el = document.getElementById('cashouts-table');
if (!d.success || !d.cashouts.length) { el.innerHTML = '<div class="empty" style="padding:24px;text-align:center">No cashouts found.</div>'; return; }
el.innerHTML = d.cashouts.map(c => cashoutRow(c, true)).join('');
}
async function loadDashCashouts() {
const d = await apiFetch('cashouts&status=pending');
const el = document.getElementById('dash-cashouts');
if (!d.success || !d.cashouts.length) { el.innerHTML = '<div class="empty">No pending cashouts 🎉</div>'; return; }
el.innerHTML = d.cashouts.map(c => cashoutRow(c, true)).join('');
}
async function resolveCashout(id, status) {
const labels = { approved:'✓ Payment Sent', rejected:'✗ Cashout Denied', deleted:'🗑 Request Deleted' };
const confirmMsgs = {
approved: 'Mark this cashout as SENT? This confirms you have sent the payment to the player.',
rejected: 'Deny this cashout request? Tokens will be returned to the player.',
deleted: 'Delete this cashout request?'
};
if (!confirm(confirmMsgs[status]||'Are you sure?')) return;
const note = (document.getElementById('cnote-'+id)?.value||'').trim();
const d = await apiFetch('resolve_cashout','POST',{id,status,note});
if (d.success) {
toast(labels[status]||'Done', status==='approved'?'ok':'err');
loadCashouts('pending');
loadDashCashouts();
loadStats();
} else toast(d.error||'Error','err');
}
// ─── USERS ─────────────────────────────────────────────────
// ─── GAMER MANAGEMENT ──────────────────────────────────────
let GM = { all: [], filtered: [], filter: 'all', current: null, chatLastId: 0, chatPoll: null };
const AVATAR_COLORS = ['#9b5de5','#e63946','#457b9d','#2ec4b6','#f4a261','#7b2fbe','#ff6b35','#00b4d8'];
function avatarColor(username) { let h=0; for(let c of username) h=(h*31+c.charCodeAt(0))%AVATAR_COLORS.length; return AVATAR_COLORS[h]; }
function avatarInit(username) { return (username||'?').charAt(0).toUpperCase(); }
async function loadUsers() {
const d = await apiFetch('users');
if (!d.success) return;
GM.all = d.users;
applyGamerFilter();
}
function setGamerFilter(f, btn) {
GM.filter = f;
document.querySelectorAll('.gm-filter').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
applyGamerFilter();
}
function filterGamers(q) {
const base = GM.filter === 'all' ? GM.all
: GM.filter === 'unverified' ? GM.all.filter(u => !parseInt(u.email_verified))
: GM.all.filter(u => u.status === GM.filter);
if (!q) { GM.filtered = base; }
else {
const lq = q.toLowerCase();
GM.filtered = base.filter(u =>
u.username.toLowerCase().includes(lq) ||
u.alias.toLowerCase().includes(lq) ||
(u.email||'').toLowerCase().includes(lq)
);
}
renderGamerGrid();
}
function applyGamerFilter() {
const q = document.getElementById('gm-search').value;
filterGamers(q);
}
function renderGamerGrid() {
const el = document.getElementById('gm-grid');
const users = GM.filtered;
// Mini stats
const total = GM.all.length;
const active = GM.all.filter(u=>u.status==='active'&&parseInt(u.email_verified)).length;
const susp = GM.all.filter(u=>u.status==='suspended').length;
const unver = GM.all.filter(u=>!parseInt(u.email_verified)).length;
document.getElementById('gm-stats-mini').innerHTML =
`<span style="color:var(--green)">${active} active</span> · <span style="color:var(--red)">${susp} suspended</span> · <span style="color:var(--yellow)">${unver} unverified</span> · ${total} total`;
if (!users.length) {
el.innerHTML = '<div class="gm-empty">No players match this filter.</div>';
return;
}
el.innerHTML = users.map(u => {
const color = avatarColor(u.username);
const init = avatarInit(u.username);
const verified = parseInt(u.email_verified);
const isSupended = u.status === 'suspended';
const cardClass = isSupended ? 'suspended' : (!verified ? 'unverified' : '');
const lastSeen = u.last_login ? timeAgo(u.last_login) : 'Never';
return `
<div class="gm-card ${cardClass}" onclick="openGamer(${u.id})">
<div class="gm-card-top">
<div class="gm-card-avatar" style="background:linear-gradient(135deg,${color},${color}aa)">${init}</div>
<div class="gm-card-info">
<div class="gm-card-name">${escHtmlA(u.username)}</div>
<div class="gm-card-alias">${escHtmlA(u.alias)}</div>
<div class="gm-card-badges">
<span class="badge b-${u.status}">${u.status}</span>
${verified ? '<span class="badge" style="background:rgba(0,230,118,.12);color:var(--green)">✓ verified</span>' : '<span class="badge b-pending">⚠ unverified</span>'}
${u.is_admin ? '<span class="badge" style="background:rgba(240,192,64,.15);color:var(--gold)">admin</span>' : ''}
</div>
</div>
</div>
<div class="gm-card-stats">
<div class="gm-stat"><div class="gm-stat-val">${parseFloat(u.tokens||0)}</div><div class="gm-stat-lbl">TOKENS</div></div>
<div class="gm-stat"><div class="gm-stat-val" style="font-size:14px;color:var(--text2)">${(u.created_at||'').substring(0,10)}</div><div class="gm-stat-lbl">JOINED</div></div>
<div class="gm-stat"><div class="gm-stat-val" style="font-size:14px;color:var(--text2)">${lastSeen}</div><div class="gm-stat-lbl">LAST SEEN</div></div>
</div>
<div class="gm-card-email">${escHtmlA(u.email||'No email')}</div>
<div class="gm-card-hover-cta">VIEW PLAYER PROFILE →</div>
</div>`;
}).join('');
}
async function openGamer(uid) {
const d = await apiFetch('user_detail&user_id=' + uid);
if (!d.success) { toast('Failed to load player', 'err'); return; }
GM.current = d.user;
GM.chatLastId = 0;
document.getElementById('gm-list-view').style.display = 'none';
document.getElementById('gm-detail-view').style.display = 'block';
renderGamerProfile(d.user);
renderGamerOverview(d.user, d.stats);
populateGamerEdit(d.user);
updateAdminToggleBtn();
loadGamerPurchases(uid);
loadGamerCashouts(uid);
loadGamerChat(uid, true);
// Switch to overview tab
switchGamerTab('overview', document.querySelector('.gm-tab'));
// Poll chat while detail open
clearInterval(GM.chatPoll);
GM.chatPoll = setInterval(() => {
if (GM.current && document.getElementById('gtab-chat').classList.contains('active')) {
loadGamerChat(GM.current.id, false);
}
}, 3000);
}
function renderGamerProfile(u) {
const color = avatarColor(u.username);
const verified = parseInt(u.email_verified);
document.getElementById('gm-profile-card').innerHTML = `
<div class="gm-profile-avatar" style="background:linear-gradient(135deg,${color},${color}99)">${avatarInit(u.username)}</div>
<div class="gm-profile-info">
<div class="gm-profile-name">${escHtmlA(u.username)}</div>
<div class="gm-profile-alias">🎮 ${escHtmlA(u.alias)}</div>
<div class="gm-profile-email">📧 ${escHtmlA(u.email||'No email')}</div>
<div style="font-size:15px;color:var(--text2);margin-top:4px">
Joined ${(u.created_at||'').substring(0,10)} ·
Last login: ${u.last_login ? timeAgo(u.last_login) : 'Never'}
</div>
</div>
<div class="gm-profile-right">
<div class="gm-profile-tokens">${parseFloat(u.tokens||0)}</div>
<div class="gm-profile-tok-lbl">TOKENS</div>
<div class="gm-profile-badges">
<span class="badge b-${u.status}">${u.status}</span>
${verified ? '<span class="badge" style="background:rgba(0,230,118,.12);color:var(--green)">✓ verified</span>' : '<span class="badge b-pending">unverified</span>'}
${u.is_admin ? '<span class="badge" style="background:rgba(240,192,64,.15);color:var(--gold)">admin</span>' : ''}
</div>
</div>`;
// Update token display in tokens tab
document.getElementById('gm-token-display').textContent = parseFloat(u.tokens||0) + ' 🪙';
// Update suspend button
const sb = document.getElementById('gm-suspend-btn');
const db2 = document.querySelector('[onclick="gmDeleteAccount()"]');
const isMasterUser = parseInt(u.id) === MASTER_ADMIN_ID;
if (isMasterUser) {
sb.textContent = '🔒 Cannot Suspend Master Admin';
sb.className = 'btn';
sb.disabled = true;
sb.style.cssText = 'opacity:.4;cursor:not-allowed;background:rgba(255,68,68,.06);color:var(--red);border:1px solid rgba(255,68,68,.2)';
if (db2) { db2.disabled = true; db2.style.opacity = '.4'; db2.style.cursor = 'not-allowed'; db2.title = 'Cannot delete master admin account'; }
} else {
sb.disabled = false;
sb.style.cssText = '';
if (db2) { db2.disabled = false; db2.style.opacity = ''; db2.style.cursor = ''; db2.title = ''; }
if (u.status === 'active') {
sb.textContent = '🔒 SUSPEND ACCOUNT (Lock Login)';
sb.className = 'btn btn-red';
} else {
sb.textContent = '🔓 ACTIVATE ACCOUNT (Restore Login)';
sb.className = 'btn btn-green';
}
}
}
function renderGamerOverview(u, stats) {
const verified = parseInt(u.email_verified);
document.getElementById('gm-info-grid').innerHTML = [
{ label: 'User ID', val: '#' + u.id },
{ label: 'Username', val: u.username },
{ label: 'Alias', val: u.alias },
{ label: 'Email', val: u.email || '—' },
{ label: 'Status', val: `<span class="badge b-${u.status}">${u.status}</span>` },
{ label: 'Email Verified',val: verified ? '<span style="color:var(--green)">✓ Yes</span>' : '<span style="color:var(--yellow)">⚠ No</span>' },
{ label: 'Token Balance', val: `<span style="color:var(--gold);font-family:\'Exo 2\',sans-serif;font-weight:700">${parseFloat(u.tokens||0)} 🪙</span>` },
{ label: 'Total Spent', val: stats ? `<span style="color:var(--green);font-weight:700">$${parseFloat(stats.total_spent||0).toFixed(2)}</span>` : '—' },
{ label: 'Tokens Purchased', val: stats ? `<span style="color:var(--gold)">${stats.total_tokens_bought} 🪙</span>` : '—' },
{ label: 'Completed Purchases', val: stats ? `<span style="color:var(--green)">${stats.completed_purchases}</span>` : '—' },
{ label: 'Pending Payments', val: stats ? `<span style="color:var(--yellow)">${stats.pending_purchases}</span>` : '—' },
{ label: 'Failed Payments', val: stats ? `<span style="color:var(--red)">${stats.failed_purchases}</span>` : '—' },
{ label: 'Cashout Requests', val: stats ? stats.total_cashouts + ' total' : '—' },
{ label: 'Joined', val: (u.created_at||'').substring(0,16) },
{ label: 'Last Login', val: u.last_login ? u.last_login.substring(0,16) : 'Never' },
{ label: 'Admin Access', val: u.is_admin ? '<span style="color:var(--gold)">Yes</span>' : 'No' },
].map(item => `
<div class="gm-info-item">
<div class="gm-info-label">${item.label}</div>
<div class="gm-info-val">${item.val}</div>
</div>`).join('');
document.getElementById('gm-action-btns').innerHTML = `
<button class="btn btn-gold" style="flex:1;min-width:140px" onclick="switchGamerTab('tokens',document.querySelectorAll('.gm-tab')[1])">🪙 Manage Tokens</button>
<button class="btn btn-cyan" style="flex:1;min-width:140px" onclick="switchGamerTab('chat',document.querySelectorAll('.gm-tab')[4])">💬 Message Player</button>
<button class="btn" style="flex:1;min-width:140px;background:rgba(155,93,229,.12);color:#c77dff;border:1px solid rgba(155,93,229,.3)" onclick="switchGamerTab('edit',document.querySelectorAll('.gm-tab')[5])">✏️ Edit Account</button>
${!parseInt(u.email_verified) ? '<button class="btn btn-gold" style="flex:1;min-width:140px" onclick="gmResendVerify()">📧 Resend Verify Email</button>' : ''}`;
}
function populateGamerEdit(u) {
document.getElementById('gm-edit-username').value = u.username || '';
document.getElementById('gm-edit-alias').value = u.alias || '';
document.getElementById('gm-edit-email').value = u.email || '';
document.getElementById('gm-edit-password').value = '';
}
async function loadGamerPurchases(uid) {
const d = await apiFetch('user_purchases&user_id=' + uid);
const el = document.getElementById('gm-purchases-table');
if (!d.success || !d.purchases.length) { el.innerHTML = '<div class="gm-empty">No purchases found.</div>'; return; }
// Summary strip
const total = d.purchases.length;
const completed = d.purchases.filter(p=>p.status==='completed').length;
const pending = d.purchases.filter(p=>p.status==='pending').length;
const failed = d.purchases.filter(p=>p.status==='failed').length;
const revenue = d.purchases.filter(p=>p.status==='completed').reduce((s,p)=>s+(p.amount_cents/100),0);
const tokens = d.purchases.filter(p=>p.status==='completed').reduce((s,p)=>s+parseInt(p.tokens),0);
el.innerHTML = `
<div class="pm-summary">
<div class="pm-sum-item"><div class="pm-sum-val" style="color:var(--green)">$${revenue.toFixed(2)}</div><div class="pm-sum-lbl">Revenue</div></div>
<div class="pm-sum-item"><div class="pm-sum-val" style="color:var(--gold)">${tokens} 🪙</div><div class="pm-sum-lbl">Tokens Sold</div></div>
<div class="pm-sum-item"><div class="pm-sum-val" style="color:var(--green)">${completed}</div><div class="pm-sum-lbl">Completed</div></div>
<div class="pm-sum-item"><div class="pm-sum-val" style="color:var(--yellow)">${pending}</div><div class="pm-sum-lbl">Pending</div></div>
<div class="pm-sum-item"><div class="pm-sum-val" style="color:var(--red)">${failed}</div><div class="pm-sum-lbl">Failed</div></div>
</div>
${d.purchases.map(p => {
const isCustom = parseInt(p.is_custom);
const isPending = p.status === 'pending';
const isManual = p.payment_method !== 'card';
const billing = [p.billing_name, p.billing_address, [p.billing_city, p.billing_state, p.billing_zip].filter(Boolean).join(' ')].filter(Boolean).join(' · ');
return `
<div class="pm-card pm-${p.status}">
<div class="pm-card-top">
<div class="pm-card-left">
<div class="pm-order-id">#${p.id} ${isCustom ? '<span class="pm-custom-badge">CUSTOM</span>' : ''}</div>
<div class="pm-date">${(p.created_at||'').substring(0,16)}</div>
</div>
<div class="pm-card-right">
<div class="pm-amount">$${(p.amount_cents/100).toFixed(2)}</div>
<div class="pm-tokens">${p.tokens} 🪙</div>
</div>
</div>
<div class="pm-card-mid">
<div class="pm-detail-row">
<span class="badge ${methodClass(p.payment_method)}">${methodLabel(p.payment_method)}</span>
<span class="badge b-${p.status}">${p.status}</span>
${p.card_brand ? `<span class="pm-card-info">💳 ${p.card_brand} ····${p.card_last4||''}</span>` : ''}
</div>
<div class="pm-detail-row" style="margin-top:6px">
<span class="pm-lbl">Platform:</span> <span class="pm-val">${pname(p.platform_id)}</span>
&nbsp;·&nbsp;
<span class="pm-lbl">Alias:</span> <span class="pm-val" style="color:var(--cyan)">${escHtmlA(p.game_alias||'—')}</span>
</div>
${billing ? `<div class="pm-detail-row" style="margin-top:4px"><span class="pm-lbl">Billing:</span> <span class="pm-val">${escHtmlA(billing)}</span></div>` : ''}
${p.billing_email ? `<div class="pm-detail-row"><span class="pm-lbl">Email:</span> <span class="pm-val" style="color:var(--cyan)">${escHtmlA(p.billing_email)}</span></div>` : ''}
${p.square_payment_id ? `<div class="pm-detail-row" style="margin-top:4px"><span class="pm-lbl">Square ID:</span> <span class="pm-val" style="font-family:monospace;font-size:15px;color:var(--text2)">${p.square_payment_id}</span></div>` : ''}
${p.failure_reason ? `<div class="pm-failure">⚠️ ${escHtmlA(p.failure_reason)}</div>` : ''}
${p.admin_note ? `<div class="pm-admin-note">📝 Admin: ${escHtmlA(p.admin_note)}</div>` : ''}
${p.receipt_url ? `<div style="margin-top:6px"><a href="${p.receipt_url}" target="_blank" class="pm-receipt-link">🧾 View Receipt</a></div>` : ''}
</div>
${isPending && isManual ? `
<div class="pm-actions">
<input type="text" class="fi-sm" id="pnote-${p.id}" placeholder="Admin note..." style="flex:1">
<button class="btn btn-green btn-sm" onclick="resolveGamerPurchase(${p.id},'completed',${uid})">✓ Approve</button>
<button class="btn btn-red btn-sm" onclick="resolveGamerPurchase(${p.id},'failed',${uid})">✗ Reject</button>
</div>` : ''}
</div>`
}).join('')}`;
}
async function resolveGamerPurchase(id, status, uid) {
const note = (document.getElementById('pnote-'+id)?.value||'').trim();
const d = await apiFetch('resolve_purchase','POST',{id,status,note});
if (d.success) {
toast(status==='completed'?'✓ Tokens credited!':'✗ Purchase rejected', status==='completed'?'ok':'err');
loadGamerPurchases(uid);
// Refresh profile token display
const uData = await apiFetch('user_detail&user_id='+uid);
if (uData.success) { GM.current = uData.user; renderGamerProfile(uData.user); }
loadStats();
} else toast(d.error||'Error','err');
}
async function loadGamerCashouts(uid) {
const d = await apiFetch('user_cashouts&user_id=' + uid);
const el = document.getElementById('gm-cashouts-table');
if (!d.success || !d.cashouts.length) { el.innerHTML = '<div class="gm-empty">No cashouts found.</div>'; return; }
el.innerHTML = `<table>
<thead><tr><th>#</th><th>Platform</th><th>Alias</th><th>Tokens</th><th>Status</th><th>Date</th></tr></thead>
<tbody>${d.cashouts.map(c => `<tr>
<td style="color:var(--text2)">#${c.id}</td>
<td>${pname(c.platform_id)}</td>
<td style="color:var(--cyan)">${escHtmlA(c.alias)}</td>
<td><strong style="color:var(--gold)">${c.tokens} 🪙</strong></td>
<td><span class="badge b-${c.status}">${c.status}</span></td>
<td style="color:var(--text2);font-size:15px">${(c.created_at||'').substring(0,16)}</td>
</tr>`).join('')}</tbody>
</table>`;
}
async function loadGamerChat(uid, scrollToBottom) {
const d = await apiFetch(`chat_thread&user_id=${uid}&since=${GM.chatLastId}`);
if (!d.success) return;
const container = document.getElementById('gm-chat-messages');
if (d.messages.length === 0 && GM.chatLastId === 0) {
container.innerHTML = '<div style="text-align:center;padding:24px;color:var(--text2);font-size:15px">No messages yet with this player.</div>';
return;
}
if (d.messages.length > 0) {
if (GM.chatLastId === 0) container.innerHTML = '';
let lastDate = '';
d.messages.forEach(msg => {
const msgDate = msg.created_at.substring(0,10);
if (msgDate !== lastDate) {
lastDate = msgDate;
const div = document.createElement('div');
div.className = 'admin-date-div';
div.textContent = formatAdminDateLabel(msgDate);
container.appendChild(div);
}
container.appendChild(buildAdminBubble(msg));
GM.chatLastId = Math.max(GM.chatLastId, parseInt(msg.id));
});
if (scrollToBottom || (container.scrollHeight - container.scrollTop - container.clientHeight < 120))
container.scrollTop = container.scrollHeight;
}
}
async function gmClearThread() {
if (!GM.current) return;
if (!confirm('Clear all chat messages with ' + GM.current.username + '?\n\nThis cannot be undone.')) return;
const d = await apiFetch('chat_clear_thread', 'POST', { user_id: GM.current.id });
if (d.success) {
document.getElementById('gm-chat-messages').innerHTML =
'<div style="color:var(--text2);font-size:15px;text-align:center;padding:24px">Conversation cleared.</div>';
GM.chatLastId = 0;
toast('Conversation cleared', 'ok');
loadChatBadge();
} else toast(d.error || 'Error', 'err');
}
async function gmSendMsg() {
const input = document.getElementById('gm-chat-input');
const msg = input.value.trim();
if (!msg || !GM.current) return;
input.value = '';
const container = document.getElementById('gm-chat-messages');
const temp = buildAdminBubble({ id:'tmp', sender:'admin', message:msg, created_at:new Date().toISOString().replace('T',' ').substring(0,19) });
temp.style.opacity = '0.6';
container.appendChild(temp);
container.scrollTop = container.scrollHeight;
const d = await apiFetch('chat_admin_send','POST',{user_id:GM.current.id, message:msg});
if (d.success) { temp.style.opacity='1'; GM.chatLastId = Math.max(GM.chatLastId, d.id); }
else { temp.remove(); toast('Send failed','err'); }
}
async function gmAdjustTokens(sign) {
const amt = parseFloat(document.getElementById('gm-tok-amount').value);
const reason = document.getElementById('gm-tok-reason').value.trim();
const msgEl = document.getElementById('gm-tok-msg');
if (isNaN(amt) || amt <= 0) { showAdminAlert(msgEl,'Enter a valid positive amount.','error'); return; }
const d = await apiFetch('adjust_tokens','POST',{user_id:GM.current.id, amount: sign * amt, reason});
if (d.success) {
GM.current.tokens = d.new_balance;
document.getElementById('gm-token-display').textContent = d.new_balance + ' 🪙';
document.getElementById('gm-profile-tokens') && (document.querySelector('.gm-profile-tokens') ? document.querySelector('.gm-profile-tokens').textContent = d.new_balance : null);
renderGamerProfile(GM.current);
showAdminAlert(msgEl, `Balance updated: ${d.new_balance} tokens`, 'success');
document.getElementById('gm-tok-amount').value = '';
loadUsers();
} else showAdminAlert(msgEl, d.error||'Error','error');
}
async function gmSetTokens() {
const newBal = parseFloat(document.getElementById('gm-tok-set').value);
const msgEl = document.getElementById('gm-tok-msg');
if (isNaN(newBal) || newBal < 0) { showAdminAlert(msgEl,'Enter a valid balance.','error'); return; }
const d = await apiFetch('set_tokens','POST',{user_id:GM.current.id, balance:newBal});
if (d.success) {
GM.current.tokens = d.new_balance;
document.getElementById('gm-token-display').textContent = d.new_balance + ' 🪙';
renderGamerProfile(GM.current);
showAdminAlert(msgEl, `Balance set to ${d.new_balance} tokens`, 'success');
document.getElementById('gm-tok-set').value = '';
loadUsers();
} else showAdminAlert(msgEl, d.error||'Error','error');
}
async function gmSaveEdit() {
const al = document.getElementById('gm-edit-alert');
const username = document.getElementById('gm-edit-username').value.trim();
const alias = document.getElementById('gm-edit-alias').value.trim();
const email = document.getElementById('gm-edit-email').value.trim();
const password = document.getElementById('gm-edit-password').value;
if (!username||!alias) { showAdminAlert(al,'Username and alias are required.','error'); return; }
const d = await apiFetch('edit_user','POST',{user_id:GM.current.id, username, alias, email, password});
if (d.success) {
GM.current = {...GM.current, username, alias, email};
renderGamerProfile(GM.current);
showAdminAlert(al,'Player info updated!','success');
loadUsers();
toast('Saved!','ok');
} else showAdminAlert(al, d.error||'Error saving.','error');
}
async function gmToggleSuspend() {
if (GM.current && parseInt(GM.current.id) === MASTER_ADMIN_ID) { toast('Cannot suspend master admin','err'); return; }
const isSuspended = GM.current.status === 'suspended';
const action = isSuspended ? 'activate' : 'suspend';
if (!isSuspended && !confirm(`Suspend ${GM.current.username}? They will be locked out immediately.`)) return;
const d = await apiFetch('toggle_user','POST',{user_id:GM.current.id});
if (d.success) {
GM.current.status = isSuspended ? 'active' : 'suspended';
renderGamerProfile(GM.current);
renderGamerOverview(GM.current, null);
toast(`Account ${isSuspended?'activated':'suspended'}`, 'ok');
loadUsers();
} else toast('Error','err');
}
async function gmSendPasswordReset() {
if (!GM.current.email) { toast('No email on file for this player','err'); return; }
const d = await apiFetch('send_password_reset','POST',{user_id:GM.current.id});
if (d.success) toast('Password reset email sent!','ok');
else toast(d.error||'Error','err');
}
async function gmResendVerify() {
if (!GM.current.email) { toast('No email on file','err'); return; }
const d = await apiFetch('resend_verification','POST',{user_id:GM.current.id});
if (d.success) toast('Verification email sent!','ok');
else toast(d.error||'Error','err');
}
async function gmToggleAdmin() {
const u = GM.current;
if (!u) return;
const making = !parseInt(u.is_admin);
const msg = making
? 'Grant ADMIN access to ' + u.username + '? They will have full admin panel access.'
: 'Remove admin access from ' + u.username + '?';
if (!confirm(msg)) return;
const d = await apiFetch('toggle_admin','POST',{user_id: u.id});
if (d.success) {
GM.current.is_admin = d.is_admin;
const msg = d.is_admin ? u.username+' is now an ADMIN' : 'Admin removed from '+u.username;
toast(msg + ' — they must log out and back in for changes to take effect', 'ok');
renderGamerProfile(GM.current);
renderGamerOverview(GM.current, null);
updateAdminToggleBtn();
loadUsers();
} else {
toast(d.error || 'Error', 'err');
}
}
function updateAdminToggleBtn() {
const u = GM.current;
const btn = document.getElementById('gm-admin-toggle-btn');
if (!btn || !u) return;
// Only master admin can see/use this button
if (!IS_MASTER_ADMIN) { btn.style.display = 'none'; return; }
btn.style.display = 'block';
const isMaster = u.id == MASTER_ADMIN_ID;
if (isMaster) {
btn.textContent = '🔒 Master Admin (Locked)';
btn.disabled = true;
btn.className = 'btn';
btn.style.cssText = 'background:rgba(240,192,64,.08);color:var(--gold);border:1px solid rgba(240,192,64,.2);cursor:not-allowed;opacity:.7';
} else if (parseInt(u.is_admin)) {
btn.textContent = '⬇️ Remove Admin Access';
btn.disabled = false;
btn.className = 'btn btn-red';
btn.style.cssText = '';
} else {
btn.textContent = '⬆️ Grant Admin Access';
btn.disabled = false;
btn.className = 'btn btn-gold';
btn.style.cssText = '';
}
}
async function gmDeleteAccount() {
if (GM.current && parseInt(GM.current.id) === MASTER_ADMIN_ID) { toast('Cannot delete master admin account','err'); return; }
if (!confirm(`⚠️ PERMANENTLY delete ${GM.current.username}?\n\nThis removes all their data and cannot be undone.`)) return;
if (!confirm(`Are you absolutely sure? Type OK to confirm deletion of ${GM.current.username}.`)) return;
const d = await apiFetch('delete_user','POST',{user_id:GM.current.id});
if (d.success) {
toast(`${GM.current.username} deleted`,'ok');
clearInterval(GM.chatPoll);
showGamerList();
loadUsers();
loadStats();
} else toast(d.error||'Error','err');
}
function showGamerList() {
clearInterval(GM.chatPoll);
GM.current = null;
document.getElementById('gm-list-view').style.display = 'block';
document.getElementById('gm-detail-view').style.display = 'none';
}
function switchGamerTab(name, btn) {
document.querySelectorAll('.gm-tab').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.gm-tab-panel').forEach(p => p.classList.remove('active'));
if (btn) btn.classList.add('active');
document.getElementById('gtab-'+name)?.classList.add('active');
if (name === 'chat' && GM.current) loadGamerChat(GM.current.id, true);
if (name === 'billing' && GM.current) loadGamerBilling(GM.current.id);
if (name === 'aliases' && GM.current) gmLoadAliases(GM.current.id);
if (name === 'payout' && GM.current) gmLoadPayout(GM.current.id);
if (name === 'platforms' && GM.current) gmLoadPlatformAccounts(GM.current.id);
if (name === 'platforms' && GM.current) gmLoadPlatformAccounts(GM.current.id);
}
function showAdminAlert(el, msg, type) {
el.textContent = msg;
el.className = `alert show alert-${type}`;
setTimeout(() => el.className = 'alert', 4000);
}
function timeAgo(dtStr) {
const d = new Date(dtStr.replace(' ','T'));
const diff = (Date.now() - d) / 1000;
if (diff < 60) return 'Just now';
if (diff < 3600) return Math.floor(diff/60) + 'm ago';
if (diff < 86400) return Math.floor(diff/3600) + 'h ago';
if (diff < 604800) return Math.floor(diff/86400) + 'd ago';
return d.toLocaleDateString('en-US',{month:'short',day:'numeric'});
}
// Alias old functions so nothing breaks
function filterUsers(q) { filterGamers(q); }
function adjustTokens(uid) {}
function toggleUser(uid) {}
// ─── FULL HISTORY ──────────────────────────────────────────
var _histPage = 1;
async function loadHistory(page) {
if (page) _histPage = page;
const el = document.getElementById('history-table');
const pgEl = document.getElementById('hist-pagination');
const ctEl = document.getElementById('hist-count');
const stEl = document.getElementById('hist-stats');
el.innerHTML = '<div style="padding:24px;text-align:center;color:var(--text2)">Loading audit log...</div>';
if (pgEl) pgEl.innerHTML = '';
const category = document.getElementById('hist-category')?.value || '';
const severity = document.getElementById('hist-severity')?.value || '';
const search = document.getElementById('hist-search')?.value.trim() || '';
const date = document.getElementById('hist-date')?.value || '';
const params = new URLSearchParams({
action: 'activity_log_v2',
page: _histPage,
limit: 20,
category, severity, search, date
});
const d = await fetch('/api/admin.php?' + params.toString()).then(r=>r.json());
if (!d.success) { el.innerHTML = '<div class="empty">Failed to load audit log.</div>'; return; }
const events = d.events || [];
const total = d.total || 0;
const pages = Math.ceil(total / 20);
if (ctEl) ctEl.textContent = total + ' total events';
// Stats bar
if (stEl && d.stats) {
const s = d.stats;
stEl.innerHTML = [
{ label:'Total Events', val: total, color:'var(--text)' },
{ label:'Critical', val: s.critical||0, color:'var(--red)' },
{ label:'Warnings', val: s.warning||0, color:'var(--gold)' },
{ label:'Unique IPs', val: s.unique_ips||0, color:'var(--cyan)' },
].map(x =>
'<div class="card" style="text-align:center;padding:10px 8px">'+
'<div style="font-family:\'Exo 2\',sans-serif;font-weight:900;font-size:20px;color:'+x.color+'">'+x.val+'</div>'+
'<div style="font-size:14px;color:var(--text2);text-transform:uppercase;letter-spacing:.5px">'+x.label+'</div></div>'
).join('');
}
if (!events.length) {
el.innerHTML = '<div class="empty" style="padding:24px;text-align:center">No events match these filters.</div>';
return;
}
const SEV_COLORS = { critical:'var(--red)', warning:'var(--gold)', info:'var(--text2)' };
const CAT_ICONS = { auth:'🔐', player:'👤', admin:'🛡️', security:'⚠️', general:'📋' };
let lastDate = '';
let rows = '';
for (const e of events) {
const dt = (e.created_at||'').substring(0,10);
const time = (e.created_at||'').substring(11,19);
if (dt !== lastDate) {
lastDate = dt;
const isToday = dt === new Date().toISOString().substring(0,10);
const label = isToday ? 'Today' : new Date(dt+'T12:00:00').toLocaleDateString('en-US',{weekday:'short',month:'short',day:'numeric',year:'numeric'});
rows += '<div style="padding:10px 0 6px;font-size:15px;font-weight:700;color:var(--text2);letter-spacing:1px;text-transform:uppercase;border-bottom:1px solid var(--border);margin-bottom:6px">'+label+'</div>';
}
const sevColor = SEV_COLORS[e.severity] || 'var(--text2)';
const catIcon = CAT_ICONS[e.category] || '📋';
const who = e.alias || e.username || '—';
const adminBy = e.admin_username ? ' <span style="font-size:14px;color:var(--gold)">by '+escHtmlA(e.admin_username)+'</span>' : '';
const action = e.action.replace(/_/g,' ');
// Build detail row
let details = '';
if (e.detail) details += '<div style="font-size:15px;color:var(--text2);margin-top:2px">'+escHtmlA(e.detail)+'</div>';
if (e.old_value && e.new_value)
details += '<div style="font-size:15px;margin-top:2px"><span style="color:var(--red)">'+escHtmlA(e.old_value)+'</span> → <span style="color:var(--green)">'+escHtmlA(e.new_value)+'</span></div>';
if (e.ip) details += '<div style="font-size:14px;color:var(--text2);margin-top:2px">IP: '+escHtmlA(e.ip)+' | Session: '+escHtmlA(e.session_id||'—')+'</div>';
if (e.page) details += '<div style="font-size:14px;color:var(--text2);">Page: '+escHtmlA(e.page)+'</div>';
if (e.user_agent)details += '<div style="font-size:15px;color:var(--text2);word-break:break-all;opacity:.7">UA: '+escHtmlA(e.user_agent.substring(0,120))+'</div>';
rows += '<div style="border-left:3px solid '+sevColor+';padding:8px 12px;margin-bottom:4px;border-radius:0 6px 6px 0;background:rgba(255,255,255,.02)">' +
'<div style="display:flex;align-items:flex-start;gap:10px">' +
'<span style="font-size:14px;flex-shrink:0;margin-top:1px">'+catIcon+'</span>' +
'<div style="flex:1;min-width:0">' +
'<span style="font-weight:700;font-size:15px;color:'+sevColor+'">'+action+'</span>'+adminBy +
details + '</div>' +
'<div style="text-align:right;flex-shrink:0;min-width:90px">' +
'<div style="font-size:14px;font-weight:700">'+escHtmlA(who)+'</div>' +
'<div style="font-size:14px;color:var(--text2)">'+time+'</div>' +
'<div style="font-size:15px;color:'+sevColor+';text-transform:uppercase;letter-spacing:.5px">'+e.severity+'</div>' +
'</div></div></div>';
}
el.innerHTML = '<div style="padding:4px 0">'+rows+'</div>';
// Pagination
if (pgEl && pages > 1) {
let pg = '';
if (_histPage > 1) pg += '<button onclick="loadHistory('+(_histPage-1)+')" class="btn btn-outline" style="padding:6px 14px;font-size:14px">← Prev</button>';
const start2 = Math.max(1, _histPage-2);
const end2 = Math.min(pages, _histPage+2);
for (let p = start2; p <= end2; p++) {
pg += '<button onclick="loadHistory('+p+')" class="btn '+(p===_histPage?'btn-gold':'btn-outline')+'" style="padding:6px 12px;font-size:14px">'+p+'</button>';
}
if (_histPage < pages) pg += '<button onclick="loadHistory('+(_histPage+1)+')" class="btn btn-outline" style="padding:6px 14px;font-size:14px">Next →</button>';
pg += '<span style="font-size:14px;color:var(--text2);align-self:center">Page '+_histPage+' of '+pages+'</span>';
pgEl.innerHTML = pg;
}
}
async function exportHistory() {
const category = document.getElementById('hist-category')?.value || '';
const severity = document.getElementById('hist-severity')?.value || '';
const search = document.getElementById('hist-search')?.value.trim() || '';
const date = document.getElementById('hist-date')?.value || '';
window.open('/api/admin.php?action=activity_log_csv&category='+encodeURIComponent(category)+'&severity='+encodeURIComponent(severity)+'&search='+encodeURIComponent(search)+'&date='+encodeURIComponent(date));
}
// ─── TOAST ─────────────────────────────────────────────────
function toast(msg,type='ok'){const el=document.getElementById('toast');el.textContent=msg;el.className='show '+type;setTimeout(()=>el.className='',3000);}
// ─── ADMIN GAMER ALIASES ──────────────────────────────────
async function gmLoadPayout(uid) {
const el = document.getElementById('gm-payout-list');
const d = await apiFetch('payout_methods_get&user_id=' + uid);
if (!d.success || !d.methods.length) {
el.innerHTML = '<div class="gm-empty">No payout methods saved by this player.</div>';
return;
}
const ICONS = { venmo:'💙', cashapp:'💚', zelle:'💜', chime:'🟢', bank:'🏦', other:'💰' };
const LABELS = { venmo:'Venmo', cashapp:'Cash App', zelle:'Zelle', chime:'Chime', bank:'Bank Transfer', other:'Other' };
el.innerHTML = d.methods.map(m => `
<div style="display:flex;align-items:center;gap:12px;padding:12px;background:${parseInt(m.is_default)?'rgba(0,229,255,.07)':'var(--bg2)'};border:1px solid ${parseInt(m.is_default)?'rgba(0,229,255,.25)':'var(--border)'};border-radius:8px;margin-bottom:8px">
<span style="font-size:24px">${ICONS[m.method_type]||'💰'}</span>
<div style="flex:1">
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px">${escHtmlA(m.label)}
${parseInt(m.is_default)?'<span style="font-size:14px;color:var(--cyan);border:1px solid rgba(0,229,255,.2);border-radius:4px;padding:2px 6px;margin-left:6px">DEFAULT</span>':''}
</div>
<div style="font-size:14px;color:var(--text2)">${LABELS[m.method_type]||m.method_type} · ${escHtmlA(m.account_handle)}</div>
</div>
</div>`).join('');
}
async function gmLoadPlatformAccounts(uid) {
const el = document.getElementById('gm-platforms-list');
const d = await apiFetch('platform_accounts_user&user_id=' + uid);
if (!d.success || !d.accounts.length) {
el.innerHTML = '<div class="gm-empty">No platform account requests for this player.</div>';
return;
}
const STATUS = { pending:'⏳ Pending', approved:'✅ Active', denied:'❌ Denied', deleted:'🗑 Deleted' };
const SCLS = { pending:'ac-pending', approved:'ac-completed', denied:'ac-failed', deleted:'ac-failed' };
el.innerHTML = d.accounts.map(a => `
<div style="border:1px solid var(--border);border-radius:8px;padding:12px;margin-bottom:8px">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:${a.status==='approved'?'10':'0'}px">
<div style="font-size:20px">${a.color?`<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:${a.color}"></span>`:'🎮'}</div>
<div style="flex:1">
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px">${escHtmlA(a.display_name||a.platform_slug)}</div>
<span class="activity-status ${SCLS[a.status]||'ac-pending'}">${STATUS[a.status]||a.status}</span>
</div>
${a.status==='approved'?`<button onclick="openPaUpdateModal(${a.id},'${escAttr(GM.current?.alias||'')}','${escAttr(a.display_name||'')}','${escAttr(a.provided_username||'')}','${escAttr(a.provided_password||'')}')" style="background:rgba(0,229,255,.08);border:1px solid rgba(0,229,255,.18);color:var(--cyan);border-radius:6px;padding:5px 10px;font-size:15px;font-weight:700;cursor:pointer">✏️ Update</button>`:
a.status==='pending'?`<button onclick="openPaModal(${a.id},'${escAttr(GM.current?.alias||'')}','${escAttr(a.display_name||'')}')" class="btn btn-green" style="padding:5px 12px;font-size:15px">✅ Approve</button>`:''}
</div>
${a.status==='approved'?`<div style="background:rgba(0,229,255,.05);border:1px solid rgba(0,229,255,.12);border-radius:6px;padding:8px 10px;font-size:14px">
<div><span style="color:var(--text2)">Username:</span> <strong>${escHtmlA(a.provided_username||'—')}</strong></div>
<div><span style="color:var(--text2)">Password:</span> <strong style="color:var(--gold)">${escHtmlA(a.provided_password||'—')}</strong></div>
${a.player_url?`<a href="${escHtmlA(a.player_url)}" target="_blank" style="color:var(--cyan);font-size:15px">↗ Open platform</a>`:''}
</div>`:''}
</div>`).join('');
}
async function gmLoadAliases(uid) {
const el = document.getElementById('gm-aliases-list');
const [aliasRes, platRes] = await Promise.all([
apiFetch('game_aliases_get&user_id=' + uid),
fetch('/api/platforms.php?action=list').then(r=>r.json())
]);
const aliases = (aliasRes.success ? aliasRes.aliases : {}) || {};
const platforms = platRes.success ? platRes.platforms : [];
if (!platforms.length) { el.innerHTML = '<div class="gm-empty">No games configured.</div>'; return; }
el.innerHTML = platforms.map(p => `
<div style="display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid var(--border)">
<div style="width:10px;height:10px;border-radius:50%;background:${p.color};box-shadow:0 0 8px ${p.color};flex-shrink:0"></div>
<div style="flex:1;min-width:0">
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;color:var(--text);margin-bottom:5px">${escHtmlA(p.name)}</div>
<input class="fi-sm" id="gm-alias-${p.id}" type="text" maxlength="100"
value="${escHtmlA(aliases[p.id]||'')}"
placeholder="No alias set..."
style="width:100%;padding:8px 12px;font-size:15px">
</div>
</div>`).join('') + '<div style="height:1px"></div>';
}
async function gmSaveAllAliases() {
if (!GM.current) return;
const al = document.getElementById('gm-aliases-alert');
const aliases = {};
document.querySelectorAll('[id^="gm-alias-"]').forEach(el => {
const slug = el.id.replace('gm-alias-', '');
aliases[slug] = el.value.trim();
});
const d = await apiFetch('game_aliases_save_all', 'POST', { user_id: GM.current.id, aliases });
if (d.success) { showAdminAlert(al, 'Aliases saved!', 'success'); toast('Aliases saved', 'ok'); }
else showAdminAlert(al, d.error || 'Save failed', 'error');
}
async function loadGamerBilling(uid) {
const d = await apiFetch('billing_get&user_id=' + uid);
const noCard = document.getElementById('gm-billing-no-card');
const cardDiv = document.getElementById('gm-billing-card-display');
const cardEl = document.getElementById('gm-billing-card');
if (d.success && d.billing) {
const b = d.billing;
// Fill form
['first','last','email','address','city','state','zip'].forEach(f => {
const el = document.getElementById('gm-b-'+f);
if (el) el.value = b[f==='first'?'first_name':f==='last'?'last_name':f] || '';
});
// Show card info
if (b.card_last4) {
cardEl.innerHTML = `
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<span style="font-family:'Exo 2',sans-serif;font-weight:900;font-size:16px;color:#fff;letter-spacing:2px">${escHtmlA(b.card_brand||'CARD')}</span>
<span style="font-size:20px">💳</span>
</div>
<div style="font-family:'Courier New',monospace;font-size:16px;color:#fff;letter-spacing:4px;margin-bottom:16px">···· ···· ···· ${escHtmlA(b.card_last4)}</div>
<div style="display:flex;justify-content:space-between;color:#fff">
<div><div style="font-size:15px;color:rgba(255,255,255,.5);margin-bottom:2px">CARD HOLDER</div><div style="font-weight:700">${escHtmlA([b.first_name,b.last_name].filter(Boolean).join(' '))||'—'}</div></div>
<div style="text-align:right"><div style="font-size:15px;color:rgba(255,255,255,.5);margin-bottom:2px">EXPIRES</div><div style="font-weight:700">${b.card_exp_month&&b.card_exp_year?b.card_exp_month+'/'+b.card_exp_year.slice(-2):'—'}</div></div>
</div>`;
cardDiv.style.display = 'block';
noCard.style.display = 'none';
} else {
cardDiv.style.display = 'none';
noCard.style.display = 'block';
}
} else {
cardDiv.style.display = 'none';
noCard.style.display = 'block';
}
}
async function gmSaveBilling() {
if (!GM.current) return;
const al = document.getElementById('gm-billing-alert');
const data = {
user_id: GM.current.id,
first_name: document.getElementById('gm-b-first')?.value.trim() || '',
last_name: document.getElementById('gm-b-last')?.value.trim() || '',
email: document.getElementById('gm-b-email')?.value.trim() || '',
address: document.getElementById('gm-b-address')?.value.trim() || '',
city: document.getElementById('gm-b-city')?.value.trim() || '',
state: (document.getElementById('gm-b-state')?.value.trim() || '').toUpperCase(),
zip: document.getElementById('gm-b-zip')?.value.trim() || '',
};
const d = await apiFetch('billing_save','POST', data);
if (d.success) { toast('Billing info saved','ok'); showAdminAlert(al,'Billing saved!','success'); }
else toast(d.error||'Error','err');
}
async function gmClearCard() {
if (!GM.current || !confirm('Remove saved card info for this player?')) return;
const d = await apiFetch('billing_clear_card','POST',{user_id:GM.current.id});
if (d.success) { toast('Card removed','ok'); loadGamerBilling(GM.current.id); }
else toast('Error','err');
}
async function gmClearAllBilling() {
if (!GM.current || !confirm('Clear ALL billing info for this player? Cannot be undone.')) return;
const d = await apiFetch('billing_clear_all','POST',{user_id:GM.current.id});
if (d.success) { toast('Billing cleared','ok'); loadGamerBilling(GM.current.id); }
else toast('Error','err');
}
// ─── PAYMENT SETTINGS ─────────────────────────────────────
const PMT_ICONS = { venmo:'💙', chime:'🟢', cashapp:'💚', zelle:'💜' };
async function loadPaymentSettings() {
const d = await apiFetch('payment_settings_list');
const el = document.getElementById('payment-methods-list');
if (!d.success) { el.innerHTML = '<div class="empty">Failed to load.</div>'; return; }
// Sort: card first, then by sort_order
const methods = [...d.methods].sort((a,b) => a.method_key==='card'?-1:b.method_key==='card'?1:a.sort_order-b.sort_order);
el.innerHTML = methods.map(m => {
const isCard = m.method_key === 'card';
const enabled = parseInt(m.is_enabled);
const icon = PMT_ICONS[m.method_key] || '💰';
const bg = enabled ? 'var(--green)' : 'rgba(255,255,255,.15)';
const knobL = enabled ? '22px' : '2px';
const clr = enabled ? 'var(--green)' : 'var(--red)';
const fields = isCard
? '<div style="padding:10px 14px;background:rgba(0,229,255,.05);border-radius:8px;font-size:14px;color:var(--text2)">'+
'Card payments run through <strong style="color:var(--cyan)">Square</strong>. When enabled, players can pay by credit or debit card. Your Square account must be active and connected.'+
'<a href="https://squareup.com/dashboard" target="_blank" style="color:var(--cyan);display:block;margin-top:6px">Open Square Dashboard →</a></div>'
: '<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px">'+
'<div><label class="gm-edit-label">Display Name</label>'+
'<input class="fi-sm" id="pmt-label-'+m.id+'" type="text" value="'+escHtmlA(m.label)+'" style="width:100%;padding:9px 12px"></div>'+
'<div><label class="gm-edit-label">Handle / Address</label>'+
'<input class="fi-sm" id="pmt-handle-'+m.id+'" type="text" value="'+escHtmlA(m.handle||'')+'" placeholder="@YourHandle" style="width:100%;padding:9px 12px"></div></div>'+
'<div style="margin-bottom:10px"><label class="gm-edit-label">Instructions (optional)</label>'+
'<input class="fi-sm" id="pmt-instructions-'+m.id+'" type="text" value="'+escHtmlA(m.instructions||'')+'" placeholder="Shown to player at checkout" style="width:100%;padding:9px 12px"></div>'+
'<button class="btn btn-gold" style="width:100%" onclick="savePaymentMethod('+m.id+')">Save</button>';
return '<div class="card" style="margin-bottom:12px;'+(isCard?'border-color:rgba(0,229,255,.25)':'')+'" id="pmt-card-'+m.id+'">'+
'<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px">'+
'<div style="font-size:26px">'+icon+'</div>'+
'<div style="flex:1"><div style="font-family:\'Exo 2\',sans-serif;font-weight:700;font-size:15px">'+escHtmlA(m.label)+'</div>'+
'<div style="font-size:15px;color:var(--text2);margin-top:1px">'+(isCard?'Square Card Processing':'Manual payment'+(m.handle?' · '+escHtmlA(m.handle):''))+'</div></div>'+
'<label style="display:flex;align-items:center;gap:8px;cursor:pointer">'+
'<span style="font-size:14px;color:'+clr+'" id="pmt-status-'+m.id+'">'+(enabled?'Enabled':'Disabled')+'</span>'+
'<div onclick="togglePaymentMethod('+m.id+','+m.is_enabled+')" style="width:44px;height:24px;border-radius:12px;background:'+bg+';position:relative;cursor:pointer;transition:background .2s;flex-shrink:0" id="pmt-toggle-'+m.id+'">'+
'<div style="position:absolute;top:2px;left:'+knobL+';width:20px;height:20px;border-radius:50%;background:#fff;transition:left .2s" id="pmt-knob-'+m.id+'"></div></div>'+
'</label></div>'+
fields+'</div>';
}).join('');
window._pmtData = d.methods;
}
async function togglePaymentMethod(id, currentEnabled) {
const newEnabled = parseInt(currentEnabled) ? 0 : 1;
const d = await apiFetch('payment_settings_update','POST',{
id, is_enabled: newEnabled,
label: document.getElementById('pmt-label-'+id)?.value || '',
handle: document.getElementById('pmt-handle-'+id)?.value || '',
instructions: document.getElementById('pmt-instructions-'+id)?.value || '',
sort_order: 0
});
if (d.success) {
// Update toggle UI
const toggle = document.getElementById('pmt-toggle-'+id);
const knob = document.getElementById('pmt-knob-'+id);
const status = document.getElementById('pmt-status-'+id);
if (toggle) toggle.style.background = newEnabled ? 'var(--green)' : 'rgba(255,255,255,.15)';
if (knob) knob.style.left = newEnabled ? '22px' : '2px';
if (status) { status.textContent = newEnabled ? 'Enabled' : 'Disabled'; status.style.color = newEnabled ? 'var(--green)' : 'var(--red)'; }
// Update onclick for next toggle
const toggleEl = document.getElementById('pmt-toggle-'+id)?.parentElement;
if (toggleEl) toggleEl.setAttribute('onclick', `togglePaymentMethod(${id},${newEnabled})`);
toast(newEnabled ? 'Payment method enabled' : 'Payment method disabled', 'ok');
} else toast(d.error||'Error','err');
}
async function savePaymentMethod(id) {
const d = await apiFetch('payment_settings_update','POST',{
id,
label: document.getElementById('pmt-label-'+id)?.value.trim() || '',
handle: document.getElementById('pmt-handle-'+id)?.value.trim() || '',
instructions: document.getElementById('pmt-instructions-'+id)?.value.trim() || '',
is_enabled: window._pmtData?.find(m=>m.id==id)?.is_enabled ?? 1,
sort_order: window._pmtData?.find(m=>m.id==id)?.sort_order ?? 0,
});
if (d.success) toast('Saved!','ok');
else toast(d.error||'Save failed','err');
}
// PAYOUT SETTINGS
async function loadPayoutSettings() {
const el = document.getElementById('payout-settings-list');
if (!el) return;
const d = await fetch('/api/payout_process.php?action=list_settings').then(r=>r.json());
if (!d.success || !d.settings.length) { el.innerHTML='<div class="empty">No payout settings.</div>'; return; }
window._payoutSettings = d.settings;
const icons = {venmo:'Venmo',cashapp:'Cash App',zelle:'Zelle',chime:'Chime',square_gift:'Square Gift Card'};
el.innerHTML = d.settings.map(s => {
const isGC = s.method_type === 'square_gift_card';
const enabled = parseInt(s.is_enabled);
return '<div class="card" style="margin-bottom:12px;' + (isGC?'border-color:rgba(0,229,255,.3)':'') + '">' +
'<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">' +
'<div style="flex:1"><div style="font-family:\'Exo 2\',sans-serif;font-weight:700;font-size:14px">' + escHtmlA(s.label) + '</div>' +
'<div style="font-size:15px;color:' + (isGC?'var(--cyan)':'var(--text2)') + '">' + (isGC?'Real-time via Square':'Manual — send outside app') + '</div></div>' +
'<label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:14px;font-weight:700">' +
'<input type="checkbox" ' + (enabled?'checked':'') + ' onchange="togglePayoutSetting(' + s.id + ',this.checked)" style="width:16px;height:16px;accent-color:var(--green)"> ' +
(enabled?'<span style="color:var(--green)">Enabled</span>':'<span style="color:var(--text2)">Disabled</span>') + '</label></div>' +
(isGC ? '<div style="font-size:14px;color:var(--text2);padding:10px;background:rgba(0,229,255,.05);border-radius:8px">Creates a Square digital gift card loaded with the cashout amount. Player redeems anywhere Square is accepted. No handle needed.</div>' :
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px">' +
'<div><label class="gm-edit-label">Your Handle</label><input class="fi-sm" type="text" id="ps-handle-' + s.id + '" value="' + escHtmlA(s.handle||'') + '" placeholder="@handle" style="width:100%;padding:8px 10px"></div>' +
'<div><label class="gm-edit-label">Sort</label><input class="fi-sm" type="number" id="ps-sort-' + s.id + '" value="' + s.sort_order + '" style="width:100%;padding:8px 10px"></div></div>' +
'<div style="margin-bottom:10px"><label class="gm-edit-label">Instructions</label><input class="fi-sm" type="text" id="ps-instr-' + s.id + '" value="' + escHtmlA(s.instructions||'') + '" placeholder="Steps to send" style="width:100%;padding:8px 10px"></div>' +
'<button class="btn btn-gold" onclick="savePayoutSetting(' + s.id + ')" style="font-size:14px;padding:8px 16px">Save</button>') +
'</div>';
}).join('');
}
async function togglePayoutSetting(id, enabled) {
await fetch('/api/payout_process.php?action=update_setting',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,is_enabled:enabled?1:0})}).then(r=>r.json());
}
async function savePayoutSetting(id) {
const d = await fetch('/api/payout_process.php?action=update_setting',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({id,handle:document.getElementById('ps-handle-'+id)?.value.trim(),instructions:document.getElementById('ps-instr-'+id)?.value.trim(),sort_order:parseInt(document.getElementById('ps-sort-'+id)?.value)||0,is_enabled:1})}).then(r=>r.json());
if (d.success) toast('Saved','ok'); else toast('Error','err');
}
// PROCESS PAYOUT MODAL
let PPM = {cashoutId:null,cashout:null,settings:[]};
async function openPayoutModal(cashoutId) {
PPM.cashoutId=cashoutId;
const modal=document.getElementById('process-payout-modal');
modal.style.display='flex';
document.getElementById('ppm-alert').className='alert';
document.getElementById('ppm-note').value='';
document.getElementById('ppm-method-detail').innerHTML='';
const [dr,sr]=await Promise.all([
fetch('/api/payout_process.php?action=cashout_detail&id='+cashoutId).then(r=>r.json()),
fetch('/api/payout_process.php?action=list_settings').then(r=>r.json()),
]);
if (!dr.success){toast('Load failed','err');closePayoutModal();return;}
PPM.cashout=dr.cashout;
PPM.settings=(sr.settings||[]).filter(s=>parseInt(s.is_enabled));
const c=PPM.cashout;
const amt=parseFloat(c.tokens).toFixed(2);
document.getElementById('ppm-player-info').innerHTML=
'<div style="display:flex;align-items:center;justify-content:space-between;gap:12px">'+
'<div><div style="font-family:\'Exo 2\',sans-serif;font-weight:700;font-size:16px">'+escHtmlA(c.alias||c.username)+'</div>'+
'<div style="font-size:14px;color:var(--text2)">@'+escHtmlA(c.username)+' | '+escHtmlA(c.email||'')+'</div>'+
'<div style="font-size:14px;color:var(--text2)">'+pname(c.platform_id)+' | '+escHtmlA(c.alias||'')+'</div></div>'+
'<div style="text-align:right"><div style="font-family:\'Exo 2\',sans-serif;font-weight:900;font-size:28px;color:var(--gold)">'+c.tokens+' Tokens</div>'+
'<div style="font-size:14px;font-weight:700;color:var(--green)">$'+amt+'</div></div></div>';
const spm=document.getElementById('ppm-saved-method');
const ph=c.saved_payout_handle||c.payout_handle;
const pt=c.saved_payout_label||c.payout_method_type||'';
spm.innerHTML=ph?
'<div style="background:rgba(0,229,255,.06);border:1px solid rgba(0,229,255,.15);border-radius:8px;padding:10px 14px">'+
'<div style="font-size:15px;font-weight:700;color:var(--text2);margin-bottom:4px">PLAYER PAYOUT METHOD</div>'+
'<div style="font-size:14px;font-weight:700">'+escHtmlA(pt)+' | <span style="color:var(--cyan)">'+escHtmlA(ph)+'</span></div></div>':
'<div style="font-size:14px;color:var(--red)">No payout method on file for this player</div>';
const sel=document.getElementById('ppm-method-select');
sel.innerHTML='<option value="">-- Select method --</option>'+
PPM.settings.map(s=>'<option value="'+escHtmlA(s.method_key)+'" data-type="'+s.method_type+'">'+
(s.method_type==='square_gift_card'?'[INSTANT] ':'')+escHtmlA(s.label)+'</option>').join('');
}
function closePayoutModal(){
document.getElementById('process-payout-modal').style.display='none';
PPM={cashoutId:null,cashout:null,settings:[]};
}
function onPayoutMethodChange(){
const key=document.getElementById('ppm-method-select').value;
const detail=document.getElementById('ppm-method-detail');
const btn=document.getElementById('ppm-submit-btn');
if (!key){detail.innerHTML='';btn.textContent='Process Payout';return;}
const s=PPM.settings.find(x=>x.method_key===key);
if (!s) return;
const amt=parseFloat(PPM.cashout?.tokens||0).toFixed(2);
const c=PPM.cashout;
if (s.method_type==='square_gift_card'){
detail.innerHTML='<div style="background:rgba(0,229,255,.07);border:1px solid rgba(0,229,255,.2);border-radius:10px;padding:14px">'+
'<div style="font-size:15px;font-weight:700;color:var(--cyan);margin-bottom:8px">INSTANT Square Gift Card</div>'+
'<div style="font-size:15px">Creates a digital gift card for <strong style="color:var(--gold)">$'+amt+'</strong> via your Square account instantly. Card number shows here — send to player via chat.</div></div>';
btn.textContent='Process via Square Gift Card';
} else {
const ph=c?.payout_handle||c?.saved_payout_handle||'(none on file)';
detail.innerHTML='<div style="background:rgba(240,192,64,.07);border:1px solid rgba(240,192,64,.2);border-radius:10px;padding:14px">'+
'<div style="font-size:15px;font-weight:700;color:var(--gold);margin-bottom:8px">Manual Processing</div>'+
'<div style="font-size:15px;line-height:2.0">'+
'1. Open <strong>'+escHtmlA(s.label)+'</strong><br>'+
'2. Send <strong style="color:var(--gold)">$'+amt+'</strong> to: <span style="color:var(--cyan)"><strong>'+escHtmlA(ph)+'</strong></span><br>'+
(s.instructions?'3. '+escHtmlA(s.instructions):'')+'</div>'+
(s.handle?'<div style="font-size:14px;color:var(--text2);margin-top:8px">Your handle: <strong>'+escHtmlA(s.handle)+'</strong></div>':'')+'</div>';
btn.textContent='Mark as Sent (Manual)';
}
}
async function submitPayout(){
const key=document.getElementById('ppm-method-select').value;
const note=document.getElementById('ppm-note').value.trim();
const al=document.getElementById('ppm-alert');
al.className='alert';
if (!key){showAdminAlert(al,'Select a payout method.','error');return;}
const s=PPM.settings.find(x=>x.method_key===key);
if (!s) return;
const isGC=s.method_type==='square_gift_card';
const amt=parseFloat(PPM.cashout?.tokens||0).toFixed(2);
if (!confirm(isGC?'Create Square gift card for $'+amt+'?':'Confirm you have sent $'+amt+' via '+s.label+'. Mark as sent?')) return;
const btn=document.getElementById('ppm-submit-btn');
btn.disabled=true; btn.textContent='Processing...';
try {
const res=await fetch('/api/payout_process.php?action=process_payout',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({cashout_id:PPM.cashoutId,payout_method_key:key,payout_type:s.method_type,note})}).then(r=>r.json());
if (res.success){
showAdminAlert(al,isGC?'Gift card created!':'Marked as sent!','success');
if (res.gift_card_gan){
al.innerHTML+='<div style="margin-top:10px;font-family:monospace;font-size:16px;color:var(--gold);word-break:break-all;user-select:all">Card #: <strong>'+res.gift_card_gan+'</strong></div>'+
'<div style="font-size:14px;color:var(--text2);margin-top:4px">Copy and send this card number to the player. Balance: $'+(res.gift_card_balance/100).toFixed(2)+'</div>';
}
toast(isGC?'Gift card sent!':'Marked sent','ok');
loadCashouts('pending');loadDashCashouts();loadStats();
if (!isGC) setTimeout(closePayoutModal,1500);
} else {
showAdminAlert(al,res.error||'Processing failed','error');
}
} catch(e){showAdminAlert(al,'Network error','error');}
finally{btn.disabled=false;btn.textContent=isGC?'Process via Square Gift Card':'Mark as Sent (Manual)';}
}
// ─── REFERRAL MANAGEMENT ──────────────────────────────────
let _refTiers = [];
function showRefSection(section) {
document.getElementById('ref-admin-list').style.display = section==='list'?'block':'none';
document.getElementById('ref-tiers-section').style.display = section==='tiers'?'block':'none';
document.getElementById('ref-shares-section').style.display = section==='shares'?'block':'none';
if (section==='tiers') loadAdminTiers();
if (section==='shares') loadAdminShares('pending', document.querySelector('#ref-shares-section .ftab'));
}
async function loadAdminReferrals(status, btn) {
document.getElementById('ref-admin-list').style.display='block';
document.getElementById('ref-tiers-section').style.display='none';
document.getElementById('ref-shares-section').style.display='none';
if (btn) { document.querySelectorAll('#section-referrals .ftab').forEach(b=>b.classList.remove('active')); btn.classList.add('active'); }
const el = document.getElementById('ref-admin-list');
el.innerHTML = '<div style="padding:16px;text-align:center;color:var(--text2)">Loading...</div>';
const d = await fetch('/api/referrals.php?action=admin_list&status='+status).then(r=>r.json());
if (!d.success||!d.referrals.length) { el.innerHTML='<div class="empty" style="padding:24px;text-align:center">No '+status+' referrals.</div>'; return; }
el.innerHTML = d.referrals.map(r => buildRefCard(r, status)).join('');
}
function buildRefCard(r, status) {
const note = r.admin_note ? '<div style="font-size:15px;color:var(--text2)">'+escHtmlA(r.admin_note)+'</div>' : '';
const awarded = r.tokens_awarded>0 ? '<div style="font-size:15px;color:var(--green)">+'+r.tokens_awarded+' tokens awarded</div>' : '';
const actions = status==='pending' ? [
'<input class="fi-sm" type="text" id="rn-'+r.id+'" placeholder="Note" style="width:100%;padding:7px 10px;font-size:14px;margin-bottom:6px">',
'<button class="btn btn-green" style="width:100%;font-size:14px;padding:7px;margin-bottom:4px" onclick="resolveReferral('+r.id+', \'verified\')">Verify + Award</button>',
'<button class="btn btn-red" style="width:100%;font-size:14px;padding:7px" onclick="resolveReferral('+r.id+', \'denied\')">Deny</button>',
].join('') : '';
return '<div class="card" style="margin-bottom:10px"><div style="display:flex;align-items:flex-start;gap:12px">'+
'<div style="flex:1"><div style="font-size:15px"><strong>'+escHtmlA(r.referrer_alias||r.referrer_name)+'</strong> referred <strong>'+escHtmlA(r.referred_alias||r.referred_name)+'</strong></div>'+
'<div style="font-size:15px;color:var(--text2);margin-top:3px">'+(r.created_at||'').substring(0,16)+' | Email verified: '+(r.email_verified?'Yes':'No')+'</div>'+
awarded+note+'</div>'+
'<div style="flex-shrink:0;min-width:140px">'+actions+'</div></div></div>';
}
async function resolveReferral(id, status) {
const note = document.getElementById('rn-'+id)?.value.trim() || '';
if (!confirm(status==='verified' ? 'Verify and award tokens?' : 'Deny this referral?')) return;
const d = await fetch('/api/referrals.php?action=resolve_referral', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({id, status, note})
}).then(r=>r.json());
if (d.success) {
toast(status==='verified' ? 'Verified! '+d.tokens_awarded+' tokens awarded' : 'Denied', status==='verified'?'ok':'err');
loadAdminReferrals('pending', document.querySelector('#section-referrals .ftab'));
loadStats();
} else toast(d.error||'Error','err');
}
async function loadAdminTiers() {
const el = document.getElementById('ref-tiers-admin-list');
const d = await fetch('/api/referrals.php?action=all_tiers').then(r=>r.json());
if (!d.success||!d.tiers.length) { el.innerHTML='<div class="empty" style="padding:20px;text-align:center">No tiers yet. Add one above.</div>'; return; }
_refTiers = d.tiers;
el.innerHTML = d.tiers.map(t => {
const inactive = !parseInt(t.is_active) ? '<span style="font-size:14px;color:var(--red);margin-left:6px">INACTIVE</span>' : '';
return '<div style="display:flex;align-items:center;gap:12px;padding:14px 18px;border-bottom:1px solid var(--border)">'+
'<div style="flex:1"><div style="font-family:\'Exo 2\',sans-serif;font-weight:700;font-size:14px">'+escHtmlA(t.name)+inactive+'</div>'+
'<div style="font-size:14px;color:var(--text2)">Min '+t.min_referrals+' refs | '+t.tokens_per_ref+' tok/ref'+(t.bonus_tokens>0?' + '+t.bonus_tokens+' milestone bonus':'')+'</div></div>'+
'<div style="display:flex;gap:6px">'+
'<button data-tid="'+t.id+'" onclick="editRefTier(this.dataset.tid)" style="background:rgba(0,229,255,.08);border:1px solid rgba(0,229,255,.2);color:var(--cyan);border-radius:6px;padding:6px 12px;font-size:14px;font-weight:700;cursor:pointer">Edit</button>'+
'<button data-tid="'+t.id+'" data-tname="'+escHtmlA(t.name)+'" onclick="deleteRefTier(this.dataset.tid,this.dataset.tname)" style="background:rgba(255,68,68,.08);border:1px solid rgba(255,68,68,.2);color:var(--red);border-radius:6px;padding:6px 12px;font-size:14px;font-weight:700;cursor:pointer">Del</button>'+
'</div></div>';
}).join('');
}
function editRefTier(id) {
const t = _refTiers.find(x=>x.id==id); if (!t) return;
document.getElementById('rt-id').value=t.id; document.getElementById('rt-name').value=t.name;
document.getElementById('rt-min').value=t.min_referrals; document.getElementById('rt-per').value=t.tokens_per_ref;
document.getElementById('rt-bonus').value=t.bonus_tokens; document.getElementById('rt-sort').value=t.sort_order;
document.getElementById('rt-desc').value=t.description||''; document.getElementById('rt-active').value=t.is_active;
document.getElementById('ref-tier-form-title').textContent = 'Editing: '+t.name;
}
function resetRefTierForm() {
['rt-id','rt-name','rt-desc'].forEach(id=>document.getElementById(id).value='');
document.getElementById('rt-min').value=1; document.getElementById('rt-per').value=5;
document.getElementById('rt-bonus').value=0; document.getElementById('rt-sort').value=0;
document.getElementById('rt-active').value=1;
document.getElementById('ref-tier-form-title').textContent='Add Referral Tier';
}
async function saveRefTier() {
const al = document.getElementById('ref-tier-alert');
const id = document.getElementById('rt-id').value;
const data = {
id: id ? parseInt(id) : undefined,
name: document.getElementById('rt-name').value.trim(),
min_referrals: parseInt(document.getElementById('rt-min').value)||1,
tokens_per_ref: parseFloat(document.getElementById('rt-per').value)||5,
bonus_tokens: parseFloat(document.getElementById('rt-bonus').value)||0,
description: document.getElementById('rt-desc').value.trim(),
sort_order: parseInt(document.getElementById('rt-sort').value)||0,
is_active: parseInt(document.getElementById('rt-active').value),
};
if (!data.name) { showAdminAlert(al,'Name is required.','error'); return; }
const d = await fetch('/api/referrals.php?action='+(id?'tier_update':'tier_create'),{
method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(data)
}).then(r=>r.json());
if (d.success) { showAdminAlert(al,id?'Updated!':'Created!','success'); resetRefTierForm(); loadAdminTiers(); toast('Saved','ok'); }
else showAdminAlert(al,d.error||'Error','error');
}
async function deleteRefTier(id, name) {
if (!confirm('Delete tier "'+name+'"?')) return;
const d = await fetch('/api/referrals.php?action=tier_delete',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id})}).then(r=>r.json());
if (d.success) { toast('Deleted','ok'); loadAdminTiers(); } else toast(d.error||'Error','err');
}
async function loadAdminShares(status, btn) {
if (btn) { document.querySelectorAll('#ref-shares-section .ftab').forEach(b=>b.classList.remove('active')); btn.classList.add('active'); }
const el = document.getElementById('ref-shares-admin-list');
const d = await fetch('/api/referrals.php?action=admin_shares&status='+status).then(r=>r.json());
if (!d.success||!d.shares.length) { el.innerHTML='<div class="empty" style="padding:20px;text-align:center">No '+status+' shares.</div>'; return; }
el.innerHTML = d.shares.map(s => {
const btns = status==='pending' ?
'<div style="display:flex;gap:6px">'+
'<button data-sid="'+s.id+'" onclick="resolveShare(this.dataset.sid,&quot;approved&quot;)" class="btn btn-green" style="font-size:14px;padding:7px 12px">Approve</button>'+
'<button data-sid="'+s.id+'" onclick="resolveShare(this.dataset.sid,&quot;denied&quot;)" class="btn btn-red" style="font-size:14px;padding:7px 12px">Deny</button></div>' : '';
return '<div class="card" style="margin-bottom:8px;display:flex;align-items:center;gap:12px">'+
'<div style="flex:1"><div style="font-size:15px;font-weight:700">'+escHtmlA(s.alias||s.username)+' | <span style="color:var(--cyan)">'+escHtmlA(s.platform)+'</span></div>'+
'<div style="font-size:15px;color:var(--text2)">'+(s.created_at||'').substring(0,16)+' | Bonus: '+s.bonus_tokens+' tokens</div></div>'+btns+'</div>';
}).join('');
}
async function resolveShare(id, status) {
const d = await fetch('/api/referrals.php?action=resolve_share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,status})}).then(r=>r.json());
if (d.success) { toast(status==='approved'?'Approved! Tokens awarded':'Denied','ok'); loadAdminShares('pending'); }
else toast(d.error||'Error','err');
}
// ─── PLATFORM ACCOUNTS ────────────────────────────────────
async function loadPlatformAccountRequests(status, btn) {
if (btn) { document.querySelectorAll('#section-platform-accounts .ftab').forEach(b=>b.classList.remove('active')); btn.classList.add('active'); }
const el = document.getElementById('pa-requests-list');
el.innerHTML = '<div style="padding:16px;text-align:center;color:var(--text2)">Loading...</div>';
const d = await apiFetch('platform_accounts_list&status='+status);
if (!d.success||!d.accounts.length) { el.innerHTML='<div class="empty" style="padding:24px;text-align:center">No '+status+' requests.</div>'; return; }
el.innerHTML = d.accounts.map(a => buildAdminPaCard(a, status==='pending')).join('');
}
async function gmLoadPlatformAccounts(uid) {
const el = document.getElementById('gm-platform-accounts-list');
const d = await apiFetch('platform_accounts_list&user_id='+uid);
if (!d.success||!d.accounts.length) { el.innerHTML='<div class="gm-empty">No platform account requests.</div>'; return; }
el.innerHTML = d.accounts.map(a => buildAdminPaCard(a, true)).join('');
}
function buildAdminPaCard(a, showActions) {
const statusLabel = { pending:'⏳ Pending', approved:'✅ Approved', denied:'❌ Denied', deleted:'🗑 Deleted' };
const statusColor = { pending:'var(--gold)', approved:'var(--green)', denied:'var(--red)', deleted:'var(--text2)' };
return '<div class="card" style="margin-bottom:12px">' +
'<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:10px">' +
'<div><div style="font-family:\'Exo 2\',sans-serif;font-weight:700;font-size:14px">' + escHtmlA(a.username||'?') + ' <span style="color:var(--text2)">· ' + escHtmlA(a.user_alias||'') + '</span></div>' +
'<div style="font-size:14px;color:var(--cyan)">' + escHtmlA(a.platform_name||a.platform_slug) + '</div>' +
'<div style="font-size:15px;color:var(--text2)">' + (a.requested_at||'').substring(0,16) + '</div></div>' +
'<span style="font-size:15px;font-weight:700;color:' + (statusColor[a.status]||'var(--text2)') + ';border:1px solid currentColor;border-radius:6px;padding:3px 10px">' + (statusLabel[a.status]||a.status) + '</span></div>' +
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px">' +
'<div><label class="gm-edit-label">Username</label><input class="fi-sm" type="text" id="pa-uname-' + a.id + '" value="' + escHtmlA(a.platform_username||'') + '" placeholder="username" style="width:100%;padding:8px 10px"></div>' +
'<div><label class="gm-edit-label">Password</label><input class="fi-sm" type="text" id="pa-pass-' + a.id + '" value="' + escHtmlA(a.platform_password||'') + '" placeholder="password" style="width:100%;padding:8px 10px"></div></div>' +
'<div style="margin-bottom:10px"><label class="gm-edit-label">Note to Player</label>' +
'<input class="fi-sm" type="text" id="pa-note-' + a.id + '" value="' + escHtmlA(a.admin_note||'') + '" placeholder="Optional note" style="width:100%;padding:8px 10px"></div>' +
'<div style="display:flex;gap:8px">' +
(a.status==='pending' ? '<button class="btn btn-green" onclick="resolvePlatformAccount('+a.id+',\'approved\')" style="flex:1;font-size:14px">Approve</button><button class="btn btn-red" onclick="resolvePlatformAccount('+a.id+',\'denied\')" style="font-size:14px;padding:8px 14px">Deny</button>' : '') +
(a.status==='approved' ? '<button class="btn btn-gold" onclick="updatePlatformAccount('+a.id+')" style="flex:1;font-size:14px">Update Credentials</button>' : '') +
'</div></div>';
}
async function resolvePlatformAccount(id, status) {
const uname = document.getElementById('pa-uname-'+id)?.value.trim();
const pass = document.getElementById('pa-pass-'+id)?.value.trim();
const note = document.getElementById('pa-note-'+id)?.value.trim();
if (status==='approved'&&!uname) { toast('Username required to approve','err'); return; }
if (!confirm(status==='approved'?'Approve and send credentials to player?':'Deny this request?')) return;
const d = await apiFetch('platform_account_resolve','POST',{id,status,platform_username:uname,platform_password:pass,admin_note:note});
if (d.success) {
toast(status==='approved'?'Approved!':'Denied','ok');
loadPlatformAccountRequests('pending', document.querySelector('#section-platform-accounts .ftab'));
if (GM.current) gmLoadPlatformAccounts(GM.current.id);
} else toast(d.error||'Error','err');
}
async function updatePlatformAccount(id) {
const d = await apiFetch('platform_account_update','POST',{
id, platform_username: document.getElementById('pa-uname-'+id)?.value.trim(),
platform_password: document.getElementById('pa-pass-'+id)?.value.trim(),
admin_note: document.getElementById('pa-note-'+id)?.value.trim()
});
if (d.success) toast('Updated','ok'); else toast(d.error||'Error','err');
}
async function sendBroadcast() {
const subject = document.getElementById('bc-subject')?.value.trim();
const message = document.getElementById('bc-message')?.value.trim();
const target = document.getElementById('bc-target')?.value || 'all';
const al = document.getElementById('bc-alert');
if (al) al.className = 'alert';
if (!subject) { if(al) showAdminAlert(al,'Subject is required.','error'); return; }
if (!message) { if(al) showAdminAlert(al,'Message is required.','error'); return; }
const btn = document.querySelector('#section-broadcasts .btn-gold');
if (btn) { btn.disabled = true; btn.textContent = 'Sending...'; }
const d = await apiFetch('broadcast_send','POST',{ subject, message, target });
if (btn) { btn.disabled = false; btn.textContent = '📢 SEND BROADCAST'; }
if (d.success) {
if (al) showAdminAlert(al, '✓ Broadcast sent to ' + (d.recipient_count||0) + ' players!', 'success');
document.getElementById('bc-subject').value = '';
document.getElementById('bc-message').value = '';
setTimeout(() => { if(al){al.className='alert';al.textContent='';} }, 4000);
loadBroadcasts();
} else {
if (al) showAdminAlert(al, d.error || 'Failed to send.', 'error');
}
}
async function loadBroadcasts() {
const el = document.getElementById('bc-list');
if (!el) return;
el.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text2)">Loading broadcasts...</div>';
let d;
try { d = await apiFetch('broadcast_list'); } catch(e) {
el.innerHTML = '<div class="empty" style="padding:24px;text-align:center;color:var(--red)">Failed to load broadcasts.</div>';
return;
}
if (!d || !d.success) {
el.innerHTML = '<div class="empty" style="padding:24px;text-align:center;color:var(--red)">' + (d?.error||'Error loading broadcasts') + '</div>';
return;
}
if (!d.broadcasts || !d.broadcasts.length) {
el.innerHTML = '<div class="empty" style="padding:24px;text-align:center;color:var(--text2)">No broadcasts sent yet.</div>';
return;
}
const TGT = { all:'👥 All', verified:'✅ Verified', unverified:'⏳ Unverified', admins:'🔑 Admins' };
el.innerHTML = d.broadcasts.map(b => {
const readPct = b.total_players > 0 ? Math.round((b.read_count/b.total_players)*100) : 0;
const dt = (b.sent_at||'').substring(0,16).replace('T',' ');
return `<div class="card" style="margin-bottom:10px">
<div style="display:flex;align-items:flex-start;gap:12px;flex-wrap:wrap">
<div style="flex:1;min-width:200px">
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:16px;color:var(--text);margin-bottom:4px">${escHtmlA(b.subject)}</div>
<div style="font-size:14px;color:var(--text2);margin-bottom:8px;line-height:1.5">${escHtmlA(b.message.substring(0,120))}${b.message.length>120?'...':''}</div>
<div style="display:flex;gap:10px;flex-wrap:wrap;font-size:13px;color:var(--text2)">
<span style="color:var(--gold);font-weight:700">${TGT[b.target]||b.target}</span>
<span>📅 ${dt}</span>
<span>👤 ${escHtmlA(b.sender_name)}</span>
<span style="color:var(--cyan)">👁 ${b.read_count}/${b.total_players} read (${readPct}%)</span>
<span style="color:var(--purple)">💬 ${b.reply_count} ${b.reply_count==1?'reply':'replies'}</span>
</div>
<div style="margin-top:8px;background:rgba(255,255,255,.08);border-radius:4px;height:5px;width:200px;max-width:100%">
<div style="height:5px;border-radius:4px;background:var(--cyan);width:${readPct}%"></div>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:6px;flex-shrink:0">
<button onclick="editBroadcast(${b.id},'${escAttr(b.subject)}','${escAttr(b.message)}','${b.target}')"
style="background:rgba(155,93,229,.1);border:1px solid rgba(155,93,229,.3);color:var(--purple);border-radius:6px;padding:7px 14px;font-size:13px;font-weight:700;cursor:pointer">✏️ Edit</button>
<button onclick="resendBroadcast(${b.id},'${escAttr(b.subject)}')"
style="background:rgba(0,229,255,.08);border:1px solid rgba(0,229,255,.2);color:var(--cyan);border-radius:6px;padding:7px 14px;font-size:13px;font-weight:700;cursor:pointer">↩ Resend</button>
<button onclick="deleteBroadcast(${b.id},'${escAttr(b.subject)}')"
style="background:rgba(255,68,68,.07);border:1px solid rgba(255,68,68,.2);color:var(--red);border-radius:6px;padding:7px 14px;font-size:13px;font-weight:700;cursor:pointer">🗑 Delete</button>
</div>
</div>
</div>`;
}).join('');
}
async function editBroadcast(id, subject, message, target) {
const newSubject = prompt('Edit Subject:', subject);
if (newSubject === null) return;
const newMessage = prompt('Edit Message:', message);
if (newMessage === null) return;
const targets = ['all','verified','unverified','admins'];
const labels = ['All Players','Verified Only','Unverified Only','Admins Only'];
const tIdx = targets.indexOf(target);
const newTarget = prompt('Send To (all/verified/unverified/admins):', target);
if (!targets.includes(newTarget)) { alert('Invalid target. Use: all, verified, unverified, or admins'); return; }
const d = await apiFetch('broadcast_edit','POST',{id, subject:newSubject, message:newMessage, target:newTarget});
if (d.success) { toast('Broadcast updated','ok'); loadBroadcasts(); }
else toast(d.error||'Failed to update','error');
}
async function resendBroadcast(id, subject) {
if (!confirm('Resend "'+subject+'" to all recipients? This will mark it as unread for everyone.')) return;
const d = await apiFetch('broadcast_resend','POST',{id});
if (d.success) { toast('Resent to '+d.recipient_count+' players','ok'); loadBroadcasts(); }
else toast(d.error||'Failed to resend','error');
}
async function openBcDrawer(id, subject) {
bcCurrentId = id;
document.getElementById('bc-drawer').style.display = 'block';
document.getElementById('bc-drawer-title').textContent = '📢 ' + subject;
document.getElementById('bc-drawer').scrollIntoView({behavior:'smooth'});
switchBcTab('replies', document.getElementById('bc-tab-replies'));
}
function closeBcDrawer() {
document.getElementById('bc-drawer').style.display = 'none';
bcCurrentId = null;
}
function switchBcTab(tab, btn) {
bcCurrentTab = tab;
document.querySelectorAll('#bc-drawer .ftab').forEach(b=>b.classList.remove('active'));
if (btn) btn.classList.add('active');
if (tab === 'replies') loadBcReplies();
else loadBcReads();
}
async function loadBcReplies() {
if (!bcCurrentId) return;
const el = document.getElementById('bc-drawer-content');
el.innerHTML = '<div style="color:var(--text2);font-size:15px;text-align:center;padding:16px">Loading...</div>';
const d = await apiFetch('broadcast_replies&broadcast_id=' + bcCurrentId);
if (!d.success || !d.replies.length) {
el.innerHTML = '<div style="color:var(--text2);font-size:15px;text-align:center;padding:16px">No replies yet.</div>';
return;
}
el.innerHTML = d.replies.map(r => {
const isAdm = parseInt(r.is_admin);
return `<div style="display:flex;gap:10px;margin-bottom:12px">
<div style="width:32px;height:32px;border-radius:50%;background:${isAdm?'linear-gradient(135deg,#f0c040,#d4a017)':'linear-gradient(135deg,#7b2fbe,#457b9d)'};display:flex;align-items:center;justify-content:center;font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px;color:#fff;flex-shrink:0">${(r.username||'?').charAt(0).toUpperCase()}</div>
<div style="flex:1;background:${isAdm?'rgba(240,192,64,.07)':'var(--bg3)'};border:1px solid ${isAdm?'rgba(240,192,64,.2)':'var(--border)'};border-radius:8px;padding:8px 12px">
<div style="font-size:15px;font-weight:700;color:${isAdm?'var(--gold)':'var(--cyan)'};margin-bottom:4px">
${escHtmlA(r.username)}${isAdm?' 🔑 (Admin)':' · '+escHtmlA(r.alias)} <span style="color:var(--text2);font-weight:400">· ${(r.created_at||'').substring(0,16)}</span>
</div>
<div style="font-size:15px;color:var(--text)">${escHtmlA(r.message)}</div>
</div>
</div>`;
}).join('');
}
async function loadBcReads() {
if (!bcCurrentId) return;
const el = document.getElementById('bc-drawer-content');
el.innerHTML = '<div style="color:var(--text2);font-size:15px;text-align:center;padding:16px">Loading...</div>';
const d = await apiFetch('broadcast_reads&broadcast_id=' + bcCurrentId);
if (!d.success || !d.reads.length) {
el.innerHTML = '<div style="color:var(--text2);font-size:15px;text-align:center;padding:16px">No reads yet.</div>';
return;
}
el.innerHTML = `<div style="display:flex;flex-wrap:wrap;gap:8px">${d.reads.map(r=>`
<div style="background:var(--bg3);border:1px solid var(--border);border-radius:8px;padding:6px 12px;font-size:14px">
<span style="font-weight:700;color:var(--text)">${escHtmlA(r.username)}</span>
<span style="color:var(--text2)"> · ${(r.read_at||'').substring(0,16)}</span>
</div>`).join('')}</div>`;
}
async function adminBroadcastReply() {
if (!bcCurrentId) return;
const input = document.getElementById('bc-reply-input');
const msg = input.value.trim();
if (!msg) return;
input.value = '';
const d = await apiFetch('broadcast_reply','POST',{broadcast_id:bcCurrentId, message:msg});
if (d.success) { loadBcReplies(); loadBroadcasts(); }
else toast(d.error||'Error','err');
}
async function deleteBroadcast(id, subject) {
if (!confirm('Delete broadcast "' + subject + '"?\n\nThis will remove all replies and read receipts. Cannot be undone.')) return;
const d = await apiFetch('broadcast_delete','POST',{id});
if (d.success) { toast('Broadcast deleted','ok'); loadBroadcasts(); if (bcCurrentId===id) closeBcDrawer(); }
else toast(d.error||'Error','err');
}
// ─── CASHOUT METHOD TYPES ─────────────────────────────────
async function loadCashoutMethods() {
const d = await apiFetch('cashout_methods_list');
const el = document.getElementById('cmt-list');
const ct = document.getElementById('cmt-count');
if (!d.success) { el.innerHTML = '<div class="empty">Failed to load.</div>'; return; }
const types = d.types || [];
if (ct) ct.textContent = types.length + ' method' + (types.length !== 1 ? 's' : '');
if (!types.length) { el.innerHTML = '<div class="empty" style="padding:24px;text-align:center">No methods yet. Add one above.</div>'; return; }
window._cmtData = types;
el.innerHTML = types.map(m => `
<div class="game-row${!parseInt(m.is_active)?' inactive-overlay':''}">
<div style="font-size:26px;flex-shrink:0">${escHtmlA(m.icon||'💰')}</div>
<div class="game-info">
<div class="game-name">${escHtmlA(m.label)}
${!parseInt(m.is_active)?'<span class="badge" style="background:rgba(255,68,68,.15);color:var(--red);font-size:15px;margin-left:6px">INACTIVE</span>':''}
</div>
<div class="game-slug">${escHtmlA(m.slug)}</div>
${m.description?`<div style="font-size:14px;color:var(--text2);margin-top:2px">${escHtmlA(m.description)}</div>`:''}
</div>
<div style="text-align:right;flex-shrink:0">
<div style="font-size:15px;color:var(--text2);margin-bottom:6px">Order: ${m.sort_order}</div>
<div class="game-actions">
<button class="game-edit-btn" style="background:rgba(0,229,255,.1);color:var(--cyan);border:1px solid rgba(0,229,255,.2)" onclick="editCashoutMethod(${m.id})">✏️ Edit</button>
<button class="game-edit-btn" style="background:rgba(255,68,68,.1);color:var(--red);border:1px solid rgba(255,68,68,.2)" onclick="deleteCashoutMethod(${m.id},'${escAttr(m.label)}')">🗑</button>
</div>
</div>
</div>`).join('');
}
function editCashoutMethod(id) {
const m = (window._cmtData||[]).find(x=>x.id==id);
if (!m) return;
document.getElementById('cmt-id').value = m.id;
document.getElementById('cmt-label').value = m.label;
document.getElementById('cmt-slug').value = m.slug;
document.getElementById('cmt-slug').disabled = true;
document.getElementById('cmt-icon').value = m.icon || '💰';
document.getElementById('cmt-desc').value = m.description || '';
document.getElementById('cmt-sort').value = m.sort_order;
document.getElementById('cmt-active').value= m.is_active;
document.getElementById('cmt-form-title').textContent = '✏️ Editing: ' + m.label;
document.getElementById('cmt-form-card').scrollIntoView({behavior:'smooth'});
}
function resetCmtForm() {
['cmt-id','cmt-label','cmt-desc'].forEach(id => document.getElementById(id).value = '');
document.getElementById('cmt-slug').value = '';
document.getElementById('cmt-slug').disabled = false;
document.getElementById('cmt-icon').value = '💰';
document.getElementById('cmt-sort').value = '99';
document.getElementById('cmt-active').value = '1';
document.getElementById('cmt-form-title').textContent = ' Add Cashout Method';
document.getElementById('cmt-form-alert').className = 'alert';
}
async function saveCashoutMethod() {
const al = document.getElementById('cmt-form-alert');
const id = document.getElementById('cmt-id').value;
const data = {
id: id ? parseInt(id) : undefined,
label: document.getElementById('cmt-label').value.trim(),
slug: document.getElementById('cmt-slug').value.trim(),
icon: document.getElementById('cmt-icon').value.trim() || '💰',
description: document.getElementById('cmt-desc').value.trim(),
sort_order: parseInt(document.getElementById('cmt-sort').value) || 99,
is_active: parseInt(document.getElementById('cmt-active').value),
};
if (!data.label) { showAdminAlert(al,'Label is required.','error'); return; }
if (!id && !data.slug) { showAdminAlert(al,'Slug is required for new methods.','error'); return; }
const action = id ? 'cashout_methods_update' : 'cashout_methods_create';
const d = await apiFetch(action,'POST',data);
if (d.success) {
showAdminAlert(al, id ? 'Method updated!' : 'Method added!', 'success');
resetCmtForm();
loadCashoutMethods();
toast(id ? 'Updated' : 'Method added', 'ok');
} else showAdminAlert(al, d.error||'Save failed','error');
}
async function deleteCashoutMethod(id, label) {
if (!confirm('Delete cashout method "' + label + '"?\n\nPlayers using this method will still have their saved payout methods, but it won\'t appear as an option for new methods.')) return;
const d = await apiFetch('cashout_methods_delete','POST',{id});
if (d.success) { toast('Deleted','ok'); loadCashoutMethods(); }
else toast(d.error||'Error','err');
}
// ─── GAME MANAGEMENT ──────────────────────────────────────
async function loadGames() {
const d = await apiFetch('platforms_admin');
const el = document.getElementById('games-list');
const ct = document.getElementById('games-count');
if (!d.success) { el.innerHTML = '<div class="empty">Failed to load games.</div>'; return; }
const games = d.platforms;
ct.textContent = games.length + ' game' + (games.length !== 1 ? 's' : '');
if (!games.length) { el.innerHTML = '<div class="empty" style="padding:24px;text-align:center">No games yet. Add one above.</div>'; return; }
el.innerHTML = games.map(g => `
<div class="game-row${!parseInt(g.is_active)?' inactive-overlay':''}">
<div class="game-color-dot" style="background:${g.color};color:${g.color}"></div>
<div class="game-info">
<div class="game-name">${escHtmlA(g.name)}
${!parseInt(g.is_active) ? '<span class="badge" style="background:rgba(255,68,68,.15);color:var(--red);font-size:15px;margin-left:6px">INACTIVE</span>' : ''}
</div>
<div class="game-slug">${escHtmlA(g.slug)}</div>
<div class="game-urls">
<div class="game-url-row">
<span class="game-url-label">PLAYER</span>
<span class="game-url-val">${escHtmlA(g.player_url)}</span>
<a href="${escHtmlA(g.player_url)}" target="_blank" class="game-url-link">↗</a>
</div>
${g.agent_link ? `<div class="game-url-row">
<span class="game-url-label" style="color:var(--gold)">AGENT LINK</span>
<span class="game-url-val">${escHtmlA(g.agent_link)}</span>
<a href="${escHtmlA(g.agent_link)}" target="_blank" class="game-url-link" style="color:var(--gold)">↗</a>
</div>` : ''}
${g.games_link ? `<div class="game-url-row">
<span class="game-url-label" style="color:var(--cyan)">GAMES LINK</span>
<span class="game-url-val">${escHtmlA(g.games_link)}</span>
<a href="${escHtmlA(g.games_link)}" target="_blank" class="game-url-link" style="color:var(--cyan)">↗</a>
</div>` : ''}
${g.agent_login ? `<div class="game-url-row">
<span class="game-url-label" style="color:var(--purple)">LOGIN</span>
<span class="game-url-val" style="${IS_MASTER_ADMIN?'':'filter:blur(5px);user-select:none;pointer-events:none'}">${escHtmlA(g.agent_login)}</span>
</div>` : ''}
${g.agent_password ? `<div class="game-url-row">
<span class="game-url-label" style="color:var(--purple)">PASSWORD</span>
<span class="game-url-val" style="${IS_MASTER_ADMIN?'':'filter:blur(5px);user-select:none;pointer-events:none'}">${escHtmlA(g.agent_password)}</span>
</div>` : ''}
${g.agent_guide ? `<div style="margin-top:6px;padding:8px 10px;background:rgba(155,93,229,0.07);border-radius:6px;font-size:12px;color:var(--text2);white-space:pre-wrap;max-height:80px;overflow:auto"><span style="color:var(--purple);font-weight:700;font-size:11px;letter-spacing:1px">AGENT GUIDE </span>${escHtmlA(g.agent_guide)}</div>` : ''}
${(g.sub_agent_login||g.sub_agent_password) ? `<div style="margin-top:4px;font-size:11px;font-weight:700;color:var(--purple);letter-spacing:1px;text-transform:uppercase;margin-bottom:2px">SUB-ACCOUNT</div>` : ''}
${g.sub_agent_login ? `<div class="game-url-row">
<span class="game-url-label" style="color:var(--purple)">LOGIN</span>
<span class="game-url-val" style="${IS_MASTER_ADMIN?'':'filter:blur(5px);user-select:none;pointer-events:none'}">${escHtmlA(g.sub_agent_login)}</span>
</div>` : ''}
${g.sub_agent_password ? `<div class="game-url-row">
<span class="game-url-label" style="color:var(--purple)">PASSWORD</span>
<span class="game-url-val" style="${IS_MASTER_ADMIN?'':'filter:blur(5px);user-select:none;pointer-events:none'}">${escHtmlA(g.sub_agent_password)}</span>
</div>` : ''}
${(g.cashier_login||g.cashier_password) ? `<div style="margin-top:4px;font-size:11px;font-weight:700;color:var(--cyan);letter-spacing:1px;text-transform:uppercase;margin-bottom:2px">CASHIER</div>` : ''}
${g.cashier_login ? `<div class="game-url-row">
<span class="game-url-label" style="color:var(--cyan)">LOGIN</span>
<span class="game-url-val" style="${IS_MASTER_ADMIN?'':'filter:blur(5px);user-select:none;pointer-events:none'}">${escHtmlA(g.cashier_login)}</span>
</div>` : ''}
${g.cashier_password ? `<div class="game-url-row">
<span class="game-url-label" style="color:var(--cyan)">PASSWORD</span>
<span class="game-url-val" style="${IS_MASTER_ADMIN?'':'filter:blur(5px);user-select:none;pointer-events:none'}">${escHtmlA(g.cashier_password)}</span>
</div>` : ''}
</div>
</div>
<div style="text-align:right;flex-shrink:0">
<div style="font-size:15px;color:var(--text2);margin-bottom:4px">Order: ${g.sort_order}</div>
<div style="font-size:11px;color:var(--text2);margin-bottom:4px" title="Last edited">✏️ ${g.updated_at ? new Date(g.updated_at).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}) : '—'}</div>
<div id="credit-total-${g.id}" style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:13px;color:var(--cyan);margin-bottom:6px">💳 —</div>
<div class="game-actions">
<button class="game-edit-btn" style="background:rgba(0,229,255,.1);color:var(--cyan);border:1px solid rgba(0,229,255,.2)" onclick="editGame(${g.id})">✏️ Edit</button>
${IS_MASTER_ADMIN ? `<button class="game-edit-btn" style="background:rgba(255,68,68,.1);color:var(--red);border:1px solid rgba(255,68,68,.2)" onclick="deleteGame(${g.id},'${escAttr(g.name)}')">🗑 Archive</button>` : ''}
</div>
</div>
</div>`).join('');
// Store for edit
window._gamesData = games;
// Load credit totals for each game
games.forEach(g => {
fetch('/api/platforms.php?action=credits_list&platform_id=' + g.id).then(r=>r.json()).then(d=>{
const el = document.getElementById('credit-total-' + g.id);
if (el && d.success) {
const t = d.total||0;
el.textContent = '💳 ' + (t%1===0 ? t.toLocaleString() : parseFloat(t).toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2}));
}
}).catch(()=>{});
});
}
function editGame(id) {
const g = (window._gamesData || []).find(x => x.id == id);
if (!g) return;
document.getElementById('gf-id').value = g.id;
document.getElementById('gf-name').value = g.name;
document.getElementById('gf-slug').value = g.slug;
document.getElementById('gf-slug').disabled = true;
document.getElementById('gf-player-url').value = g.player_url;
document.getElementById('gf-color').value = g.color || '#f0c040';
document.getElementById('gf-color-hex').value = g.color || '#f0c040';
document.getElementById('gf-sort').value = g.sort_order;
document.getElementById('gf-active').value = g.is_active;
document.getElementById('game-form-title').textContent = '✏️ Editing: ' + g.name;
if (IS_MASTER_ADMIN) {
// Show edit mode, hide view mode
document.getElementById('gf-agent-edit').style.display = 'block';
document.getElementById('gf-agent-view').style.display = 'none';
document.getElementById('gf-agent-link').value = g.agent_link || '';
document.getElementById('gf-agent-login').value = g.agent_login || '';
document.getElementById('gf-agent-password').value = g.agent_password || '';
document.getElementById('gf-games-link').value = g.games_link || '';
document.getElementById('gf-agent-guide').value = g.agent_guide || '';
document.getElementById('gf-sub-agent-login').value = g.sub_agent_login || '';
document.getElementById('gf-sub-agent-password').value = g.sub_agent_password || '';
document.getElementById('gf-cashier-login').value = g.cashier_login || '';
document.getElementById('gf-cashier-password').value = g.cashier_password || '';
document.getElementById('gf-credit-btn').disabled = false;
} else {
// Show view mode, hide edit mode
document.getElementById('gf-agent-edit').style.display = 'none';
document.getElementById('gf-agent-view').style.display = 'block';
document.getElementById('gf-credit-btn').disabled = true;
const agentFields = [
{label:'Agent Login', key:'agent_login', isUrl:false, isCred:true},
{label:'Agent Password', key:'agent_password', isUrl:false, isCred:true},
{label:'Agent Link', key:'agent_link', isUrl:true, isCred:false},
{label:'Games Link', key:'games_link', isUrl:true, isCred:false},
{label:'Agent Guide', key:'agent_guide', isUrl:false, isCred:false},
{label:'Sub-Account Agent Login', key:'sub_agent_login', isUrl:false, isCred:true},
{label:'Sub-Account Agent Password',key:'sub_agent_password', isUrl:false, isCred:true},
{label:'Cashier Login', key:'cashier_login', isUrl:false, isCred:true},
{label:'Cashier Password', key:'cashier_password', isUrl:false, isCred:true},
];
const content = document.getElementById('gf-agent-view-content');
content.innerHTML = agentFields.map(f => {
const val = g[f.key] || '';
if (!val) return '';
const openBtn = f.isUrl ? `<a href="${escHtmlA(val)}" target="_blank" rel="noopener"
style="background:rgba(0,229,255,0.1);border:1px solid rgba(0,229,255,0.25);color:var(--cyan);border-radius:5px;padding:3px 9px;font-size:12px;font-weight:700;cursor:pointer;flex-shrink:0;text-decoration:none">
↗ Open
</a>` : '';
const valStyle = f.isCred ? 'filter:blur(5px);user-select:none;pointer-events:none' : '';
const copyBtn = f.isCred ? '' : `<button onclick="copyToClipboard(${JSON.stringify(val)},this)"
style="background:rgba(155,93,229,0.15);border:1px solid rgba(155,93,229,0.3);color:var(--purple);border-radius:5px;padding:3px 10px;font-size:12px;font-weight:700;cursor:pointer;flex-shrink:0">
📋 Copy
</button>`;
return `<div style="display:flex;align-items:center;gap:8px;padding:7px 10px;background:rgba(155,93,229,0.05);border-radius:6px">
<span style="font-size:12px;font-weight:700;color:var(--purple);min-width:160px;flex-shrink:0">${escHtmlA(f.label)}</span>
<span style="flex:1;color:var(--text);font-size:14px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;${valStyle}">${escHtmlA(val)}</span>
${openBtn}
${copyBtn}
</div>`;
}).join('');
if (!content.innerHTML.trim()) {
content.innerHTML = '<div style="color:var(--text2);font-size:14px;padding:8px 0">No agent info has been set for this game.</div>';
}
}
// Load credit total
fetch('/api/platforms.php?action=credits_list&platform_id=' + g.id).then(r=>r.json()).then(d=>{
if (d.success) {
const t = d.total||0;
document.getElementById('gf-credit-total').textContent = t%1===0 ? t.toLocaleString() : parseFloat(t).toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2});
}
});
// Non-master admins need the form card revealed when editing
document.getElementById('game-form-card').style.display = 'block';
document.getElementById('game-form-card').scrollIntoView({behavior:'smooth'});
}
function resetGameForm() {
document.getElementById('gf-id').value = '';
document.getElementById('gf-name').value = '';
document.getElementById('gf-slug').value = '';
document.getElementById('gf-slug').disabled = false;
document.getElementById('gf-player-url').value = '';
document.getElementById('gf-color').value = '#f0c040';
document.getElementById('gf-color-hex').value = '#f0c040';
document.getElementById('gf-sort').value = '99';
document.getElementById('gf-active').value = '1';
document.getElementById('gf-credit-total').textContent = '—';
document.getElementById('gf-credit-btn').disabled = true;
if (IS_MASTER_ADMIN) {
// Master admin: show edit panel (visible for new games too), clear all fields
document.getElementById('gf-agent-edit').style.display = 'block';
document.getElementById('gf-agent-view').style.display = 'none';
document.getElementById('gf-agent-link').value = '';
document.getElementById('gf-agent-login').value = '';
document.getElementById('gf-agent-password').value = '';
document.getElementById('gf-games-link').value = '';
document.getElementById('gf-agent-guide').value = '';
document.getElementById('gf-sub-agent-login').value = '';
document.getElementById('gf-sub-agent-password').value = '';
document.getElementById('gf-cashier-login').value = '';
document.getElementById('gf-cashier-password').value = '';
} else {
// Non-master: hide both panels; they only appear when editing
document.getElementById('gf-agent-edit').style.display = 'none';
document.getElementById('gf-agent-view').style.display = 'none';
}
document.getElementById('game-form-title').textContent = ' Add New Game';
document.getElementById('game-form-alert').className = 'alert';
}
function syncColorPicker(hex) {
if (/^#[0-9a-fA-F]{6}$/.test(hex)) document.getElementById('gf-color').value = hex;
}
async function saveGame() {
const al = document.getElementById('game-form-alert');
const id = document.getElementById('gf-id').value;
const data = {
id: id ? parseInt(id) : undefined,
name: document.getElementById('gf-name').value.trim(),
slug: document.getElementById('gf-slug').value.trim(),
player_url: document.getElementById('gf-player-url').value.trim(),
agent_link: document.getElementById('gf-agent-link').value.trim(),
agent_login: document.getElementById('gf-agent-login').value.trim(),
agent_password: document.getElementById('gf-agent-password').value.trim(),
games_link: document.getElementById('gf-games-link').value.trim(),
agent_guide: document.getElementById('gf-agent-guide').value.trim(),
sub_agent_login: document.getElementById('gf-sub-agent-login').value.trim(),
sub_agent_password: document.getElementById('gf-sub-agent-password').value.trim(),
cashier_login: document.getElementById('gf-cashier-login').value.trim(),
cashier_password: document.getElementById('gf-cashier-password').value.trim(),
color: document.getElementById('gf-color-hex').value.trim() || document.getElementById('gf-color').value,
sort_order: parseInt(document.getElementById('gf-sort').value) || 99,
is_active: parseInt(document.getElementById('gf-active').value),
};
if (!data.name || !data.player_url) { showAdminAlert(al,'Name and Player URL are required.','error'); return; }
if (!id && !data.slug) { showAdminAlert(al,'Slug is required for new games.','error'); return; }
const action = id ? 'platforms_update' : 'platforms_create';
const d = await apiFetch(action, 'POST', data);
if (d.success) {
showAdminAlert(al, id ? 'Game updated!' : 'Game added!', 'success');
resetGameForm();
loadGames();
toast(id ? 'Game updated' : 'Game added', 'ok');
} else {
showAdminAlert(al, d.error || 'Save failed.', 'error');
}
}
async function deleteGame(id, name) {
if (!confirm('Archive "' + name + '"?\n\nThis hides it from active games but does NOT permanently delete it. You can restore it from the Archived Games section below.')) return;
const d = await apiFetch('platforms_delete','POST',{id});
if (d.success) { toast('Game archived','ok'); loadGames(); loadArchivedGames(); }
else toast(d.error||'Error','err');
}
async function loadArchivedGames() {
const list = document.getElementById('archived-games-list');
if (!list) return;
const d = await apiFetch('platforms_archived');
if (!d.success) { list.innerHTML='<div style="padding:16px 18px;color:var(--red)">Failed to load.</div>'; return; }
if (!d.platforms.length) {
list.innerHTML='<div style="padding:16px 18px;color:var(--text2);font-size:14px">No archived games.</div>';
return;
}
list.innerHTML = d.platforms.map(g=>`
<div style="display:flex;align-items:center;justify-content:space-between;padding:12px 18px;border-bottom:1px solid var(--border);gap:10px">
<div style="flex:1;min-width:0">
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;color:var(--text)">${escHtmlA(g.name)}</div>
<div style="font-size:12px;font-family:monospace;color:var(--text2);margin-top:2px">${escHtmlA(g.slug)}</div>
<div style="font-size:11px;color:var(--text2);margin-top:2px">Archived: ${g.deleted_at ? new Date(g.deleted_at).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}) : '—'}</div>
</div>
<div style="display:flex;gap:6px;flex-shrink:0">
<button onclick="restoreGame(${g.id},'${escAttr(g.name)}')"
style="background:rgba(0,230,118,.1);border:1px solid rgba(0,230,118,.25);color:var(--green);border-radius:6px;padding:6px 12px;font-size:13px;font-weight:700;cursor:pointer">
↩ Restore
</button>
<button onclick="purgeGame(${g.id},'${escAttr(g.name)}')"
style="background:rgba(255,68,68,.08);border:1px solid rgba(255,68,68,.2);color:var(--red);border-radius:6px;padding:6px 12px;font-size:13px;font-weight:700;cursor:pointer">
🗑 Delete Permanently
</button>
</div>
</div>`).join('');
}
async function restoreGame(id, name) {
if (!confirm('Restore "' + name + '" back to active games?')) return;
const d = await apiFetch('platforms_restore','POST',{id});
if (d.success) { toast('Game restored','ok'); loadGames(); loadArchivedGames(); }
else toast(d.error||'Error','err');
}
async function purgeGame(id, name) {
if (!confirm('PERMANENTLY delete "' + name + '"?\n\nThis cannot be undone. Historical purchase records will still reference the slug.')) return;
if (!confirm('Are you absolutely sure? This is irreversible.')) return;
const d = await apiFetch('platforms_purge','POST',{id});
if (d.success) { toast('Game permanently deleted','ok'); loadArchivedGames(); }
else toast(d.error||'Error','err');
}
// ── CREDIT ACCOUNTING ─────────────────────────────────────────────────────
let _creditPlatformId = null;
async function openCreditModal() {
const id = document.getElementById('gf-id').value;
if (!id) return;
_creditPlatformId = parseInt(id);
const g = (window._gamesData||[]).find(x=>x.id==id);
document.getElementById('cm-platform-name').textContent = g ? g.name : ('Platform #' + id);
document.getElementById('credit-modal').style.display = 'flex';
document.getElementById('cm-date').value = new Date().toISOString().slice(0,10);
resetCreditForm();
await loadCreditEntries();
}
function closeCreditModal() {
document.getElementById('credit-modal').style.display = 'none';
_creditPlatformId = null;
}
async function loadCreditEntries() {
if (!_creditPlatformId) return;
const d = await fetch('/api/platforms.php?action=credits_list&platform_id=' + _creditPlatformId).then(r=>r.json());
if (!d.success) { document.getElementById('cm-list').innerHTML='<div style="color:var(--red);padding:16px">Failed to load.</div>'; return; }
const total = d.total || 0;
document.getElementById('cm-total').textContent = total % 1 === 0 ? total.toLocaleString() : parseFloat(total).toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2});
document.getElementById('gf-credit-total').textContent = total % 1 === 0 ? total.toLocaleString() : parseFloat(total).toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2});
const list = document.getElementById('cm-list');
if (!d.credits.length) { list.innerHTML='<div style="color:var(--text2);text-align:center;padding:20px">No credit entries yet.</div>'; return; }
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: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=>{
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>`;
}
function editCreditEntry(id) {
const rows = document.querySelectorAll('#cm-list tbody tr');
// Re-fetch from last API response stored on rows via data or refetch
fetch('/api/platforms.php?action=credits_list&platform_id=' + _creditPlatformId).then(r=>r.json()).then(d=>{
if (!d.success) return;
const c = d.credits.find(x=>x.id==id);
if (!c) return;
document.getElementById('cm-entry-id').value = c.id;
document.getElementById('cm-credits').value = c.credits_purchased;
document.getElementById('cm-date').value = c.credit_date;
document.getElementById('cm-method').value = c.payment_method || '';
document.getElementById('cm-notes').value = c.notes || '';
document.getElementById('cm-form-title').textContent = '✏️ Editing Entry #' + c.id;
document.getElementById('cm-credits').focus();
});
}
function resetCreditForm() {
document.getElementById('cm-entry-id').value = '';
document.getElementById('cm-credits').value = '';
document.getElementById('cm-date').value = new Date().toISOString().slice(0,10);
document.getElementById('cm-method').value = '';
document.getElementById('cm-notes').value = '';
document.getElementById('cm-form-title').textContent = ' Add Credit Entry';
document.getElementById('cm-form-alert').className = 'alert';
}
async function saveCreditEntry() {
const al = document.getElementById('cm-form-alert');
const entryId = document.getElementById('cm-entry-id').value;
const credits = parseFloat(document.getElementById('cm-credits').value);
const date = document.getElementById('cm-date').value;
const method = document.getElementById('cm-method').value.trim();
const notes = document.getElementById('cm-notes').value.trim();
if (!credits || credits <= 0) { showAdminAlert(al,'Credits Purchased must be greater than 0.','error'); return; }
if (!date) { showAdminAlert(al,'Date is required.','error'); return; }
const action = entryId ? 'credits_update' : 'credits_create';
const payload = entryId
? {id:parseInt(entryId),credits_purchased:credits,credit_date:date,payment_method:method,notes}
: {platform_id:_creditPlatformId,credits_purchased:credits,credit_date:date,payment_method:method,notes};
const d = await fetch(`/api/platforms.php?action=${action}`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}).then(r=>r.json());
if (d.success) {
showAdminAlert(al, entryId ? 'Entry updated!' : 'Entry added!', 'success');
resetCreditForm();
await loadCreditEntries();
} else {
showAdminAlert(al, d.error||'Error saving entry.','error');
}
}
async function deleteCreditEntry(id) {
if (!confirm('Delete this credit entry? This cannot be undone.')) return;
const d = await fetch('/api/platforms.php?action=credits_delete',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id})}).then(r=>r.json());
if (d.success) { toast('Entry deleted','ok'); await loadCreditEntries(); }
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);
}
}
// ── BACKUP SYSTEM ────────────────────────────────────────────
async function loadBackups() {
const list = document.getElementById('backup-list');
const count = document.getElementById('backup-count');
list.innerHTML = '<div style="padding:24px;text-align:center;color:var(--text2);font-size:15px">Loading...</div>';
const d = await fetch('/api/backup.php?action=list').then(r=>r.json());
if (!d.success) { list.innerHTML='<div style="padding:24px;text-align:center;color:var(--red)">Failed to load backups.</div>'; return; }
count.textContent = d.backups.length + ' / 7';
if (!d.backups.length) {
list.innerHTML='<div style="padding:32px;text-align:center;color:var(--text2);font-size:15px">No backups yet. Click <strong>Create Backup Now</strong> to make the first one.</div>';
return;
}
list.innerHTML = d.backups.map((b, i) => {
const sizeMB = (b.size / 1048576).toFixed(2);
const isLatest = i === 0;
return `<div style="display:flex;align-items:center;gap:14px;padding:14px 18px;border-bottom:1px solid var(--border);flex-wrap:wrap;${isLatest?'background:rgba(0,229,255,.025)':''}">
<div style="font-size:22px;flex-shrink:0">${isLatest ? '🟢' : '💿'}</div>
<div style="flex:1;min-width:160px">
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px;color:var(--text)">${escHtmlA(b.name)}</div>
<div style="font-size:13px;color:var(--text2);margin-top:2px">${escHtmlA(b.created)} · ${sizeMB} MB${isLatest?' · <span style="color:var(--cyan);font-weight:700">Latest</span>':''}</div>
</div>
<div style="display:flex;gap:8px;flex-shrink:0">
<a href="/api/backup.php?action=download&file=${encodeURIComponent(b.name)}"
style="background:rgba(0,229,255,.1);border:1px solid rgba(0,229,255,.25);color:var(--cyan);border-radius:var(--rs);padding:7px 14px;font-family:'Exo 2',sans-serif;font-weight:700;font-size:13px;text-decoration:none;white-space:nowrap">
⬇ Download
</a>
<button onclick="deleteBackup('${escHtmlA(b.name)}')"
style="background:rgba(255,68,68,.08);border:1px solid rgba(255,68,68,.2);color:var(--red);border-radius:var(--rs);padding:7px 12px;font-family:'Exo 2',sans-serif;font-weight:700;font-size:13px;cursor:pointer">
🗑
</button>
</div>
</div>`;
}).join('') + '<div style="padding:10px 18px;font-size:13px;color:var(--text2);border-top:1px solid var(--border)">Oldest backup is automatically removed when a new one is created beyond the 7-backup limit.</div>';
}
async function createBackup() {
const btn = document.getElementById('backup-create-btn');
const al = document.getElementById('backup-alert');
btn.disabled = true;
btn.textContent = '⏳ Creating...';
al.className = 'alert';
showAdminAlert(al, 'Creating backup — this may take up to 30 seconds…', 'info');
try {
const d = await fetch('/api/backup.php?action=create', {method:'POST'}).then(r=>r.json());
if (d.success) {
const sizeMB = (d.size / 1048576).toFixed(2);
showAdminAlert(al, `✅ Backup created: ${d.name} (${sizeMB} MB)`, 'success');
loadBackups();
loadPlatformStats();
} else {
showAdminAlert(al, '❌ ' + (d.error || 'Backup failed'), 'error');
}
} catch(e) {
showAdminAlert(al, '❌ Request failed — server may have timed out', 'error');
}
btn.disabled = false;
btn.textContent = '📦 Create Backup Now';
}
async function deleteBackup(name) {
if (!confirm(`Delete backup "${name}"?\nThis cannot be undone.`)) return;
const d = await fetch('/api/backup.php?action=delete', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({name})}).then(r=>r.json());
if (d.success) { toast('Backup deleted', 'ok'); loadBackups(); }
else toast(d.error || 'Delete failed', 'err');
}
// Sync hex input with color picker
document.addEventListener('DOMContentLoaded', function() {
const picker = document.getElementById('gf-color');
const hex = document.getElementById('gf-color-hex');
if (picker && hex) {
picker.addEventListener('input', function() { hex.value = picker.value; });
}
});
// ─── COMPOSE: send message to any player ──────────────────
let composePlayer = null; // { id, username, alias, email }
function searchComposePlayers(q) {
const dd = document.getElementById('compose-dropdown');
if (!q || q.length < 1) { dd.style.display = 'none'; return; }
const lq = q.toLowerCase();
const matches = (allUsers.length ? allUsers : GM.all || []).filter(u =>
u.username.toLowerCase().includes(lq) ||
(u.alias||'').toLowerCase().includes(lq) ||
(u.email||'').toLowerCase().includes(lq)
).filter(u => !u.is_admin).slice(0, 8);
if (!matches.length) {
dd.innerHTML = '<div style="padding:12px;color:var(--text2);font-size:15px;text-align:center">No players found</div>';
dd.style.display = 'block'; return;
}
dd.innerHTML = matches.map(u => {
const color = avatarColor(u.username);
const init = avatarInit(u.username);
return `<div onclick="selectComposePicker(${u.id},'${escAttr(u.username)}','${escAttr(u.alias||'')}','${escAttr(u.email||'')}','${color}')"
style="display:flex;align-items:center;gap:10px;padding:10px 14px;cursor:pointer;transition:background .1s"
onmouseover="this.style.background='rgba(255,255,255,.04)'" onmouseout="this.style.background='none'">
<div style="width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,${color},${color}99);display:flex;align-items:center;justify-content:center;font-family:'Exo 2',sans-serif;font-weight:700;font-size:14px;color:#fff;flex-shrink:0">${init}</div>
<div style="min-width:0">
<div style="font-weight:700;font-size:15px;color:var(--text)">${escHtmlA(u.username)} <span style="color:var(--cyan);font-weight:400">· ${escHtmlA(u.alias||'')}</span></div>
<div style="font-size:15px;color:var(--text2)">${escHtmlA(u.email||'No email')} · ${u.tokens||0} 🪙</div>
</div>
</div>`;
}).join('');
dd.style.display = 'block';
}
function selectComposePicker(id, username, alias, email, color) {
composePlayer = { id, username, alias, email };
const sel = document.getElementById('compose-selected');
const av = document.getElementById('compose-player-avatar');
const nm = document.getElementById('compose-player-name');
const inf = document.getElementById('compose-player-info');
av.textContent = username.charAt(0).toUpperCase();
av.style.background = `linear-gradient(135deg,${color},${color}99)`;
nm.textContent = username + ' · ' + alias;
inf.textContent = email || 'No email';
sel.style.display = 'flex';
document.getElementById('compose-search').value = '';
document.getElementById('compose-dropdown').style.display = 'none';
document.getElementById('compose-msg').focus();
}
function clearComposePicker() {
composePlayer = null;
document.getElementById('compose-selected').style.display = 'none';
document.getElementById('compose-search').value = '';
document.getElementById('compose-alert').className = 'alert';
}
async function adminComposeSend() {
const al = document.getElementById('compose-alert');
const msg = document.getElementById('compose-msg').value.trim();
if (!composePlayer) { showAdminAlert(al, 'Select a player first.', 'error'); return; }
if (!msg) { showAdminAlert(al, 'Type a message first.', 'error'); return; }
const d = await apiFetch('chat_admin_send', 'POST', { user_id: composePlayer.id, message: msg });
if (d.success) {
document.getElementById('compose-msg').value = '';
showAdminAlert(al, '✓ Message sent to ' + composePlayer.username + '! They will see it in their Support chat.', 'success');
loadChatInbox(true); // refresh inbox
loadChatBadge();
// Optional: open the thread immediately
setTimeout(() => {
openChatThread(composePlayer.id, composePlayer.username, composePlayer.alias);
}, 800);
} else {
showAdminAlert(al, d.error || 'Failed to send.', 'error');
}
}
// Close compose dropdown when clicking outside
document.addEventListener('click', function(e) {
const dd = document.getElementById('compose-dropdown');
const search = document.getElementById('compose-search');
if (dd && !dd.contains(e.target) && e.target !== search) {
dd.style.display = 'none';
}
});
async function loadPendingSignups() {
const d = await apiFetch('pending_signups');
const el = document.getElementById('pending-list');
if (!d.success || !d.pending.length) {
el.innerHTML = '<div class="card"><div class="empty" style="padding:24px;text-align:center">✅ No pending registrations.</div></div>';
document.getElementById('badge-pending').style.display = 'none';
return;
}
const badge = document.getElementById('badge-pending');
if (badge) { badge.textContent = d.pending.length; badge.style.display = 'inline'; }
el.innerHTML = d.pending.map(p => {
const color = avatarColor(p.username);
const init = avatarInit(p.username);
const created = (p.created_at||'').substring(0,16);
const expires = (p.expires_at||'').substring(0,16);
const now = new Date();
const expDate = new Date(p.expires_at);
const expiring = (expDate - now) < 3600000; // < 1hr
return `
<div class="card" style="margin-bottom:10px;display:flex;align-items:center;gap:14px;flex-wrap:wrap">
<div style="width:44px;height:44px;border-radius:50%;background:linear-gradient(135deg,${color},${color}99);display:flex;align-items:center;justify-content:center;font-family:'Exo 2',sans-serif;font-weight:700;font-size:18px;color:#fff;flex-shrink:0">${init}</div>
<div style="flex:1;min-width:180px">
<div style="font-family:'Exo 2',sans-serif;font-weight:700;font-size:15px;color:var(--text)">${escHtmlA(p.username)}</div>
<div style="font-size:14px;color:var(--cyan);margin-top:1px">${escHtmlA(p.alias)}</div>
<div style="font-size:15px;color:var(--text2);margin-top:2px">${escHtmlA(p.email||'No email')}</div>
<div style="font-size:14px;margin-top:4px;display:flex;gap:10px;flex-wrap:wrap">
<span style="color:var(--text2)">Registered: ${created}</span>
<span style="color:${expiring?'var(--red)':'var(--text2)'}">Expires: ${expires}${expiring?' ⚠️':''}</span>
</div>
</div>
<div style="display:flex;gap:8px;flex-shrink:0">
<button class="btn btn-green" style="padding:8px 16px;font-size:14px" onclick="approvePending(${p.id},'${escAttr(p.username)}')">
✓ Approve
</button>
<button class="btn btn-red" style="padding:8px 16px;font-size:14px" onclick="deletePending(${p.id},'${escAttr(p.username)}')">
✗ Delete
</button>
</div>
</div>`;
}).join('');
}
async function approvePending(id, username) {
if (!confirm('Approve ' + username + '?\n\nThis will create their account immediately and bypass email verification.')) return;
const d = await apiFetch('approve_pending', 'POST', { id });
if (d.success) {
toast('✓ ' + d.username + ' approved and account created!', 'ok');
loadPendingSignups();
loadStats();
loadUsers();
} else {
toast(d.error || 'Error approving signup', 'err');
}
}
async function deletePending(id, username) {
if (!confirm('Delete pending signup for ' + username + '?\n\nThey will need to register again.')) return;
const d = await apiFetch('delete_pending', 'POST', { id });
if (d.success) { toast('Deleted', 'ok'); loadPendingSignups(); loadStats(); }
else toast('Error', 'err');
}
// ─── ADMIN CHAT ────────────────────────────────────────────
let adminChat = { userId: null, lastId: 0, polling: null, inboxPolling: null };
function showSec(name) {
document.querySelectorAll('.section').forEach(s=>s.classList.remove('active'));
document.querySelectorAll('.nav-item').forEach(b=>b.classList.remove('active'));
const sec = document.getElementById('section-'+name);
if (sec) sec.classList.add('active');
// Find the matching nav button safely
document.querySelectorAll('.nav-item').forEach(b => {
if (b.getAttribute('onclick') && b.getAttribute('onclick').includes("'"+name+"'")) {
b.classList.add('active');
}
});
if (name === 'chat') {
showChatInbox();
loadChatInbox();
// Auto-refresh inbox every 5s when inbox is visible
clearInterval(adminChat.inboxPolling);
adminChat.inboxPolling = setInterval(() => {
if (!adminChat.userId) { loadChatInbox(true); } // silent refresh
}, 5000);
} else {
clearInterval(adminChat.inboxPolling);
}
if (name === 'pending') loadPendingSignups();
if (name === 'history') loadHistory(1);
if (name === 'users') { loadUsers(); showGamerList(); }
if (name === 'referrals') { loadAdminReferrals('pending', document.querySelector('#section-referrals .ftab')); }
if (name === 'platform-accounts') loadPlatformAccountRequests('pending', document.querySelector('#section-platform-accounts .ftab'));
if (name === 'broadcasts') loadBroadcasts();
if (name === 'games') { loadGames(); loadArchivedGames(); resetGameForm(); }
if (name === 'payments') loadPaymentSettings();
if (name === 'payout-settings') loadPayoutSettings();
if (name === 'cashout-methods') loadCashoutMethods();
if (name === 'backups') loadBackups();
}
async function loadChatInbox(silent) {
const el = document.getElementById('chat-inbox-list');
if (!silent) el.innerHTML = '<div class="empty" style="padding:24px">Loading conversations...</div>';
const d = await apiFetch('chat_inbox');
if (!d.success) return;
if (!d.inbox.length) {
el.innerHTML = '<div class="empty" style="padding:30px;text-align:center">&#128172; No messages yet.<br><span style="color:var(--text2);font-size:14px">Users will appear here when they send a message.</span></div>';
return;
}
// Update unread badge in nav
const totalUnread = d.inbox.reduce((s,r) => s + parseInt(r.unread_count||0), 0);
const badge = document.getElementById('badge-chat');
if (badge) { badge.textContent = totalUnread > 9 ? '9+' : totalUnread; badge.style.display = totalUnread > 0 ? 'inline' : 'none'; }
el.innerHTML = d.inbox.map(row => {
const init = (row.username||'?').charAt(0).toUpperCase();
const unread = parseInt(row.unread_count) > 0;
const preview = (row.last_sender === 'admin' ? 'You: ' : '') + (row.last_message || '').substring(0, 65);
const time = formatAdminTime(row.last_time);
return `<div class="chat-inbox-row${unread?' unread':''}" onclick="openChatThread(${row.user_id},'${escAttr(row.username)}','${escAttr(row.alias)}')">
<div class="ci-avatar" style="background:linear-gradient(135deg,${avatarColor(row.username)},${avatarColor(row.username)}aa)">${init}</div>
<div class="ci-body">
<div class="ci-top">
<div class="ci-name">${escHtmlA(row.username)} <span style="color:var(--cyan);font-weight:400;font-size:14px">· ${escHtmlA(row.alias)}</span></div>
<div class="ci-time">${time}</div>
</div>
<div class="ci-preview${unread?' unread-preview':''}">${escHtmlA(preview)}</div>
</div>
${unread ? `<div class="ci-badge">${row.unread_count > 9 ? '9+' : row.unread_count}</div>` : ''}
</div>`;
}).join('');
}
function showChatInbox() {
document.getElementById('chat-inbox-view').style.display = 'block';
document.getElementById('chat-thread-view').style.display = 'none';
clearInterval(adminChat.polling);
adminChat.userId = null;
adminChat.lastId = 0;
}
async function openChatThread(userId, username, alias) {
clearInterval(adminChat.inboxPolling);
adminChat.userId = userId;
adminChat.lastId = 0;
document.getElementById('chat-inbox-view').style.display = 'none';
document.getElementById('chat-thread-view').style.display = 'block';
document.getElementById('admin-chat-name').textContent = username + ' · ' + alias;
document.getElementById('admin-chat-meta').textContent = 'Loading...';
document.getElementById('admin-chat-avatar').textContent = username.charAt(0).toUpperCase();
document.getElementById('admin-chat-messages').innerHTML = '<div style="color:var(--text2);font-size:14px;text-align:center;padding:20px">Loading...</div>';
await loadAdminThread(true);
clearInterval(adminChat.polling);
adminChat.polling = setInterval(() => loadAdminThread(false), 3000);
}
async function loadAdminThread(scrollToBottom) {
if (!adminChat.userId) return;
const d = await apiFetch('chat_thread&user_id=' + adminChat.userId + '&since=' + adminChat.lastId);
if (!d.success) return;
const container = document.getElementById('admin-chat-messages');
if (d.messages.length === 0 && adminChat.lastId === 0) {
container.innerHTML = '<div style="color:var(--text2);font-size:15px;text-align:center;padding:24px">No messages in this thread yet.</div>';
}
if (d.messages.length > 0) {
if (adminChat.lastId === 0) container.innerHTML = '';
let lastDate = '';
d.messages.forEach(msg => {
if (container.querySelector('[data-id="' + msg.id + '"]')) return;
const msgDate = msg.created_at.substring(0,10);
if (msgDate !== lastDate) {
lastDate = msgDate;
const div = document.createElement('div');
div.className = 'admin-date-div';
div.textContent = formatAdminDateLabel(msgDate);
container.appendChild(div);
}
const bubble = buildAdminBubble(msg);
if (msg.sender === 'user' && adminChat.lastId > 0) {
bubble.style.animation = 'fadeUp .25s ease';
}
container.appendChild(bubble);
adminChat.lastId = Math.max(adminChat.lastId, parseInt(msg.id));
});
if (scrollToBottom || (container.scrollHeight - container.scrollTop - container.clientHeight < 160)) {
container.scrollTop = container.scrollHeight;
}
}
if (d.user) {
document.getElementById('admin-chat-meta').textContent = 'Alias: ' + d.user.alias + ' · ' + d.user.tokens + ' tokens';
}
}
function buildAdminBubble(msg) {
const isAdmin = msg.sender === 'admin';
const wrap = document.createElement('div');
wrap.className = 'admin-bubble-wrap ' + (isAdmin ? 'from-admin' : 'from-user');
wrap.dataset.id = msg.id;
const time = msg.created_at.substring(11,16);
wrap.innerHTML =
'<div class="admin-bubble ' + (isAdmin ? 'from-admin' : 'from-user') + '">' + escHtmlA(msg.message) + '</div>' +
'<div class="admin-bubble-time">' + (isAdmin ? 'You · ' : 'User · ') + time + '</div>';
return wrap;
}
async function adminClearThread() {
if (!adminChat.userId) return;
const name = document.getElementById('admin-chat-name')?.textContent || 'this player';
if (!confirm('Clear all messages with ' + name + '?\n\nThis permanently deletes the entire conversation and cannot be undone.')) return;
const d = await apiFetch('chat_clear_thread', 'POST', { user_id: adminChat.userId });
if (d.success) {
document.getElementById('admin-chat-messages').innerHTML =
'<div style="color:var(--text2);font-size:15px;text-align:center;padding:24px">Conversation cleared.</div>';
adminChat.lastId = 0;
toast('Thread cleared', 'ok');
loadChatBadge();
} else toast(d.error || 'Error', 'err');
}
async function adminClearAllChats() {
if (!confirm('Clear ALL chat messages from ALL players?\n\nThis permanently deletes every conversation and cannot be undone.')) return;
if (!confirm('Are you absolutely sure? This will remove all chat history for every player.')) return;
const d = await apiFetch('chat_clear_all', 'POST', {});
if (d.success) {
toast('All chats cleared', 'ok');
showChatInbox();
loadChatInbox();
loadChatBadge();
} else toast(d.error || 'Error', 'err');
}
async function adminSendMsg() {
const input = document.getElementById('admin-chat-input');
const msg = input.value.trim();
if (!msg || !adminChat.userId) return;
input.value = '';
const container = document.getElementById('admin-chat-messages');
const tempWrap = buildAdminBubble({ id: 'temp_' + Date.now(), sender:'admin', message:msg, created_at: new Date().toISOString().replace('T',' ').substring(0,19) });
tempWrap.style.opacity = '0.65';
container.appendChild(tempWrap);
container.scrollTop = container.scrollHeight;
const d = await apiFetch('chat_admin_send','POST',{ user_id: adminChat.userId, message: msg });
if (d.success) {
tempWrap.style.opacity = '1';
tempWrap.dataset.id = d.id;
adminChat.lastId = Math.max(adminChat.lastId, d.id);
loadChatBadge();
} else {
tempWrap.remove();
input.value = msg;
toast('Send failed','err');
}
}
async function loadChatBadge() {
const d = await apiFetch('chat_unread');
if (d.success) {
const b = document.getElementById('badge-chat');
const alert = document.getElementById('unread-chat-alert');
const count = document.getElementById('unread-chat-alert-count');
const text = document.getElementById('unread-chat-alert-text');
if (b) { b.textContent = d.count; b.style.display = d.count > 0 ? 'inline-block' : 'none'; }
if (alert) alert.style.display = d.count > 0 ? 'flex' : 'none';
if (count) count.textContent = d.count;
if (text) text.textContent = d.count + ' unread message' + (d.count !== 1 ? 's' : '') + ' waiting for a response';
}
}
function formatAdminTime(dt) {
if (!dt) return '';
const d = new Date(dt.replace(' ','T'));
const now = new Date();
const diff = (now - d) / 1000;
if (diff < 60) return 'Just now';
if (diff < 3600) return Math.floor(diff/60) + 'm ago';
if (diff < 86400) return d.toLocaleTimeString('en-US',{hour:'numeric',minute:'2-digit'});
return d.toLocaleDateString('en-US',{month:'short',day:'numeric'});
}
function formatAdminDateLabel(dateStr) {
const d = new Date(dateStr + 'T00:00:00');
const today = new Date(); today.setHours(0,0,0,0);
const diff = Math.round((today - d) / 86400000);
if (diff === 0) return 'Today';
if (diff === 1) return 'Yesterday';
return d.toLocaleDateString('en-US',{weekday:'long',month:'short',day:'numeric'});
}
function escHtmlA(s) { return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\n/g,'<br>'); }
function openFieldUrl(inputId) {
const url = document.getElementById(inputId)?.value?.trim();
if (!url) return false;
window.open(url, '_blank', 'noopener');
return false;
}
function copyToClipboard(text, btn) {
navigator.clipboard.writeText(text).then(() => {
const orig = btn.innerHTML;
btn.innerHTML = '✅ Copied';
btn.style.background = 'rgba(0,230,118,0.15)';
btn.style.borderColor = 'rgba(0,230,118,0.4)';
btn.style.color = 'var(--green)';
setTimeout(() => {
btn.innerHTML = orig;
btn.style.background = '';
btn.style.borderColor = '';
btn.style.color = '';
}, 1800);
}).catch(() => toast('Copy failed — try manually','err'));
}
function escAttr(s) { return String(s||'').replace(/'/g,"\\'").replace(/"/g,'&quot;'); }
// Poll chat badge every 10s
setInterval(loadChatBadge, 10000);
loadChatBadge();
</script>
<div style="position:fixed;bottom:8px;right:12px;font-size:14px;color:rgba(255,255,255,.2);font-family:'Exo 2',sans-serif;pointer-events:none;z-index:999">
TomTomGames Admin v1.0.0
</div>
</body>
</html>