skydesportensdag/wwwroot/admin.js

197 lines
8.1 KiB
JavaScript

const tableBody = document.querySelector("#visitors");
const count = document.querySelector("#count");
const message = document.querySelector("#message");
const csvButton = document.querySelector("#export-csv");
const printButton = document.querySelector("#print-list");
let visitors = [];
let sortKey = "createdAt";
let sortDirection = -1;
const sortCollator = new Intl.Collator("da", { numeric: true, sensitivity: "base" });
function sortVisitors() {
// Move existing rows so unsaved result inputs are retained.
const rows = new Map([...tableBody.querySelectorAll("tr")].map((row) =>
[row.querySelector(".result-form")?.dataset.id, row]));
const value = (visitor) => {
const row = rows.get(String(visitor.id));
if (sortKey === "result") return row?.querySelector('[name="result"]')?.value ?? visitor.result;
if (sortKey === "x10") return row?.querySelector('[name="x10"]')?.value ?? visitor.x10;
return visitor[sortKey];
};
visitors.sort((a, b) => {
const av = value(a), bv = value(b);
if (av === null || av === undefined || av === "") return bv === null || bv === undefined || bv === "" ? a.id - b.id : 1;
if (bv === null || bv === undefined || bv === "") return -1;
return sortDirection * sortCollator.compare(String(av), String(bv)) || a.id - b.id;
});
for (const visitor of visitors) {
const row = rows.get(String(visitor.id));
if (row) tableBody.append(row);
}
document.querySelectorAll("button[data-sort]").forEach((button) => {
const active = button.dataset.sort === sortKey;
button.closest("th").setAttribute("aria-sort", active ? (sortDirection === 1 ? "ascending" : "descending") : "none");
button.querySelector(".sort-arrow").textContent = active ? (sortDirection === 1 ? " ↑" : " ↓") : " ↕";
});
}
document.querySelectorAll("button[data-sort]").forEach((button) => {
button.addEventListener("click", () => {
sortDirection = sortKey === button.dataset.sort ? -sortDirection : 1;
sortKey = button.dataset.sort;
sortVisitors();
});
});
function formatDateTime(value) {
return new Intl.DateTimeFormat("da-DK", {
dateStyle: "short",
timeStyle: "short"
}).format(new Date(value));
}
function showError(text) {
message.textContent = text;
message.className = "notice error";
message.hidden = false;
}
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
async function loadVisitors() {
csvButton.disabled = true;
printButton.disabled = true;
try {
const response = await fetch("/api/visitors");
if (!response.ok) {
showError("Listen kunne ikke hentes. Tjek admin-login og databaseforbindelse.");
count.textContent = "Fejl";
return;
}
visitors = await response.json();
updateParticipantMap(visitors);
message.hidden = true;
csvButton.disabled = false;
printButton.disabled = false;
count.textContent = `${visitors.length} registreret`;
if (visitors.length === 0) {
tableBody.innerHTML = '<tr><td colspan="11" class="empty">Ingen besøgende registreret endnu.</td></tr>';
return;
}
tableBody.innerHTML = visitors.map((visitor) => `
<tr>
<td><strong>${visitor.id}</strong></td>
<td>${escapeHtml(visitor.name)}</td>
<td>${escapeHtml(visitor.streetAddress)}</td>
<td>${escapeHtml(visitor.postalCode)}</td>
<td>${escapeHtml(visitor.city)}</td>
<td>${escapeHtml(visitor.birthday)}</td>
<td>${visitor.email ? `<a href="mailto:${escapeHtml(visitor.email)}">${escapeHtml(visitor.email)}</a>` : ""}</td>
<td>${escapeHtml(formatDateTime(visitor.createdAt))}</td>
<td>
<form id="result-${visitor.id}" class="result-form no-print" data-id="${visitor.id}">
<input name="result" value="${escapeHtml(visitor.result)}" maxlength="100" aria-label="Resultat for ${escapeHtml(visitor.name)}" placeholder="Resultat">
<span class="result-status" role="status"></span>
</form>
<span class="print-result">${escapeHtml(visitor.result)}</span>
</td>
<td>
<input class="x10-input no-print" form="result-${visitor.id}" name="x10" type="number" min="0" step="1" value="${escapeHtml(visitor.x10)}" aria-label="X-10 for ${escapeHtml(visitor.name)}">
<button class="save-result-button no-print" type="submit" form="result-${visitor.id}">Gem</button>
<span class="print-result print-x10">${escapeHtml(visitor.x10)}</span>
</td>
<td class="no-print"><button type="button" class="delete-button" data-id="${visitor.id}" aria-label="Slet ${escapeHtml(visitor.name)}">Slet</button></td>
</tr>
`).join("");
sortVisitors();
} catch {
showError("Der er ikke forbindelse til besøgslisten lige nu.");
count.textContent = "Fejl";
}
}
tableBody.addEventListener("submit", async (event) => {
const form = event.target.closest(".result-form");
if (!form) return;
event.preventDefault();
const button = form.closest("tr").querySelector(".save-result-button");
if (button.disabled) return;
const status = form.querySelector(".result-status");
const result = form.elements.result.value.trim();
const x10 = form.elements.x10.value === "" ? null : Number(form.elements.x10.value);
button.disabled = true;
status.textContent = "Gemmer...";
try {
const response = await fetch(`/api/visitors/${form.dataset.id}/result`, {
method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ result, x10 })
});
if (!response.ok) throw new Error("Resultat kunne ikke gemmes.");
const saved = await response.json();
const visitor = visitors.find((item) => String(item.id) === form.dataset.id);
if (visitor) { visitor.result = saved.result; visitor.x10 = saved.x10; }
form.elements.result.value = saved.result;
form.parentElement.querySelector(".print-result").textContent = saved.result;
form.elements.x10.value = saved.x10 ?? "";
form.closest("tr").querySelector(".print-x10").textContent = saved.x10 ?? "";
status.textContent = "Gemt";
} catch {
status.textContent = "Ikke gemt. Prøv igen.";
} finally { button.disabled = false; }
});
tableBody.addEventListener("click", async (event) => {
const button = event.target.closest("button[data-id]");
if (!button || button.disabled) return;
const visitor = visitors.find((item) => String(item.id) === button.dataset.id);
if (!visitor || !window.confirm(`Slet registreringen for ${visitor.name}? Dette kan ikke fortrydes.`)) return;
button.disabled = true;
csvButton.disabled = true;
printButton.disabled = true;
try {
const response = await fetch(`/api/visitors/${visitor.id}`, { method: "DELETE" });
if (!response.ok && response.status !== 404) throw new Error("Sletning mislykkedes.");
await loadVisitors();
} catch {
showError("Registreringen kunne ikke slettes. Prøv igen.");
button.disabled = false;
csvButton.disabled = false;
printButton.disabled = false;
}
});
function csvCell(value) {
let text = String(value ?? "");
// Prevent spreadsheet programs from evaluating visitor input as formulas.
if (/^[\s]*[=+@-]/.test(text)) text = "'" + text;
return `"${text.replaceAll('"', '""')}"`;
}
csvButton.addEventListener("click", () => {
const rows = [
["Skivenr.", "Navn", "Vejnavn og husnummer", "Postnummer", "Postby", "Fødselsdag", "Email", "Registreret", "Resultat", "X-10"],
...visitors.map((visitor) => [visitor.id, visitor.name, visitor.streetAddress, visitor.postalCode,
visitor.city, visitor.birthday, visitor.email, formatDateTime(visitor.createdAt), visitor.result, visitor.x10])
];
const csv = "\uFEFF" + rows.map((row) => row.map(csvCell).join(";")).join("\r\n") + "\r\n";
const url = URL.createObjectURL(new Blob([csv], { type: "text/csv;charset=utf-8" }));
const link = document.createElement("a");
link.href = url;
link.download = "skydesportens-dag-besogende.csv";
link.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
});
printButton.addEventListener("click", () => window.print());
loadVisitors();