diff --git a/Contracts/Dtos.cs b/Contracts/Dtos.cs
index 0092679..f715388 100644
--- a/Contracts/Dtos.cs
+++ b/Contracts/Dtos.cs
@@ -1,3 +1,5 @@
+using GameList.Domain;
+
namespace GameList.Contracts;
public record SetNameRequest(string Name);
@@ -6,7 +8,8 @@ public record SuggestionDto(int Id, string Name, string? Genre, string? Descript
public record VoteRequest(int SuggestionId, int Score);
public record ResultsOpenRequest(bool ResultsOpen);
public record VoteFinalizeRequest(bool Final);
-public record VoteStatusDto(Guid PlayerId, string Name, bool Finalized, bool HasJoker);
+public record VoteStatusDto(Guid PlayerId, string Name, string Username, Phase Phase, bool Finalized, bool HasJoker);
public record LinkSuggestionsRequest(int SourceSuggestionId, int TargetSuggestionId);
public record UnlinkSuggestionsRequest(int SuggestionId);
public record GrantJokerRequest(Guid PlayerId);
+public record DeletePlayerRequest(Guid PlayerId);
diff --git a/Endpoints/AdminEndpoints.cs b/Endpoints/AdminEndpoints.cs
index 738fcb8..48c3f3d 100644
--- a/Endpoints/AdminEndpoints.cs
+++ b/Endpoints/AdminEndpoints.cs
@@ -42,9 +42,13 @@ public static class AdminEndpoints
var voters = await db.Players
.AsNoTracking()
- .Where(p => p.CurrentPhase == Phase.Vote || p.Suggestions.Any())
.OrderBy(p => p.DisplayName ?? p.Username)
- .Select(p => new VoteStatusDto(p.Id, p.DisplayName ?? p.Username, p.VotesFinal, p.HasJoker))
+ .Select(p => new VoteStatusDto(p.Id,
+ p.DisplayName ?? p.Username,
+ p.Username,
+ p.CurrentPhase,
+ p.VotesFinal,
+ p.HasJoker))
.ToListAsync();
var waiting = voters.Where(v => !v.Finalized).Select(v => v.Name).ToList();
@@ -69,6 +73,41 @@ public static class AdminEndpoints
return Results.Ok(new { player.Id, player.HasJoker });
});
+ admin.MapDelete("/players/{playerId:guid}", async (Guid playerId, HttpContext ctx, AppDbContext db, IConfiguration config) =>
+ {
+ if (!await EndpointHelpers.IsAdmin(ctx, db, config)) return Results.Unauthorized();
+
+ var player = await db.Players
+ .Include(p => p.Suggestions)
+ .FirstOrDefaultAsync(p => p.Id == playerId);
+ if (player is null) return Results.NotFound(new { error = "Player not found." });
+
+ await using var tx = await db.Database.BeginTransactionAsync();
+
+ // Remove votes cast by the player
+ await db.Votes.Where(v => v.PlayerId == playerId).ExecuteDeleteAsync();
+
+ // Collect suggestions authored by the player
+ var suggestionIds = player.Suggestions.Select(s => s.Id).ToList();
+ if (suggestionIds.Count > 0)
+ {
+ // Break links pointing to these suggestions
+ await db.Suggestions
+ .Where(s => s.ParentSuggestionId != null && suggestionIds.Contains(s.ParentSuggestionId.Value))
+ .ExecuteUpdateAsync(s => s.SetProperty(x => x.ParentSuggestionId, (int?)null));
+
+ // Remove votes for these suggestions to avoid orphaned rows
+ await db.Votes.Where(v => suggestionIds.Contains(v.SuggestionId)).ExecuteDeleteAsync();
+ }
+
+ // Delete player (cascades suggestions)
+ db.Players.Remove(player);
+ await db.SaveChangesAsync();
+ await tx.CommitAsync();
+
+ return Results.Ok(new { DeletedPlayerId = playerId });
+ });
+
admin.MapPost("/link-suggestions", async ([FromBody] LinkSuggestionsRequest request, HttpContext ctx, AppDbContext db, IConfiguration config) =>
{
var player = await EndpointHelpers.GetAuthenticatedPlayer(ctx, db);
diff --git a/wwwroot/app.js b/wwwroot/app.js
index 0b3bd37..94e62ed 100644
--- a/wwwroot/app.js
+++ b/wwwroot/app.js
@@ -204,17 +204,38 @@ function setupHandlers() {
});
}
- const grantJokerBtn = $("grant-joker");
- if (grantJokerBtn) {
- grantJokerBtn.addEventListener("click", async () => {
- const playerId = $("joker-player")?.value;
- if (!playerId) return toast(t("admin.jokerSelectFirst"), true);
- try {
- await adminApi.grantJoker(playerId);
- toast(t("admin.jokerGranted"));
- await refreshPhaseData();
- } catch (err) {
- toast(err.message, true);
+ const playerTable = $("admin-player-table");
+ if (playerTable) {
+ playerTable.addEventListener("click", async (e) => {
+ const grantBtn = e.target.closest("[data-grant-joker]");
+ const deleteBtn = e.target.closest("[data-delete-player]");
+ if (grantBtn) {
+ const playerId = grantBtn.dataset.grantJoker;
+ try {
+ await adminApi.grantJoker(playerId);
+ toast(t("admin.jokerGranted"));
+ await refreshPhaseData();
+ } catch (err) {
+ toast(err.message, true);
+ }
+ } else if (deleteBtn) {
+ const playerId = deleteBtn.dataset.deletePlayer;
+ const name = deleteBtn.dataset.name || "";
+ openConfirmModal({
+ title: t("admin.deleteTitle"),
+ body: t("admin.deleteBody", { name }),
+ confirmLabel: t("admin.deleteConfirm"),
+ onConfirm: async (close) => {
+ try {
+ await adminApi.deletePlayer(playerId);
+ toast(t("admin.deleteDone"));
+ close();
+ await refreshPhaseData();
+ } catch (err) {
+ toast(err.message, true);
+ }
+ },
+ });
}
});
}
diff --git a/wwwroot/index.html b/wwwroot/index.html
index d56fd99..bee822d 100644
--- a/wwwroot/index.html
+++ b/wwwroot/index.html
@@ -144,22 +144,27 @@
Admin
-
+
-
-
Jokers
-
-
-
Link games