Add evaluation components (F2)

- DayEvaluationComponent: shows day expense summary per person
- PersonEvaluationComponent: shows person expense summary + details

🤖 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 16:32:49 +01:00
parent 1cbd4c17e7
commit bc99bc25db
2 changed files with 376 additions and 0 deletions

View File

@ -0,0 +1,186 @@
@using Koogle.Application.DTOs
@using Koogle.Application.Interfaces
@using Koogle.Domain.Enums
@inject IPersonExpenseService PersonExpenseService
@inject ISnackbar Snackbar
@inject NavigationManager NavigationManager
@if (_isLoading)
{
<MudProgressLinear Indeterminate="true" Color="Color.Primary" />
}
else if (_error is not null)
{
<MudAlert Severity="Severity.Error" Class="mb-4">@_error</MudAlert>
}
else if (_evaluation is not null)
{
<MudCard Elevation="2">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">Auswertung: @_evaluation.PostDate.ToString("dd.MM.yyyy")</MudText>
</CardHeaderContent>
<CardHeaderActions>
<MudChip T="string" Size="Size.Small" Color="GetStatusColor(_evaluation.Status)">
@GetStatusLabel(_evaluation.Status)
</MudChip>
</CardHeaderActions>
</MudCardHeader>
<MudCardContent>
<!-- Summary -->
<div class="d-flex justify-space-between mb-4">
<MudText Typo="Typo.body1">
<strong>Gesamt:</strong> @_evaluation.TotalAmount.ToString("C")
</MudText>
<MudText Typo="Typo.body2" Color="Color.Secondary">
@_evaluation.ExpenseCount Strafen
</MudText>
</div>
@if (_evaluation.PersonEvaluations.Count == 0)
{
<MudText Typo="Typo.body2" Color="Color.Secondary">Keine Strafen erfasst</MudText>
}
else
{
<MudTable Items="_evaluation.PersonEvaluations" Dense="true" Hover="true">
<HeaderContent>
<MudTh>Person</MudTh>
<MudTh Style="text-align: right">Offen</MudTh>
<MudTh Style="text-align: right">Bezahlt</MudTh>
<MudTh Style="text-align: right">Gesamt</MudTh>
<MudTh Style="text-align: center">Anzahl</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Person">
@if (ShowPersonLinks)
{
<MudLink OnClick="@(() => NavigateToPersonDetails(context.PersonId))">
@context.PersonName
</MudLink>
}
else
{
@context.PersonName
}
</MudTd>
<MudTd DataLabel="Offen" Style="text-align: right">
@if (context.OpenAmount > 0)
{
<MudText Color="Color.Warning">@context.OpenAmount.ToString("C")</MudText>
}
else
{
<MudText>-</MudText>
}
</MudTd>
<MudTd DataLabel="Bezahlt" Style="text-align: right">
@if (context.PaidAmount > 0)
{
<MudText Color="Color.Success">@context.PaidAmount.ToString("C")</MudText>
}
else
{
<MudText>-</MudText>
}
</MudTd>
<MudTd DataLabel="Gesamt" Style="text-align: right">
<strong>@context.TotalAmount.ToString("C")</strong>
</MudTd>
<MudTd DataLabel="Anzahl" Style="text-align: center">
@context.ExpenseCount
</MudTd>
</RowTemplate>
</MudTable>
}
</MudCardContent>
</MudCard>
}
@code {
/// <summary>
/// The unique identifier of the day to evaluate.
/// </summary>
[Parameter]
public Guid DayId { get; set; }
/// <summary>
/// Whether to show links to person details.
/// </summary>
[Parameter]
public bool ShowPersonLinks { get; set; } = true;
/// <summary>
/// Event callback when data is loaded.
/// </summary>
[Parameter]
public EventCallback<DayEvaluationDto> OnLoaded { get; set; }
private DayEvaluationDto? _evaluation;
private bool _isLoading = true;
private string? _error;
protected override async Task OnParametersSetAsync()
{
if (DayId != Guid.Empty)
{
await LoadEvaluationAsync();
}
}
/// <summary>
/// Reloads the evaluation data.
/// </summary>
public async Task RefreshAsync()
{
await LoadEvaluationAsync();
}
private async Task LoadEvaluationAsync()
{
try
{
_isLoading = true;
_error = null;
_evaluation = await PersonExpenseService.GetDayEvaluationAsync(DayId);
if (OnLoaded.HasDelegate)
{
await OnLoaded.InvokeAsync(_evaluation);
}
}
catch (Exception ex)
{
_error = $"Fehler beim Laden der Auswertung: {ex.Message}";
Snackbar.Add(_error, Severity.Error);
}
finally
{
_isLoading = false;
}
}
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 NavigateToPersonDetails(Guid personId)
{
NavigationManager.NavigateTo($"/persons/{personId}");
}
}

View File

