From 23a1008a31624046dbbeec3733ebbf27f6985dac Mon Sep 17 00:00:00 2001 From: beo3000 Date: Sat, 27 Dec 2025 08:33:34 +0100 Subject: [PATCH] Complete phase H7: DayDetails Tabs Integration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GameBoardPanel: Dynamic board component rendering - CompletedGamesList: Shows game history for day - DayDetails: 3 tabs (Details/Eingabe/Tafel) + Start/End Game buttons 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- docs/IMPLEMENTATION_PLAN.md | 2 +- .../Components/Game/CompletedGamesList.razor | 143 ++++++++++++++++++ .../Components/Game/GameBoardPanel.razor | 85 +++++++++++ .../Components/Pages/Days/DayDetails.razor | 119 ++++++++++++++- 4 files changed, 347 insertions(+), 2 deletions(-) create mode 100644 src/Koogle.Web/Components/Game/CompletedGamesList.razor create mode 100644 src/Koogle.Web/Components/Game/GameBoardPanel.razor diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md index b8870de..12cf7b6 100644 --- a/docs/IMPLEMENTATION_PLAN.md +++ b/docs/IMPLEMENTATION_PLAN.md @@ -954,7 +954,7 @@ Web: Store/GameState/, Components/Game/, Hubs/GameHub | ✓ | H4 | Games | Training Game | 5 | | ✓ | H5 | Games | Scheiss-Spiel + Trigger-Integration | 6 | | ✓ | H6 | UI | Game Setup Dialog | 4 | -| ☐ | H7 | Integration | DayDetails Tabs | 3 | +| ✓ | H7 | Integration | DayDetails Tabs | 3 | | ☐ | H8 | Features | Undo (unbegrenzt, ohne Redo) | 2 | | ☐ | H9 | Persistenz | DB Persistence & Recovery | 4 | | ☐ | H9b | Sync | SignalR komplett + Auto-Reconnect | 5 | diff --git a/src/Koogle.Web/Components/Game/CompletedGamesList.razor b/src/Koogle.Web/Components/Game/CompletedGamesList.razor new file mode 100644 index 0000000..640f115 --- /dev/null +++ b/src/Koogle.Web/Components/Game/CompletedGamesList.razor @@ -0,0 +1,143 @@ +@using Fluxor +@using Koogle.Domain.Enums +@using Koogle.Web.Store.GameState +@using MudBlazor + +@inherits Fluxor.Blazor.Web.Components.FluxorComponent + +@inject IState GameState +@inject IDispatcher Dispatcher + + + + + + Abgeschlossene Spiele + + @if (GameState.Value.CompletedGames.Count > 0) + { + + @GameState.Value.CompletedGames.Count + + } + + + @if (GameState.Value.IsLoading) + { + + } + + @if (GameState.Value.CompletedGames.Count == 0) + { + + Noch keine abgeschlossenen Spiele an diesem Spieltag. + + } + else + { + + + Spieltyp + Gestartet + Beendet + Spieler + Status + + + + + + @GetGameDisplayName(context.GameTypeName) + + + + @(context.StartedAt?.ToString("HH:mm") ?? "-") + + + @(context.CompletedAt?.ToString("HH:mm") ?? "-") + + + @context.ParticipantCount + + + + @GetStatusLabel(context.Status) + + + + + } + + +@code { + /// + /// ID of the day to load completed games for. + /// + [Parameter] + public Guid DayId { get; set; } + + protected override void OnInitialized() + { + base.OnInitialized(); + + if (DayId != Guid.Empty) + { + Dispatcher.Dispatch(new LoadCompletedGamesAction(DayId)); + } + } + + protected override void OnParametersSet() + { + base.OnParametersSet(); + + if (DayId != Guid.Empty) + { + Dispatcher.Dispatch(new LoadCompletedGamesAction(DayId)); + } + } + + private static string GetGameIcon(string gameTypeName) => gameTypeName switch + { + "Training" => Icons.Material.Filled.FitnessCenter, + "Shit" => Icons.Material.Filled.Casino, + _ => Icons.Material.Filled.SportsScore + }; + + private static string GetGameDisplayName(string gameTypeName) => gameTypeName switch + { + "Training" => "Training", + "Shit" => "Scheiss-Spiel", + _ => gameTypeName + }; + + private static Color GetStatusColor(GameStatus status) => status switch + { + GameStatus.Completed => Color.Success, + GameStatus.Aborted => Color.Warning, + GameStatus.Active => Color.Info, + _ => Color.Default + }; + + private static string GetStatusIcon(GameStatus status) => status switch + { + GameStatus.Completed => Icons.Material.Filled.CheckCircle, + GameStatus.Aborted => Icons.Material.Filled.Cancel, + GameStatus.Active => Icons.Material.Filled.PlayCircle, + _ => Icons.Material.Filled.Circle + }; + + private static string GetStatusLabel(GameStatus status) => status switch + { + GameStatus.Completed => "Beendet", + GameStatus.Aborted => "Abgebrochen", + GameStatus.Active => "Aktiv", + _ => status.ToString() + }; +} diff --git a/src/Koogle.Web/Components/Game/GameBoardPanel.razor b/src/Koogle.Web/Components/Game/GameBoardPanel.razor new file mode 100644 index 0000000..47e5dac --- /dev/null +++ b/src/Koogle.Web/Components/Game/GameBoardPanel.razor @@ -0,0 +1,85 @@ +@using Fluxor +@using Koogle.Application.Games +@using Koogle.Domain.Interfaces +@using Koogle.Web.Store.GameState +@using MudBlazor + +@inherits Fluxor.Blazor.Web.Components.FluxorComponent + +@inject IState GameState +@inject GameDefinitionRegistry GameRegistry + + + @if (!GameState.Value.IsGameActive) + { + + Kein aktives Spiel. Starte ein neues Spiel, um die Tafel zu sehen. + + } + else if (string.IsNullOrEmpty(GameState.Value.GameTypeName)) + { + + Spieltyp nicht erkannt. + + } + else + { + @* Header with game type info *@ + + + + @_gameDefinition?.DisplayName Tafel + + + Aktiv + + + + @* Dynamic game board component *@ + @if (_gameDefinition?.BoardComponentType != null) + { + + } + else + { + + Keine Tafel-Komponente für diesen Spieltyp verfügbar. + + } + } + + +@code { + private IGameDefinition? _gameDefinition; + + protected override void OnInitialized() + { + base.OnInitialized(); + GameState.StateChanged += OnGameStateChanged; + UpdateGameDefinition(); + } + + private void OnGameStateChanged(object? sender, EventArgs e) + { + UpdateGameDefinition(); + InvokeAsync(StateHasChanged); + } + + private void UpdateGameDefinition() + { + var typeName = GameState.Value.GameTypeName; + if (!string.IsNullOrEmpty(typeName)) + { + _gameDefinition = GameRegistry.Get(typeName); + } + else + { + _gameDefinition = null; + } + } + + public void Dispose() + { + GameState.StateChanged -= OnGameStateChanged; + } +} diff --git a/src/Koogle.Web/Components/Pages/Days/DayDetails.razor b/src/Koogle.Web/Components/Pages/Days/DayDetails.razor index b9dcacc..a03a345 100644 --- a/src/Koogle.Web/Components/Pages/Days/DayDetails.razor +++ b/src/Koogle.Web/Components/Pages/Days/DayDetails.razor @@ -7,9 +7,14 @@ @using Koogle.Application.DTOs @using Koogle.Domain.Enums @using Koogle.Web.Store.DayState +@using Koogle.Web.Store.GameState +@using Koogle.Web.Store.PersonState +@using Koogle.Web.Components.Game @using Microsoft.AspNetCore.Authorization @inject IState DayState +@inject IState GameState +@inject IState PersonState @inject IDispatcher Dispatcher @inject ISnackbar Snackbar @inject IDialogService DialogService @@ -46,6 +51,28 @@ else + @if (Day.Status == DayStatus.Started && !GameState.Value.IsGameActive) + { + + Neues Spiel + + } + @if (GameState.Value.IsGameActive) + { + + Spiel beenden + + } @if (Day.Status != DayStatus.Closed) { - + + + + @@ -381,6 +411,24 @@ else + + + + + + + + + + + + + + + } @code { @@ -391,6 +439,7 @@ else private IReadOnlyList Expenses => DayState.Value.SelectedDayExpenses; private Guid? _selectedParticipantId; + private int _activeTabIndex = 0; private DayParticipantDto? SelectedParticipant => Day?.Participants.FirstOrDefault(p => p.PersonId == _selectedParticipantId); private IReadOnlyList OneClickExpenses => DayState.Value.AvailableExpenses.Where(e => e.IsOneClick && !e.IsVariable).ToList(); @@ -721,4 +770,72 @@ else Snackbar.Add("Strafe wird gelöscht...", Severity.Info); } } + + // Game-related methods + + private async Task OpenGameSetupDialog() + { + if (Day is null) return; + + var parameters = new DialogParameters + { + { "DayId", Day.Id } + }; + + var options = new DialogOptions + { + MaxWidth = MaxWidth.Medium, + FullWidth = true, + CloseOnEscapeKey = true + }; + + var dialog = await DialogService.ShowAsync("Neues Spiel starten", parameters, options); + var result = await dialog.Result; + + if (result != null && !result.Canceled) + { + // Game started - switch to Eingabe tab + _activeTabIndex = 1; + Snackbar.Add("Spiel gestartet!", Severity.Success); + } + } + + private void EndGame() + { + Dispatcher.Dispatch(new EndGameAction(GameStatus.Completed)); + _activeTabIndex = 0; // Switch back to Details tab + Snackbar.Add("Spiel wird beendet...", Severity.Info); + } + + private string? GetCurrentPlayerName() + { + var playerId = GameState.Value.Participants.CurrentPlayerId; + if (playerId is null) return null; + + return GetPlayerName(playerId.Value); + } + + private string GetPlayerName(Guid personId) + { + // First try to get from PersonState + var person = PersonState.Value.Persons.FirstOrDefault(p => p.Id == personId); + if (person != null) return person.Name; + + // Fallback to day participants + var participant = Day?.Participants.FirstOrDefault(p => p.PersonId == personId); + return participant?.PersonName ?? "Unbekannt"; + } + + private async Task ShowPlayerSelector() + { + // TODO: Implement player selector dialog + await Task.CompletedTask; + Snackbar.Add("Spieler-Auswahl noch nicht implementiert", Severity.Info); + } + + private Task HandleThrowCompleted(ThrowResult result) + { + // Throw was completed - game logic will handle state updates + return Task.CompletedTask; + } }