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.
|
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.
|
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 class="entry-filters">
|
||||||
<div><label for="entry-search">Buchungen suchen</label>
|
<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>
|
<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))
|
@if (Entries.Any(e => e.IsDeleted))
|
||||||
{
|
{
|
||||||
<div class="form-check form-switch history-toggle">
|
<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>
|
<label class="form-check-label" for="show-deleted">Gelöschte Buchungen anzeigen (@Entries.Count(e => e.IsDeleted))</label>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ namespace Duempelkas.App.Components.Accounts;
|
|||||||
public partial class EntryTable
|
public partial class EntryTable
|
||||||
{
|
{
|
||||||
private bool showDeleted;
|
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 searchText = string.Empty;
|
||||||
private string sort = "date-asc";
|
private string sort = "date-asc";
|
||||||
private List<EntryDto> VisibleEntries => Duempelkas.App.Services.EntryListQuery.Apply(Entries, searchText, showDeleted, sort);
|
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 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 int? HighlightedEntryId { get; set; }
|
||||||
[Parameter] public string EmptyMessage { get; set; } = "Keine Buchungen vorhanden.";
|
[Parameter] public string EmptyMessage { get; set; } = "Keine Buchungen vorhanden.";
|
||||||
#region Parameters
|
#region Parameters
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ namespace Duempelkas.App.Services;
|
|||||||
|
|
||||||
public interface ISettingsService
|
public interface ISettingsService
|
||||||
{
|
{
|
||||||
|
Task<bool> GetShowDeletedEntriesAsync();
|
||||||
|
Task SetShowDeletedEntriesAsync(bool show);
|
||||||
Task<int> GetSetupStepAsync() => Task.FromResult(0);
|
Task<int> GetSetupStepAsync() => Task.FromResult(0);
|
||||||
Task SetSetupStepAsync(int step) => Task.CompletedTask;
|
Task SetSetupStepAsync(int step) => Task.CompletedTask;
|
||||||
Task<string?> GetClubNameAsync();
|
Task<string?> GetClubNameAsync();
|
||||||
|
|||||||
@@ -12,6 +12,15 @@ public class SettingsService : ISettingsService
|
|||||||
|
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
|
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()
|
public async Task<string?> GetClubNameAsync()
|
||||||
{
|
{
|
||||||
var settings = await LoadAsync();
|
var settings = await LoadAsync();
|
||||||
@@ -59,6 +68,7 @@ public class SettingsService : ISettingsService
|
|||||||
|
|
||||||
private class AppSettings
|
private class AppSettings
|
||||||
{
|
{
|
||||||
|
public bool ShowDeletedEntries { get; set; }
|
||||||
public string? ClubName { get; set; }
|
public string? ClubName { get; set; }
|
||||||
public int SetupStep { get; set; }
|
public int SetupStep { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,6 +174,8 @@ public class DashboardPdfStatementServiceTests : IDisposable
|
|||||||
|
|
||||||
private sealed class FixedSettingsService : ISettingsService
|
private sealed class FixedSettingsService : ISettingsService
|
||||||
{
|
{
|
||||||
|
public Task<bool> GetShowDeletedEntriesAsync() => Task.FromResult(false);
|
||||||
|
public Task SetShowDeletedEntriesAsync(bool show) => Task.CompletedTask;
|
||||||
private readonly string? _clubName;
|
private readonly string? _clubName;
|
||||||
|
|
||||||
public FixedSettingsService(string? clubName)
|
public FixedSettingsService(string? clubName)
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ public class EntryPresentationTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void Actions_AreDirectIconButtonsWithTitlesAndBookingSpecificNames()
|
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 entry = new EntryDto(1, 1, "2026-001", EntryType.Expense, DateTime.Today, "Porto", 4.20m, false, false, null, null, null);
|
||||||
var edited = 0;
|
var edited = 0;
|
||||||
var table = context.Render<EntryTable>(p => p.Add(x => x.Entries, [entry]).Add(x => x.OnEditEntry, id => edited = id));
|
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]
|
[Fact]
|
||||||
public void Search_FindsKnownBookingInLargeList_AndResetsWithoutChangingAmounts()
|
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,
|
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();
|
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);
|
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;
|
context.Services.GetRequiredService<ReportingPeriod>().Year = 2025;
|
||||||
var page = context.Render<Routes>();
|
var page = context.Render<Routes>();
|
||||||
page.WaitForAssertion(() => Assert.Contains("Jahr: 2025", page.Markup));
|
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.WaitForAssertion(() => Assert.Contains("Vorjahr", page.Markup));
|
||||||
page.FindAll("button").Single(b => b.TextContent.Contains("Zurück")).Click();
|
page.FindAll("button").Single(b => b.TextContent.Contains("Zurück")).Click();
|
||||||
page.WaitForAssertion(() => Assert.Contains("Jahr: 2025", page.Markup));
|
page.WaitForAssertion(() => Assert.Contains("Jahr: 2025", page.Markup));
|
||||||
page.Find(".account-card").Click();
|
page.WaitForElement(".account-card").Click();
|
||||||
page.WaitForElement(".summary-section");
|
page.WaitForElement(".summary-section");
|
||||||
page.FindAll("button").Single(b => b.TextContent.Contains("Einnahme oder Ausgabe erfassen")).Click();
|
page.FindAll("button").Single(b => b.TextContent.Contains("Einnahme oder Ausgabe erfassen")).Click();
|
||||||
var dialog = page.FindComponent<AddEntryDialog>();
|
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.Trim() == "Alle Buchungen").Click();
|
||||||
page.FindAll("button").Single(b => b.TextContent.Contains("Zurück")).Click();
|
page.FindAll("button").Single(b => b.TextContent.Contains("Zurück")).Click();
|
||||||
page.WaitForAssertion(() => Assert.Contains("Alle Jahre", page.Markup));
|
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));
|
page.WaitForAssertion(() => Assert.Contains("Vorjahr", page.Markup));
|
||||||
Assert.Null(context.Services.GetRequiredService<ReportingPeriod>().Year);
|
Assert.Null(context.Services.GetRequiredService<ReportingPeriod>().Year);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user