Add admin vote status list and ready/wait messaging
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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" }),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export const state = {
|
|||||||
myVotes: [],
|
myVotes: [],
|
||||||
results: [],
|
results: [],
|
||||||
votesRendered: false,
|
votesRendered: false,
|
||||||
|
adminVoteStatus: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function clearUserState() {
|
export function clearUserState() {
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user