Complete phase H7: DayDetails Tabs Integration

- 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 <noreply@anthropic.com>
This commit is contained in:
beo3000 2025-12-27 08:33:34 +01:00
parent 5afc8fd251
commit 23a1008a31
4 changed files with 347 additions and 2 deletions

View File

@ -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 |

View File

@ -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> GameState
@inject IDispatcher Dispatcher
<MudPaper Class="pa-4">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-4">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.History" />
<MudText Typo="Typo.h6">Abgeschlossene Spiele</MudText>
</MudStack>
@if (GameState.Value.CompletedGames.Count > 0)
{
<MudChip T="string" Size="Size.Small" Color="Color.Default">
@GameState.Value.CompletedGames.Count
</MudChip>
}
</MudStack>
@if (GameState.Value.IsLoading)
{
<MudProgressLinear Indeterminate="true" Class="mb-4" />
}
@if (GameState.Value.CompletedGames.Count == 0)
{
<MudAlert Severity="Severity.Info" Dense="true">
Noch keine abgeschlossenen Spiele an diesem Spieltag.
</MudAlert>
}
else
{
<MudTable Items="@GameState.Value.CompletedGames"
Dense="true"
Hover="true"
Striped="true"
Breakpoint="Breakpoint.Sm">
<HeaderContent>
<MudTh>Spieltyp</MudTh>
<MudTh>Gestartet</MudTh>
<MudTh>Beendet</MudTh>
<MudTh>Spieler</MudTh>
<MudTh>Status</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Spieltyp">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@GetGameIcon(context.GameTypeName)" Size="Size.Small" />
<MudText>@GetGameDisplayName(context.GameTypeName)</MudText>
</MudStack>
</MudTd>
<MudTd DataLabel="Gestartet">
@(context.StartedAt?.ToString("HH:mm") ?? "-")
</MudTd>
<MudTd DataLabel="Beendet">
@(context.CompletedAt?.ToString("HH:mm") ?? "-")
</MudTd>
<MudTd DataLabel="Spieler">
@context.ParticipantCount
</MudTd>
<MudTd DataLabel="Status">
<MudChip T="string"
Size="Size.Small"
Color="@GetStatusColor(context.Status)"
Icon="@GetStatusIcon(context.Status)">
@GetStatusLabel(context.Status)
</MudChip>
</MudTd>
</RowTemplate>
</MudTable>
}
</MudPaper>
@code {
/// <summary>
/// ID of the day to load completed games for.
/// </summary>
[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()
};
}

View File

@ -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> GameState
@inject GameDefinitionRegistry GameRegistry
<MudPaper Class="pa-4">
@if (!GameState.Value.IsGameActive)
{
<MudAlert Severity="Severity.Info">
Kein aktives Spiel. Starte ein neues Spiel, um die Tafel zu sehen.
</MudAlert>
}
else if (string.IsNullOrEmpty(GameState.Value.GameTypeName))
{
<MudAlert Severity="Severity.Warning">
Spieltyp nicht erkannt.
</MudAlert>
}
else
{
@* Header with game type info *@
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-4">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIcon Icon="@Icons.Material.Filled.TableChart" />
<MudText Typo="Typo.h6">@_gameDefinition?.DisplayName Tafel</MudText>
</MudStack>
<MudChip T="string" Color="Color.Success" Size="Size.Small">
Aktiv
</MudChip>
</MudStack>
@* Dynamic game board component *@
@if (_gameDefinition?.BoardComponentType != null)
{
<DynamicComponent Type="@_gameDefinition.BoardComponentType" />
}
else
{
<MudAlert Severity="Severity.Warning">
Keine Tafel-Komponente für diesen Spieltyp verfügbar.
</MudAlert>
}
}
</MudPaper>
@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;
}
}

View File

@ -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> DayState
@inject IState<GameState> GameState
@inject IState<PersonState> PersonState
@inject IDispatcher Dispatcher
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@ -46,6 +51,28 @@ else
</MudStack>
</MudItem>
<MudItem xs="12" md="6" Class="d-flex justify-end align-center">
@if (Day.Status == DayStatus.Started && !GameState.Value.IsGameActive)
{
<MudButton Variant="Variant.Filled"
Color="Color.Success"
StartIcon="@Icons.Material.Filled.SportsScore"
OnClick="OpenGameSetupDialog"
Disabled="DayState.Value.IsLoading || Day.Participants.Count == 0"
Class="mr-2">
Neues Spiel
</MudButton>
}
@if (GameState.Value.IsGameActive)
{
<MudButton Variant="Variant.Outlined"
Color="Color.Warning"
StartIcon="@Icons.Material.Filled.Stop"
OnClick="EndGame"
Disabled="DayState.Value.IsLoading"
Class="mr-2">
Spiel beenden
</MudButton>
}
@if (Day.Status != DayStatus.Closed)
{
<MudButton Variant="Variant.Filled"
@ -71,7 +98,10 @@ else
</MudGrid>
</MudPaper>
<!-- Main Content -->
<!-- Main Content with Tabs -->
<MudTabs @bind-ActivePanelIndex="_activeTabIndex" Elevation="0" Rounded="true" ApplyEffectsToContainer="true" PanelClass="pa-4">
<!-- Tab 1: Details (existing content) -->
<MudTabPanel Text="Details" Icon="@Icons.Material.Filled.Info">
<MudGrid>
<!-- Participants Section -->
<MudItem xs="12" md="6">
@ -381,6 +411,24 @@ else
</MudPaper>
</MudItem>
</MudGrid>
<!-- Completed Games List at bottom of Details tab -->
<CompletedGamesList DayId="DayId" />
</MudTabPanel>
<!-- Tab 2: Eingabe (Game Input) -->
<MudTabPanel Text="Eingabe" Icon="@Icons.Material.Filled.SportsScore" Disabled="@(!GameState.Value.IsGameActive)">
<GameInputPanel CurrentPlayerName="@GetCurrentPlayerName()"
PlayerNameResolver="@GetPlayerName"
OnShowPlayerSelector="@ShowPlayerSelector"
OnThrowCompleted="@HandleThrowCompleted" />
</MudTabPanel>
<!-- Tab 3: Tafel (Game Board) -->
<MudTabPanel Text="Tafel" Icon="@Icons.Material.Filled.TableChart" Disabled="@(!GameState.Value.IsGameActive)">
<GameBoardPanel />
</MudTabPanel>
</MudTabs>
}
@code {
@ -391,6 +439,7 @@ else
private IReadOnlyList<PersonExpenseDto> Expenses => DayState.Value.SelectedDayExpenses;
private Guid? _selectedParticipantId;
private int _activeTabIndex = 0;
private DayParticipantDto? SelectedParticipant => Day?.Participants.FirstOrDefault(p => p.PersonId == _selectedParticipantId);
private IReadOnlyList<ExpenseDto> 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<GameSetupDialog>("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;
}
}