diff --git a/Program.cs b/Program.cs index 6062a8f..c2924b1 100644 --- a/Program.cs +++ b/Program.cs @@ -35,16 +35,17 @@ app.MapPost("/api/visitors", async (VisitorInput input, Database database) => command.CommandText = """ insert into visitors (name, street_address, postal_code, city, birthday, email) values ($name, $street, $postal, $city, $birthday, $email) + returning id """; command.Parameters.AddWithValue("$name", input.Name.Trim()); command.Parameters.AddWithValue("$street", input.StreetAddress.Trim()); command.Parameters.AddWithValue("$postal", input.PostalCode.Trim()); command.Parameters.AddWithValue("$city", input.City.Trim()); command.Parameters.AddWithValue("$birthday", DateOnly.Parse(input.Birthday).ToString("yyyy-MM-dd")); - command.Parameters.AddWithValue("$email", input.Email.Trim().ToLowerInvariant()); - await command.ExecuteNonQueryAsync(); + command.Parameters.AddWithValue("$email", input.Email?.Trim().ToLowerInvariant() ?? ""); + var number = Convert.ToInt64(await command.ExecuteScalarAsync()); - return Results.Created("/api/visitors", null); + return Results.Created("/api/visitors", new { number }); }); app.MapGet("/api/visitors", async (Database database) => @@ -53,7 +54,7 @@ app.MapGet("/api/visitors", async (Database database) => await using var connection = await database.OpenConnectionAsync(); await using var command = connection.CreateCommand(); command.CommandText = """ - select id, name, street_address, postal_code, city, birthday, email, created_at + select id, name, street_address, postal_code, city, birthday, email, created_at, result, x10 from visitors order by created_at desc, id desc """; @@ -69,13 +70,31 @@ app.MapGet("/api/visitors", async (Database database) => reader.GetString(4), DateOnly.Parse(reader.GetString(5)), reader.GetString(6), - DateTime.SpecifyKind(reader.GetDateTime(7), DateTimeKind.Utc) + DateTime.SpecifyKind(reader.GetDateTime(7), DateTimeKind.Utc), + reader.GetString(8), + reader.IsDBNull(9) ? null : reader.GetInt32(9) )); } return Results.Ok(visitors); }).RequireAuthorization(); +app.MapPut("/api/visitors/{id:long}/result", async (long id, VisitorResultInput input, Database database) => +{ + var result = input.Result?.Trim() ?? ""; + if (input.X10 < 0) + return Results.ValidationProblem(new Dictionary { ["x10"] = ["X-10 skal være et heltal på mindst 0."] }); + if (result.Length > 100) + return Results.ValidationProblem(new Dictionary { ["result"] = ["Resultatet må højst være 100 tegn."] }); + await using var connection = await database.OpenConnectionAsync(); + await using var command = connection.CreateCommand(); + command.CommandText = "update visitors set result = $result, x10 = $x10 where id = $id"; + command.Parameters.AddWithValue("$x10", (object?)input.X10 ?? DBNull.Value); + command.Parameters.AddWithValue("$result", result); + command.Parameters.AddWithValue("$id", id); + return await command.ExecuteNonQueryAsync() == 0 ? Results.NotFound() : Results.Ok(new { result, x10 = input.X10 }); +}).RequireAuthorization(); + app.MapDelete("/api/visitors/{id:long}", async (long id, Database database) => { await using var connection = await database.OpenConnectionAsync(); @@ -122,17 +141,18 @@ static Dictionary Validate(VisitorInput input) errors["birthday"] = ["Fødselsdag mangler eller er ugyldig."]; } - if (string.IsNullOrWhiteSpace(input.Email) || !input.Email.Contains('@') || !input.Email.Contains('.')) + if (!string.IsNullOrWhiteSpace(input.Email) && (!input.Email.Contains('@') || !input.Email.Contains('.'))) { - errors["email"] = ["Email mangler eller er ugyldig."]; + errors["email"] = ["Email er ugyldig."]; } return errors; } -public sealed record VisitorInput(string Name, string StreetAddress, string PostalCode, string City, string Birthday, string Email); +public sealed record VisitorInput(string Name, string StreetAddress, string PostalCode, string City, string Birthday, string? Email); -public sealed record VisitorListItem(long Id, string Name, string StreetAddress, string PostalCode, string City, DateOnly Birthday, string Email, DateTime CreatedAt); +public sealed record VisitorResultInput(string? Result, int? X10); +public sealed record VisitorListItem(long Id, string Name, string StreetAddress, string PostalCode, string City, DateOnly Birthday, string Email, DateTime CreatedAt, string Result, int? X10); public sealed class Database { @@ -181,6 +201,19 @@ public sealed class Database """; await command.ExecuteNonQueryAsync(); + await using var schemaCommand = connection.CreateCommand(); + schemaCommand.CommandText = "select count(*) from pragma_table_info('visitors') where name = 'result'"; + if (Convert.ToInt64(await schemaCommand.ExecuteScalarAsync()) == 0) + { + schemaCommand.CommandText = "alter table visitors add column result text not null default ''"; + await schemaCommand.ExecuteNonQueryAsync(); + } + schemaCommand.CommandText = "select count(*) from pragma_table_info('visitors') where name = 'x10'"; + if (Convert.ToInt64(await schemaCommand.ExecuteScalarAsync()) == 0) + { + schemaCommand.CommandText = "alter table visitors add column x10 integer"; + await schemaCommand.ExecuteNonQueryAsync(); + } } } diff --git a/wwwroot/admin.html b/wwwroot/admin.html index 65224b2..7bff671 100644 --- a/wwwroot/admin.html +++ b/wwwroot/admin.html @@ -16,11 +16,13 @@

Vestfalster Skytteforening

Besøgende

- Henter... - -
- - +
+
+ + +
+ Henter... +
@@ -33,18 +35,21 @@ - - - - - - - + + + + + + + + + + - +
NavnVejnavn og husnummerPostnummerPostbyFødselsdagEmailRegistreret Handling
Henter besøgende...
Henter besøgende...
diff --git a/wwwroot/admin.js b/wwwroot/admin.js index 6fdcf41..33aaf33 100644 --- a/wwwroot/admin.js +++ b/wwwroot/admin.js @@ -4,6 +4,44 @@ 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", { @@ -46,28 +84,71 @@ async function loadVisitors() { count.textContent = `${visitors.length} registreret`; if (visitors.length === 0) { - tableBody.innerHTML = 'Ingen besøgende registreret endnu.'; + tableBody.innerHTML = 'Ingen besøgende registreret endnu.'; return; } tableBody.innerHTML = visitors.map((visitor) => ` + ${visitor.id} ${escapeHtml(visitor.name)} ${escapeHtml(visitor.streetAddress)} ${escapeHtml(visitor.postalCode)} ${escapeHtml(visitor.city)} ${escapeHtml(visitor.birthday)} - ${escapeHtml(visitor.email)} + ${visitor.email ? `${escapeHtml(visitor.email)}` : ""} ${escapeHtml(formatDateTime(visitor.createdAt))} + +
+ + +
+ ${escapeHtml(visitor.result)} + + + + + ${escapeHtml(visitor.x10)} + `).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; @@ -97,9 +178,9 @@ function csvCell(value) { csvButton.addEventListener("click", () => { const rows = [ - ["Navn", "Vejnavn og husnummer", "Postnummer", "Postby", "Fødselsdag", "Email", "Registreret"], - ...visitors.map((visitor) => [visitor.name, visitor.streetAddress, visitor.postalCode, - visitor.city, visitor.birthday, visitor.email, formatDateTime(visitor.createdAt)]) + ["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" })); diff --git a/wwwroot/app.js b/wwwroot/app.js index 29e3391..d14bccd 100644 --- a/wwwroot/app.js +++ b/wwwroot/app.js @@ -35,9 +35,13 @@ form.addEventListener("submit", async (event) => { return; } + const registration = await response.json(); form.reset(); citySuggestions.replaceChildren(); - showMessage("Tak, du er registreret.", "success"); + showMessage(`Tak, du er registreret. Dit skivenummer er ${registration.number}. Skriv nummeret på dine skiver.`, "success"); + const numberText = document.createElement("strong"); + numberText.textContent = `Dit skivenummer er ${registration.number}`; + message.replaceChildren("Tak, du er registreret. ", numberText, ". Skriv nummeret på dine skiver."); } catch { showMessage("Der er ikke forbindelse til registreringen lige nu.", "error"); } finally { diff --git a/wwwroot/index.html b/wwwroot/index.html index 2054abd..c766e78 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -13,10 +13,10 @@

Vestfalster Skytteforening

Skydesportens Dag

Registrer dig som besøgende. Oplysningerne bruges kun i forbindelse med arrangementet.

- +