From c069699f16d8ba9ac341cfe83a88b12ddd281b81 Mon Sep 17 00:00:00 2001 From: troogs Date: Mon, 2 Nov 2026 21:30:25 +0100 Subject: [PATCH] feat(ui): persist deleted entry visibility across accounts --- docs/usability-umsetzung.md | 8 +++ .../Components/Accounts/EntryTable.razor | 4 +- .../Components/Accounts/EntryTable.razor.cs | 4 +- .../Services/ISettingsService.cs | 2 + .../Services/SettingsService.cs | 10 ++++ .../DashboardPdfStatementServiceTests.cs | 2 + .../EntryPresentationTests.cs | 3 +- tests/Duempelkas.Tests/EntrySearchTests.cs | 3 +- .../HistoryPreferenceTests.cs | 53 +++++++++++++++++++ tests/Duempelkas.Tests/YearNavigationTests.cs | 6 +-- 10 files changed, 88 insertions(+), 7 deletions(-) create mode 100644 tests/Duempelkas.Tests/HistoryPreferenceTests.cs diff --git a/docs/usability-umsetzung.md b/docs/usability-umsetzung.md index e192120..540d559 100644 --- a/docs/usability-umsetzung.md +++ b/docs/usability-umsetzung.md @@ -129,3 +129,11 @@ Stift und Papierkorb sind in beiden Achsen zentriert; die Symbolschrift verwende Debug-Tests: 60/60 bestanden. Zusätzliche Browserprüfung mit lokalem Produktions-CSS: Symbole bei 100 und 200 % in beiden Designs innerhalb von 0,6 Pixeln zentriert, Buttons mindestens 44 × 44 Pixel. Kontrast der sichtbaren Tabellenköpfe 17,85:1 (hell) und 12,89:1 (dunkel). Alle 16 bisherigen Offline-Layoutkombinationen bestehen weiterhin; dunkler Tabellenkopf und Symbole zusätzlich visuell geprüft. Auch die Release-Suite besteht mit 60/60 Tests. Desktop Debug und Release bauen ohne Warnungen oder Fehler; `git diff --check` ist sauber. + +## Dauerhafte Anzeige gelöschter Buchungen + +Die Historienansicht wird als benutzerweite Einstellung in settings.json gespeichert und beim Öffnen jeder Kontoliste geladen. Sie bleibt über Kontowechsel und Neustarts aktiv, bis der Switch ausgeschaltet wird. „Filter zurücksetzen“ ändert nur Suche und Sortierung. Bestehende Einstellungsdateien ohne diesen Wert starten weiterhin mit ausgeblendeten gelöschten Buchungen. + +62/62 Tests in Debug und Release bestanden. Neue Regressionen prüfen Kontowechsel, Filterreset, Ausschalten, erneutes Laden durch eine neue SettingsService-Instanz und Erhalt der übrigen Einstellungen. Navigationstest wartet auf die tatsächlich geladene Kontokarte. Debug-Desktop-Build ohne Warnungen/Fehler. Der vorhandene Publish-Task wird anschließend mit seinen unveränderten Release-/win-x64-/Einzeldatei-Argumenten ausgeführt. Die bereits vorhandene lokale Anpassung an AccountCard.razor bleibt separat erhalten und fließt in die lokale Veröffentlichung ein. + +Publish-Task erfolgreich abgeschlossen: Publish/Duempelkas-v26.306.exe (69.325.134 Bytes). Die ältere veröffentlichte Version bleibt erhalten. Die veröffentlichte Release-Anwendung wurde nicht gegen die Vereinsdaten gestartet. diff --git a/src/Duempelkas.App/Components/Accounts/EntryTable.razor b/src/Duempelkas.App/Components/Accounts/EntryTable.razor index 66f05d7..84be275 100644 --- a/src/Duempelkas.App/Components/Accounts/EntryTable.razor +++ b/src/Duempelkas.App/Components/Accounts/EntryTable.razor @@ -1,3 +1,5 @@ +@inject ISettingsService Settings +
@@ -17,7 +19,7 @@ @if (Entries.Any(e => e.IsDeleted)) {
- +
} diff --git a/src/Duempelkas.App/Components/Accounts/EntryTable.razor.cs b/src/Duempelkas.App/Components/Accounts/EntryTable.razor.cs index 74fe20e..25ae56a 100644 --- a/src/Duempelkas.App/Components/Accounts/EntryTable.razor.cs +++ b/src/Duempelkas.App/Components/Accounts/EntryTable.razor.cs @@ -6,11 +6,13 @@ namespace Duempelkas.App.Components.Accounts; public partial class EntryTable { private bool showDeleted; + protected override async Task OnInitializedAsync() => showDeleted = await Settings.GetShowDeletedEntriesAsync(); + private Task SaveHistoryPreference() => Settings.SetShowDeletedEntriesAsync(showDeleted); private string searchText = string.Empty; private string sort = "date-asc"; private List VisibleEntries => Duempelkas.App.Services.EntryListQuery.Apply(Entries, searchText, showDeleted, sort); private decimal ResultSum => VisibleEntries.Where(e => !e.IsDeleted).Sum(Duempelkas.App.Services.EntryListQuery.SignedAmount); - private void ResetFilters() { searchText = string.Empty; sort = "date-asc"; showDeleted = false; } + private void ResetFilters() { searchText = string.Empty; sort = "date-asc"; } [Parameter] public int? HighlightedEntryId { get; set; } [Parameter] public string EmptyMessage { get; set; } = "Keine Buchungen vorhanden."; #region Parameters diff --git a/src/Duempelkas.App/Services/ISettingsService.cs b/src/Duempelkas.App/Services/ISettingsService.cs index 821d814..6856904 100644 --- a/src/Duempelkas.App/Services/ISettingsService.cs +++ b/src/Duempelkas.App/Services/ISettingsService.cs @@ -2,6 +2,8 @@ namespace Duempelkas.App.Services; public interface ISettingsService { + Task GetShowDeletedEntriesAsync(); + Task SetShowDeletedEntriesAsync(bool show); Task GetSetupStepAsync() => Task.FromResult(0); Task SetSetupStepAsync(int step) => Task.CompletedTask; Task GetClubNameAsync(); diff --git a/src/Duempelkas.Infrastructure/Services/SettingsService.cs b/src/Duempelkas.Infrastructure/Services/SettingsService.cs index 2e26f54..c9aa2d3 100644 --- a/src/Duempelkas.Infrastructure/Services/SettingsService.cs +++ b/src/Duempelkas.Infrastructure/Services/SettingsService.cs @@ -12,6 +12,15 @@ public class SettingsService : ISettingsService private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; + public async Task GetShowDeletedEntriesAsync() => (await LoadAsync()).ShowDeletedEntries; + + public async Task SetShowDeletedEntriesAsync(bool show) + { + var settings = await LoadAsync(); + settings.ShowDeletedEntries = show; + await SaveAsync(settings); + } + public async Task GetClubNameAsync() { var settings = await LoadAsync(); @@ -59,6 +68,7 @@ public class SettingsService : ISettingsService private class AppSettings { + public bool ShowDeletedEntries { get; set; } public string? ClubName { get; set; } public int SetupStep { get; set; } } diff --git a/tests/Duempelkas.Tests/DashboardPdfStatementServiceTests.cs b/tests/Duempelkas.Tests/DashboardPdfStatementServiceTests.cs index c34a982..9bddb33 100644 --- a/tests/Duempelkas.Tests/DashboardPdfStatementServiceTests.cs +++ b/tests/Duempelkas.Tests/DashboardPdfStatementServiceTests.cs @@ -174,6 +174,8 @@ public class DashboardPdfStatementServiceTests : IDisposable private sealed class FixedSettingsService : ISettingsService { + public Task GetShowDeletedEntriesAsync() => Task.FromResult(false); + public Task SetShowDeletedEntriesAsync(bool show) => Task.CompletedTask; private readonly string? _clubName; public FixedSettingsService(string? clubName) diff --git a/tests/Duempelkas.Tests/EntryPresentationTests.cs b/tests/Duempelkas.Tests/EntryPresentationTests.cs index c41e411..027f7dd 100644 --- a/tests/Duempelkas.Tests/EntryPresentationTests.cs +++ b/tests/Duempelkas.Tests/EntryPresentationTests.cs @@ -20,7 +20,8 @@ public class EntryPresentationTests [Fact] public void Actions_AreDirectIconButtonsWithTitlesAndBookingSpecificNames() { - using var context = new BunitContext(); + using var fixture = new LedgerUiFixture(); + var context = fixture.Context; var entry = new EntryDto(1, 1, "2026-001", EntryType.Expense, DateTime.Today, "Porto", 4.20m, false, false, null, null, null); var edited = 0; var table = context.Render(p => p.Add(x => x.Entries, [entry]).Add(x => x.OnEditEntry, id => edited = id)); diff --git a/tests/Duempelkas.Tests/EntrySearchTests.cs b/tests/Duempelkas.Tests/EntrySearchTests.cs index 1533aae..6a4417d 100644 --- a/tests/Duempelkas.Tests/EntrySearchTests.cs +++ b/tests/Duempelkas.Tests/EntrySearchTests.cs @@ -12,7 +12,8 @@ public class EntrySearchTests [Fact] public void Search_FindsKnownBookingInLargeList_AndResetsWithoutChangingAmounts() { - using var context = new BunitContext(); + using var fixture = new LedgerUiFixture(); + var context = fixture.Context; var entries = Enumerable.Range(1, 400).Select(i => new EntryDto(i, 1, $"2026-{i:D3}", EntryType.Income, new(2026, 1, 1), i == 321 ? "Älteres Jubiläum" : $"Beitrag {i}", i == 321 ? 12.50m : i, false, false, null, null, null)).ToList(); var original = entries.Sum(e => e.Amount); diff --git a/tests/Duempelkas.Tests/HistoryPreferenceTests.cs b/tests/Duempelkas.Tests/HistoryPreferenceTests.cs new file mode 100644 index 0000000..bfd21f7 --- /dev/null +++ b/tests/Duempelkas.Tests/HistoryPreferenceTests.cs @@ -0,0 +1,53 @@ +using Bunit; +using Duempelkas.App.Components.Accounts; +using Duempelkas.App.Services; +using Duempelkas.App.Services.Models; +using Duempelkas.Domain.Enums; +using Duempelkas.Infrastructure.Services; +using Xunit; + +namespace Duempelkas.Tests; + +public class HistoryPreferenceTests +{ + [Fact] + public async Task PreferenceSurvivesRestart_AndPreservesOtherSettings() + { + var folder = Path.Combine(Path.GetTempPath(), "duempelkas-history-" + Guid.NewGuid().ToString("N")); + try + { + var settings = new SettingsService(folder); + Assert.False(await settings.GetShowDeletedEntriesAsync()); + await settings.SetClubNameAsync("Testverein"); + await settings.SetShowDeletedEntriesAsync(true); + var restarted = new SettingsService(folder); + Assert.True(await restarted.GetShowDeletedEntriesAsync()); + Assert.Equal("Testverein", await restarted.GetClubNameAsync()); + await restarted.SetSetupStepAsync(2); + Assert.True(await new SettingsService(folder).GetShowDeletedEntriesAsync()); + await restarted.SetShowDeletedEntriesAsync(false); + Assert.False(await new SettingsService(folder).GetShowDeletedEntriesAsync()); + } + finally { if (Directory.Exists(folder)) Directory.Delete(folder, true); } + } + + [Fact] + public async Task HistoryStaysEnabledAcrossAccountsAndFilterReset_UntilSwitchedOff() + { + using var f = new LedgerUiFixture(); + List Rows(int account) => [new(account, account, "2026-001", EntryType.Income, + DateTime.Today, "Deleted test", 10m, true, false, null, null, null)]; + var first = f.Context.Render(p => p.Add(x => x.Entries, Rows(1))); + Assert.Empty(first.FindAll("tbody tr")); + await first.Find("#show-deleted").ChangeAsync(new() { Value = true }); + Assert.True(await f.Service().GetShowDeletedEntriesAsync()); + first.Find(".filter-reset").Click(); + Assert.Single(first.FindAll("tbody tr")); + var second = f.Context.Render(p => p.Add(x => x.Entries, Rows(2))); + second.WaitForAssertion(() => Assert.Single(second.FindAll("tbody tr"))); + await second.Find("#show-deleted").ChangeAsync(new() { Value = false }); + Assert.False(await f.Service().GetShowDeletedEntriesAsync()); + var returning = f.Context.Render(p => p.Add(x => x.Entries, Rows(1))); + returning.WaitForAssertion(() => Assert.Empty(returning.FindAll("tbody tr"))); + } +} diff --git a/tests/Duempelkas.Tests/YearNavigationTests.cs b/tests/Duempelkas.Tests/YearNavigationTests.cs index 7c71f85..20405bc 100644 --- a/tests/Duempelkas.Tests/YearNavigationTests.cs +++ b/tests/Duempelkas.Tests/YearNavigationTests.cs @@ -34,11 +34,11 @@ public class YearNavigationTests context.Services.GetRequiredService().Year = 2025; var page = context.Render(); page.WaitForAssertion(() => Assert.Contains("Jahr: 2025", page.Markup)); - page.Find(".account-card").Click(); + page.WaitForElement(".account-card").Click(); page.WaitForAssertion(() => Assert.Contains("Vorjahr", page.Markup)); page.FindAll("button").Single(b => b.TextContent.Contains("Zurück")).Click(); page.WaitForAssertion(() => Assert.Contains("Jahr: 2025", page.Markup)); - page.Find(".account-card").Click(); + page.WaitForElement(".account-card").Click(); page.WaitForElement(".summary-section"); page.FindAll("button").Single(b => b.TextContent.Contains("Einnahme oder Ausgabe erfassen")).Click(); var dialog = page.FindComponent(); @@ -60,7 +60,7 @@ public class YearNavigationTests page.FindAll("button").Single(b => b.TextContent.Trim() == "Alle Buchungen").Click(); page.FindAll("button").Single(b => b.TextContent.Contains("Zurück")).Click(); page.WaitForAssertion(() => Assert.Contains("Alle Jahre", page.Markup)); - page.Find(".account-card").Click(); + page.WaitForElement(".account-card").Click(); page.WaitForAssertion(() => Assert.Contains("Vorjahr", page.Markup)); Assert.Null(context.Services.GetRequiredService().Year); }