const loginPanel = document.querySelector("#login-panel"); const loginForm = document.querySelector("#login-form"); const keyInput = document.querySelector("#admin-key"); const loginError = document.querySelector("#login-error"); const dashboard = document.querySelector("#dashboard"); const headerActions = document.querySelector("#header-actions"); const createForm = document.querySelector("#create-form"); const namesInput = document.querySelector("#guest-names"); const createError = document.querySelector("#create-error"); const createButton = document.querySelector("#create-button"); const createdEmpty = document.querySelector("#created-empty"); const createdLinks = document.querySelector("#created-links"); const createdList = document.querySelector("#created-list"); const responseBody = document.querySelector("#response-body"); const tableEmpty = document.querySelector("#table-empty"); const responseDialog = document.querySelector("#response-dialog"); const responseEditorForm = document.querySelector("#response-editor-form"); const responseDialogTitle = document.querySelector("#response-dialog-title"); const adminAnswer = document.querySelector("#admin-answer"); const adminCompanionWrap = document.querySelector("#admin-companion-wrap"); const adminCompanionName = document.querySelector("#admin-companion-name"); const responseEditorError = document.querySelector("#response-editor-error"); const responseSaveButton = document.querySelector("#response-save-button"); let adminKey = sessionStorage.getItem("inviteAdminKey") || ""; let lastCreated = []; let editingToken = ""; const answerLabels = { not_coming: "Kommt nicht", alone: "Kommt alleine", with_someone: "Mit Begleitung" }; function authHeaders(extra = {}) { return { ...extra, Authorization: `Bearer ${adminKey}` }; } function formatDate(value) { if (!value) return "—"; return new Intl.DateTimeFormat("de-AT", { dateStyle: "short", timeStyle: "short", timeZone: "Europe/Vienna" }).format(new Date(value)); } async function copyText(text, button) { await navigator.clipboard.writeText(text); const old = button.textContent; button.textContent = "Kopiert!"; setTimeout(() => { button.textContent = old; }, 1200); } function cell(row, text) { const td = document.createElement("td"); td.textContent = text; row.append(td); return td; } function toggleAdminCompanion() { const needsCompanion = adminAnswer.value === "with_someone"; adminCompanionWrap.hidden = !needsCompanion; adminCompanionName.required = needsCompanion; } function openResponseEditor(invite) { editingToken = invite.token; responseDialogTitle.textContent = invite.guestName; adminAnswer.value = invite.answer || ""; adminCompanionName.value = invite.companionName || ""; responseEditorError.hidden = true; toggleAdminCompanion(); responseDialog.showModal(); adminAnswer.focus(); } function renderTable(invites) { responseBody.replaceChildren(); tableEmpty.hidden = invites.length > 0; for (const invite of [...invites].reverse()) { const row = document.createElement("tr"); cell(row, invite.guestName); const statusCell = document.createElement("td"); const status = document.createElement("span"); status.className = `status ${invite.answer || ""}`; status.textContent = answerLabels[invite.answer] || (invite.firstOpenedAt ? "Geöffnet" : "Noch offen"); statusCell.append(status); row.append(statusCell); cell(row, invite.companionName || "—"); cell(row, formatDate(invite.firstOpenedAt)); cell(row, invite.lastOpenedAt ? `${formatDate(invite.lastOpenedAt)} (${invite.openCount}×)` : "—"); cell(row, formatDate(invite.lastSavedAt)); const linkCell = document.createElement("td"); const copyButton = document.createElement("button"); copyButton.type = "button"; copyButton.className = "link-button"; copyButton.textContent = "Link kopieren"; copyButton.addEventListener("click", () => copyText(invite.link, copyButton)); linkCell.append(copyButton); row.append(linkCell); const actionCell = document.createElement("td"); const editButton = document.createElement("button"); editButton.type = "button"; editButton.className = "link-button secondary"; editButton.textContent = invite.answer ? "Antwort ändern" : "Antwort setzen"; editButton.addEventListener("click", () => openResponseEditor(invite)); actionCell.append(editButton); row.append(actionCell); responseBody.append(row); } } function renderCreated(invites) { lastCreated = invites; createdList.replaceChildren(); createdEmpty.hidden = true; createdLinks.hidden = false; for (const invite of invites) { const item = document.createElement("li"); const name = document.createElement("strong"); name.textContent = invite.name; const link = document.createElement("a"); link.href = invite.link; link.target = "_blank"; link.rel = "noopener"; link.textContent = invite.link; const button = document.createElement("button"); button.type = "button"; button.textContent = "Kopieren"; button.addEventListener("click", () => copyText(invite.link, button)); item.append(name, link, button); createdList.append(item); } } async function loadDashboard() { const response = await fetch("/api/admin/invites", { headers: authHeaders() }); if (!response.ok) throw new Error("Der Admin-Schlüssel ist nicht gültig."); const data = await response.json(); document.querySelector("#stat-total").textContent = data.summary.total; document.querySelector("#stat-opened").textContent = data.summary.opened; document.querySelector("#stat-responded").textContent = data.summary.responded; document.querySelector("#stat-attending").textContent = data.summary.attending; renderTable(data.invites); loginPanel.hidden = true; dashboard.hidden = false; headerActions.hidden = false; } loginForm.addEventListener("submit", async event => { event.preventDefault(); loginError.hidden = true; adminKey = keyInput.value; try { await loadDashboard(); sessionStorage.setItem("inviteAdminKey", adminKey); } catch (error) { loginError.textContent = error.message; loginError.hidden = false; } }); createForm.addEventListener("submit", async event => { event.preventDefault(); createError.hidden = true; const names = namesInput.value.split(/\r?\n/).map(name => name.trim()).filter(Boolean); createButton.disabled = true; createButton.textContent = "Links werden erstellt …"; try { const response = await fetch("/api/admin/invites", { method: "POST", headers: authHeaders({ "Content-Type": "application/json" }), body: JSON.stringify({ names }) }); const data = await response.json(); if (!response.ok) throw new Error(data.error || "Links konnten nicht erstellt werden."); renderCreated(data.invites); namesInput.value = ""; await loadDashboard(); } catch (error) { createError.textContent = error.message; createError.hidden = false; } finally { createButton.disabled = false; createButton.textContent = "Persönliche Links erstellen"; } }); document.querySelector("#copy-all-button").addEventListener("click", event => { const text = lastCreated.map(invite => `${invite.name}: ${invite.link}`).join("\n"); copyText(text, event.currentTarget); }); document.querySelector("#refresh-button").addEventListener("click", async event => { event.currentTarget.disabled = true; try { await loadDashboard(); } finally { event.currentTarget.disabled = false; } }); document.querySelector("#logout-button").addEventListener("click", () => { sessionStorage.removeItem("inviteAdminKey"); window.location.reload(); }); adminAnswer.addEventListener("change", () => { responseEditorError.hidden = true; toggleAdminCompanion(); if (!adminCompanionWrap.hidden) adminCompanionName.focus(); }); function closeResponseDialog() { responseDialog.close(); editingToken = ""; } document.querySelector("#response-dialog-close").addEventListener("click", closeResponseDialog); document.querySelector("#response-dialog-cancel").addEventListener("click", closeResponseDialog); responseEditorForm.addEventListener("submit", async event => { event.preventDefault(); responseEditorError.hidden = true; const answer = adminAnswer.value; const companionName = adminCompanionName.value.trim(); if (!answer) { responseEditorError.textContent = "Bitte wähle eine Antwort aus."; responseEditorError.hidden = false; return; } if (answer === "with_someone" && !companionName) { responseEditorError.textContent = "Bitte gib den Namen der Begleitung an."; responseEditorError.hidden = false; adminCompanionName.focus(); return; } responseSaveButton.disabled = true; responseSaveButton.textContent = "Wird gespeichert …"; try { const response = await fetch(`/api/admin/invites/${encodeURIComponent(editingToken)}/rsvp`, { method: "PATCH", headers: authHeaders({ "Content-Type": "application/json" }), body: JSON.stringify({ answer, companionName }) }); const data = await response.json(); if (!response.ok) throw new Error(data.error || "Antwort konnte nicht gespeichert werden."); closeResponseDialog(); await loadDashboard(); } catch (error) { responseEditorError.textContent = error.message; responseEditorError.hidden = false; } finally { responseSaveButton.disabled = false; responseSaveButton.textContent = "Antwort speichern"; } }); document.querySelector("#csv-button").addEventListener("click", async event => { event.currentTarget.disabled = true; try { const response = await fetch("/api/admin/responses.csv", { headers: authHeaders() }); if (!response.ok) throw new Error("CSV konnte nicht geladen werden."); const blob = await response.blob(); const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = "zusagen.csv"; link.click(); URL.revokeObjectURL(link.href); } finally { event.currentTarget.disabled = false; } }); loadDashboard().catch(() => { sessionStorage.removeItem("inviteAdminKey"); adminKey = ""; loginPanel.hidden = false; dashboard.hidden = true; headerActions.hidden = true; });