feat: include balance for home page
This commit is contained in:
@@ -99,28 +99,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stats row -->
|
|
||||||
<div class="row g-3 mb-3">
|
|
||||||
<div class="col-4">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-label">Real Sends</div>
|
|
||||||
<div class="stat-value" id="stat-real" style="color:#4ade80;">—</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-4">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-label">Sandbox Sends</div>
|
|
||||||
<div class="stat-value" id="stat-sandbox" style="color:var(--text-muted);">—</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-4">
|
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-label">Total Logged</div>
|
|
||||||
<div class="stat-value" id="stat-total">—</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="btn btn-sm btn-secondary" onclick="markAllSandbox()">
|
<button class="btn btn-sm btn-secondary" onclick="markAllSandbox()">
|
||||||
<i class="fa-solid fa-flask me-1"></i>
|
<i class="fa-solid fa-flask me-1"></i>
|
||||||
Mark all existing sends as sandbox
|
Mark all existing sends as sandbox
|
||||||
@@ -227,10 +205,6 @@
|
|||||||
document.getElementById('sandboxToggle').checked = data.sandbox_mode;
|
document.getElementById('sandboxToggle').checked = data.sandbox_mode;
|
||||||
document.getElementById('sandboxBanner').style.display = data.sandbox_mode ? '' : 'none';
|
document.getElementById('sandboxBanner').style.display = data.sandbox_mode ? '' : 'none';
|
||||||
|
|
||||||
document.getElementById('stat-real').textContent = data.delivery_log_real_sent ?? '—';
|
|
||||||
document.getElementById('stat-sandbox').textContent = data.delivery_log_sandbox ?? '—';
|
|
||||||
document.getElementById('stat-total').textContent = data.delivery_log_total ?? '—';
|
|
||||||
|
|
||||||
document.getElementById('count-log').textContent = data.delivery_log_total ?? '—';
|
document.getElementById('count-log').textContent = data.delivery_log_total ?? '—';
|
||||||
document.getElementById('count-attendees').textContent = data.attendees ?? '—';
|
document.getElementById('count-attendees').textContent = data.attendees ?? '—';
|
||||||
document.getElementById('count-templates').textContent = data.templates ?? '—';
|
document.getElementById('count-templates').textContent = data.templates ?? '—';
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/db.php';
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||||
|
error_out('Method not allowed', 405);
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = db();
|
||||||
|
$row = $db->query(
|
||||||
|
"SELECT setting_value FROM `system` WHERE setting_name = 'clickatell_api_key'"
|
||||||
|
)->fetch();
|
||||||
|
|
||||||
|
$api_key = $row['setting_value'] ?? '';
|
||||||
|
if ($api_key === '') {
|
||||||
|
json_out(['error' => 'API key not configured', 'balance' => null]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$ch = curl_init('https://platform.clickatell.com/public-client/balance');
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => 10,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'Authorization: ' . $api_key,
|
||||||
|
'Accept: application/json',
|
||||||
|
'Content-Type: application/json',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$body = curl_exec($ch);
|
||||||
|
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$err = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($err !== '') {
|
||||||
|
json_out(['error' => 'Request failed: ' . $err, 'balance' => null]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($body ?: '', true);
|
||||||
|
|
||||||
|
if ($status !== 200 || $data === null) {
|
||||||
|
json_out(['error' => 'Unexpected response (HTTP ' . $status . ')', 'balance' => null, 'raw' => $body]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$credit = null;
|
||||||
|
$currency = $data['currency'] ?? null;
|
||||||
|
|
||||||
|
if (isset($data['balance']) && is_numeric($data['balance'])) {
|
||||||
|
// Flat format: {"balance": 89.99, "currency": "GBP"}
|
||||||
|
$credit = $data['balance'];
|
||||||
|
} elseif (isset($data['balance']) && is_array($data['balance']) && isset($data['balance'][0]['credit'])) {
|
||||||
|
// Array format: {"balance": [{"credit": "99.50", ...}]}
|
||||||
|
$credit = $data['balance'][0]['credit'];
|
||||||
|
$currency = $currency ?? ($data['balance'][0]['currency'] ?? null);
|
||||||
|
} elseif (isset($data['credit'])) {
|
||||||
|
$credit = $data['credit'];
|
||||||
|
} elseif (isset($data['data']['credit'])) {
|
||||||
|
$credit = $data['data']['credit'];
|
||||||
|
}
|
||||||
|
|
||||||
|
json_out(['balance' => $credit, 'currency' => $currency]);
|
||||||
+53
-9
@@ -92,9 +92,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 col-sm-3">
|
<div class="col-6 col-sm-3">
|
||||||
<div class="stat-card border-0">
|
<div class="stat-card border-0" id="spendCard" role="button" tabindex="0" style="cursor:pointer;">
|
||||||
<div class="stat-label">Estimated Cost</div>
|
<div class="stat-label" id="stat-spend-label">Balance</div>
|
||||||
<div class="stat-value" id="stat-cost">—</div>
|
<div class="stat-value" id="stat-spend">—</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -280,23 +280,67 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let spendView = 'balance';
|
||||||
|
let spendData = { balance: null, currency: null, estimatedCost: null };
|
||||||
|
|
||||||
|
function formatCurrency(amount, currency) {
|
||||||
|
if (amount == null || Number.isNaN(Number(amount))) return '—';
|
||||||
|
const sym = currency === 'GBP' ? '£' : currency === 'USD' ? '$' : currency === 'EUR' ? '€' : (currency ?? '');
|
||||||
|
return sym + Number(amount).toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSpendCard() {
|
||||||
|
const labelEl = document.getElementById('stat-spend-label');
|
||||||
|
const valueEl = document.getElementById('stat-spend');
|
||||||
|
if (!labelEl || !valueEl) return;
|
||||||
|
|
||||||
|
if (spendView === 'cost') {
|
||||||
|
labelEl.textContent = 'EST. COST';
|
||||||
|
valueEl.textContent = formatCurrency(spendData.estimatedCost, spendData.currency);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
labelEl.textContent = 'Balance';
|
||||||
|
valueEl.textContent = formatCurrency(spendData.balance, spendData.currency);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSpendCard() {
|
||||||
|
spendView = spendView === 'balance' ? 'cost' : 'balance';
|
||||||
|
renderSpendCard();
|
||||||
|
}
|
||||||
|
|
||||||
async function loadStats() {
|
async function loadStats() {
|
||||||
try {
|
try {
|
||||||
const [att, log] = await Promise.all([
|
const [att, log, bal, cost, admin] = await Promise.all([
|
||||||
fetch('/api/attendees.php').then(r => r.json()),
|
fetch('/api/attendees.php').then(r => r.json()),
|
||||||
fetch('/api/delivery-log.php').then(r => r.json()),
|
fetch('/api/delivery-log.php').then(r => r.json()),
|
||||||
|
fetch('/api/balance.php').then(r => r.json()),
|
||||||
|
fetch('/api/settings.php?key=clickatell_cost_per_sms').then(r => r.json()),
|
||||||
|
fetch('/api/admin.php').then(r => r.json()),
|
||||||
]);
|
]);
|
||||||
document.getElementById('stat-attendees').textContent = att.total ?? '—';
|
document.getElementById('stat-attendees').textContent = att.total ?? '—';
|
||||||
document.getElementById('stat-sent').textContent = log.stats?.total_sent ?? '—';
|
document.getElementById('stat-sent').textContent = log.stats?.total_sent ?? '—';
|
||||||
const rate = log.stats?.success_rate;
|
const rate = log.stats?.success_rate;
|
||||||
document.getElementById('stat-rate').textContent = rate != null ? rate + '%' : '—';
|
document.getElementById('stat-rate').textContent = rate != null ? rate + '%' : '—';
|
||||||
const sent = log.stats?.total_sent;
|
spendData.balance = bal.balance != null ? Number(bal.balance) : null;
|
||||||
const costPerSms = parseFloat((await fetch('/api/settings.php?key=clickatell_cost_per_sms').then(r => r.json())).value ?? '0.04849');
|
spendData.currency = bal.currency ?? 'GBP';
|
||||||
document.getElementById('stat-cost').textContent = sent != null
|
|
||||||
? '£' + (sent * costPerSms).toFixed(2)
|
const realSent = Number(admin.delivery_log_real_sent);
|
||||||
: '—';
|
const costPerSms = Number(cost.value);
|
||||||
|
spendData.estimatedCost = Number.isFinite(realSent) && Number.isFinite(costPerSms)
|
||||||
|
? realSent * costPerSms
|
||||||
|
: null;
|
||||||
|
|
||||||
|
renderSpendCard();
|
||||||
} catch { /* DB unavailable — stats stay as dashes */ }
|
} catch { /* DB unavailable — stats stay as dashes */ }
|
||||||
}
|
}
|
||||||
|
document.getElementById('spendCard')?.addEventListener('click', toggleSpendCard);
|
||||||
|
document.getElementById('spendCard')?.addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
toggleSpendCard();
|
||||||
|
}
|
||||||
|
});
|
||||||
loadStats();
|
loadStats();
|
||||||
setInterval(() => loadStats(), 10_000);
|
setInterval(() => loadStats(), 10_000);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user