Good-morning

Dashboard

Kontrol sesi, tiket, transaksi, dan akses user dalam satu layar.

<%= new Date().toLocaleDateString('id-ID', { day:'2-digit', month:'short', year:'numeric' }) %>
<% if (typeof success !== 'undefined' && success) { %>
<%= success %>
<% } %> <% if (typeof error !== 'undefined' && error) { %>
<%= error %>
<% } %>
<% const userLookup = new Map((users || []).map(u => [String(u.uid || ''), u])); const rawTransactionLogs = Array.isArray(transactionLogs) ? transactionLogs : []; const mappedTransactionLogs = rawTransactionLogs.map(log => { const owner = userLookup.get(String(log.userId || '')) || {}; const userName = log.username || owner.username || log.donorName || 'Supporter'; const idGame = log.mlId ? `${log.mlId}${log.mlServer ? ` (${log.mlServer})` : ''}` : (owner.mlId ? `${owner.mlId}${owner.mlServer ? ` (${owner.mlServer})` : ''}` : '-'); const activityType = String(log.activityType || '').toLowerCase(); const type = log.activityLabel || ( activityType === 'ticket' ? 'Beli Ticket' : 'Donate Biasa' ); const tone = String(log.status || '').toLowerCase() === 'pending' ? 'pending' : (activityType === 'ticket' ? 'buy' : 'donate'); const status = String(log.status || '').toLowerCase() === 'pending' ? 'pending' : 'success'; return { at: Number(log.createdAt || log.updatedAt || 0), userName, idGame, type, tone, status }; }).filter(log => Number(log.at || 0) > 0); const combinedTransactionLogs = mappedTransactionLogs .sort((a, b) => Number(b.at || 0) - Number(a.at || 0)); const rawWebhookReceipts = Array.isArray(webhookReceipts) ? webhookReceipts : []; const shortenMiddle = (value, start = 18, end = 8) => { const text = String(value || '-'); return text.length > start + end + 3 ? `${text.slice(0, start)}...${text.slice(-end)}` : text; }; const getDecisionLabel = decision => ({ ticket_issued: 'Issued', ticket_recovered: 'Recovered', pending_missing_ml_id: 'Pending ID', pending_ml_id_not_registered: 'Pending Akun', pending_user_not_found: 'Pending User', pending_manual_review: 'Review', ignored_non_ticket: 'Donate', ignored_unpaid: 'Unpaid', ignored_invalid_auth: 'Auth Fail', duplicate_ticket: 'Duplicate', duplicate_success_log: 'Duplicate', test_notification: 'Test', health_empty_post: 'Health', processed: 'Processed', received: 'Received' }[String(decision || '')] || String(decision || 'received').replace(/^pending_/, 'Pending ').replace(/^ignored_/, 'Ignore ').replace(/_/g, ' ')); const getDecisionTone = decision => { const text = String(decision || ''); if (text === 'ticket_issued' || text === 'ticket_recovered') return 'is-buy'; if (text.startsWith('pending_')) return 'is-pending'; if (text.startsWith('ignored_') || text.startsWith('duplicate_')) return 'is-use'; if (text === 'test_notification') return 'is-donate'; return ''; }; const getIssueHint = receipt => { const decision = String(receipt.decision || ''); if (decision === 'pending_missing_ml_id') return 'ID ML belum terbaca'; if (decision === 'pending_ml_id_not_registered') return 'Akun belum terdaftar'; if (decision === 'ignored_invalid_auth') return 'Token notifikasi salah'; if (decision === 'ignored_non_ticket') return 'Nominal bukan tiket'; if (decision.indexOf('duplicate') === 0) return 'Duplikat lama'; return receipt.message || receipt.reason || 'Butuh cek admin'; }; const combinedWebhookReceipts = rawWebhookReceipts.map(receipt => ({ at: Number(receipt.createdAt || receipt.updatedAt || 0), decision: receipt.decision || 'received', decisionLabel: getDecisionLabel(receipt.decision || 'received'), decisionTone: getDecisionTone(receipt.decision || 'received'), auth: receipt.hasToken || receipt.hasSignature ? 'ada' : 'tidak ada', paymentRef: receipt.paymentRef || '-', paymentRefShort: shortenMiddle(receipt.paymentRef || '-', 18, 8), amount: Number(receipt.amount || 0), ml: receipt.mlId ? `${receipt.mlId}${receipt.mlServer ? ` (${receipt.mlServer})` : ''}` : '-', message: receipt.reason || receipt.messagePreview || '-', response: receipt.responseStatus ? `${receipt.responseStatus} ${receipt.responseBody || ''}` : '-' })).map(receipt => ({ ...receipt, issueHint: getIssueHint(receipt) })).filter(receipt => Number(receipt.at || 0) > 0) .sort((a, b) => Number(b.at || 0) - Number(a.at || 0)); const totalTicketsCount = tickets.length; const activeTicketsCount = tickets.filter(t => t.status === 'active').length; const usedTicketsCount = tickets.filter(t => t.status === 'used').length; const ticketSalesCount = tickets.filter(t => t.status === 'active' || t.status === 'used').length; const pendingTransactionCount = combinedTransactionLogs.filter(log => log.status === 'pending').length; const issueRows = combinedWebhookReceipts .filter(receipt => String(receipt.decision || '').indexOf('ignored') === 0 || String(receipt.decision || '').indexOf('pending') === 0 || receipt.auth === 'tidak ada') .slice(0, 4); %>

