Files
imf-sms-dashboard/web/delivery-log.html
T

269 lines
9.9 KiB
HTML

<!doctype html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Delivery Log — IMFestival SMS Dashboard</title>
<link href="assets/vendor/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" rel="stylesheet"
crossorigin="anonymous">
<link href="assets/css/dashboard.css" rel="stylesheet">
<link href="https://cdn.datatables.net/2.3.8/css/dataTables.dataTables.min.css" rel="stylesheet">
</head>
<body>
<!-- ─── Mobile top bar ─── -->
<div class="topbar">
<span style="font-weight:600;font-size:0.95rem;">Delivery Log</span>
<span class="nav-username ms-auto" style="font-size:0.82rem;color:var(--text-muted);">User</span>
</div>
<!-- ─── Desktop sidebar ─── -->
<div class="sidebar">
<div class="brand">
<span style="margin: 0 auto;">IMFestival SMS Dashboard</span>
</div>
<nav class="d-flex flex-column gap-1 mt-2">
<a href="index.html" class="nav-link-custom">
<i class="fa-solid fa-house fa-fw"></i>
Home
</a>
<a href="attendees.html" class="nav-link-custom">
<i class="fa-solid fa-users fa-fw"></i>
Attendees
</a>
<a href="templates.html" class="nav-link-custom">
<i class="fa-solid fa-comment-sms fa-fw"></i>
SMS Templates
</a>
<a href="schedule.html" class="nav-link-custom">
<i class="fa-solid fa-calendar-days fa-fw"></i>
Schedule
</a>
<a href="delivery-log.html" class="nav-link-custom active">
<i class="fa-solid fa-list-check fa-fw"></i>
Delivery Log
</a>
</nav>
<div class="mt-auto pt-3" style="border-top:1px solid var(--border);">
<div class="nav-link-custom" style="cursor:default;">
<i class="fa-solid fa-circle-user fa-fw"></i>
<span class="nav-username" style="font-size:0.82rem;">User</span>
</div>
</div>
</div>
<!-- ─── Main content ─── -->
<div class="main">
<div class="main-inner">
<div id="sandboxBanner" class="alert alert-warning"
style="display:none;font-size:0.85rem;font-weight:600;text-align:center;">
<i class="fa-solid fa-flask me-1"></i> Sandbox mode is active — texts are tagged as a test and excluded from
cost calculations.
</div>
<div class="page-header">
<h1>Delivery Log</h1>
<p>A record of all messages sent and failed.</p>
</div>
<!-- Summary stats -->
<div class="row g-3 mb-4">
<div class="col-4">
<div class="stat-card">
<div class="stat-label">Sent</div>
<div class="stat-value" id="stat-sent" style="color:#4ade80;"></div>
</div>
</div>
<div class="col-4">
<div class="stat-card">
<div class="stat-label">Failed</div>
<div class="stat-value" id="stat-failed" style="color:#f87171;"></div>
</div>
</div>
<div class="col-4">
<div class="stat-card">
<div class="stat-label">Success Rate</div>
<div class="stat-value" id="stat-rate"></div>
</div>
</div>
</div>
<div class="card p-4">
<div class="table-responsive">
<table class="table table-striped mb-0" id="logTable">
<thead>
<tr>
<th>Recipient</th>
<th>Template</th>
<th>Mobile</th>
<th>Status</th>
<th>Sent at</th>
</tr>
</thead>
<tbody id="logBody">
<tr>
<td colspan="5" class="text-muted text-center py-4">Loading…</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"
integrity="sha256-OaVG6prZf4v69dPg6PhVattBXkcOWQB62pdZ3ORyrao=" crossorigin="anonymous"></script>
<script src="https://cdn.datatables.net/2.3.8/js/dataTables.min.js"></script>
<script>
let logTable = null;
let _lastDataKey = '';
function esc(str) {
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function errorDesc(msg) {
if (!msg) return '';
try {
const parsed = JSON.parse(msg);
// Top-level error (older Clickatell responses)
if (parsed.error) return parsed.error;
// Clickatell v2: { messages: [{ error, errorDescription }] }
const m = Array.isArray(parsed.messages) ? parsed.messages[0] : null;
if (m?.error) return m.error;
if (m?.errorDescription) return m.errorDescription;
return parsed.message || msg;
} catch {
return msg;
}
}
function fmtDate(str) {
if (!str) return '—';
return new Date(str).toLocaleString('en-GB', {
day: 'numeric', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit'
});
}
async function loadLog(isRefresh = false) {
const tbody = document.getElementById('logBody');
try {
const data = await fetch('/api/delivery-log.php?all=1').then(r => r.json());
const stats = data.stats ?? {};
const entries = data.entries ?? [];
document.getElementById('stat-sent').textContent = stats.total_sent ?? '—';
document.getElementById('stat-failed').textContent = stats.total_failed ?? '—';
document.getElementById('stat-rate').textContent = stats.success_rate != null ? stats.success_rate + '%' : '—';
// Skip expensive DOM rebuild if nothing has changed
const dataKey = `${entries.length}:${stats.total_sent}:${stats.total_failed}`;
if (isRefresh && dataKey === _lastDataKey) return;
_lastDataKey = dataKey;
// Preserve current page position before destroying the table
const prevPage = logTable ? logTable.page() : 0;
// Destroy existing DataTable before rewriting DOM
if (logTable) { logTable.destroy(); logTable = null; }
if (entries.length === 0) {
tbody.innerHTML = `<tr><td colspan="5" class="text-muted text-center py-4">No messages sent yet.</td></tr>`;
return;
}
tbody.innerHTML = entries.map(e => {
const statusBadge = e.status === 'failed'
? `<span class="badge-failed" style="cursor:help;"${e.error_message ? ` data-bs-toggle="tooltip" data-bs-placement="left" data-bs-title="${esc(errorDesc(e.error_message))}"` : ''}>Failed</span>`
: `<span class="badge-delivered">Sent</span>`;
return `
<tr>
<td>${esc(e.attendee_name ?? '—')}</td>
<td style="color:var(--text-muted);">${e.message_body
? `<span style="cursor:help;border-bottom:1px dashed var(--text-muted);" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-title="${esc(e.message_body)}">${esc(e.template_name)}</span>`
: esc(e.template_name)}</td>
<td style="color:var(--text-muted);">${esc(e.mobile_number)}</td>
<td>${statusBadge}</td>
<td style="color:var(--text-muted);font-size:0.82rem;" data-order="${e.sent_at ?? ''}">${fmtDate(e.sent_at)}</td>
</tr>
`;
}).join('');
logTable = new DataTable('#logTable', {
order: [[4, 'desc']],
columnDefs: [{ orderable: false, targets: 3 }],
pageLength: 25,
drawCallback: () => {
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
bootstrap.Tooltip.getOrCreateInstance(el);
});
},
});
if (isRefresh && prevPage > 0 && prevPage < logTable.page.info().pages) {
logTable.page(prevPage).draw(false);
}
} catch {
tbody.innerHTML = `<tr><td colspan="5" class="text-danger text-center py-4">Could not load delivery log.</td></tr>`;
}
}
loadLog();
setInterval(() => loadLog(true), 10_000);
</script>
<script>
async function checkAuth() {
try {
const r = await fetch('/api/admin.php', { redirect: 'manual' });
if (r.type === 'opaqueredirect' || !r.ok) { window.location.reload(); return; }
const d = await r.json();
document.getElementById('sandboxBanner').style.display = d.sandbox_mode ? '' : 'none';
document.querySelectorAll('.admin-nav-link').forEach(el => el.classList.toggle('d-none', !d.is_admin));
const name = d.name || d.username;
if (name) {
document.querySelectorAll('.nav-username').forEach(el => { el.textContent = name; });
}
} catch { }
}
checkAuth();
document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') checkAuth(); });
</script>
<nav class="bottom-nav" aria-label="Navigation">
<a href="index.html" class="bottom-nav-item">
<i class="fa-solid fa-house"></i>
<span>Home</span>
</a>
<a href="attendees.html" class="bottom-nav-item">
<i class="fa-solid fa-users"></i>
<span>Attendees</span>
</a>
<a href="templates.html" class="bottom-nav-item">
<i class="fa-solid fa-comment-sms"></i>
<span>Templates</span>
</a>
<a href="schedule.html" class="bottom-nav-item">
<i class="fa-solid fa-calendar-days"></i>
<span>Schedule</span>
</a>
<a href="delivery-log.html" class="bottom-nav-item active">
<i class="fa-solid fa-list-check"></i>
<span>Log</span>
</a>
<a href="admin.html" class="bottom-nav-item admin-nav-link d-none">
<i class="fa-solid fa-user-shield"></i>
<span>Admin</span>
</a>
</nav>
</body>
</html>