Files
imf-sms-dashboard/web/templates.html
T

308 lines
13 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>SMS Templates — 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="assets/css/chat.css" rel="stylesheet">
</head>
<body>
<!-- ─── Mobile top bar ─── -->
<div class="topbar">
<span style="font-weight:600;font-size:0.95rem;">SMS Templates</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 active">
<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">
<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>SMS Templates</h1>
<p>Create message templates using <code>{name}</code>, <code>{workshop}</code> and <code>{workshop_time}</code>
placeholders.</p>
</div>
<div class="card p-4">
<h5 id="formTitle">New Template</h5>
<input id="templateName" class="form-control mb-2" placeholder="Template name">
<input type="hidden" id="editId" value="">
<textarea id="template" class="form-control mb-1" rows="5" maxlength="160"></textarea>
<small id="charCount" class="d-block mb-3"></small>
<div class="d-flex gap-2 mb-4">
<button class="btn btn-primary" id="saveBtn" onclick="saveTemplate()">
<i class="fa-solid fa-floppy-disk me-1"></i> <span id="saveBtnLabel">Save Template</span>
</button>
<button class="btn btn-sm btn-secondary d-none" id="cancelEditBtn" onclick="cancelEdit()">
Cancel
</button>
</div>
<h5>Preview</h5>
<section>
<div class="from-them" id="preview"></div>
</section>
</div>
<!-- Saved templates -->
<div class="card p-4 mt-4">
<div class="d-flex align-items-center justify-content-between mb-3">
<h5 class="mb-0">Saved Templates</h5>
<span class="text-muted" id="templateCount" style="font-size:0.82rem;"></span>
</div>
<div class="d-flex flex-column gap-3" id="templateList">
<p class="text-muted" style="font-size:0.85rem;">Loading…</p>
</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="assets/js/nav.js"></script>
<script>
const MAX_CHARS = 160;
// GSM-7 basic + extended character set
const GSM7 = new Set('@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞÆæßÉ !"#¤%&\'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑܧ¿abcdefghijklmnopqrstuvwxyzäöñüà€[]{}\\^|~');
const GSM7_EXTENDED = new Set('€[]{}\\^|~');
function smsEncoding(text) {
const isUnicode = [...text].some(c => !GSM7.has(c));
const limit = isUnicode ? 70 : 160;
const length = isUnicode ? text.length : [...text].reduce((n, c) => n + (GSM7_EXTENDED.has(c) ? 2 : 1), 0);
return { length, limit, isUnicode };
}
function updateCharCount(text) {
const { length, limit, isUnicode } = smsEncoding(text);
const remaining = limit - length;
const el = document.getElementById('charCount');
el.innerHTML = `${length}/${limit} characters${isUnicode ? ' &nbsp;<span style="color:#f87171;font-size:0.8em;"><i class="fa-solid fa-triangle-exclamation"></i> Unicode encoding — limit reduced to 70</span>' : ''}`;
el.className = `d-block mb-3 ${remaining <= 10 ? (remaining <= 0 ? 'text-danger' : 'text-warning') : 'text-muted'}`;
}
function updatePreview() {
const template = document.getElementById('template').value;
const previewEl = document.getElementById('preview');
const previewSection = previewEl.closest('section');
if (template.trim() === '') {
previewSection.style.visibility = 'hidden';
previewEl.textContent = '';
} else {
previewSection.style.visibility = 'visible';
previewEl.textContent = template
.replaceAll('{name}', 'Jordan')
.replaceAll('{workshop}', 'Axe Throwing')
.replaceAll('{workshop_time}', '13:30');
}
updateCharCount(template);
}
async function saveTemplate() {
const name = document.getElementById('templateName').value.trim();
const body = document.getElementById('template').value.trim();
const editId = document.getElementById('editId').value;
if (!name || !body) { alert('Please enter a template name and message.'); return; }
const isEdit = editId !== '';
const url = isEdit ? `/api/templates.php?id=${editId}` : '/api/templates.php';
const method = isEdit ? 'PUT' : 'POST';
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, body }),
});
if (!res.ok) {
const err = await res.json();
alert('Error: ' + (err.error ?? 'Unknown error'));
return;
}
cancelEdit();
loadTemplates();
}
function editTemplate(id, name, body) {
document.getElementById('editId').value = id;
document.getElementById('templateName').value = name;
document.getElementById('template').value = body;
document.getElementById('formTitle').textContent = 'Edit Template';
document.getElementById('saveBtnLabel').textContent = 'Update Template';
document.getElementById('cancelEditBtn').classList.remove('d-none');
document.getElementById('template').focus();
updatePreview();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function cancelEdit() {
document.getElementById('editId').value = '';
document.getElementById('templateName').value = '';
document.getElementById('template').value = '';
document.getElementById('formTitle').textContent = 'New Template';
document.getElementById('saveBtnLabel').textContent = 'Save Template';
document.getElementById('cancelEditBtn').classList.add('d-none');
updatePreview();
}
async function deleteTemplate(id) {
if (!confirm('Delete this template?')) return;
const res = await fetch(`/api/templates.php?id=${id}`, { method: 'DELETE' });
if (!res.ok) { alert('Could not delete template.'); return; }
loadTemplates();
}
function esc(str) {
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
async function loadTemplates() {
const list = document.getElementById('templateList');
const countEl = document.getElementById('templateCount');
list.innerHTML = '<p class="text-muted" style="font-size:0.85rem;">Loading…</p>';
try {
const data = await fetch('/api/templates.php').then(r => r.json());
const templates = data.templates ?? [];
countEl.textContent = templates.length + ' template' + (templates.length !== 1 ? 's' : '');
if (templates.length === 0) {
list.innerHTML = '<p class="text-muted" style="font-size:0.85rem;">No templates saved yet. Create one above.</p>';
return;
}
list.innerHTML = templates.map(t => {
const chars = t.body.length;
const date = new Date(t.updated_at).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' });
return `
<div style="background:var(--bg-surface);border:1px solid var(--border);border-radius:10px;padding:1rem;">
<div class="d-flex align-items-start justify-content-between gap-2 mb-2">
<div>
<div style="font-weight:600;font-size:0.9rem;">${esc(t.name)}</div>
<div class="text-muted" style="font-size:0.78rem;">Last edited ${date}</div>
</div>
<div class="d-flex gap-2">
<button class="btn btn-sm btn-accent"
onclick='editTemplate(${t.id}, ${JSON.stringify(t.name)}, ${JSON.stringify(t.body)})'>
<i class="fa-solid fa-pen fa-fw"></i>
</button>
<button class="btn btn-sm btn-danger"
onclick="deleteTemplate(${t.id})">
<i class="fa-solid fa-trash fa-fw"></i>
</button>
</div>
</div>
<section class="bubble-on-surface">
<div class="from-them">${esc(t.body.replaceAll('{name}', 'Jordan').replaceAll('{workshop}', 'Axe Throwing').replaceAll('{workshop_time}', '13:30'))}</div>
</section>
<div class="d-flex align-items-center gap-2 mt-2">
<span style="font-size:0.75rem;background:rgba(79,142,247,0.12);color:var(--accent);border-radius:6px;padding:0.15em 0.55em;">${chars}/160 chars</span>
${parseInt(t.scheduled_count) > 0 ? `<span style="font-size:0.75rem;background:rgba(74,222,128,0.12);color:#4ade80;border-radius:6px;padding:0.15em 0.55em;"><i class="fa-solid fa-clock me-1"></i>Scheduled</span>` : ''}
</div>
</div>
`;
}).join('');
} catch {
list.innerHTML = '<p class="text-danger" style="font-size:0.85rem;">Could not load templates.</p>';
}
}
document.getElementById('template').addEventListener('input', updatePreview);
updatePreview();
loadTemplates();
</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 active">
<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">
<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>