mirror of
https://github.com/myronblair/tomtomgames
synced 2026-06-30 17:51:08 -05:00
Game Management: soft-delete, slug reuse, new game form fix, master-admin gating
- DB: added is_deleted, deleted_at columns to platforms table - Soft delete: archive button moves games to archived section instead of hard delete - Archived section: master admin can restore (reactivates) or permanently delete - Slug reuse: creating a game with an archived slug reactivates the old record - New game form: master admin always sees add form + agent info; other admins hidden - Edit: non-master admins have form card revealed on edit - Delete/Add buttons: only visible to master admin - api/platforms.php: public and admin_list queries exclude archived games - api/admin.php: platforms_archived, platforms_restore, platforms_purge actions added Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+72
-9
@@ -774,8 +774,8 @@ tr:hover td{background:rgba(255,255,255,.015)}
|
||||
<div class="section" id="section-games">
|
||||
<div class="page-title">🕹️ Game Management</div>
|
||||
|
||||
<!-- Add / Edit form -->
|
||||
<div class="card" id="game-form-card" style="margin-bottom:16px">
|
||||
<!-- 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="">
|
||||
@@ -910,6 +910,17 @@ tr:hover td{background:rgba(255,255,255,.015)}
|
||||
</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 ─────────────────────────────────── -->
|
||||
@@ -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() {
|
||||
<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>
|
||||
<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)}')">🗑</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('');
|
||||
@@ -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='<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');
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
+48
-10
@@ -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':
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user