282 lines
10 KiB
JavaScript
282 lines
10 KiB
JavaScript
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;
|
||
});
|