Add Days List Page (E2)

🤖 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-25 09:41:54 +01:00
parent e1a0969d6e
commit 642c4d11bf
2 changed files with 361 additions and 0 deletions

View File

@ -0,0 +1,147 @@
@using Koogle.Application.DTOs
@using Koogle.Domain.Enums
@using Fluxor
@using Koogle.Web.Store.PersonState
@inject IState<PersonState> PersonState
@inject IDispatcher Dispatcher
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">
@(IsEditMode ? "Spieltag bearbeiten" : "Neuer Spieltag")
</MudText>
</TitleContent>
<DialogContent>
<MudForm @ref="_form" @bind-IsValid="_isValid">
<MudDatePicker @bind-Date="_postDate"
Label="Datum"
Required="true"
RequiredError="Datum ist erforderlich"
DateFormat="dd.MM.yyyy"
Class="mb-4" />
@if (IsEditMode)
{
<MudSelect T="DayStatus" @bind-Value="_status"
Label="Status"
AnchorOrigin="Origin.BottomCenter"
Class="mb-4">
<MudSelectItem Value="DayStatus.New">Neu</MudSelectItem>
<MudSelectItem Value="DayStatus.Started">Gestartet</MudSelectItem>
<MudSelectItem Value="DayStatus.Postponed">Verschoben</MudSelectItem>
<MudSelectItem Value="DayStatus.Closed">Abgeschlossen</MudSelectItem>
</MudSelect>
}
else
{
<MudText Typo="Typo.subtitle2" Class="mb-2">Teilnehmer vorauswählen</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary" Class="mb-2">
Mitglieder sind automatisch vorausgewählt. Sie können Teilnehmer später noch ändern.
</MudText>
@if (PersonState.Value.IsLoading)
{
<MudProgressLinear Indeterminate="true" Class="mb-4" />
}
else
{
<MudChipSet T="Guid" SelectionMode="SelectionMode.MultiSelection" @bind-SelectedValues="_selectedPersonIds" Class="mb-4">
@foreach (var person in PersonState.Value.Persons.OrderByDescending(p => p.PersonStatus == PersonStatus.Member).ThenBy(p => p.Name))
{
<MudChip T="Guid"
Value="@person.Id"
Default="@(person.PersonStatus == PersonStatus.Member)"
Color="@(person.PersonStatus == PersonStatus.Member ? Color.Primary : Color.Default)"
Variant="Variant.Outlined">
@person.Name
@if (person.PersonStatus == PersonStatus.Guest)
{
<MudText Typo="Typo.caption" Class="ml-1">(Gast)</MudText>
}
</MudChip>
}
</MudChipSet>
}
}
</MudForm>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Abbrechen</MudButton>
<MudButton Color="Color.Primary" Variant="Variant.Filled" Disabled="!_isValid" OnClick="Submit">
@(IsEditMode ? "Speichern" : "Erstellen")
</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter]
private IMudDialogInstance? MudDialog { get; set; }
[Parameter]
public DaySummaryDto? Day { get; set; }
private MudForm? _form;
private bool _isValid;
private DateTime? _postDate = DateTime.Today;
private DayStatus _status = DayStatus.New;
private IReadOnlyCollection<Guid> _selectedPersonIds = [];
private bool IsEditMode => Day is not null;
protected override void OnInitialized()
{
if (Day is not null)
{
_postDate = Day.PostDate;
_status = Day.Status;
}
else
{
// Load persons for selection when creating new day
Dispatcher.Dispatch(new LoadPersonsAction());
// Pre-select all members
_selectedPersonIds = PersonState.Value.Persons
.Where(p => p.PersonStatus == PersonStatus.Member)
.Select(p => p.Id)
.ToList();
}
}
protected override void OnAfterRender(bool firstRender)
{
if (firstRender && !IsEditMode)
{
// Update selection after persons are loaded
StateHasChanged();
}
}
private void Cancel() => MudDialog?.Cancel();
private void Submit()
{
if (!_isValid || _postDate is null) return;
if (IsEditMode)
{
var dto = new UpdateDayDto
{
Id = Day!.Id,
PostDate = _postDate.Value,
Status = _status
};
MudDialog?.Close(DialogResult.Ok(dto));
}
else
{
var dto = new CreateDayDto
{
PostDate = _postDate.Value,
ParticipantIds = _selectedPersonIds.ToList()
};
MudDialog?.Close(DialogResult.Ok(dto));
}
}
}

View File

