mirror of
https://github.com/myronblair/tomtomgames
synced 2026-06-30 17:51:08 -05:00
Add Credit Accounting section to game management
- New table: platform_credits (id, platform_id, credits_purchased, credit_date, payment_method, notes) - API: credits_list, credits_create, credits_update, credits_delete actions (admin-only) - Admin form: Credit Accounting box showing Available Credits total; Manage Credits button opens modal - Modal: Total Credits header, add/edit/delete entries with credits, date, payment method, notes - Game list cards: show live credit total per game (cyan, loads async) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+203
-1
@@ -845,6 +845,22 @@ tr:hover td{background:rgba(255,255,255,.015)}
|
||||
</div>
|
||||
</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>
|
||||
@@ -1015,6 +1031,63 @@ tr:hover td{background:rgba(255,255,255,.015)}
|
||||
</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">
|
||||
@@ -2789,7 +2862,8 @@ async function loadGames() {
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align:right;flex-shrink:0">
|
||||
<div style="font-size:15px;color:var(--text2);margin-bottom:6px">Order: ${g.sort_order}</div>
|
||||
<div style="font-size:15px;color:var(--text2);margin-bottom:4px">Order: ${g.sort_order}</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>
|
||||
<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>
|
||||
@@ -2798,6 +2872,16 @@ async function loadGames() {
|
||||
</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) {
|
||||
@@ -2822,6 +2906,14 @@ function editGame(id) {
|
||||
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;
|
||||
document.getElementById('gf-credit-btn').disabled = false;
|
||||
// Load credit total for this platform
|
||||
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});
|
||||
}
|
||||
});
|
||||
document.getElementById('game-form-card').scrollIntoView({behavior:'smooth'});
|
||||
}
|
||||
|
||||
@@ -2844,6 +2936,8 @@ function resetGameForm() {
|
||||
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;
|
||||
document.getElementById('game-form-title').textContent = '➕ Add New Game';
|
||||
document.getElementById('game-form-alert').className = 'alert';
|
||||
}
|
||||
@@ -2894,6 +2988,114 @@ async function deleteGame(id, name) {
|
||||
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:right;color:var(--text2);font-size:12px;font-weight:700;letter-spacing:.5px">CREDITS</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=>`<tr style="border-bottom:1px solid rgba(255,255,255,0.05)">
|
||||
<td style="padding:9px 6px;color:var(--text)">${escHtmlA(c.credit_date)}</td>
|
||||
<td style="padding:9px 6px;text-align:right;font-family:'Exo 2',sans-serif;font-weight:700;color:var(--cyan)">${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:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escHtmlA(c.notes||'')}</td>
|
||||
<td style="padding:9px 6px;text-align:right;white-space:nowrap">
|
||||
<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>
|
||||
</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 apiFetch(action,'POST',payload);
|
||||
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 apiFetch('credits_delete','POST',{id});
|
||||
if (d.success) { toast('Entry deleted','ok'); await loadCreditEntries(); }
|
||||
else toast(d.error||'Error','err');
|
||||
}
|
||||
|
||||
// Sync hex input with color picker
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const picker = document.getElementById('gf-color');
|
||||
|
||||
@@ -104,6 +104,72 @@ switch ($action) {
|
||||
echo json_encode(['success'=>true]);
|
||||
break;
|
||||
|
||||
// ── Admin: list credits for a platform ───────────────
|
||||
case 'credits_list':
|
||||
if (!$isAdmin) { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
|
||||
$pid = (int)($_GET['platform_id'] ?? 0);
|
||||
if (!$pid) { echo json_encode(['success'=>false,'error'=>'platform_id required']); exit; }
|
||||
$rows = db()->prepare("SELECT * FROM platform_credits WHERE platform_id=? ORDER BY credit_date DESC, id DESC");
|
||||
$rows->execute([$pid]);
|
||||
$credits = $rows->fetchAll();
|
||||
$total = db()->prepare("SELECT COALESCE(SUM(credits_purchased),0) FROM platform_credits WHERE platform_id=?");
|
||||
$total->execute([$pid]);
|
||||
echo json_encode(['success'=>true,'credits'=>$credits,'total'=>(float)$total->fetchColumn()]);
|
||||
break;
|
||||
|
||||
// ── Admin: add credit entry ───────────────────────────
|
||||
case 'credits_create':
|
||||
if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
|
||||
$d = json_decode(file_get_contents('php://input'), true);
|
||||
$pid = (int)($d['platform_id'] ?? 0);
|
||||
$credits = (float)($d['credits_purchased'] ?? 0);
|
||||
$date = $d['credit_date'] ?? date('Y-m-d');
|
||||
$method = substr(trim($d['payment_method'] ?? ''), 0, 100);
|
||||
$notes = trim($d['notes'] ?? '');
|
||||
if (!$pid || $credits <= 0 || !$date) { echo json_encode(['success'=>false,'error'=>'platform_id, credits_purchased, and credit_date are required']); exit; }
|
||||
$stmt = db()->prepare("INSERT INTO platform_credits (platform_id,credits_purchased,credit_date,payment_method,notes) VALUES (?,?,?,?,?)");
|
||||
$stmt->execute([$pid,$credits,$date,$method,$notes]);
|
||||
$newId = db()->lastInsertId();
|
||||
$total = db()->prepare("SELECT COALESCE(SUM(credits_purchased),0) FROM platform_credits WHERE platform_id=?");
|
||||
$total->execute([$pid]);
|
||||
echo json_encode(['success'=>true,'id'=>$newId,'total'=>(float)$total->fetchColumn()]);
|
||||
break;
|
||||
|
||||
// ── Admin: update credit entry ────────────────────────
|
||||
case 'credits_update':
|
||||
if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); exit; }
|
||||
$d = json_decode(file_get_contents('php://input'), true);
|
||||
$id = (int)($d['id'] ?? 0);
|
||||
$credits = (float)($d['credits_purchased'] ?? 0);
|
||||
$date = $d['credit_date'] ?? date('Y-m-d');
|
||||
$method = substr(trim($d['payment_method'] ?? ''), 0, 100);
|
||||
$notes = trim($d['notes'] ?? '');
|
||||
if (!$id || $credits <= 0 || !$date) { echo json_encode(['success'=>false,'error'=>'id, credits_purchased, and credit_date are required']); exit; }
|
||||
db()->prepare("UPDATE platform_credits SET credits_purchased=?,credit_date=?,payment_method=?,notes=? WHERE id=?")
|
||||
->execute([$credits,$date,$method,$notes,$id]);
|
||||
$row = db()->prepare("SELECT platform_id FROM platform_credits WHERE id=?");
|
||||
$row->execute([$id]);
|
||||
$pid = (int)($row->fetchColumn() ?: 0);
|
||||
$total = db()->prepare("SELECT COALESCE(SUM(credits_purchased),0) FROM platform_credits WHERE platform_id=?");
|
||||
$total->execute([$pid]);
|
||||
echo json_encode(['success'=>true,'total'=>(float)$total->fetchColumn()]);
|
||||
break;
|
||||
|
||||
// ── Admin: delete credit entry ────────────────────────
|
||||
case 'credits_delete':
|
||||
if (!$isAdmin || $_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['success'=>false,'error'=>'Forbidden']); 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; }
|
||||
$row = db()->prepare("SELECT platform_id FROM platform_credits WHERE id=?");
|
||||
$row->execute([$id]);
|
||||
$pid = (int)($row->fetchColumn() ?: 0);
|
||||
db()->prepare("DELETE FROM platform_credits WHERE id=?")->execute([$id]);
|
||||
$total = db()->prepare("SELECT COALESCE(SUM(credits_purchased),0) FROM platform_credits WHERE platform_id=?");
|
||||
$total->execute([$pid]);
|
||||
echo json_encode(['success'=>true,'total'=>(float)$total->fetchColumn()]);
|
||||
break;
|
||||
|
||||
default:
|
||||
echo json_encode(['success'=>false,'error'=>'Unknown action']);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user