merge day detils

This commit is contained in:
beo3000 2025-12-25 15:04:24 +01:00
commit 27be43fe53
4 changed files with 472 additions and 2 deletions

View File

@ -7,7 +7,8 @@
"Bash(git checkout:*)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(mkdir:*)"
"Bash(mkdir:*)",
"WebSearch"
]
}
}

View File

@ -343,7 +343,7 @@ NavMenu.razor:
| ✓ | D4 | Expenses | Expense Pages | 1 Razor |
| ✓ | E1 | Days | DayState Fluxor | 4 State-Dateien |
| ✓ | E2 | Days | Days List Page | 1 Razor |
| | E3 | Days | Day Details Page | 1 Razor |
| | E3 | Days | Day Details Page | 1 Razor |
| ☐ | E4 | Days | PersonExpense Management | Components in DayDetails |
| ☐ | F1 | Dashboard | Dashboard Page | 1 Razor |
| ☐ | F2 | Dashboard | Evaluation Components | 2 Shared Components |

View File

@ -0,0 +1,62 @@
@using Koogle.Application.DTOs
@using Koogle.Domain.Enums
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">Teilnehmer hinzufügen</MudText>
</TitleContent>
<DialogContent>
@if (AvailablePersons.Count == 0)
{
<MudText Color="Color.Secondary">Keine weiteren Personen verfügbar</MudText>
}
else
{
<MudText Class="mb-4">Wählen Sie eine Person aus:</MudText>
<MudList T="PersonDto" Dense="true" @bind-SelectedValue="_selectedPerson" SelectionMode="SelectionMode.SingleSelection">
@foreach (var person in AvailablePersons.OrderByDescending(p => p.PersonStatus == PersonStatus.Member).ThenBy(p => p.Name))
{
<MudListItem T="PersonDto" Value="person">
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudAvatar Size="Size.Small" Color="@(person.PersonStatus == PersonStatus.Member ? Color.Primary : Color.Secondary)">
@person.Name[0]
</MudAvatar>
<MudText>@person.Name</MudText>
@if (person.PersonStatus == PersonStatus.Guest)
{
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="Color.Secondary">Gast</MudChip>
}
</MudStack>
</MudListItem>
}
</MudList>
}
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Abbrechen</MudButton>
<MudButton Color="Color.Primary" Variant="Variant.Filled" Disabled="@(_selectedPerson is null)" OnClick="Submit">
Hinzufügen
</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter]
private IMudDialogInstance? MudDialog { get; set; }
[Parameter]
public IReadOnlyList<PersonDto> AvailablePersons { get; set; } = [];
[Parameter]
public Guid DayId { get; set; }
private PersonDto? _selectedPerson;
private void Cancel() => MudDialog?.Cancel();
private void Submit()
{
if (_selectedPerson is null) return;
MudDialog?.Close(DialogResult.Ok(_selectedPerson.Id));
}
}

View File

@ -0,0 +1,407 @@
@page "/days/{DayId:guid}"
@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>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
{
<!-- 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.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 -->
<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">
@foreach (var participant in Day.Participants.OrderBy(p => p.PersonName))
{
<MudListItem T="DayParticipantDto">
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Style="width: 100%">
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudAvatar Size="Size.Small" Color="@(participant.PersonStatus == PersonStatus.Member ? Color.Primary : Color.Secondary)">
@participant.PersonName[0]
</MudAvatar>
<MudText>@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))" />
}
</MudStack>
</MudListItem>
}
</MudList>
}
</MudPaper>
</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>
<!-- PersonExpense placeholder for E4 -->
<MudItem xs="12">
<MudPaper Class="pa-4">
<MudText Typo="Typo.h6" Class="mb-4">
<MudIcon Icon="@Icons.Material.Filled.AttachMoney" Class="mr-2" />
Strafen / Kosten
</MudText>
<MudAlert Severity="Severity.Info">
Die Verwaltung von Strafen und Kosten wird in Phase E4 implementiert.
</MudAlert>
</MudPaper>
</MudItem>
</MudGrid>
}
@code {
[Parameter]
public Guid DayId { get; set; }
private DayDto? Day => DayState.Value.SelectedDay;
protected override void OnInitialized()
{
base.OnInitialized();
Dispatcher.Dispatch(new LoadDayDetailsAction(DayId));
Dispatcher.Dispatch(new LoadAvailablePersonsAction());
}
protected override void OnParametersSet()
{
base.OnParametersSet();
if (Day?.Id != DayId)
{
Dispatcher.Dispatch(new LoadDayDetailsAction(DayId));
}
}
private void ClearError()
{
Dispatcher.Dispatch(new ClearDayErrorAction());
}
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 => "Starten",
DayStatus.Started => "Abschließen",
DayStatus.Postponed => "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 void AdvanceStatus()
{
if (Day is null) 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ö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);
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 Guid personId)
{
var dto = new AddDayParticipantDto
{
DayId = Day.Id,
PersonId = personId
};
Dispatcher.Dispatch(new AddDayParticipantAction(dto));
Snackbar.Add("Teilnehmer wird hinzugefügt...", Severity.Info);
}
}
private async Task RemoveParticipant(DayParticipantDto participant)
{
if (Day is null) return;
var parameters = new DialogParameters
{
{ "ContentText", $"Möchten Sie \"{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);
}
}
}