1051 lines
43 KiB
Plaintext
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();
|
|
}
|
|
}
|