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

-
+
- +
+ + + + + + + + + + + +
NameUsernameStatusJokerDelete
+
-