mirror of
https://github.com/myronblair/tomtomgames
synced 2026-06-30 17:51:08 -05:00
Auto-debit platform credits when purchase is approved
When a pending purchase is resolved as completed: - Inserts a debit row into platform_credits for the matching platform (joins token_purchases.platform_id slug → platforms.id) - Debit notes include purchase #, player name, username, token count, amount, method - Total shown in credit modal now subtracts debits from credits (net balance) Credit history table updates: - CREDIT/DEBIT type badges, debit rows tinted red with − prefix - Debit rows show "Purchase #X ↗" button that closes modal, jumps to the Purchases section (all tab), and highlights that purchase row - Edit/delete buttons hidden on auto-generated debit rows Also fixes: resolve_purchase was echoing $sent (undefined variable bug) Also fixes: purchaseCard div now has id="pr-N" so jump-highlight works Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+43
-12
@@ -1228,7 +1228,7 @@ function purchaseCard(p, showActions=true) {
|
||||
'<button class="btn btn-red" data-pid="'+p.id+'" onclick="resolvePurchase(this.dataset.pid,"failed")" 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" style="margin-bottom:10px;'+(isPending?'border-color:rgba(240,192,64,.25);background:rgba(240,192,64,.02)':'')+'">'+
|
||||
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">'+
|
||||
@@ -3173,22 +3173,37 @@ async function loadCreditEntries() {
|
||||
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">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=>`<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('')}
|
||||
${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>`;
|
||||
}
|
||||
|
||||
@@ -3249,6 +3264,22 @@ async function deleteCreditEntry(id) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Sync hex input with color picker
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const picker = document.getElementById('gf-color');
|
||||
|
||||
+20
-4
@@ -104,17 +104,33 @@ switch ($action) {
|
||||
db()->beginTransaction();
|
||||
try {
|
||||
if ($status === 'completed') {
|
||||
// Credit tokens to user
|
||||
logAdminAction('TOKENS_ADJUSTED', $adminId, 'user', isset($targetId)?(int)$targetId:0, 'Manual token adjustment: '.($data['tokens']??0).' tokens', '', ($data['tokens']??''), 'critical');
|
||||
db()->prepare("UPDATE users SET tokens=tokens+? WHERE id=?")->execute([$purchase['tokens'], $purchase['user_id']]);
|
||||
}
|
||||
db()->prepare("UPDATE token_purchases SET status=?,admin_note=? WHERE id=?")->execute([$status, $note, $id]);
|
||||
db()->commit();
|
||||
echo json_encode($sent ? ['success'=>true] : ['success'=>false,'error'=>'Failed to send reset email. Please try again.']);
|
||||
} catch (Exception $e) {
|
||||
db()->rollBack();
|
||||
echo json_encode(['success'=>false,'error'=>'DB error']);
|
||||
echo json_encode(['success'=>false,'error'=>'DB error']); exit;
|
||||
}
|
||||
|
||||
// Insert debit entry into platform_credits when approved
|
||||
if ($status === 'completed' && !empty($purchase['platform_id'])) {
|
||||
$platRow = db()->prepare("SELECT id FROM platforms WHERE slug=?");
|
||||
$platRow->execute([$purchase['platform_id']]);
|
||||
$platNumId = (int)$platRow->fetchColumn();
|
||||
if ($platNumId) {
|
||||
$userRow = db()->prepare("SELECT username FROM users WHERE id=?");
|
||||
$userRow->execute([$purchase['user_id']]);
|
||||
$username = $userRow->fetchColumn() ?: 'User#'.$purchase['user_id'];
|
||||
$amtDollars = number_format($purchase['amount_cents'] / 100, 2);
|
||||
$playerLabel = trim($purchase['player_name'] ?: $purchase['game_alias'] ?: $username);
|
||||
$debitNotes = "Purchase #{$id} · {$playerLabel} ({$username}) · {$purchase['tokens']} tokens · \${$amtDollars} via {$purchase['payment_method']}";
|
||||
db()->prepare("INSERT INTO platform_credits (platform_id, credits_purchased, credit_date, payment_method, notes, type, purchase_ref_id) VALUES (?,?,CURDATE(),?,?,?,?)")
|
||||
->execute([$platNumId, $purchase['tokens'], $purchase['payment_method'], $debitNotes, 'debit', $id]);
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(['success'=>true]);
|
||||
break;
|
||||
|
||||
// ─── CASHOUTS ─────────────────────────────────────────────
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@ switch ($action) {
|
||||
$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 = db()->prepare("SELECT COALESCE(SUM(CASE WHEN type='debit' THEN -credits_purchased ELSE credits_purchased END),0) FROM platform_credits WHERE platform_id=?");
|
||||
$total->execute([$pid]);
|
||||
echo json_encode(['success'=>true,'credits'=>$credits,'total'=>(float)$total->fetchColumn()]);
|
||||
break;
|
||||
|
||||
@@ -282,6 +282,27 @@ CREATE TABLE `platforms` (
|
||||
UNIQUE KEY `slug` (`slug`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `platform_credits`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `platform_credits` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`platform_id` int(11) NOT NULL,
|
||||
`credits_purchased` decimal(12,2) NOT NULL DEFAULT 0.00,
|
||||
`credit_date` date NOT NULL,
|
||||
`payment_method` varchar(100) DEFAULT NULL,
|
||||
`notes` text DEFAULT NULL,
|
||||
`type` enum('credit','debit') NOT NULL DEFAULT 'credit',
|
||||
`purchase_ref_id` int(11) DEFAULT NULL,
|
||||
`created_at` datetime DEFAULT current_timestamp(),
|
||||
`updated_at` datetime DEFAULT current_timestamp() ON UPDATE current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `platform_id` (`platform_id`),
|
||||
KEY `credit_date` (`credit_date`),
|
||||
KEY `idx_pc_type` (`type`),
|
||||
KEY `idx_pc_ref` (`purchase_ref_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
DROP TABLE IF EXISTS `referral_social_shares`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
|
||||
Reference in New Issue
Block a user