KoogleApp/src/Koogle.Web/Components/Pages/Days/DayDetails.razor

1051 lines
43 KiB
Plaintext

@page "/days/{DayId:guid}"
@attribute [Authorize(Policy = "ClubViewer")]
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
@implements IAsyncDisposable
@using Fluxor
@using Koogle.Application.DTOs
@using Koogle.Domain.Enums
@using Koogle.Web.Store.DayState
@using Koogle.Web.Store.GameState
@using Koogle.Web.Store.GifState
@using Koogle.Web.Store.PersonState
@using Koogle.Web.Store.TimerState
@using Koogle.Web.Components.Game
@using Koogle.Web.Hubs
@using Koogle.Web.Services
@using Microsoft.AspNetCore.Authorization
@inject IState<DayState> DayState
@inject IState<GameState> GameState
@inject IState<GifPlaybackState> GifState
@inject IState<PersonState> PersonState
@inject IState<TimerState> TimerState
@inject IDispatcher Dispatcher
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject NavigationManager NavigationManager
@inject GameHubService HubService
<PageTitle>Spieltag Details</PageTitle>
@if (DayState.Value.IsLoading && DayState.Value.SelectedDay is null)
{
<MudProgressLinear Indeterminate="true" />
}
else if (DayState.Value.Error is not null)
{
<MudAlert Severity="Severity.Error" Class="mb-4" ShowCloseIcon="true" CloseIconClicked="ClearError">
@DayState.Value.Error
</MudAlert>
}
else if (Day is null)
{
<MudAlert Severity="Severity.Warning">Spieltag nicht gefunden</MudAlert>
}
else
{
<!-- GIF Player Overlay -->
<GifPlayer ShowRating="true" />
<!-- Header -->
<MudPaper Class="pa-4 mb-4">
<MudGrid>
<MudItem xs="12" md="6">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
<MudIconButton Icon="@Icons.Material.Filled.ArrowBack" OnClick="NavigateBack" />
<MudText Typo="Typo.h5">@Day.PostDate.ToString("dddd, dd. MMMM yyyy")</MudText>
<MudChip T="string" Size="Size.Medium" Color="GetStatusColor(Day.Status)">
@GetStatusLabel(Day.Status)
</MudChip>
</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"
Color="GetNextStatusColor(Day.Status)"
StartIcon="@GetNextStatusIcon(Day.Status)"
OnClick="AdvanceStatus"
Disabled="DayState.Value.IsLoading"
Class="mr-2">
@GetNextStatusLabel(Day.Status)
</MudButton>
}
@if (Day.Status == DayStatus.New)
{
<MudButton Variant="Variant.Outlined"
Color="Color.Error"
StartIcon="@Icons.Material.Filled.Delete"
OnClick="ConfirmDelete"
Disabled="DayState.Value.IsLoading">
Löschen
</MudButton>
}
</MudItem>
</MudGrid>
</MudPaper>
<!-- 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">
<MudPaper Class="pa-4">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-4">
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.People" Class="mr-2"/>
Teilnehmer (@Day.ParticipantCount)
</MudText>
@if (Day.Status != DayStatus.Closed)
{
<MudButton Variant="Variant.Text"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.PersonAdd"
OnClick="OpenAddParticipantDialog"
Size="Size.Small">
Hinzufügen
</MudButton>
}
</MudStack>
@if (Day.Participants.Count == 0)
{
<MudText Color="Color.Secondary">Keine Teilnehmer</MudText>
}
else
{
<MudList T="DayParticipantDto" Dense="true" Class="participant-list">
@foreach (var participant in Day.Participants.OrderBy(p => p.PersonName))
{
var isSelected = _selectedParticipantId == participant.PersonId;
<MudListItem T="DayParticipantDto"
Class="@(isSelected ? "selected-participant" : "selectable-participant")"
OnClick="@(() => ToggleParticipantSelection(participant.PersonId))">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Style="width: 100%">
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudBadge Visible="@isSelected"
Color="Color.Success"
Icon="@Icons.Material.Filled.Check"
Overlap="true"
Bordered="true">
<MudAvatar Size="Size.Small"
Color="@(isSelected ? Color.Success : (participant.PersonStatus == PersonStatus.Member ? Color.Primary : Color.Secondary))"
Style="@(isSelected ? "border: 2px solid var(--mud-palette-success)" : "")">
@participant.PersonName[0]
</MudAvatar>
</MudBadge>
<MudText Style="@(isSelected ? "font-weight: 600" : "")">@participant.PersonName</MudText>
@if (participant.PersonStatus == PersonStatus.Guest)
{
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="Color.Secondary">Gast</MudChip>
}
</MudStack>
@if (Day.Status != DayStatus.Closed)
{
<MudIconButton Icon="@Icons.Material.Filled.PersonRemove"
Size="Size.Small"
Color="Color.Error"
OnClick="@(() => RemoveParticipant(participant))"
OnClickStopPropagation="true"/>
}
</MudStack>
</MudListItem>
}
</MudList>
@if (_selectedParticipantId.HasValue)
{
<MudStack Row="true" AlignItems="AlignItems.Center" Class="mt-2">
<MudChip T="string" Color="Color.Success" Size="Size.Small" OnClose="ClearParticipantSelection">
@SelectedParticipant?.PersonName ausgewählt
</MudChip>
</MudStack>
}
}
</MudPaper>
</MudItem>
<!-- Statistics -->
<MudItem xs="12" md="6">
<PlayerStatisticsChart DayId="DayState.Value.SelectedDay.Id"/>
</MudItem>
<!-- PersonExpense Section -->
<MudItem xs="12">
<MudPaper Class="pa-4">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="mb-4">
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.AttachMoney" Class="mr-2"/>
Strafen / Kosten (@FilteredExpenses.Count@(_selectedParticipantId.HasValue && Expenses.Count != FilteredExpenses.Count ? $"/{Expenses.Count}" : ""))
</MudText>
@if (Day.Status != DayStatus.Closed)
{
<MudStack Row="true" Spacing="1">
@if (OneClickExpenses.Count > 0 && _selectedParticipantId.HasValue)
{
<MudMenu Icon="@Icons.Material.Filled.FlashOn"
Color="Color.Warning"
Variant="Variant.Filled"
Size="Size.Small"
Title="Schnellzuweisung"
AnchorOrigin="Origin.BottomRight"
TransformOrigin="Origin.TopRight">
@foreach (var expense in OneClickExpenses.OrderBy(e => e.Name))
{
<MudMenuItem OnClick="@(() => QuickAssignExpense(expense))">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Style="min-width: 200px">
<MudText>@expense.Name</MudText>
<MudText Color="Color.Secondary">@expense.Price.ToString("C")</MudText>
</MudStack>
</MudMenuItem>
}
</MudMenu>
}
<MudButton Variant="Variant.Text"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="OpenAddExpenseDialog"
Size="Size.Small"
Disabled="@(Day.Participants.Count == 0)">
Strafe hinzufügen
</MudButton>
</MudStack>
}
</MudStack>
@if (DayState.Value.IsLoadingExpenses)
{
<MudProgressLinear Indeterminate="true" Class="mb-4"/>
}
@if (Day.Participants.Count == 0)
{
<MudAlert Severity="Severity.Info">
Füge zuerst Teilnehmer hinzu, um Strafen zuweisen zu können.
</MudAlert>
}
else if (FilteredExpenses.Count == 0)
{
<MudText Color="Color.Secondary">
@if (_selectedParticipantId.HasValue)
{
<text>Keine Strafen für @SelectedParticipant?.PersonName</text>
}
else
{
<text>Keine Strafen zugewiesen</text>
}
</MudText>
}
else
{
<!-- Summary Section -->
<MudGrid Class="mb-4">
<MudItem xs="12" md="4">
<MudPaper Class="pa-3" Elevation="0" Outlined="true">
<MudStack AlignItems="AlignItems.Center">
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">Gesamt</MudText>
<MudText Typo="Typo.h5">@TotalExpenseAmount.ToString("C")</MudText>
</MudStack>
</MudPaper>
</MudItem>
<MudItem xs="6" md="4">
<MudPaper Class="pa-3" Elevation="0" Outlined="true">
<MudStack AlignItems="AlignItems.Center">
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">Offen</MudText>
<MudText Typo="Typo.h5" Color="Color.Warning">@OpenExpenseAmount.ToString("C")</MudText>
</MudStack>
</MudPaper>
</MudItem>
<MudItem xs="6" md="4">
<MudPaper Class="pa-3" Elevation="0" Outlined="true">
<MudStack AlignItems="AlignItems.Center">
<MudText Typo="Typo.subtitle2" Color="Color.Secondary">Bezahlt</MudText>
<MudText Typo="Typo.h5" Color="Color.Success">@PaidExpenseAmount.ToString("C")</MudText>
</MudStack>
</MudPaper>
</MudItem>
</MudGrid>
<!-- Expense Table -->
<MudTable Items="@FilteredExpenses" Dense="true" Hover="true" Striped="true" Breakpoint="Breakpoint.Sm">
<HeaderContent>
@if (!_selectedParticipantId.HasValue)
{
<MudTh>Person</MudTh>
}
<MudTh>Strafe</MudTh>
<MudTh>Uhrzeit</MudTh>
<MudTh Style="text-align: right">Preis</MudTh>
<MudTh>Status</MudTh>
<MudTh Style="text-align: right">Aktionen</MudTh>
</HeaderContent>
<RowTemplate>
@if (!_selectedParticipantId.HasValue)
{
<MudTd DataLabel="Person">
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudAvatar Size="Size.Small" Color="Color.Primary">
@context.PersonName[0]
</MudAvatar>
<MudText>@context.PersonName</MudText>
</MudStack>
</MudTd>
}
<MudTd DataLabel="Strafe">@context.Name</MudTd>
<MudTd DataLabel="Uhrzeit">@context.CreatedAt.ToString("HH:mm")</MudTd>
<MudTd DataLabel="Preis" Style="text-align: right">@context.Price.ToString("C")</MudTd>
<MudTd DataLabel="Status">
<MudChip T="string"
Size="Size.Small"
Color="@(context.PersonExpenseStatus == PersonExpenseStatus.Open ? Color.Warning : Color.Success)"
Icon="@(context.PersonExpenseStatus == PersonExpenseStatus.Open ? Icons.Material.Filled.HourglassEmpty : Icons.Material.Filled.Check)">
@(context.PersonExpenseStatus == PersonExpenseStatus.Open ? "Offen" : "Bezahlt")
</MudChip>
</MudTd>
<MudTd DataLabel="Aktionen" Style="text-align: right">
<MudStack Row="true" Spacing="1" Justify="Justify.FlexEnd">
@if (context.PersonExpenseStatus == PersonExpenseStatus.Open)
{
<MudIconButton Icon="@Icons.Material.Filled.Check"
Size="Size.Small"
Color="Color.Success"
OnClick="@(() => MarkAsPaid(context))"
Title="Als bezahlt markieren"/>
}
else
{
<MudIconButton Icon="@Icons.Material.Filled.Undo"
Size="Size.Small"
Color="Color.Warning"
OnClick="@(() => MarkAsOpen(context))"
Title="Als offen markieren"/>
}
@if (Day.Status != DayStatus.Closed)
{
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Size="Size.Small"
Color="Color.Error"
OnClick="@(() => ConfirmDeleteExpense(context))"
Title="Löschen"/>
}
</MudStack>
</MudTd>
</RowTemplate>
</MudTable>
}
</MudPaper>
</MudItem>
<!-- Completed Games List at bottom of Details tab -->
<MudItem xs="12" md="6">
<CompletedGamesList DayId="DayId"/>
</MudItem>
<!-- Status/Info Section -->
<MudItem xs="12" md="6">
<MudPaper Class="pa-4">
<MudText Typo="Typo.h6" Class="mb-4">
<MudIcon Icon="@Icons.Material.Filled.Info" Class="mr-2"/>
Details
</MudText>
<MudSimpleTable Dense="true" Hover="true" Striped="true">
<tbody>
<tr>
<td><strong>Status</strong></td>
<td>
<MudChip T="string" Size="Size.Small" Color="GetStatusColor(Day.Status)">
@GetStatusLabel(Day.Status)
</MudChip>
</td>
</tr>
<tr>
<td><strong>Datum</strong></td>
<td>@Day.PostDate.ToString("dd.MM.yyyy")</td>
</tr>
<tr>
<td><strong>Teilnehmer</strong></td>
<td>@Day.ParticipantCount</td>
</tr>
<tr>
<td><strong>Erstellt</strong></td>
<td>@Day.CreatedAt.ToString("dd.MM.yyyy HH:mm")</td>
</tr>
@if (Day.ModifiedAt.HasValue)
{
<tr>
<td><strong>Geändert</strong></td>
<td>@Day.ModifiedAt.Value.ToString("dd.MM.yyyy HH:mm")</td>
</tr>
}
</tbody>
</MudSimpleTable>
<MudDivider Class="my-4"/>
<MudText Typo="Typo.subtitle2" Class="mb-2">Status-Workflow</MudText>
<MudStack Row="true" Spacing="2" AlignItems="AlignItems.Center" Class="mt-2">
<MudChip T="string" Size="Size.Small"
Color="@(Day.Status == DayStatus.New ? Color.Info : Color.Success)"
Icon="@(Day.Status == DayStatus.New ? Icons.Material.Filled.FiberNew : Icons.Material.Filled.Check)">
Neu
</MudChip>
<MudIcon Icon="@Icons.Material.Filled.ArrowForward" Size="Size.Small" Color="Color.Default"/>
<MudChip T="string" Size="Size.Small"
Color="@GetWorkflowStepColor(DayStatus.Started)"
Icon="@GetWorkflowStepIcon(DayStatus.Started)">
Gestartet
</MudChip>
<MudIcon Icon="@Icons.Material.Filled.ArrowForward" Size="Size.Small" Color="Color.Default"/>
<MudChip T="string" Size="Size.Small"
Color="@GetWorkflowStepColor(DayStatus.Closed)"
Icon="@GetWorkflowStepIcon(DayStatus.Closed)">
Abgeschlossen
</MudChip>
</MudStack>
</MudPaper>
</MudItem>
</MudGrid>
</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)">
@if (TimerState.Value.IsRunning)
{
<div class="d-flex justify-center mb-4">
<ThrowTimer OnTimerAborted="HandleTimerAborted" />
</div>
}
<GameBoardPanel />
</MudTabPanel>
</MudTabs>
}
@code {
[Parameter]
public Guid DayId { get; set; }
private DayDto? Day => DayState.Value.SelectedDay;
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();
private IReadOnlyList<PersonExpenseDto> FilteredExpenses => (_selectedParticipantId.HasValue
? Expenses.Where(e => e.PersonId == _selectedParticipantId.Value)
: Expenses).OrderByDescending(e => e.CreatedAt).ToList();
private decimal TotalExpenseAmount => FilteredExpenses.Sum(e => e.Price);
private decimal OpenExpenseAmount => FilteredExpenses.Where(e => e.PersonExpenseStatus == PersonExpenseStatus.Open).Sum(e => e.Price);
private decimal PaidExpenseAmount => FilteredExpenses.Where(e => e.PersonExpenseStatus == PersonExpenseStatus.Done).Sum(e => e.Price);
protected override void OnInitialized()
{
base.OnInitialized();
Dispatcher.Dispatch(new LoadDayDetailsAction(DayId));
Dispatcher.Dispatch(new LoadAvailablePersonsAction());
Dispatcher.Dispatch(new LoadDayExpensesAction(DayId));
Dispatcher.Dispatch(new LoadAvailableExpensesAction());
Dispatcher.Dispatch(new LoadActiveGameAction(DayId));
}
protected override void OnParametersSet()
{
base.OnParametersSet();
if (Day?.Id != DayId)
{
Dispatcher.Dispatch(new LoadDayDetailsAction(DayId));
Dispatcher.Dispatch(new LoadDayExpensesAction(DayId));
Dispatcher.Dispatch(new LoadActiveGameAction(DayId));
}
}
private void ClearError()
{
Dispatcher.Dispatch(new ClearDayErrorAction());
}
private void ToggleParticipantSelection(Guid personId)
{
_selectedParticipantId = _selectedParticipantId == personId ? null : personId;
}
private void ClearParticipantSelection()
{
_selectedParticipantId = null;
}
private void NavigateBack()
{
NavigationManager.NavigateTo("/days");
}
private static string GetStatusLabel(DayStatus status) => status switch
{
DayStatus.New => "Neu",
DayStatus.Started => "Gestartet",
DayStatus.Postponed => "Verschoben",
DayStatus.Closed => "Abgeschlossen",
_ => status.ToString()
};
private static Color GetStatusColor(DayStatus status) => status switch
{
DayStatus.New => Color.Info,
DayStatus.Started => Color.Warning,
DayStatus.Postponed => Color.Secondary,
DayStatus.Closed => Color.Success,
_ => Color.Default
};
private static string GetNextStatusLabel(DayStatus status) => status switch
{
DayStatus.New => "Spieltag starten",
DayStatus.Started => "Spieltag abschliessen",
DayStatus.Postponed => "Spieltag fortsetzen",
_ => ""
};
private static Color GetNextStatusColor(DayStatus status) => status switch
{
DayStatus.New => Color.Warning,
DayStatus.Started => Color.Success,
DayStatus.Postponed => Color.Warning,
_ => Color.Default
};
private static string GetNextStatusIcon(DayStatus status) => status switch
{
DayStatus.New => Icons.Material.Filled.PlayArrow,
DayStatus.Started => Icons.Material.Filled.Done,
DayStatus.Postponed => Icons.Material.Filled.PlayArrow,
_ => Icons.Material.Filled.Help
};
private Color GetWorkflowStepColor(DayStatus targetStatus)
{
if (Day is null) return Color.Default;
var statusOrder = new[] { DayStatus.New, DayStatus.Started, DayStatus.Closed };
var currentIndex = Array.IndexOf(statusOrder, Day.Status);
var targetIndex = Array.IndexOf(statusOrder, targetStatus);
if (targetIndex < currentIndex) return Color.Success; // Completed
if (targetIndex == currentIndex) return GetStatusColor(Day.Status); // Current
return Color.Default; // Future
}
private string GetWorkflowStepIcon(DayStatus targetStatus)
{
if (Day is null) return Icons.Material.Filled.Circle;
var statusOrder = new[] { DayStatus.New, DayStatus.Started, DayStatus.Closed };
var currentIndex = Array.IndexOf(statusOrder, Day.Status);
var targetIndex = Array.IndexOf(statusOrder, targetStatus);
if (targetIndex < currentIndex) return Icons.Material.Filled.Check; // Completed
if (targetIndex == currentIndex) return targetStatus switch
{
DayStatus.Started => Icons.Material.Filled.PlayArrow,
DayStatus.Closed => Icons.Material.Filled.Done,
_ => Icons.Material.Filled.Circle
};
return Icons.Material.Filled.Circle; // Future
}
private async Task AdvanceStatus()
{
if (Day is null)
return;
if (Day.Status == DayStatus.Started )
{
var res = await DialogService.ShowMessageBox("Tag abschließen?", "Spieltag wirklich beenden?", yesText: "Ja", noText: "Nein");
if (res.HasValue && !res.Value)
{
return;
}
}
Dispatcher.Dispatch(new AdvanceDayStatusAction(Day.Id));
Snackbar.Add("Status wird aktualisiert...", Severity.Info);
}
private async Task ConfirmDelete()
{
if (Day is null) return;
var parameters = new DialogParameters
{
{ "ContentText", $"Möchtest du den Spieltag vom {Day.PostDate:dd.MM.yyyy} wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden." },
{ "ButtonText", "Löschen" },
{ "Color", Color.Error }
};
var dialog = await DialogService.ShowAsync<Koogle.Web.Components.Shared.ConfirmDialog>("Spieltag löschen", parameters);
var result = await dialog.Result;
if (result != null && !result.Canceled)
{
Dispatcher.Dispatch(new DeleteDayAction(Day.Id));
Snackbar.Add("Spieltag wird gelöscht...", Severity.Info);
NavigationManager.NavigateTo("/days");
}
}
private async Task OpenAddParticipantDialog()
{
if (Day is null) return;
var existingIds = Day.Participants.Select(p => p.PersonId).ToHashSet();
var availablePersons = DayState.Value.AvailablePersons
.Where(p => !existingIds.Contains(p.Id))
.ToList();
if (availablePersons.Count == 0)
{
Snackbar.Add("Alle Personen sind bereits Teilnehmer", Severity.Info);
return;
}
var parameters = new DialogParameters
{
{ "AvailablePersons", availablePersons },
{ "DayId", Day.Id }
};
var dialog = await DialogService.ShowAsync<AddParticipantDialog>("Teilnehmer hinzufügen", parameters);
var result = await dialog.Result;
if (result != null && !result.Canceled && result.Data is List<Guid> personIds)
{
foreach (var personId in personIds)
{
var dto = new AddDayParticipantDto
{
DayId = Day.Id,
PersonId = personId
};
Dispatcher.Dispatch(new AddDayParticipantAction(dto));
}
Snackbar.Add($"{personIds.Count} Teilnehmer werden hinzugefügt...", Severity.Info);
}
}
private async Task RemoveParticipant(DayParticipantDto participant)
{
if (Day is null) return;
var parameters = new DialogParameters
{
{ "ContentText", $"Möchtest du \"{participant.PersonName}\" wirklich von diesem Spieltag entfernen?" },
{ "ButtonText", "Entfernen" },
{ "Color", Color.Warning }
};
var dialog = await DialogService.ShowAsync<Koogle.Web.Components.Shared.ConfirmDialog>("Teilnehmer entfernen", parameters);
var result = await dialog.Result;
if (result != null && !result.Canceled)
{
var dto = new RemoveDayParticipantDto
{
DayId = Day.Id,
PersonId = participant.PersonId
};
Dispatcher.Dispatch(new RemoveDayParticipantAction(dto));
Snackbar.Add("Teilnehmer wird entfernt...", Severity.Info);
}
}
// PersonExpense Methods
private async Task OpenAddExpenseDialog()
{
if (Day is null) return;
var availableExpenses = DayState.Value.AvailableExpenses;
if (availableExpenses.Count == 0)
{
Snackbar.Add("Keine Strafen-Vorlagen definiert. Bitte erstelle zuerst Vorlagen unter \"Strafen\".", Severity.Warning);
return;
}
var parameters = new DialogParameters
{
{ "AvailableExpenses", availableExpenses },
{ "Participants", Day.Participants },
{ "DayId", Day.Id },
{ "PreselectedPersonId", _selectedParticipantId }
};
var dialog = await DialogService.ShowAsync<AddPersonExpenseDialog>("Strafe hinzufügen", parameters);
var result = await dialog.Result;
if (result != null && !result.Canceled && result.Data is not null)
{
if (result.Data is CreateInversePersonExpenseDto inverseDto)
{
Dispatcher.Dispatch(new CreateInversePersonExpenseAction(inverseDto));
Snackbar.Add("Inverse Strafe wird erstellt...", Severity.Info);
}
else if (result.Data is CreatePersonExpenseDto createDto)
{
Dispatcher.Dispatch(new CreatePersonExpenseAction(createDto));
Snackbar.Add("Strafe wird hinzugefügt...", Severity.Info);
}
}
}
private void QuickAssignExpense(ExpenseDto expense)
{
if (Day is null || !_selectedParticipantId.HasValue) return;
if (expense.IsInverse)
{
var dto = new CreateInversePersonExpenseDto
{
ExcludedPersonId = _selectedParticipantId.Value,
ExpenseId = expense.Id,
DayId = Day.Id,
GameId = null
};
Dispatcher.Dispatch(new CreateInversePersonExpenseAction(dto));
Snackbar.Add($"{expense.Name} für alle außer {SelectedParticipant?.PersonName}...", Severity.Info);
}
else
{
var dto = new CreatePersonExpenseDto
{
PersonId = _selectedParticipantId.Value,
ExpenseId = expense.Id,
DayId = Day.Id,
GameId = null,
Price = null
};
Dispatcher.Dispatch(new CreatePersonExpenseAction(dto));
Snackbar.Add($"{expense.Name} für {SelectedParticipant?.PersonName}...", Severity.Info);
}
}
private void MarkAsPaid(PersonExpenseDto expense)
{
var dto = new UpdatePersonExpenseStatusDto
{
Id = expense.Id,
Status = PersonExpenseStatus.Done
};
Dispatcher.Dispatch(new UpdatePersonExpenseStatusAction(dto));
Snackbar.Add("Status wird aktualisiert...", Severity.Info);
}
private void MarkAsOpen(PersonExpenseDto expense)
{
var dto = new UpdatePersonExpenseStatusDto
{
Id = expense.Id,
Status = PersonExpenseStatus.Open
};
Dispatcher.Dispatch(new UpdatePersonExpenseStatusAction(dto));
Snackbar.Add("Status wird aktualisiert...", Severity.Info);
}
private async Task ConfirmDeleteExpense(PersonExpenseDto expense)
{
if (Day is null) return;
var parameters = new DialogParameters
{
{ "ContentText", $"Möchtest du die Strafe \"{expense.Name}\" für \"{expense.PersonName}\" wirklich löschen?" },
{ "ButtonText", "Löschen" },
{ "Color", Color.Error }
};
var dialog = await DialogService.ShowAsync<Koogle.Web.Components.Shared.ConfirmDialog>("Strafe löschen", parameters);
var result = await dialog.Result;
if (result != null && !result.Canceled)
{
Dispatcher.Dispatch(new DeletePersonExpenseAction(expense.Id, Day.Id));
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 async Task EndGame()
{
var res = await DialogService.ShowMessageBox("Spiel beenden", "Spiel wirklich beenden?", yesText:"Ja", noText:"Nein");
if (res.HasValue && res.Value)
{
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()
{
if (!GameState.Value.IsGameActive) return;
var parameters = new DialogParameters
{
{ "PlayerIds", GameState.Value.Participants.PlayerIds },
{ "CurrentPlayerId", GameState.Value.Participants.CurrentPlayerId },
{ "PlayerNameResolver", (Func<Guid, string>)GetPlayerName },
{ "GameModel", GameState.Value.GameModel },
{ "FilterByGameLogic", true }
};
var options = new DialogOptions
{
MaxWidth = MaxWidth.Small,
FullWidth = true,
CloseOnEscapeKey = true
};
var dialog = await DialogService.ShowAsync<PlayerSelectorDialog>("Spieler auswählen", parameters, options);
var result = await dialog.Result;
if (result != null && !result.Canceled && result.Data is Guid selectedPlayerId)
{
Dispatcher.Dispatch(new SetCurrentPlayerAction(selectedPlayerId));
Snackbar.Add($"Spieler gewechselt zu {GetPlayerName(selectedPlayerId)}", Severity.Success);
}
}
private const int TimerDurationSeconds = 3;
private Task HandleThrowCompleted(ThrowResult result)
{
// Switch to Tafel tab and start timer
_activeTabIndex = 2;
Dispatcher.Dispatch(new StartTimerAction(TimerDurationSeconds));
return Task.CompletedTask;
}
private void HandleTimerAborted()
{
// User clicked timer button - switch back to Eingabe tab
_activeTabIndex = 1;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
TimerState.StateChanged += OnTimerStateChanged;
GameState.StateChanged += OnGameStateChanged;
// Subscribe to SignalR events
HubService.OnGameStateUpdated += HandleHubGameStateUpdated;
HubService.OnThrowRecorded += HandleHubThrowRecorded;
HubService.OnGameStarted += HandleHubGameStarted;
HubService.OnGameEnded += HandleHubGameEnded;
HubService.OnGifTriggered += HandleHubGifTriggered;
// Start SignalR connection and join day group
await HubService.StartAsync();
await HubService.JoinDayAsync(DayId);
// If there's an active game, join its group too
if (GameState.Value.ActiveGameId.HasValue)
{
await HubService.JoinGameAsync(GameState.Value.ActiveGameId.Value);
_previousGameId = GameState.Value.ActiveGameId.Value;
}
}
}
private void OnTimerStateChanged(object? sender, EventArgs e)
{
// When timer stops (completed naturally), switch back to Eingabe tab
if (!TimerState.Value.IsRunning && _activeTabIndex == 2)
{
_activeTabIndex = 1;
InvokeAsync(StateHasChanged);
}
}
private Guid? _previousGameId;
private async void OnGameStateChanged(object? sender, EventArgs e)
{
var currentGameId = GameState.Value.ActiveGameId;
// Game started - join game group
if (currentGameId.HasValue && _previousGameId != currentGameId)
{
if (_previousGameId.HasValue)
{
await HubService.LeaveGameAsync(_previousGameId.Value);
}
await HubService.JoinGameAsync(currentGameId.Value);
_previousGameId = currentGameId;
}
// Game ended - leave game group
else if (!currentGameId.HasValue && _previousGameId.HasValue)
{
await HubService.LeaveGameAsync(_previousGameId.Value);
_previousGameId = null;
}
}
// SignalR event handlers
private void HandleHubGameStateUpdated(GameStateSerializationDto state)
{
// Dispatch action to update local state from remote
Dispatcher.Dispatch(new RemoteGameStateUpdatedAction(state));
InvokeAsync(StateHasChanged);
}
private void HandleHubThrowRecorded(Guid gameId, Guid playerId, int pinsKnocked)
{
// Throw was recorded by another client - refresh state
Snackbar.Add($"Wurf empfangen: {pinsKnocked} Pins", Severity.Info);
InvokeAsync(StateHasChanged);
}
private void HandleHubGameStarted(Guid gameId, string gameTypeName)
{
// New game started - reload active game
Dispatcher.Dispatch(new LoadActiveGameAction(DayId));
Snackbar.Add($"Neues Spiel gestartet: {gameTypeName}", Severity.Success);
InvokeAsync(StateHasChanged);
}
private void HandleHubGameEnded(GameResultDto result)
{
// Game ended - reload completed games
Dispatcher.Dispatch(new LoadCompletedGamesAction(DayId));
var message = result.WinnerName != null
? $"Spiel beendet! Gewinner: {result.WinnerName}"
: "Spiel beendet!";
Snackbar.Add(message, Severity.Success);
_activeTabIndex = 0;
InvokeAsync(StateHasChanged);
}
private void HandleHubGifTriggered(GifPlaybackDto gif, ThrowEventType eventType)
{
// GIF triggered from another client - dispatch action to show it
Dispatcher.Dispatch(new GifPlaybackStartedAction(gif, eventType));
InvokeAsync(StateHasChanged);
}
public new async ValueTask DisposeAsync()
{
TimerState.StateChanged -= OnTimerStateChanged;
GameState.StateChanged -= OnGameStateChanged;
// Unsubscribe from SignalR events
HubService.OnGameStateUpdated -= HandleHubGameStateUpdated;
HubService.OnThrowRecorded -= HandleHubThrowRecorded;
HubService.OnGameStarted -= HandleHubGameStarted;
HubService.OnGameEnded -= HandleHubGameEnded;
HubService.OnGifTriggered -= HandleHubGifTriggered;
// Leave groups and stop hub connection
if (_previousGameId.HasValue)
{
await HubService.LeaveGameAsync(_previousGameId.Value);
}
await HubService.LeaveDayAsync(DayId);
await HubService.DisposeAsync();
await base.DisposeAsync();
}
}