// events page — list + title edit + detach-and-delete, with filters // filter/sort/pagination state persists to url query params // depends on: app.js let eventOffset = 0; let currentEvent = null; function getEventFilters() { return { keyword: document.getElementById("ef-keyword").value.trim(), min_articles: document.getElementById("ef-min").value.trim(), from: document.getElementById("ef-from").value, to: document.getElementById("ef-to").value, sort: document.getElementById("ef-sort").value, }; } function syncUrl() { const f = getEventFilters(); queryWrite({ ...f, // default sort isn't worth surfacing in the url sort: f.sort && f.sort !== "created_desc" ? f.sort : "", offset: eventOffset > 0 ? eventOffset : "", }); } async function loadEvents() { const f = getEventFilters(); const params = new URLSearchParams({ limit: PAGE, offset: eventOffset }); if (f.keyword) params.set("keyword", f.keyword); if (f.min_articles) params.set("min_articles", f.min_articles); if (f.from) params.set("from", f.from + "T00:00:00"); if (f.to) params.set("to", f.to + "T23:59:59"); if (f.sort) params.set("sort", f.sort); const data = await api(`/admin/api/events?${params}`); const tbody = document.getElementById("eventTable"); tbody.innerHTML = data.rows.map(r => ` ${r.id} ${escapeHtml(r.title)} ${r.article_count} ${r.created_at ? r.created_at.slice(0, 16) : "—"} `).join(""); const total = data.total; document.getElementById("ePageInfo").textContent = `${eventOffset + 1}–${Math.min(eventOffset + PAGE, total)} of ${total.toLocaleString()}`; document.getElementById("ePrevBtn").disabled = eventOffset === 0; document.getElementById("eNextBtn").disabled = eventOffset + PAGE >= total; } function openEvent(id, title) { currentEvent = { id, title }; document.getElementById("em-title").value = title; document.getElementById("eventOverlay").classList.add("open"); } document.addEventListener("DOMContentLoaded", () => { // restore filter state from url queryApplyToInputs({ "ef-keyword": "keyword", "ef-min": "min_articles", "ef-from": "from", "ef-to": "to", "ef-sort": "sort", }); eventOffset = parseInt(queryGet("offset"), 10) || 0; document.getElementById("eSearchBtn").onclick = () => { eventOffset = 0; syncUrl(); loadEvents(); }; document.getElementById("ePrevBtn").onclick = () => { eventOffset = Math.max(0, eventOffset - PAGE); syncUrl(); loadEvents(); }; document.getElementById("eNextBtn").onclick = () => { eventOffset += PAGE; syncUrl(); loadEvents(); }; document.querySelector(".filters").addEventListener("keydown", e => { if (e.key === "Enter") { eventOffset = 0; syncUrl(); loadEvents(); } }); document.getElementById("eCancelBtn").onclick = () => document.getElementById("eventOverlay").classList.remove("open"); document.getElementById("eSaveBtn").onclick = async () => { if (!currentEvent) return; try { await api(`/admin/api/events/${currentEvent.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title: document.getElementById("em-title").value }), }); document.getElementById("eventOverlay").classList.remove("open"); toast("Saved"); loadEvents(); } catch (e) { toast("Save failed", true); } }; document.getElementById("eDeleteBtn").onclick = async () => { if (!currentEvent) return; if (!confirm(`Delete event #${currentEvent.id}? Articles will be detached but not deleted.`)) return; try { await api(`/admin/api/events/${currentEvent.id}`, { method: "DELETE" }); document.getElementById("eventOverlay").classList.remove("open"); toast("Event deleted"); loadEvents(); loadGlobalStats(); } catch (e) { toast("Delete failed", true); } }; loadEvents(); });