Total Pengguna

<%= users.length %>
Akun terdaftar

Total Ticket

<%= totalTicketsCount %>
Semua tiket

Ticket Terpakai

<%= usedTicketsCount %>
Sudah dipakai

Perlu Cek

<%= pendingTransactionCount + issueRows.length %>
Butuh cek admin

Penjualan Tiket Terbaru

5 Transaksi Terakhir
<% const sortedTickets = (tickets || []).slice() .sort((a, b) => new Date(b.purchasedAt || b.createdAt) - new Date(a.purchasedAt || a.createdAt)) .slice(0, 5); if (sortedTickets.length) { sortedTickets.forEach(t => { const buyer = userLookup.get(String(t.userId || '')) || {}; const dateStr = new Date(t.purchasedAt || t.createdAt).toLocaleDateString('id-ID', { day:'2-digit', month:'short', hour:'2-digit', minute:'2-digit' }); const ref = String(t.paymentRef || '-'); const refShort = ref.length > 10 ? ref.substring(0, 8) + '...' : ref; %> <% }); } else { %> <% } %>
Waktu Pembeli Qty Ref Status
<%= dateStr %> <%= buyer.username || 'User' %> 1 <%= refShort %> <%= String(t.status || '').toUpperCase() %>
Belum ada penjualan tiket.
Operational Pulse <%= issueRows.length ? 'Ada issue yang perlu dicek' : 'Semua jalur utama terlihat aman' %>
<%= activeTicketsCount %> tiket aktif <%= combinedTransactionLogs.length %> log transaksi <%= issueRows.length %> issue notifikasi

Semua Transaksi Logs

30 terbaru
<% if (combinedTransactionLogs.length) { %> <% combinedTransactionLogs.slice(0, 30).forEach(log => { %> <% }) %> <% } else { %> <% } %>
Waktu Pengguna ID Game Aktivitas Status
<%= new Date(log.at).toLocaleString('id-ID', { day:'2-digit', month:'short', hour:'2-digit', minute:'2-digit' }) %> <%= log.userName %> <%= log.idGame %> <%= log.type %> <%= log.status %>
Belum ada transaksi.
<% if (typeof webhookDebugEnabled !== 'undefined' && webhookDebugEnabled) { %>

Log Notifikasi Pembayaran

Log teknis ini mencatat setiap notifikasi SociaBuzz yang sampai ke backend, termasuk request yang ditolak/diabaikan sebelum menjadi transaksi.

<% if (combinedWebhookReceipts.length) { %> <% combinedWebhookReceipts.slice(0, 30).forEach(receipt => { %> <% }) %> <% } else { %> <% } %>
Waktu Keputusan Auth Ref Nominal ID Game Alasan Resp
<%= new Date(receipt.at).toLocaleString('id-ID', { day:'2-digit', month:'short', hour:'2-digit', minute:'2-digit' }) %> <%= receipt.decisionLabel %> <%= receipt.auth %> <%= receipt.paymentRefShort %> <%= receipt.amount ? receipt.amount.toLocaleString('id-ID') : '-' %> <%= receipt.ml %> <%= receipt.message %> <%= receipt.response %>
Belum ada notifikasi pembayaran yang tercatat.
<% } %>

Konfigurasi Sesi Aktif

Buka Panel Match

Jam Empat Sesi - Hari Ini

Atur jam bebas untuk hari yang dipilih. Jumlah sesi tetap empat.
Sesi 1
Sesi 2
Sesi 3
Sesi 4

Terapkan Template

Pilih template untuk dipasang langsung ke hari pilihan (Hari Ini).
<% if (typeof templates !== 'undefined' && templates && Object.keys(templates).length > 0) { %>
Kelola Template:
<% Object.entries(templates).forEach(([id, t]) => { %>
<%= t.name %>
<% }) %>
<% } %>

Simpan Sebagai Template

Simpan konfigurasi jam & rank saat ini menjadi template baru.

Status Jadwal (Hari Ini - Sesi 4)

Player ID Game Role Tiket Daftar

Tambah Peserta Manual

Cari akun aktif lewat username minimal 2 huruf atau ID MLBB full. Jika tidak dipilih dari hasil cari, data masuk sebagai akun temp.

Filter Sesi

Daftar Peserta Sesi (Hari Ini - Sesi 4)

PlayerID GameRoleMMR

Manajemen Tiket

Ringkasan tiket: Aktif (bisa dipakai), Terpakai (sudah dipakai), Expired (kedaluwarsa).
Username ID Game Role Tiket Aktif Riwayat Aksi

Pengguna

<% if (users && users.length > 0) { %> <% users.forEach(u => { const wa=String(u.whatsapp||'').trim(); %> <% }) %> <% } else { %> <% } %>
UsernameID GameRole MLWhatsAppEmailDaftar Pada
<%= u.username %> <%= u.mlId %> (<%= u.mlServer %>) <%= u.roleMl || 'Belum diatur' %> <%= new Date(u.createdAt).toLocaleDateString('id-ID') %>
Belum ada user terdaftar.