Add admin status combobox to move voters back to suggest
This commit is contained in:
@@ -47,7 +47,7 @@ async function refreshWithUiErrorHandling() {
|
||||
|
||||
function scheduleNextRefresh() {
|
||||
refreshTimerId = window.setTimeout(async () => {
|
||||
if (!document.hidden) {
|
||||
if (!document.hidden && !state.adminStatusSelectActive) {
|
||||
await refreshWithUiErrorHandling();
|
||||
}
|
||||
scheduleNextRefresh();
|
||||
@@ -59,7 +59,7 @@ function startRefreshScheduler() {
|
||||
refreshSchedulerStarted = true;
|
||||
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (!document.hidden) {
|
||||
if (!document.hidden && !state.adminStatusSelectActive) {
|
||||
refreshWithUiErrorHandling();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -55,3 +55,9 @@
|
||||
border: 1px solid #e3d4bd;
|
||||
background: #fffaf3;
|
||||
}
|
||||
|
||||
.admin-status-select {
|
||||
width: 100%;
|
||||
min-width: 140px;
|
||||
background: #fffaf3;
|
||||
}
|
||||
|
||||
@@ -150,6 +150,7 @@ Nein. Vorschläge und Bewertungen sind schreibgeschützt. Wende dich bei Bedarf
|
||||
### Was können Admin-Konten tun?
|
||||
|
||||
- Joker während der Abstimmung vergeben
|
||||
- Einen Bewerter zurück in die Vorschlagsphase setzen (stärker als ein Joker; sparsam einsetzen)
|
||||
- Doppelte Vorschläge verknüpfen oder trennen
|
||||
- Vorschläge löschen
|
||||
- Abstimmungsstatus einsehen (wer finalisiert hat)
|
||||
|
||||
@@ -154,6 +154,7 @@ No. Suggestions and votes are read-only. Contact an admin for assistance.
|
||||
### What can admin accounts do?
|
||||
|
||||
- Grant jokers during Vote
|
||||
- Move a voter back to Suggest (stronger than a joker; use sparingly)
|
||||
- Link or unlink duplicate suggestions
|
||||
- Delete suggestions
|
||||
- View vote readiness (who has finalized)
|
||||
|
||||
@@ -121,6 +121,8 @@
|
||||
"admin.statusSuggesting": "Suggesting",
|
||||
"admin.statusVoting": "Voting",
|
||||
"admin.statusFinished": "Finished",
|
||||
"admin.statusMoveToSuggest": "Move to Suggest",
|
||||
"admin.statusUpdated": "Player phase updated",
|
||||
"admin.deleteTitle": "Delete account?",
|
||||
"admin.deleteBody": "Delete player \"{name}\" and all their games and votes? This cannot be undone.",
|
||||
"admin.deleteConfirm": "Delete",
|
||||
@@ -281,6 +283,8 @@
|
||||
"admin.statusSuggesting": "Vorschlagen",
|
||||
"admin.statusVoting": "Bewerten",
|
||||
"admin.statusFinished": "Fertig",
|
||||
"admin.statusMoveToSuggest": "Zur Vorschlagsphase",
|
||||
"admin.statusUpdated": "Spielerphase aktualisiert",
|
||||
"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",
|
||||
|
||||
@@ -15,8 +15,20 @@ function displayPlayerStatus(player) {
|
||||
return phase;
|
||||
}
|
||||
|
||||
function buildStatusSelect(player) {
|
||||
const statusText = displayPlayerStatus(player);
|
||||
const canMoveToSuggest = player.phase === "Vote";
|
||||
return `
|
||||
<select class="chip admin-status-select" data-set-player-phase="${player.playerId}" aria-label="${t("admin.playerStatus")}">
|
||||
<option value="" selected>${statusText}</option>
|
||||
<option value="Suggest" ${canMoveToSuggest ? "" : "disabled"}>${t("admin.statusMoveToSuggest")}</option>
|
||||
</select>
|
||||
`;
|
||||
}
|
||||
|
||||
export function renderAdminVoteStatus() {
|
||||
if (!state.me?.isAdmin) return;
|
||||
if (state.adminStatusSelectActive) return;
|
||||
const statusBadge = $("admin-ready-status");
|
||||
const table = $("admin-player-table")?.querySelector("tbody");
|
||||
if (!state.adminVoteStatus || !statusBadge || !table) return;
|
||||
@@ -24,14 +36,13 @@ export function renderAdminVoteStatus() {
|
||||
table.innerHTML = "";
|
||||
state.adminVoteStatus.voters.forEach((v) => {
|
||||
const tr = document.createElement("tr");
|
||||
const statusText = displayPlayerStatus(v);
|
||||
const gamesTooltip = escapeHtml((v.suggestionTitles || []).join(", "));
|
||||
const nameText = escapeHtml(truncate(v.name, 28));
|
||||
const userText = escapeHtml(truncate(v.username, 24));
|
||||
tr.innerHTML = `
|
||||
<td title="${escapeHtml(v.name)}">${nameText}</td>
|
||||
<td class="muted small" title="${escapeHtml(v.username)}">${userText}</td>
|
||||
<td>${statusText}</td>
|
||||
<td>${buildStatusSelect(v)}</td>
|
||||
<td title="${gamesTooltip}">${v.suggestionCount ?? 0}</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>
|
||||
|
||||
@@ -59,6 +59,8 @@ 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 } }),
|
||||
setPlayerPhase: (playerId, phase) =>
|
||||
request("/api/admin/player-phase", { method: "POST", body: { playerId, phase } }),
|
||||
deletePlayer: (playerId) => request(`/api/admin/players/${playerId}`, { method: "DELETE" }),
|
||||
linkSuggestions: (sourceSuggestionId, targetSuggestionId) =>
|
||||
request("/api/admin/link-suggestions", { method: "POST", body: { sourceSuggestionId, targetSuggestionId } }),
|
||||
|
||||
@@ -91,6 +91,44 @@ function setupLinkApply(runSerializedRefresh) {
|
||||
function setupPlayerTableActions(runSerializedRefresh) {
|
||||
const playerTable = $("admin-player-table");
|
||||
if (!playerTable) return;
|
||||
const phaseSelectSelector = "[data-set-player-phase]";
|
||||
|
||||
playerTable.addEventListener("focusin", (e) => {
|
||||
if (e.target.matches?.(phaseSelectSelector)) {
|
||||
state.adminStatusSelectActive = true;
|
||||
}
|
||||
});
|
||||
|
||||
playerTable.addEventListener("focusout", (e) => {
|
||||
if (!e.target.matches?.(phaseSelectSelector)) return;
|
||||
window.setTimeout(() => {
|
||||
const focused = document.activeElement;
|
||||
state.adminStatusSelectActive =
|
||||
!!focused?.matches?.(phaseSelectSelector);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
playerTable.addEventListener("change", async (e) => {
|
||||
const select = e.target.closest(phaseSelectSelector);
|
||||
if (!select) return;
|
||||
const playerId = select.dataset.setPlayerPhase;
|
||||
const phase = select.value;
|
||||
if (!playerId || !phase) return;
|
||||
select.disabled = true;
|
||||
|
||||
try {
|
||||
await adminApi.setPlayerPhase(playerId, phase);
|
||||
toast(t("admin.statusUpdated"));
|
||||
state.adminStatusSelectActive = false;
|
||||
await runSerializedRefresh();
|
||||
} catch (err) {
|
||||
select.value = "";
|
||||
toast(err.message, true);
|
||||
} finally {
|
||||
select.disabled = false;
|
||||
state.adminStatusSelectActive = false;
|
||||
}
|
||||
});
|
||||
|
||||
playerTable.addEventListener("click", async (e) => {
|
||||
const grantBtn = e.target.closest("[data-grant-joker]");
|
||||
|
||||
@@ -15,6 +15,7 @@ export const state = {
|
||||
results: [],
|
||||
votesRendered: false,
|
||||
adminVoteStatus: null,
|
||||
adminStatusSelectActive: false,
|
||||
};
|
||||
|
||||
export function clearUserState() {
|
||||
@@ -31,6 +32,7 @@ export function clearUserState() {
|
||||
state.myVotes = [];
|
||||
state.results = [];
|
||||
state.votesRendered = false;
|
||||
state.adminStatusSelectActive = false;
|
||||
const adminCard = document.getElementById("admin-card");
|
||||
if (adminCard) adminCard.classList.add("hidden");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user