Add admin vote status list and ready/wait messaging

This commit is contained in:
2026-02-04 23:31:12 +01:00
parent 76dfc4ea46
commit 3179144bc7
8 changed files with 53 additions and 0 deletions

View File

@@ -6,3 +6,4 @@ public record SuggestionDto(int Id, string Name, string? Genre, string? Descript
public record VoteRequest(int SuggestionId, int Score); public record VoteRequest(int SuggestionId, int Score);
public record ResultsOpenRequest(bool ResultsOpen); public record ResultsOpenRequest(bool ResultsOpen);
public record VoteFinalizeRequest(bool Final); public record VoteFinalizeRequest(bool Final);
public record VoteStatusDto(Guid PlayerId, string Name, bool Finalized);

View File

@@ -34,6 +34,22 @@ public static class AdminEndpoints
return Results.Ok(new { currentState.ResultsOpen, currentState.UpdatedAt }); return Results.Ok(new { currentState.ResultsOpen, currentState.UpdatedAt });
}); });
admin.MapGet("/vote-status", async (HttpContext ctx, AppDbContext db, IConfiguration config) =>
{
if (!await EndpointHelpers.IsAdmin(ctx, db, config)) return Results.Unauthorized();
var voters = await db.Players
.AsNoTracking()
.Where(p => p.CurrentPhase == Phase.Vote)
.OrderBy(p => p.DisplayName ?? p.Username)
.Select(p => new VoteStatusDto(p.Id, p.DisplayName ?? p.Username, p.VotesFinal))
.ToListAsync();
var waiting = voters.Where(v => !v.Finalized).Select(v => v.Name).ToList();
var ready = waiting.Count == 0;
return Results.Ok(new { voters, ready, waiting });
});
admin.MapPost("/reset", async (HttpContext ctx, AppDbContext db, IConfiguration config) => admin.MapPost("/reset", async (HttpContext ctx, AppDbContext db, IConfiguration config) =>
{ {
if (!await EndpointHelpers.IsAdmin(ctx, db, config)) return Results.Unauthorized(); if (!await EndpointHelpers.IsAdmin(ctx, db, config)) return Results.Unauthorized();

View File

@@ -143,6 +143,10 @@
<h3 data-i18n="admin.title">Admin</h3> <h3 data-i18n="admin.title">Admin</h3>
<button id="admin-close" class="ghost"></button> <button id="admin-close" class="ghost"></button>
</div> </div>
<div class="stack" id="admin-vote-status">
<div class="badge" id="admin-ready-status"></div>
<ul class="status-list" id="admin-voter-list"></ul>
</div>
<label class="stack toggle-row"> <label class="stack toggle-row">
<input type="checkbox" id="results-open" /> <input type="checkbox" id="results-open" />
<span data-i18n="admin.resultsOpenToggle">Allow results phase</span> <span data-i18n="admin.resultsOpenToggle">Allow results phase</span>

View File

@@ -53,6 +53,7 @@ export const api = {
export const adminApi = { export const adminApi = {
setResultsOpen: (resultsOpen) => request("/api/admin/results", { method: "POST", body: { resultsOpen } }), setResultsOpen: (resultsOpen) => request("/api/admin/results", { method: "POST", body: { resultsOpen } }),
voteStatus: () => request("/api/admin/vote-status"),
reset: () => request("/api/admin/reset", { method: "POST" }), reset: () => request("/api/admin/reset", { method: "POST" }),
factoryReset: () => request("/api/admin/factory-reset", { method: "POST" }), factoryReset: () => request("/api/admin/factory-reset", { method: "POST" }),
}; };

View File

@@ -69,6 +69,9 @@ export async function refreshPhaseData() {
state.votesRendered = false; state.votesRendered = false;
await loadVoteData(); await loadVoteData();
} }
if (state.me?.isAdmin) {
state.adminVoteStatus = await adminApi.voteStatus();
}
} catch (err) { } catch (err) {
if (handleAuthError(err, clearUserState)) return; if (handleAuthError(err, clearUserState)) return;
throw err; throw err;

View File

@@ -104,6 +104,8 @@ const translations = {
"admin.factoryReset": "Factory reset", "admin.factoryReset": "Factory reset",
"admin.resetDone": "Reset complete", "admin.resetDone": "Reset complete",
"admin.factoryResetDone": "Factory reset complete", "admin.factoryResetDone": "Factory reset complete",
"admin.readyForResults": "Ready for results",
"admin.waitingForPlayers": "Waiting for players: {names}",
"toast.unexpected": "Unexpected error", "toast.unexpected": "Unexpected error",
"toast.registered": "Registered", "toast.registered": "Registered",
@@ -230,6 +232,8 @@ const translations = {
"admin.factoryReset": "Werkseinstellung", "admin.factoryReset": "Werkseinstellung",
"admin.resetDone": "Zurücksetzen abgeschlossen", "admin.resetDone": "Zurücksetzen abgeschlossen",
"admin.factoryResetDone": "Werkseinstellung abgeschlossen", "admin.factoryResetDone": "Werkseinstellung abgeschlossen",
"admin.readyForResults": "Bereit für Ergebnisse",
"admin.waitingForPlayers": "Warten auf: {names}",
"toast.unexpected": "Unerwarteter Fehler", "toast.unexpected": "Unerwarteter Fehler",
"toast.registered": "Registriert", "toast.registered": "Registriert",

View File

@@ -13,6 +13,7 @@ export const state = {
myVotes: [], myVotes: [],
results: [], results: [],
votesRendered: false, votesRendered: false,
adminVoteStatus: null,
}; };
export function clearUserState() { export function clearUserState() {

View File

@@ -653,6 +653,27 @@ function missingVotesCount() {
return missing < 0 ? 0 : missing; return missing < 0 ? 0 : missing;
} }
function renderAdminVoteStatus() {
if (!state.me?.isAdmin) return;
const list = $("admin-voter-list");
const status = $("admin-ready-status");
if (!state.adminVoteStatus || !list || !status) return;
list.innerHTML = "";
state.adminVoteStatus.voters.forEach((v) => {
const li = document.createElement("li");
li.textContent = `${v.name}${v.finalized ? "✅" : "⏳"}`;
list.appendChild(li);
});
const waiting = state.adminVoteStatus.waiting;
const ready = waiting.length === 0;
status.textContent = ready
? t("admin.readyForResults")
: t("admin.waitingForPlayers", { names: waiting.join(", ") });
status.className = ready ? "badge" : "badge warning";
}
function openDeleteConfirmModal(s) { function openDeleteConfirmModal(s) {
const overlay = document.createElement("div"); const overlay = document.createElement("div");
overlay.className = "edit-modal"; overlay.className = "edit-modal";
@@ -762,6 +783,8 @@ export function updatePhaseNav() {
voteStatusText.textContent = state.votesFinal ? t("nav.voteFinalized") : t("nav.voteHint"); voteStatusText.textContent = state.votesFinal ? t("nav.voteFinalized") : t("nav.voteHint");
} }
renderAdminVoteStatus();
// Toggle admin-only back buttons // Toggle admin-only back buttons
const backButtons = ["nav-vote-prev"]; const backButtons = ["nav-vote-prev"];
backButtons.forEach((id) => { backButtons.forEach((id) => {