feat(usability): explain deleted bookings and linked transfer undo (U10)

This commit is contained in:
2026-11-02 19:26:23 +01:00
parent 2d36ab0b88
commit 1686f5769c
11 changed files with 146 additions and 14 deletions
@@ -3,6 +3,10 @@
<td data-label="Datum">@Entry.Date.ToString("dd.MM.yyyy")</td>
<td data-label="Beschreibung" class="entry-description">
@Entry.Title
@if (Entry.IsDeleted)
{
<p class="deleted-status">Gelöscht – zählt nicht zum Kontostand</p>
}
@if (Entry.IsTransfer && !string.IsNullOrEmpty(Entry.LinkedAccountName))
{
<small class="text-muted ms-1">
@@ -1,3 +1,7 @@
@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="table-responsive entry-list">
<table class="table table-hover table-sm align-middle">
<thead class="table-light">
@@ -10,7 +14,7 @@
</tr>
</thead>
<tbody>
@foreach (var entry in Entries)
@foreach (var entry in VisibleEntries)
{
<EntryRow @key="entry.Id" Highlighted="@(entry.Id == HighlightedEntryId)" Entry="entry" OnDelete="OnDeleteEntry" OnRestore="OnRestoreEntry" OnEdit="OnEditEntry" />
}
@@ -18,7 +22,7 @@
</table>
</div>
@if (!Entries.Any())
@if (!VisibleEntries.Any())
{
<div class="text-center py-3 text-muted">@EmptyMessage</div>
}
@@ -5,6 +5,8 @@ namespace Duempelkas.App.Components.Accounts;
public partial class EntryTable
{
private bool showDeleted;
private IEnumerable<EntryDto> VisibleEntries => Entries.Where(e => !e.IsDeleted || showDeleted);
[Parameter] public int? HighlightedEntryId { get; set; }
[Parameter] public string EmptyMessage { get; set; } = "Keine Buchungen vorhanden.";
#region Parameters
@@ -1,9 +1,9 @@
<ModalDialog @ref="modal" Title="@Title" OnCancel="OnCancel" >
<ModalDialog @ref="modal" Title="@Title" OnCancel="OnCancel" Busy="Busy">
<h5>@Title</h5>
<p>@Message</p>
<div class="d-flex justify-content-end gap-2">
<button type="button" class="@ConfirmButtonClass" @onclick="Confirm"><i class="@ConfirmIconClass"></i> @ConfirmText</button>
<button type="button" class="btn btn-outline-secondary" data-safe-focus @onclick="Cancel"><i class="bi bi-x-lg"></i> @CancelText</button>
<button type="button" disabled="@Busy" class="@ConfirmButtonClass" @onclick="Confirm"><i class="@ConfirmIconClass"></i> @ConfirmText</button>
<button type="button" disabled="@Busy" class="btn btn-outline-secondary" data-safe-focus @onclick="Cancel"><i class="bi bi-x-lg"></i> @CancelText</button>
</div>
</ModalDialog>
@@ -4,6 +4,7 @@ namespace Duempelkas.App.Components.Dialogs;
public partial class ConfirmDialog
{
[Parameter] public bool Busy { get; set; }
#region Parameters
[Parameter]
@@ -34,7 +35,7 @@ public partial class ConfirmDialog
#region Actions
private async Task Confirm() => await OnConfirm.InvokeAsync();
private async Task Confirm() { if (!Busy) await OnConfirm.InvokeAsync(); }
private async Task Cancel() { if (modal != null) await modal.RequestCloseAsync(); else await OnCancel.InvokeAsync(); }
@@ -133,7 +133,7 @@
@if (confirmDeleteEntryId.HasValue)
{
<ConfirmDialog Title="Buchung löschen"
Message="@($"Soll \"{confirmDeleteEntryTitle}\" wirklich gelöscht werden?")"
Message="@(EntryChangeMessage(confirmDeleteEntryId, false))" Busy="entryActionBusy"
ConfirmText="Ja, löschen"
CancelText="Nein, abbrechen"
ConfirmButtonClass="btn btn-danger"
@@ -145,7 +145,7 @@
@if (confirmRestoreEntryId.HasValue)
{
<ConfirmDialog Title="Buchung wiederherstellen"
Message="@($"Soll \"{confirmRestoreEntryTitle}\" wiederhergestellt werden?")"
Message="@(EntryChangeMessage(confirmRestoreEntryId, true))" Busy="entryActionBusy"
ConfirmText="Ja, wiederherstellen"
CancelText="Nein, abbrechen"
ConfirmButtonClass="btn btn-warning"
@@ -30,6 +30,8 @@ public partial class AccountDetail
private int? selectedYear { get => Period.Year; set => Period.Year = value; }
private List<int> availableYears = [];
private bool entryActionBusy;
private Guid? undoToastId;
private int? confirmDeleteEntryId;
private string? confirmDeleteEntryTitle;
@@ -144,19 +146,27 @@ public partial class AccountDetail
private void CancelDeleteConfirm()
{
if (entryActionBusy) return;
confirmDeleteEntryId = null;
confirmDeleteEntryTitle = null;
}
private async Task HandleConfirmDelete()
{
if (confirmDeleteEntryId.HasValue)
if (entryActionBusy || !confirmDeleteEntryId.HasValue) return;
entryActionBusy = true;
try
{
await EntryService.DeleteEntryAsync(confirmDeleteEntryId.Value);
var id = confirmDeleteEntryId.Value;
await EntryService.DeleteEntryAsync(id);
confirmDeleteEntryId = null;
confirmDeleteEntryTitle = null;
await LoadAll();
if (undoToastId.HasValue) Toasts.Dismiss(undoToastId.Value);
undoToastId = Toasts.Show("Buchung gelöscht.", ToastKind.Success,
actionLabel: "Rückgängig", action: () => UndoEntryChange(id, true));
}
catch (Exception ex) { Toasts.Show("Die Buchung konnte nicht geändert werden. Bitte erneut versuchen.", ToastKind.Error, ex.Message); }
finally { entryActionBusy = false; }
}
private void RequestRestoreEntry(int entryId)
@@ -167,19 +177,57 @@ public partial class AccountDetail
private void CancelRestoreConfirm()
{
if (entryActionBusy) return;
confirmRestoreEntryId = null;
confirmRestoreEntryTitle = null;
}
private async Task HandleConfirmRestore()
{
if (confirmRestoreEntryId.HasValue)
if (entryActionBusy || !confirmRestoreEntryId.HasValue) return;
entryActionBusy = true;
try
{
await EntryService.RestoreEntryAsync(confirmRestoreEntryId.Value);
var id = confirmRestoreEntryId.Value;
await EntryService.RestoreEntryAsync(id);
confirmRestoreEntryId = null;
confirmRestoreEntryTitle = null;
await LoadAll();
if (undoToastId.HasValue) Toasts.Dismiss(undoToastId.Value);
undoToastId = Toasts.Show("Buchung wiederhergestellt.", ToastKind.Success,
actionLabel: "Rückgängig", action: () => UndoEntryChange(id, false));
}
catch (Exception ex) { Toasts.Show("Die Buchung konnte nicht geändert werden. Bitte erneut versuchen.", ToastKind.Error, ex.Message); }
finally { entryActionBusy = false; }
}
private async Task UndoEntryChange(int id, bool restore)
{
if (entryActionBusy) return;
entryActionBusy = true;
try
{
var current = (await EntryService.GetEntriesAsync(AccountId, (int?)null)).FirstOrDefault(e => e.Id == id);
if (current == null || current.IsDeleted != restore)
{
Toasts.Show("Die Buchung wurde inzwischen geändert. Bitte die aktuelle Liste prüfen.");
return;
}
if (restore) await EntryService.RestoreEntryAsync(id); else await EntryService.DeleteEntryAsync(id);
if (undoToastId.HasValue) Toasts.Dismiss(undoToastId.Value);
await LoadAll();
Toasts.Show("Änderung rückgängig gemacht.", ToastKind.Success);
}
finally { entryActionBusy = false; }
}
private string EntryChangeMessage(int? id, bool restore)
{
var entry = entries?.FirstOrDefault(e => e.Id == id);
if (entry == null) return "Bitte die aktuelle Buchung prüfen.";
var description = $"{entry.Date:dd.MM.yyyy}, {entry.Title}, {FormatAmount(entry.Amount)}, Konto {account?.Name}.";
return description + (entry.IsTransfer
? $" Diese Umbuchung betrifft {account?.Name} und {entry.LinkedAccountName}. Die Buchungen auf beiden Konten werden gemeinsam {(restore ? "wiederhergestellt und wieder mitgerechnet" : "gelöscht und nicht mehr mitgerechnet")}."
: $" Die Buchung wird {(restore ? "wiederhergestellt und wieder mitgerechnet" : "gelöscht und nicht mehr mitgerechnet") }.");
}
private void RequestEditEntry(int entryId)