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:
2026-06-04 18:45:31 +00:00
parent 185c27f6b4
commit 0c96b0ad7c
3 changed files with 122 additions and 21 deletions
+48 -10
View File
@@ -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
View File
@@ -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;