@ -0,0 +1,214 @@
@page "/days"
@attribute [Authorize(Policy = "ClubViewer")]
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
@using Fluxor
@using Koogle.Application.DTOs
@using Koogle.Domain.Enums
@using Koogle.Web.Store.DayState
@using Microsoft.AspNetCore.Authorization
@inject IState<DayState> DayState
@inject IDispatcher Dispatcher
@inject ISnackbar Snackbar
@inject IDialogService DialogService
@inject NavigationManager NavigationManager
<PageTitle>Spieltage</PageTitle>
<MudText Typo="Typo.h4" Class="mb-4">Spieltage</MudText>
@if (DayState.Value.Error is not null)
{
<MudAlert Severity="Severity.Error" Class="mb-4" ShowCloseIcon="true" CloseIconClicked="ClearError">
@DayState.Value.Error
</MudAlert>
}
<MudTable Items="DayState.Value.Days" Dense="true" Hover="true" Loading="DayState.Value.IsLoading"
OnRowClick="@((TableRowClickEventArgs<DaySummaryDto> args) => { if (args.Item is not null) NavigateToDayDetails(args.Item); })">
<ToolBarContent>
<MudSelect T="int?" @bind-Value="_yearFilter" Label="Jahr" Clearable="true" Class="mr-4" Style="width: 120px;">
@foreach (var year in AvailableYears)
{
<MudSelectItem Value="@((int?)year)">@year</MudSelectItem>
}
</MudSelect>
<MudSelect T="DayStatus?" @bind-Value="_statusFilter" Label="Status" Clearable="true" Class="mr-4" Style="width: 150px;">
<MudSelectItem Value="@((DayStatus?)DayStatus.New)">Neu</MudSelectItem>
<MudSelectItem Value="@((DayStatus?)DayStatus.Started)">Gestartet</MudSelectItem>
<MudSelectItem Value="@((DayStatus?)DayStatus.Postponed)">Verschoben</MudSelectItem>
<MudSelectItem Value="@((DayStatus?)DayStatus.Closed)">Abgeschlossen</MudSelectItem>
</MudSelect>
<MudButton Variant="Variant.Outlined" Color="Color.Default" StartIcon="@Icons.Material.Filled.FilterAlt"
OnClick="ApplyFilter" Class="mr-2">
Filtern
</MudButton>
<MudSpacer />
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add"
OnClick="OpenCreateDialog">
Neuer Spieltag
</MudButton>
</ToolBarContent>
<HeaderContent>
<MudTh><MudTableSortLabel SortBy="new Func<DaySummaryDto, object>(x => x.PostDate)">Datum</MudTableSortLabel></MudTh>
<MudTh>Status</MudTh>
<MudTh>Teilnehmer</MudTh>
<MudTh>Aktionen</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Datum">@context.PostDate.ToString("dddd, dd. MMMM yyyy")</MudTd>
<MudTd DataLabel="Status">
<MudChip T="string" Size="Size.Small" Color="GetStatusColor(context.Status)">
@GetStatusLabel(context.Status)
</MudChip>
</MudTd>
<MudTd DataLabel="Teilnehmer">
<MudChip T="string" Size="Size.Small" Color="Color.Default" Icon="@Icons.Material.Filled.People">
@context.ParticipantCount
</MudChip>
</MudTd>
<MudTd DataLabel="Aktionen">
<MudTooltip Text="Details anzeigen">
<MudIconButton Icon="@Icons.Material.Filled.Visibility"
Size="Size.Small"
OnClick="@(() => NavigateToDayDetails(context))"
OnClick:stopPropagation="true"/>
</MudTooltip>
<MudTooltip Text="Bearbeiten">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Size="Size.Small"
Disabled="context.Status == DayStatus.Closed"
OnClick="@(() => OpenEditDialog(context))"
OnClick:stopPropagation="true"/>
</MudTooltip>
<MudTooltip Text="Löschen">
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Size="Size.Small"
Color="Color.Error"
Disabled="context.Status == DayStatus.Closed"
OnClick="@(() => ConfirmDelete(context))"
OnClick:stopPropagation="true"/>
</MudTooltip>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager />
</PagerContent>
<NoRecordsContent>
<MudText>Keine Spieltage gefunden</MudText>
</NoRecordsContent>
</MudTable>
@code {
private int? _yearFilter;
private DayStatus? _statusFilter;
private IEnumerable<int> AvailableYears
{
get
{
var currentYear = DateTime.Now.Year;
return Enumerable.Range(currentYear - 5, 7).Reverse();
}
}
protected override void OnInitialized()
{
base.OnInitialized();
_yearFilter = DateTime.Now.Year;
LoadDays();
}
private void LoadDays()
{
var filter = new DayFilterDto
{
Year = _yearFilter,
Status = _statusFilter
};
Dispatcher.Dispatch(new LoadDaysAction(filter));
}
private void ApplyFilter()
{
LoadDays();
}
private void ClearError()
{
Dispatcher.Dispatch(new ClearDayErrorAction());
}
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 void NavigateToDayDetails(DaySummaryDto day)
{
NavigationManager.NavigateTo($"/days/{day.Id}");
}
private async Task OpenCreateDialog()
{
var dialog = await DialogService.ShowAsync<DayEditDialog>("Neuer Spieltag");
var result = await dialog.Result;
if (result != null && !result.Canceled && result.Data is CreateDayDto dto)
{
Dispatcher.Dispatch(new CreateDayAction(dto));
Snackbar.Add("Spieltag wird erstellt...", Severity.Info);
}
}
private async Task OpenEditDialog(DaySummaryDto day)
{
var parameters = new DialogParameters
{
{ "Day", day }
};
var dialog = await DialogService.ShowAsync<DayEditDialog>("Spieltag bearbeiten", parameters);
var result = await dialog.Result;
if (result != null && !result.Canceled && result.Data is UpdateDayDto dto)
{
Dispatcher.Dispatch(new UpdateDayAction(dto));
Snackbar.Add("Spieltag wird aktualisiert...", Severity.Info);
}
}
private async Task ConfirmDelete(DaySummaryDto day)
{
var parameters = new DialogParameters
{
{ "ContentText", $"Möchten Sie 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);
}
}
}