diff --git a/admin/index.php b/admin/index.php index e03d11a..39c3580 100644 --- a/admin/index.php +++ b/admin/index.php @@ -774,8 +774,8 @@ tr:hover td{background:rgba(255,255,255,.015)}
đŸ•šī¸ Game Management
- -
+ +
➕ Add New Game
@@ -910,6 +910,17 @@ tr:hover td{background:rgba(255,255,255,.015)}
+ + + +
+
+
đŸ—„ī¸ Archived Games
+ +
+
Loading...
+
+
@@ -1161,6 +1172,8 @@ loadStats(); loadPurchases('pending'); loadCashouts('pending'); loadUsers(); +// Initialize game form state on page load +if (typeof resetGameForm === 'function') resetGameForm(); async function loadStats() { const d = await apiFetch('stats'); @@ -2897,7 +2910,7 @@ async function loadGames() {
đŸ’ŗ —
- + ${IS_MASTER_ADMIN ? `` : ''}
`).join(''); @@ -2990,6 +3003,8 @@ function editGame(id) { 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'}); } @@ -3005,10 +3020,10 @@ function resetGameForm() { document.getElementById('gf-active').value = '1'; document.getElementById('gf-credit-total').textContent = '—'; document.getElementById('gf-credit-btn').disabled = true; - // Always hide both agent panels when clearing - document.getElementById('gf-agent-edit').style.display = 'none'; - document.getElementById('gf-agent-view').style.display = 'none'; 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 = ''; @@ -3018,6 +3033,10 @@ function resetGameForm() { 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'; @@ -3063,9 +3082,53 @@ async function saveGame() { } async function deleteGame(id, name) { - if (!confirm('Delete game "' + name + '"? This cannot be undone.\n\nNote: existing purchase records referencing this game will still show the old platform ID.')) return; + 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 deleted','ok'); loadGames(); } + 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='
Failed to load.
'; return; } + if (!d.platforms.length) { + list.innerHTML='
No archived games.
'; + return; + } + list.innerHTML = d.platforms.map(g=>` +
+
+
${escHtmlA(g.name)}
+
${escHtmlA(g.slug)}
+
Archived: ${g.deleted_at ? new Date(g.deleted_at).toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}) : '—'}
+
+
+ + +
+
`).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'); } @@ -3365,7 +3428,7 @@ function showSec(name) { 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(); + if (name === 'games') { loadGames(); loadArchivedGames(); } if (name === 'payments') loadPaymentSettings(); if (name === 'payout-settings') loadPayoutSettings(); if (name === 'cashout-methods') loadCashoutMethods(); diff --git a/api/admin.php b/api/admin.php index bbe3426..ed843e4 100644 --- a/api/admin.php +++ b/api/admin.php @@ -776,17 +776,44 @@ switch ($action) { echo json_encode($sent ? ['success'=>true] : ['success'=>false,'error'=>'Failed to send reset email. Please try again.']); break; - // ─── PLATFORMS: admin list ──────────────────────────── + // ─── PLATFORMS: admin list (active + inactive, no archived) ── case 'platforms_admin': - $rows = db()->query("SELECT * FROM platforms ORDER BY sort_order ASC, id ASC")->fetchAll(); + $rows = db()->query("SELECT * FROM platforms WHERE is_deleted=0 ORDER BY sort_order ASC, id ASC")->fetchAll(); echo json_encode(['success'=>true,'platforms'=>$rows]); break; + // ─── PLATFORMS: archived list ───────────────────────── + case 'platforms_archived': + $rows = db()->query("SELECT * FROM platforms WHERE is_deleted=1 ORDER BY deleted_at DESC")->fetchAll(); + echo json_encode(['success'=>true,'platforms'=>$rows]); + break; + + // ─── PLATFORMS: restore archived ────────────────────── + case 'platforms_restore': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $d = json_decode(file_get_contents('php://input'), true); + $id = (int)($d['id'] ?? 0); + if (!$id) { echo json_encode(['success'=>false,'error'=>'ID required']); exit; } + db()->prepare("UPDATE platforms SET is_deleted=0, deleted_at=NULL, updated_at=NOW() WHERE id=?")->execute([$id]); + echo json_encode(['success'=>true]); + break; + + // ─── PLATFORMS: permanent delete (archived only) ────── + case 'platforms_purge': + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + $d = json_decode(file_get_contents('php://input'), true); + $id = (int)($d['id'] ?? 0); + if (!$id) { echo json_encode(['success'=>false,'error'=>'ID required']); exit; } + db()->prepare("DELETE FROM platforms WHERE id=? AND is_deleted=1")->execute([$id]); + echo json_encode(['success'=>true]); + break; + // ─── PLATFORMS: create ──────────────────────────────── case 'platforms_create': if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + if ((int)($_SESSION['user_id'] ?? 0) !== MASTER_ADMIN_ID) { echo json_encode(['success'=>false,'error'=>'Only master admin can add games']); exit; } $d = json_decode(file_get_contents('php://input'), true); - $isMasterAdmin = (int)($_SESSION['user_id'] ?? 0) === MASTER_ADMIN_ID; + $isMasterAdmin = true; $slug = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($d['slug'] ?? ''))); $name = substr(trim($d['name'] ?? ''), 0, 100); $purl = substr(trim($d['player_url'] ?? ''), 0, 500); @@ -803,11 +830,21 @@ switch ($action) { $cashier_login = $isMasterAdmin ? substr(trim($d['cashier_login'] ?? ''), 0, 200) : ''; $cashier_password = $isMasterAdmin ? substr(trim($d['cashier_password'] ?? ''), 0, 200) : ''; if (!$slug||!$name||!$purl) { echo json_encode(['success'=>false,'error'=>'Slug, name, and player URL required']); exit; } - try { - db()->prepare("INSERT INTO platforms (slug,name,player_url,agent_link,agent_login,agent_password,games_link,agent_guide,sub_agent_login,sub_agent_password,cashier_login,cashier_password,color,sort_order,is_active) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)") - ->execute([$slug,$name,$purl,$agent_link,$agent_login,$agent_password,$games_link,$agent_guide,$sub_agent_login,$sub_agent_password,$cashier_login,$cashier_password,$color,$sort,$active]); - echo json_encode(['success'=>true,'id'=>db()->lastInsertId()]); - } catch (Exception $e) { echo json_encode(['success'=>false,'error'=>'Slug already exists']); } + // Check if slug belongs to an archived platform — reactivate it instead of inserting + $existing = db()->prepare("SELECT id FROM platforms WHERE slug=? AND is_deleted=1 LIMIT 1"); + $existing->execute([$slug]); + $archivedId = $existing->fetchColumn(); + if ($archivedId) { + db()->prepare("UPDATE platforms SET name=?,player_url=?,agent_link=?,agent_login=?,agent_password=?,games_link=?,agent_guide=?,sub_agent_login=?,sub_agent_password=?,cashier_login=?,cashier_password=?,color=?,sort_order=?,is_active=?,is_deleted=0,deleted_at=NULL,updated_at=NOW() WHERE id=?") + ->execute([$name,$purl,$agent_link,$agent_login,$agent_password,$games_link,$agent_guide,$sub_agent_login,$sub_agent_password,$cashier_login,$cashier_password,$color,$sort,$active,$archivedId]); + echo json_encode(['success'=>true,'id'=>$archivedId,'restored'=>true]); + } else { + try { + db()->prepare("INSERT INTO platforms (slug,name,player_url,agent_link,agent_login,agent_password,games_link,agent_guide,sub_agent_login,sub_agent_password,cashier_login,cashier_password,color,sort_order,is_active) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)") + ->execute([$slug,$name,$purl,$agent_link,$agent_login,$agent_password,$games_link,$agent_guide,$sub_agent_login,$sub_agent_password,$cashier_login,$cashier_password,$color,$sort,$active]); + echo json_encode(['success'=>true,'id'=>db()->lastInsertId()]); + } catch (Exception $e) { echo json_encode(['success'=>false,'error'=>'Slug already in use by an active game']); } + } break; // ─── PLATFORMS: update ──────────────────────────────── @@ -841,13 +878,14 @@ switch ($action) { echo json_encode(['success'=>true]); break; - // ─── PLATFORMS: delete ──────────────────────────────── + // ─── PLATFORMS: soft-delete (archive) ──────────────── case 'platforms_delete': if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false]); exit; } + if ((int)($_SESSION['user_id'] ?? 0) !== MASTER_ADMIN_ID) { echo json_encode(['success'=>false,'error'=>'Only master admin can archive games']); exit; } $d = json_decode(file_get_contents('php://input'), true); $id = (int)($d['id'] ?? 0); if (!$id) { echo json_encode(['success'=>false,'error'=>'ID required']); exit; } - db()->prepare("DELETE FROM platforms WHERE id=?")->execute([$id]); + db()->prepare("UPDATE platforms SET is_deleted=1, deleted_at=NOW(), updated_at=NOW() WHERE id=?")->execute([$id]); echo json_encode(['success'=>true]); break; case 'billing_get': diff --git a/api/platforms.php b/api/platforms.php index 0cf8929..8ccb59c 100644 --- a/api/platforms.php +++ b/api/platforms.php @@ -12,7 +12,7 @@ switch ($action) { // ── Public: active platforms for player app ─────────── case 'list': - $stmt = db()->query("SELECT slug,name,player_url,color,icon_path FROM platforms WHERE is_active=1 ORDER BY sort_order ASC, id ASC"); + $stmt = db()->query("SELECT slug,name,player_url,color,icon_path FROM platforms WHERE is_active=1 AND is_deleted=0 ORDER BY sort_order ASC, id ASC"); $rows = $stmt->fetchAll(); // Normalize to match old CFG format $out = array_map(fn($r) => [ @@ -27,7 +27,7 @@ switch ($action) { // ── Admin: full list including agent fields and inactive ─ case 'admin_list': if (!$isAdmin) { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; } - $rows = db()->query("SELECT * FROM platforms ORDER BY sort_order ASC, id ASC")->fetchAll(); + $rows = db()->query("SELECT * FROM platforms WHERE is_deleted=0 ORDER BY sort_order ASC, id ASC")->fetchAll(); echo json_encode(['success'=>true, 'platforms'=>$rows]); break;