chore: move js to single file
This commit is contained in:
+3
-114
@@ -121,123 +121,12 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="assets/js/nav.js"></script>
|
||||
<script src="assets/js/app.js"></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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user