307 lines
12 KiB
HTML
307 lines
12 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>
|
|
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 ? ' <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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|
|
|
|
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>
|