feat(ui): persist deleted entry visibility across accounts
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@inject ISettingsService Settings
|
||||
|
||||
<div class="entry-filters">
|
||||
<div><label for="entry-search">Buchungen suchen</label>
|
||||
<input id="entry-search" type="search" class="form-control" @bind="searchText" @bind:event="oninput" placeholder="Beschreibung, Nummer, Datum oder Betrag" /></div>
|
||||
@@ -17,7 +19,7 @@
|
||||
@if (Entries.Any(e => e.IsDeleted))
|
||||
{
|
||||
<div class="form-check form-switch history-toggle">
|
||||
<input id="show-deleted" class="form-check-input" type="checkbox" role="switch" @bind="showDeleted" />
|
||||
<input id="show-deleted" class="form-check-input" type="checkbox" role="switch" @bind="showDeleted" @bind:after="SaveHistoryPreference" />
|
||||
<label class="form-check-label" for="show-deleted">Gelöschte Buchungen anzeigen (@Entries.Count(e => e.IsDeleted))</label>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -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<EntryDto> 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
|
||||
|
||||
@@ -2,6 +2,8 @@ namespace Duempelkas.App.Services;
|
||||
|
||||
public interface ISettingsService
|
||||
{
|
||||
Task<bool> GetShowDeletedEntriesAsync();
|
||||
Task SetShowDeletedEntriesAsync(bool show);
|
||||
Task<int> GetSetupStepAsync() => Task.FromResult(0);
|
||||
Task SetSetupStepAsync(int step) => Task.CompletedTask;
|
||||
Task<string?> GetClubNameAsync();
|
||||
|
||||
@@ -12,6 +12,15 @@ public class SettingsService : ISettingsService
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
||||
|
||||
public async Task<bool> GetShowDeletedEntriesAsync() => (await LoadAsync()).ShowDeletedEntries;
|
||||
|
||||
public async Task SetShowDeletedEntriesAsync(bool show)
|
||||
{
|
||||
var settings = await LoadAsync();
|
||||
settings.ShowDeletedEntries = show;
|
||||
await SaveAsync(settings);
|
||||
}
|
||||
|
||||
public async Task<string?> 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; }
|
||||
}
|
||||
|
||||
@@ -174,6 +174,8 @@ public class DashboardPdfStatementServiceTests : IDisposable
|
||||
|
||||
private sealed class FixedSettingsService : ISettingsService
|
||||
{
|
||||
public Task<bool> GetShowDeletedEntriesAsync() => Task.FromResult(false);
|
||||
public Task SetShowDeletedEntriesAsync(bool show) => Task.CompletedTask;
|
||||
private readonly string? _clubName;
|
||||
|
||||
public FixedSettingsService(string? clubName)
|
||||
|
||||
@@ -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<EntryTable>(p => p.Add(x => x.Entries, [entry]).Add(x => x.OnEditEntry, id => edited = id));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<EntryDto> 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<EntryTable>(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<ISettingsService>().GetShowDeletedEntriesAsync());
|
||||
first.Find(".filter-reset").Click();
|
||||
Assert.Single(first.FindAll("tbody tr"));
|
||||
var second = f.Context.Render<EntryTable>(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<ISettingsService>().GetShowDeletedEntriesAsync());
|
||||
var returning = f.Context.Render<EntryTable>(p => p.Add(x => x.Entries, Rows(1)));
|
||||
returning.WaitForAssertion(() => Assert.Empty(returning.FindAll("tbody tr")));
|
||||
}
|
||||
}
|
||||
@@ -34,11 +34,11 @@ public class YearNavigationTests
|
||||
context.Services.GetRequiredService<ReportingPeriod>().Year = 2025;
|
||||
var page = context.Render<Routes>();
|
||||
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<AddEntryDialog>();
|
||||
@@ -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<ReportingPeriod>().Year);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user