feat(ui): constrain decimal input and add stacked timed toasts with uniform actions

This commit is contained in:
2026-11-02 19:19:07 +01:00
parent 22e509d982
commit 86a73b862a
24 changed files with 315 additions and 103 deletions
@@ -1,3 +1,4 @@
@inject ToastService Toasts
@inject ReportingPeriod Period
@page "/accounts/{AccountId:int}"
@inject IAccountService AccountService
@@ -9,21 +10,7 @@
<div class="container-fluid">
<OperationNotice Result="fileResult" />
@if (fileBusy) { <p role="status" aria-live="polite">Dateiaktion läuft … Bitte warten.</p> }
@if (lastSaved != null)
{
<div class="alert alert-success" role="status" aria-live="polite">
@lastSaved.Kind über @FormatAmount(lastSaved.Amount) @(lastSaved.Edited ? "aktualisiert" : "gespeichert").
</div>
}
@if (savedOutsideYear.HasValue)
{
<div class="alert alert-info" role="status">
Die Buchung wurde im Jahr @savedOutsideYear gespeichert und liegt außerhalb der aktuellen Ansicht.
<button class="btn btn-outline-primary" @onclick="ShowSavedYear">Buchung im Jahr @savedOutsideYear anzeigen</button>
</div>
}
@if (account == null)
{
<div class="text-center py-5">
@@ -41,21 +28,21 @@
</div>
</div>
<div class="d-flex align-items-center gap-2 py-3 flex-wrap">
<div class="action-toolbar">
<button class="btn btn-secondary btn-nav" @onclick="NavigateBack">
<i class="bi bi-arrow-left"></i> Zurück
<i class="bi bi-arrow-left"></i><span>Zurück</span>
</button>
<button class="btn btn-success btn-nav" @onclick="() => showAddEntry = true">
<i class="bi bi-plus-lg"></i> Einnahme oder Ausgabe erfassen
<i class="bi bi-plus-lg"></i> <span>Einnahme oder<br /> Ausgabe erfassen</span>
</button>
<button class="btn btn-info btn-nav" @onclick="() => showAddTransfer = true">
<i class="bi bi-arrow-left-right"></i> Geld zwischen Konten verschieben
<i class="bi bi-arrow-left-right"></i> <span>Geld zwischen<br /> Konten verschieben</span>
</button>
<button class="btn btn-dark btn-nav" @onclick="HandleExport" disabled="@fileBusy">
<i class="bi bi-file-earmark-pdf"></i> Als PDF speichern
<i class="bi bi-file-earmark-pdf"></i> <span>Als PDF<br /> speichern</span>
</button>
<button class="btn btn-nav btn-primary" @onclick="OpenYearFilterDialog">
<i class="bi bi-funnel"></i> @GetFilterLabel()
<i class="bi bi-funnel"></i><span>@GetFilterLabel()</span>
</button>
</div>
@@ -1,3 +1,4 @@
using Duempelkas.App.Services;
using Duempelkas.App.Services.Models;
using Microsoft.AspNetCore.Components;
@@ -10,6 +11,10 @@ public partial class AccountDetail
[Parameter]
public int AccountId { get; set; }
[SupplyParameterFromQuery(Name = "jahr")]
public int? QueryYear { get; set; }
private int? previousQueryYear;
#endregion
#region Fields
@@ -53,6 +58,8 @@ public partial class AccountDetail
protected override async Task OnParametersSetAsync()
{
if (QueryYear.HasValue && QueryYear != previousQueryYear) selectedYear = QueryYear;
previousQueryYear = QueryYear;
await LoadAll();
}
@@ -115,6 +122,12 @@ public partial class AccountDetail
await LoadAll();
lastSaved = saved;
savedOutsideYear = selectedYear.HasValue && selectedYear.Value != saved.Date.Year ? saved.Date.Year : null;
var nav = Navigation;
var period = Period;
var accountId = AccountId;
Toasts.Show($"{saved.Kind} über {FormatAmount(saved.Amount)} {(saved.Edited ? "aktualisiert" : "gespeichert")}.", ToastKind.Success,
actionLabel: savedOutsideYear.HasValue ? $"Buchung im Jahr {saved.Date.Year} anzeigen" : null,
action: savedOutsideYear.HasValue ? () => { period.Year = saved.Date.Year; nav.NavigateTo($"/accounts/{accountId}?jahr={saved.Date.Year}"); return Task.CompletedTask; } : null);
showAddEntry = false;
showAddTransfer = false;
editingEntry = null;
@@ -187,6 +200,7 @@ public partial class AccountDetail
private async Task HandleExport()
{
if (fileBusy) return;
var busyToast = Toasts.Show("Dateiaktion läuft … Bitte warten.", persistent: true);
fileBusy = true;
fileResult = null;
try
@@ -199,12 +213,13 @@ public partial class AccountDetail
}
catch (Exception ex) { fileResult = FileOperationResult.Error(ex); }
finally { fileBusy = false; }
finally { fileBusy = false; Toasts.Dismiss(busyToast); }
}
private async Task HandleOpenSavedPdf()
{
if (fileBusy) return;
var busyToast = Toasts.Show("Dateiaktion läuft … Bitte warten.", persistent: true);
fileBusy = true;
fileResult = null;
try
@@ -218,7 +233,7 @@ public partial class AccountDetail
}
catch (Exception ex) { fileResult = FileOperationResult.Error(ex); }
finally { fileBusy = false; }
finally { fileBusy = false; Toasts.Dismiss(busyToast); }
}
private void CancelOpenSavedPdf()