Rework admin player table and account deletion

This commit is contained in:
2026-02-05 13:19:23 +01:00
parent 19b5dc2491
commit 69d31b4113
7 changed files with 147 additions and 70 deletions

View File

@@ -57,6 +57,7 @@ export const adminApi = {
reset: () => request("/api/admin/reset", { method: "POST" }),
factoryReset: () => request("/api/admin/factory-reset", { method: "POST" }),
grantJoker: (playerId) => request("/api/admin/joker", { method: "POST", body: { playerId } }),
deletePlayer: (playerId) => request(`/api/admin/players/${playerId}`, { method: "DELETE" }),
linkSuggestions: (sourceSuggestionId, targetSuggestionId) =>
request("/api/admin/link-suggestions", { method: "POST", body: { sourceSuggestionId, targetSuggestionId } }),
unlinkSuggestions: (suggestionId) =>

View File

@@ -110,12 +110,20 @@ const translations = {
"admin.factoryResetDone": "Factory reset complete",
"admin.readyForResults": "Ready for results",
"admin.waitingForPlayers": "Waiting for players: {names}",
"admin.jokerTitle": "Jokers",
"admin.jokerSelect": "Player",
"admin.jokerGive": "Grant joker",
"admin.playerName": "Name",
"admin.playerUsername": "Username",
"admin.playerStatus": "Status",
"admin.playerJoker": "Joker",
"admin.playerDelete": "Delete",
"admin.grantJokerChip": "Grant",
"admin.statusSuggesting": "Suggesting",
"admin.statusVoting": "Voting",
"admin.statusFinished": "Finished",
"admin.deleteTitle": "Delete account?",
"admin.deleteBody": "Delete player \"{name}\" and all their games and votes? This cannot be undone.",
"admin.deleteConfirm": "Delete",
"admin.deleteDone": "Player deleted",
"admin.jokerGranted": "Joker granted",
"admin.jokerSelectFirst": "Pick a player first.",
"admin.jokerPlaceholder": "Pick a player",
"admin.linkTitle": "Link games",
"admin.linkSource": "Game to link",
"admin.linkTarget": "Link to (parent)",
@@ -261,12 +269,20 @@ const translations = {
"admin.factoryResetDone": "Werkseinstellung abgeschlossen",
"admin.readyForResults": "Bereit für Ergebnisse",
"admin.waitingForPlayers": "Warten auf: {names}",
"admin.jokerTitle": "Joker",
"admin.jokerSelect": "Spieler",
"admin.jokerGive": "Joker vergeben",
"admin.playerName": "Name",
"admin.playerUsername": "Benutzername",
"admin.playerStatus": "Status",
"admin.playerJoker": "Joker",
"admin.playerDelete": "Löschen",
"admin.grantJokerChip": "Joker",
"admin.statusSuggesting": "Vorschlagen",
"admin.statusVoting": "Bewerten",
"admin.statusFinished": "Fertig",
"admin.deleteTitle": "Konto löschen?",
"admin.deleteBody": "Spieler \"{name}\" samt Spielen und Stimmen löschen? Dies kann nicht rückgängig gemacht werden.",
"admin.deleteConfirm": "Löschen",
"admin.deleteDone": "Spieler gelöscht",
"admin.jokerGranted": "Joker vergeben",
"admin.jokerSelectFirst": "Wähle zuerst einen Spieler.",
"admin.jokerPlaceholder": "Spieler wählen",
"admin.linkTitle": "Spiele verknüpfen",
"admin.linkSource": "Spiel verknüpfen",
"admin.linkTarget": "Verknüpfen mit",

View File

@@ -9,6 +9,10 @@ const sortByName = (items) =>
(items ?? [])
.slice()
.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: "base" }));
const truncate = (text, max) => {
if (!text) return "";
return text.length > max ? `${text.slice(0, max - 1)}` : text;
};
export function setAuthUI(isAuthed) {
const main = document.querySelector("main");
@@ -715,20 +719,22 @@ function updateMissingBadgeFromDom() {
function renderAdminVoteStatus() {
if (!state.me?.isAdmin) return;
const list = $("admin-voter-list");
const status = $("admin-ready-status");
const jokerWrap = $("admin-joker");
const jokerSelect = $("joker-player");
if (!state.adminVoteStatus || !list || !status) return;
const statusBadge = $("admin-ready-status");
const table = $("admin-player-table")?.querySelector("tbody");
if (!state.adminVoteStatus || !statusBadge || !table) return;
list.innerHTML = "";
table.innerHTML = "";
state.adminVoteStatus.voters.forEach((v) => {
const li = document.createElement("li");
const name = v.name?.length > 24 ? `${v.name.slice(0, 21)}` : v.name;
const jokerMark = v.hasJoker ? " 🎟" : "";
li.textContent = `${name}${jokerMark}${v.finalized ? "✅" : "⏳"}`;
li.title = v.name;
list.appendChild(li);
const tr = document.createElement("tr");
const statusText = displayPlayerStatus(v);
tr.innerHTML = `
<td title="${v.name}">${truncate(v.name, 28)}</td>
<td class="muted small" title="${v.username}">${truncate(v.username, 24)}</td>
<td>${statusText}</td>
<td><button class="chip" data-grant-joker="${v.playerId}" type="button">${v.hasJoker ? "🎟" : t("admin.grantJokerChip")}</button></td>
<td><button class="chip danger-chip" data-delete-player="${v.playerId}" data-name="${v.name}" type="button">✕</button></td>
`;
table.appendChild(tr);
});
const waiting = state.adminVoteStatus.waiting;
@@ -736,33 +742,19 @@ function renderAdminVoteStatus() {
const waitingDisplay = waiting.map((name) =>
name?.length > 24 ? `${name.slice(0, 21)}` : name,
);
status.textContent = ready
statusBadge.textContent = ready
? t("admin.readyForResults")
: t("admin.waitingForPlayers", { names: waitingDisplay.join(", ") });
status.className = ready ? "badge" : "badge warning";
statusBadge.className = ready ? "badge" : "badge warning";
}
if (jokerWrap) jokerWrap.classList.toggle("hidden", state.phase !== "Vote");
if (jokerSelect && state.phase === "Vote") {
const previous = jokerSelect.value;
jokerSelect.innerHTML = "";
const placeholder = document.createElement("option");
placeholder.value = "";
placeholder.disabled = true;
placeholder.selected = true;
placeholder.textContent = t("admin.jokerPlaceholder");
jokerSelect.appendChild(placeholder);
state.adminVoteStatus.voters.forEach((v) => {
const opt = document.createElement("option");
opt.value = v.playerId;
opt.textContent = v.hasJoker ? `${v.name} — 🎟` : v.name;
jokerSelect.appendChild(opt);
});
if (previous && Array.from(jokerSelect.options).some((o) => o.value === previous)) {
jokerSelect.value = previous;
}
}
function displayPlayerStatus(player) {
if (!player) return "";
const phase = player.phase;
if (phase === "Suggest") return t("admin.statusSuggesting");
if (phase === "Vote") return player.finalized ? t("admin.statusFinished") : t("admin.statusVoting");
if (phase === "Results") return t("admin.statusFinished");
return phase;
}
function renderAdminLinker() {