From 642c4d11bf7e858a02a6b50cfa34109df85107a1 Mon Sep 17 00:00:00 2001 From: beo3000 Date: Thu, 25 Dec 2025 09:41:54 +0100 Subject: [PATCH] Add Days List Page (E2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../Components/Pages/Days/DayEditDialog.razor | 147 ++++++++++++ .../Components/Pages/Days/Days.razor | 214 ++++++++++++++++++ 2 files changed, 361 insertions(+) create mode 100644 src/Koogle.Web/Components/Pages/Days/DayEditDialog.razor create mode 100644 src/Koogle.Web/Components/Pages/Days/Days.razor diff --git a/src/Koogle.Web/Components/Pages/Days/DayEditDialog.razor b/src/Koogle.Web/Components/Pages/Days/DayEditDialog.razor new file mode 100644 index 0000000..46fa1c3 --- /dev/null +++ b/src/Koogle.Web/Components/Pages/Days/DayEditDialog.razor @@ -0,0 +1,147 @@ +@using Koogle.Application.DTOs +@using Koogle.Domain.Enums +@using Fluxor +@using Koogle.Web.Store.PersonState + +@inject IState PersonState +@inject IDispatcher Dispatcher + + + + + @(IsEditMode ? "Spieltag bearbeiten" : "Neuer Spieltag") + + + + + + + @if (IsEditMode) + { + + Neu + Gestartet + Verschoben + Abgeschlossen + + } + else + { + Teilnehmer vorauswählen + + Mitglieder sind automatisch vorausgewählt. Sie können Teilnehmer später noch ändern. + + + @if (PersonState.Value.IsLoading) + { + + } + else + { + + @foreach (var person in PersonState.Value.Persons.OrderByDescending(p => p.PersonStatus == PersonStatus.Member).ThenBy(p => p.Name)) + { + + @person.Name + @if (person.PersonStatus == PersonStatus.Guest) + { + (Gast) + } + + } + + } + } + + + + Abbrechen + + @(IsEditMode ? "Speichern" : "Erstellen") + + + + +@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 _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)); + } + } +} diff --git a/src/Koogle.Web/Components/Pages/Days/Days.razor b/src/Koogle.Web/Components/Pages/Days/Days.razor new file mode 100644 index 0000000..9b460e0 --- /dev/null +++ b/src/Koogle.Web/Components/Pages/Days/Days.razor @@ -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 +@inject IDispatcher Dispatcher +@inject ISnackbar Snackbar +@inject IDialogService DialogService +@inject NavigationManager NavigationManager + +Spieltage + +Spieltage + +@if (DayState.Value.Error is not null) +{ + + @DayState.Value.Error + +} + + + + + @foreach (var year in AvailableYears) + { + @year + } + + + Neu + Gestartet + Verschoben + Abgeschlossen + + + Filtern + + + + Neuer Spieltag + + + + Datum + Status + Teilnehmer + Aktionen + + + @context.PostDate.ToString("dddd, dd. MMMM yyyy") + + + @GetStatusLabel(context.Status) + + + + + @context.ParticipantCount + + + + + + + + + + + + + + + + + + + Keine Spieltage gefunden + + + +@code { + private int? _yearFilter; + private DayStatus? _statusFilter; + + private IEnumerable 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("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("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("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); + } + } +}