@ -0,0 +1,190 @@
@using Koogle.Application.DTOs
@using Koogle.Application.Interfaces
@using Koogle.Domain.Enums
@inject IPersonExpenseService PersonExpenseService
@inject IPersonService PersonService
@inject ISnackbar Snackbar
@inject NavigationManager NavigationManager
@if (_isLoading)
{
<MudProgressLinear Indeterminate="true" Color="Color.Primary" />
}
else if (_error is not null)
{
<MudAlert Severity="Severity.Error" Class="mb-4">@_error</MudAlert>
}
else if (_evaluation is not null)
{
<MudCard Elevation="2">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">Auswertung: @_evaluation.PersonName</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent>
<!-- Summary Cards -->
<MudGrid Class="mb-4">
<MudItem xs="6" sm="3">
<div class="d-flex flex-column align-center">
<MudText Typo="Typo.h5" Color="Color.Warning">@_evaluation.TotalOpenAmount.ToString("C")</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">Offen</MudText>
</div>
</MudItem>
<MudItem xs="6" sm="3">
<div class="d-flex flex-column align-center">
<MudText Typo="Typo.h5" Color="Color.Success">@_evaluation.TotalPaidAmount.ToString("C")</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">Bezahlt</MudText>
</div>
</MudItem>
<MudItem xs="6" sm="3">
<div class="d-flex flex-column align-center">
<MudText Typo="Typo.h5">@_evaluation.TotalExpenseCount</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">Strafen</MudText>
</div>
</MudItem>
<MudItem xs="6" sm="3">
<div class="d-flex flex-column align-center">
<MudText Typo="Typo.h5">@_evaluation.DaysParticipated</MudText>
<MudText Typo="Typo.caption" Color="Color.Secondary">Teilnahmen</MudText>
</div>
</MudItem>
</MudGrid>
@if (ShowExpenseDetails && _expenses.Count > 0)
{
<MudDivider Class="my-4" />
<MudText Typo="Typo.subtitle1" Class="mb-2">Strafenliste</MudText>
<MudTable Items="_expenses" Dense="true" Hover="true">
<HeaderContent>
<MudTh>Datum</MudTh>
<MudTh>Strafe</MudTh>
<MudTh Style="text-align: right">Betrag</MudTh>
<MudTh Style="text-align: center">Status</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Datum">
@if (ShowDayLinks)
{
<MudLink OnClick="@(() => NavigateToDayDetails(context.DayId))">
@context.DayPostDate.ToString("dd.MM.yyyy")
</MudLink>
}
else
{
@context.DayPostDate.ToString("dd.MM.yyyy")
}
</MudTd>
<MudTd DataLabel="Strafe">@context.Name</MudTd>
<MudTd DataLabel="Betrag" Style="text-align: right">@context.Price.ToString("C")</MudTd>
<MudTd DataLabel="Status" Style="text-align: center">
<MudChip T="string" Size="Size.Small" Color="GetExpenseStatusColor(context.PersonExpenseStatus)">
@GetExpenseStatusLabel(context.PersonExpenseStatus)
</MudChip>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager PageSizeOptions="new[] { 10, 25, 50 }" />
</PagerContent>
</MudTable>
}
</MudCardContent>
</MudCard>
}
@code {
/// <summary>
/// The unique identifier of the person to evaluate.
/// </summary>
[Parameter]
public Guid PersonId { get; set; }
/// <summary>
/// Whether to show the detailed expense list.
/// </summary>
[Parameter]
public bool ShowExpenseDetails { get; set; } = true;
/// <summary>
/// Whether to show links to day details.
/// </summary>
[Parameter]
public bool ShowDayLinks { get; set; } = true;
/// <summary>
/// Event callback when data is loaded.
/// </summary>
[Parameter]
public EventCallback<PersonEvaluationSummaryDto> OnLoaded { get; set; }
private PersonEvaluationSummaryDto? _evaluation;
private List<PersonExpenseDto> _expenses = [];
private bool _isLoading = true;
private string? _error;
protected override async Task OnParametersSetAsync()
{
if (PersonId != Guid.Empty)
{
await LoadEvaluationAsync();
}
}
/// <summary>
/// Reloads the evaluation data.
/// </summary>
public async Task RefreshAsync()
{
await LoadEvaluationAsync();
}
private async Task LoadEvaluationAsync()
{
try
{
_isLoading = true;
_error = null;
_evaluation = await PersonExpenseService.GetPersonEvaluationAsync(PersonId);
if (ShowExpenseDetails)
{
_expenses = await PersonExpenseService.GetByPersonIdAsync(PersonId);
}
if (OnLoaded.HasDelegate)
{
await OnLoaded.InvokeAsync(_evaluation);
}
}
catch (Exception ex)
{
_error = $"Fehler beim Laden der Auswertung: {ex.Message}";
Snackbar.Add(_error, Severity.Error);
}
finally
{
_isLoading = false;
}
}
private static string GetExpenseStatusLabel(PersonExpenseStatus status) => status switch
{
PersonExpenseStatus.Open => "Offen",
PersonExpenseStatus.Done => "Bezahlt",
_ => status.ToString()
};
private static Color GetExpenseStatusColor(PersonExpenseStatus status) => status switch
{
PersonExpenseStatus.Open => Color.Warning,
PersonExpenseStatus.Done => Color.Success,
_ => Color.Default
};
private void NavigateToDayDetails(Guid dayId)
{
NavigationManager.NavigateTo($"/days/{dayId}");
}
}