chore: move js to single file

This commit is contained in:
2026-06-10 17:23:09 +01:00
parent 4cefa3be76
commit ed65314fa5
9 changed files with 65 additions and 1508 deletions
+3 -114
View File
@@ -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, '&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>