197 lines
8.1 KiB
JavaScript
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("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """)
|
|
.replaceAll("'", "'");
|
|
}
|
|
|
|
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();
|