fix(ui): refine amount entry, date warnings and booking actions

This commit is contained in:
2026-11-02 20:31:09 +01:00
parent 71eed68e21
commit aa392815e5
17 changed files with 140 additions and 39 deletions
@@ -26,15 +26,14 @@
}
else
{
<details class="entry-action-menu">
<summary aria-label="@($"Aktionen für {Entry.DisplayId} {Entry.Title}")">Aktionen</summary>
<button type="button" class="btn btn-outline-secondary" @onclick="() => OnEdit.InvokeAsync(Entry.Id)" aria-label="@($"{Entry.DisplayId} {Entry.Title} bearbeiten")">
<i class="bi bi-pencil" aria-hidden="true"></i> Bearbeiten
<div class="entry-action-buttons">
<button type="button" class="btn btn-outline-secondary" title="Buchung bearbeiten" @onclick="() => OnEdit.InvokeAsync(Entry.Id)" aria-label="@($"{Entry.DisplayId} {Entry.Title} bearbeiten")">
<i class="bi bi-pencil" aria-hidden="true"></i>
</button>
<button type="button" class="btn btn-outline-danger" @onclick="() => OnDelete.InvokeAsync(Entry.Id)" aria-label="@($"{Entry.DisplayId} {Entry.Title} löschen")">
<i class="bi bi-trash" aria-hidden="true"></i> Löschen
<button type="button" class="btn btn-outline-danger" title="Buchung löschen" @onclick="() => OnDelete.InvokeAsync(Entry.Id)" aria-label="@($"{Entry.DisplayId} {Entry.Title} löschen")">
<i class="bi bi-trash" aria-hidden="true"></i>
</button>
</details>
</div>
}
</td>
</tr>
@@ -16,7 +16,10 @@
}
@if (Entries.Any(e => e.IsDeleted))
{
<label class="history-toggle"><input type="checkbox" @bind="showDeleted" /> Gelöschte Buchungen anzeigen (@Entries.Count(e => e.IsDeleted))</label>
<div class="form-check form-switch history-toggle">
<input id="show-deleted" class="form-check-input" type="checkbox" role="switch" @bind="showDeleted" />
<label class="form-check-label" for="show-deleted">Gelöschte Buchungen anzeigen (@Entries.Count(e => e.IsDeleted))</label>
</div>
}
<div class="table-responsive entry-list">
<table class="table table-hover table-sm align-middle">
@@ -1,8 +1,6 @@
<input id="@Id" type="text" inputmode="decimal" class="form-control" data-amount-guard
data-allow-negative="@AllowNegative.ToString().ToLowerInvariant()"
@bind:get="Value" @bind:set="SetValue" @bind:event="oninput" disabled="@Disabled"
aria-describedby="@(Id + "-rejected")" />
<span id="@(Id + "-rejected")" class="input-rejection text-danger" role="status">@rejection</span>
@bind:get="Value" @bind:set="SetValue" @bind:event="oninput" disabled="@Disabled" />
@code {
[Parameter] public string Id { get; set; } = "amount";
@@ -10,15 +8,12 @@
[Parameter] public EventCallback<string> ValueChanged { get; set; }
[Parameter] public bool Disabled { get; set; }
[Parameter] public bool AllowNegative { get; set; }
private string? rejection;
private async Task SetValue(string value)
{
if (!BookingInput.IsAmountDraft(value, AllowNegative))
{
rejection = "Diese Eingabe wurde nicht übernommen. Bitte einen Betrag wie 12,50 eingeben.";
return;
}
rejection = null;
await ValueChanged.InvokeAsync(value);
}
}
@@ -0,0 +1,33 @@
<input @attributes="Attributes" id="@Id" type="date" value="@renderedValue" @oninput="Input" @onblur="CommitAsync" />
@code {
[Parameter] public string Id { get; set; } = "booking-date";
[Parameter] public string Value { get; set; } = string.Empty;
[Parameter] public EventCallback<string> ValueChanged { get; set; }
[Parameter] public EventCallback<bool> PendingChangesChanged { get; set; }
[Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? Attributes { get; set; }
private string? lastValue;
private string renderedValue = string.Empty;
private string draft = string.Empty;
protected override void OnParametersSet()
{
if (Value != lastValue) draft = renderedValue = Value;
lastValue = Value;
}
private async Task Input(ChangeEventArgs args)
{
draft = args.Value?.ToString() ?? string.Empty;
await PendingChangesChanged.InvokeAsync(draft != Value);
}
public async Task CommitAsync()
{
// Native calendars close when their surrounding dialog layout changes. Validate only
// after leaving the date field, so year warnings never interrupt calendar navigation.
lastValue = draft;
await ValueChanged.InvokeAsync(lastValue);
await PendingChangesChanged.InvokeAsync(false);
}
}
@@ -1,7 +1,7 @@
@inject ToastService Toasts
@inject IEntryService EntryService
<ModalDialog @ref="modal" Title="Einnahme oder Ausgabe" OnCancel="OnCancel" Busy="isSaving" Dirty="@(committedResult == null && IsDirty)">
<ModalDialog @ref="modal" Title="Einnahme oder Ausgabe" OnCancel="OnCancel" Busy="isSaving" Dirty="@(committedResult == null && (IsDirty || pendingDateChanges))">
<h5 id="entry-dialog-title">@(EditEntry != null ? "Buchung bearbeiten" : "Einnahme oder Ausgabe erfassen") – @AccountName</h5>
<form @onsubmit="Save" @onsubmit:preventDefault="true" aria-busy="@(isSaving ? "true" : "false")">
@@ -24,7 +24,7 @@
<label class="form-label" for="entry-date">Datum</label>
<div>
<input id="entry-date" type="date" value="@dateText" @oninput="e => dateText = e.Value?.ToString() ?? string.Empty" aria-describedby="entry-date-help" aria-invalid="@(date == default ? "true" : "false")" />
<BookingDateInput @ref="bookingDate" Id="entry-date" @bind-Value="dateText" PendingChangesChanged="pending => pendingDateChanges = pending" aria-describedby="entry-date-help" aria-invalid="@(date == default ? "true" : "false")" />
@if (date == default) { <p id="entry-date-help" class="text-danger">Bitte ein gültiges Datum eingeben.</p> }
</div>
@@ -43,9 +43,12 @@
@if (OutsideYear && date != default)
{
<div class="alert alert-warning" role="status">
Datum @date.ToString("dd.MM.yyyy"): Diese Buchung liegt außerhalb von Jahr @ContextYear und wird dort nicht angezeigt.
<div class="alert alert-warning booking-year-warning" role="status">
<i class="bi bi-exclamation-triangle-fill" aria-hidden="true"></i>
<div><strong>Das Buchungsdatum liegt in einem anderen Jahr.</strong>
<p>Du erfasst die Buchung am @date.ToString("dd.MM.yyyy") und damit außerhalb des angezeigten Jahres @ContextYear. Sie wird im Jahr @date.Year gespeichert und erscheint erst, wenn du dieses Jahr oder „Alle Buchungen“ auswählst.</p>
<label><input type="checkbox" checked="@(confirmDifferentYear && confirmedYear == date.Year)" @onchange="e => { confirmDifferentYear = e.Value is true; confirmedYear = date.Year; }" /> Ich möchte die Buchung im Jahr @date.Year speichern.</label>
</div>
</div>
}
@@ -59,4 +62,4 @@
</form>
</ModalDialog>
@code { private ModalDialog? modal; }
@code { private ModalDialog? modal; private BookingDateInput? bookingDate; private bool pendingDateChanges; }
@@ -76,6 +76,8 @@ public partial class AddEntryDialog
private async Task Save()
{
if (isSaving || completed) return;
if (committedResult == null && bookingDate != null) await bookingDate.CommitAsync();
if (isSaving || completed || (committedResult == null && !CanSave)) return;
var busyToast = Toasts.Show("Wird gespeichert …", persistent: true);
isSaving = true;
@@ -2,7 +2,7 @@
@inject IEntryService EntryService
@inject IAccountService AccountService
<ModalDialog @ref="modal" Title="Geld zwischen Konten verschieben" OnCancel="OnCancel" Busy="isSaving" Dirty="@(committedResult == null && IsDirty)">
<ModalDialog @ref="modal" Title="Geld zwischen Konten verschieben" OnCancel="OnCancel" Busy="isSaving" Dirty="@(committedResult == null && (IsDirty || pendingDateChanges))">
<h5 class="mb-3">@(EditEntry != null ? "Umbuchung bearbeiten" : "Neue Umbuchung")</h5>
<p class="text-muted">Erfasst nur die Buchung. Führt keine Banküberweisung aus.</p>
@@ -30,7 +30,7 @@
</select>
<label class="form-label" for="transfer-date">Datum</label>
<input id="transfer-date" type="date" class="form-control" value="@dateText" @oninput="e => dateText = e.Value?.ToString() ?? string.Empty" />
<BookingDateInput @ref="bookingDate" Id="transfer-date" class="form-control" @bind-Value="dateText" PendingChangesChanged="pending => pendingDateChanges = pending" />
<label class="form-label" for="transfer-title">Bezeichnung</label>
<input id="transfer-title" type="text" class="form-control" @bind="title" @bind:event="oninput" placeholder="Beschreibung der Umbuchung" />
@@ -40,11 +40,14 @@
<p class="text-muted">Betrag ohne Minuszeichen.</p></div>
</div>
@if (OutsideYear)
@if (OutsideYear && date != default)
{
<div class="alert alert-warning" role="status">
Datum @date.ToString("dd.MM.yyyy"): Diese Buchung liegt außerhalb von Jahr @ContextYear und wird dort nicht angezeigt.
<div class="alert alert-warning booking-year-warning" role="status">
<i class="bi bi-exclamation-triangle-fill" aria-hidden="true"></i>
<div><strong>Das Buchungsdatum liegt in einem anderen Jahr.</strong>
<p>Du erfasst die Buchung am @date.ToString("dd.MM.yyyy") und damit außerhalb des angezeigten Jahres @ContextYear. Sie wird im Jahr @date.Year gespeichert und erscheint erst, wenn du dieses Jahr oder „Alle Buchungen“ auswählst.</p>
<label><input type="checkbox" checked="@(confirmDifferentYear && confirmedYear == date.Year)" @onchange="e => { confirmDifferentYear = e.Value is true; confirmedYear = date.Year; }" /> Ich möchte die Buchung im Jahr @date.Year speichern.</label>
</div>
</div>
}
@@ -58,4 +61,4 @@
</form>
</ModalDialog>
@code { private ModalDialog? modal; }
@code { private ModalDialog? modal; private BookingDateInput? bookingDate; private bool pendingDateChanges; }
@@ -91,6 +91,8 @@ public partial class AddTransferDialog
private async Task Save()
{
if (isSaving || completed) return;
if (committedResult == null && bookingDate != null) await bookingDate.CommitAsync();
if (isSaving || completed || (committedResult == null && !CanSave)) return;
var busyToast = Toasts.Show("Wird gespeichert …", persistent: true);
isSaving = true;