feat(usability): preserve reporting year and locate outside-period bookings (U04)
This commit is contained in:
@@ -27,3 +27,7 @@ Strukturierte Ergebnisse und gemeinsame Statusanzeige mit optionalen technischen
|
|||||||
## U03 – Gleiche Bedeutung in PDF und Oberfläche
|
## U03 – Gleiche Bedeutung in PDF und Oberfläche
|
||||||
|
|
||||||
Einzelkonto und Übersicht enthalten nur wirksame Buchungen. Zeitraum und Bedeutung des Endstands sind ausdrücklich benannt, auch bei „Alle Jahre“ mit Anfangsbestand. Im Einzelkonto zählen Umbuchungen als Zu-/Abgang; die Übersicht zeigt beide Konten in einer Zeile und erklärt die unveränderte Vereinssumme. Prüfung: 19/19 Debug-Tests. PDF-Textextraktion prüft 58,30 € Kontostand, 108,30 € Vereinssumme, ausgeschlossene gelöschte/spätere Buchungen und Richtung. Einzelkonto, gemeinsame Übersicht und Seiten 2/3 eines dreiseitigen Auszugs gerendert und visuell auf Lesbarkeit, wiederholte Kopfzeilen und Abschluss geprüft.
|
Einzelkonto und Übersicht enthalten nur wirksame Buchungen. Zeitraum und Bedeutung des Endstands sind ausdrücklich benannt, auch bei „Alle Jahre“ mit Anfangsbestand. Im Einzelkonto zählen Umbuchungen als Zu-/Abgang; die Übersicht zeigt beide Konten in einer Zeile und erklärt die unveränderte Vereinssumme. Prüfung: 19/19 Debug-Tests. PDF-Textextraktion prüft 58,30 € Kontostand, 108,30 € Vereinssumme, ausgeschlossene gelöschte/spätere Buchungen und Richtung. Einzelkonto, gemeinsame Übersicht und Seiten 2/3 eines dreiseitigen Auszugs gerendert und visuell auf Lesbarkeit, wiederholte Kopfzeilen und Abschluss geprüft.
|
||||||
|
|
||||||
|
## U04 – Jahreskontext
|
||||||
|
|
||||||
|
Gemeinsame explizite Jahresauswahl bleibt beim Wechsel Übersicht/Konto/Zurück erhalten, auch „Alle Jahre“. Auswahl enthält aktuelles und gewähltes Jahr auch ohne Buchungen. Liste und Summen benennen den Zeitraum; Leerzustände unterscheiden fehlende Buchungen und leeren Zeitraum. Abweichendes Datum bleibt unverändert, erfordert eine gezielte Bestätigung im Formular und bietet nach Erfolg „Buchung im Jahr … anzeigen“. Prüfung: 21/21 Debug-Tests, einschließlich gerenderter Navigation 2025 → Konto → Zurück, Buchung 2026 bei Filter 2025 und anschließendem Aufruf des gespeicherten Jahres. Dabei bestehende doppelte DbContext-Registrierung korrigiert, da die Singleton-Factory sonst scoped Optionen konsumiert.
|
||||||
|
|||||||
@@ -20,6 +20,6 @@
|
|||||||
|
|
||||||
@if (!Entries.Any())
|
@if (!Entries.Any())
|
||||||
{
|
{
|
||||||
<div class="text-center py-3 text-muted">Keine Buchungen vorhanden.</div>
|
<div class="text-center py-3 text-muted">@EmptyMessage</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace Duempelkas.App.Components.Accounts;
|
|||||||
|
|
||||||
public partial class EntryTable
|
public partial class EntryTable
|
||||||
{
|
{
|
||||||
|
[Parameter] public string EmptyMessage { get; set; } = "Keine Buchungen vorhanden.";
|
||||||
#region Parameters
|
#region Parameters
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
|
|||||||
@@ -24,9 +24,17 @@
|
|||||||
<input type="number" class="form-control no-spinner" @bind="amount" step="0.01" min="0.01" />
|
<input type="number" class="form-control no-spinner" @bind="amount" step="0.01" min="0.01" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (OutsideYear)
|
||||||
|
{
|
||||||
|
<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.
|
||||||
|
<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 class="d-flex justify-content-end gap-2">
|
<div class="d-flex justify-content-end gap-2">
|
||||||
<button class="btn btn-success" @onclick="Save"
|
<button class="btn btn-success" @onclick="Save"
|
||||||
disabled="@(string.IsNullOrWhiteSpace(title) || amount <= 0)">
|
disabled="@(string.IsNullOrWhiteSpace(title) || amount <= 0 || (OutsideYear && (!confirmDifferentYear || confirmedYear != date.Year)))">
|
||||||
<i class="bi bi-check-lg"></i> @(EditEntry != null ? "Speichern" : "Hinzufügen")
|
<i class="bi bi-check-lg"></i> @(EditEntry != null ? "Speichern" : "Hinzufügen")
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline-secondary" @onclick="Cancel"><i class="bi bi-x-lg"></i> Abbrechen</button>
|
<button class="btn btn-outline-secondary" @onclick="Cancel"><i class="bi bi-x-lg"></i> Abbrechen</button>
|
||||||
|
|||||||
@@ -15,13 +15,18 @@ public partial class AddEntryDialog
|
|||||||
public EntryDto? EditEntry { get; set; }
|
public EntryDto? EditEntry { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback OnSave { get; set; }
|
public EventCallback<DateTime> OnSave { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback OnCancel { get; set; }
|
public EventCallback OnCancel { get; set; }
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
[Parameter] public int? ContextYear { get; set; }
|
||||||
|
private bool confirmDifferentYear;
|
||||||
|
private int? confirmedYear;
|
||||||
|
private bool OutsideYear => ContextYear.HasValue && date.Year != ContextYear.Value;
|
||||||
|
|
||||||
#region Fields
|
#region Fields
|
||||||
|
|
||||||
private EntryType entryType = EntryType.Income;
|
private EntryType entryType = EntryType.Income;
|
||||||
@@ -50,6 +55,7 @@ public partial class AddEntryDialog
|
|||||||
|
|
||||||
private async Task Save()
|
private async Task Save()
|
||||||
{
|
{
|
||||||
|
if (OutsideYear && (!confirmDifferentYear || confirmedYear != date.Year)) return;
|
||||||
if (EditEntry != null)
|
if (EditEntry != null)
|
||||||
{
|
{
|
||||||
await EntryService.UpdateEntryAsync(EditEntry.Id, date, title.Trim(), amount);
|
await EntryService.UpdateEntryAsync(EditEntry.Id, date, title.Trim(), amount);
|
||||||
@@ -59,7 +65,7 @@ public partial class AddEntryDialog
|
|||||||
await EntryService.CreateEntryAsync(AccountId, entryType, date, title.Trim(), amount);
|
await EntryService.CreateEntryAsync(AccountId, entryType, date, title.Trim(), amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
await OnSave.InvokeAsync();
|
await OnSave.InvokeAsync(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Cancel() => await OnCancel.InvokeAsync();
|
private async Task Cancel() => await OnCancel.InvokeAsync();
|
||||||
|
|||||||
@@ -25,9 +25,17 @@
|
|||||||
<input type="number" class="form-control no-spinner" @bind="amount" step="0.01" min="0.01" />
|
<input type="number" class="form-control no-spinner" @bind="amount" step="0.01" min="0.01" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (OutsideYear)
|
||||||
|
{
|
||||||
|
<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.
|
||||||
|
<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 class="d-flex justify-content-end gap-2">
|
<div class="d-flex justify-content-end gap-2">
|
||||||
<button class="btn btn-success" @onclick="Save"
|
<button class="btn btn-success" @onclick="Save"
|
||||||
disabled="@(!CanSave)"><i class="bi bi-arrow-left-right"></i> @(EditEntry != null ? "Speichern" : "Hinzufügen")</button>
|
disabled="@(!CanSave || (OutsideYear && (!confirmDifferentYear || confirmedYear != date.Year)))"><i class="bi bi-arrow-left-right"></i> @(EditEntry != null ? "Speichern" : "Hinzufügen")</button>
|
||||||
<button class="btn btn-outline-secondary" @onclick="Cancel"><i class="bi bi-x-lg"></i> Abbrechen</button>
|
<button class="btn btn-outline-secondary" @onclick="Cancel"><i class="bi bi-x-lg"></i> Abbrechen</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,13 +14,18 @@ public partial class AddTransferDialog
|
|||||||
public EntryDto? EditEntry { get; set; }
|
public EntryDto? EditEntry { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback OnSave { get; set; }
|
public EventCallback<DateTime> OnSave { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback OnCancel { get; set; }
|
public EventCallback OnCancel { get; set; }
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
[Parameter] public int? ContextYear { get; set; }
|
||||||
|
private bool confirmDifferentYear;
|
||||||
|
private int? confirmedYear;
|
||||||
|
private bool OutsideYear => ContextYear.HasValue && date.Year != ContextYear.Value;
|
||||||
|
|
||||||
#region Fields
|
#region Fields
|
||||||
|
|
||||||
private List<AccountSummaryDto> accounts = new();
|
private List<AccountSummaryDto> accounts = new();
|
||||||
@@ -61,6 +66,7 @@ public partial class AddTransferDialog
|
|||||||
|
|
||||||
private async Task Save()
|
private async Task Save()
|
||||||
{
|
{
|
||||||
|
if (OutsideYear && (!confirmDifferentYear || confirmedYear != date.Year)) return;
|
||||||
if (EditEntry != null)
|
if (EditEntry != null)
|
||||||
{
|
{
|
||||||
await EntryService.UpdateTransferAsync(EditEntry.Id, targetAccountId, date, title.Trim(), amount);
|
await EntryService.UpdateTransferAsync(EditEntry.Id, targetAccountId, date, title.Trim(), amount);
|
||||||
@@ -70,7 +76,7 @@ public partial class AddTransferDialog
|
|||||||
await EntryService.CreateTransferAsync(SourceAccountId, targetAccountId, date, title.Trim(), amount);
|
await EntryService.CreateTransferAsync(SourceAccountId, targetAccountId, date, title.Trim(), amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
await OnSave.InvokeAsync();
|
await OnSave.InvokeAsync(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Cancel() => await OnCancel.InvokeAsync();
|
private async Task Cancel() => await OnCancel.InvokeAsync();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
@inject ReportingPeriod Period
|
||||||
@page "/accounts/{AccountId:int}"
|
@page "/accounts/{AccountId:int}"
|
||||||
@inject IAccountService AccountService
|
@inject IAccountService AccountService
|
||||||
@inject IEntryService EntryService
|
@inject IEntryService EntryService
|
||||||
@@ -10,6 +11,13 @@
|
|||||||
<OperationNotice Result="fileResult" />
|
<OperationNotice Result="fileResult" />
|
||||||
@if (fileBusy) { <p role="status" aria-live="polite">Dateiaktion läuft … Bitte warten.</p> }
|
@if (fileBusy) { <p role="status" aria-live="polite">Dateiaktion läuft … Bitte warten.</p> }
|
||||||
|
|
||||||
|
@if (savedOutsideYear.HasValue)
|
||||||
|
{
|
||||||
|
<div class="alert alert-info" role="status">
|
||||||
|
Die Buchung wurde im Jahr @savedOutsideYear gespeichert und liegt außerhalb der aktuellen Ansicht.
|
||||||
|
<button class="btn btn-outline-primary" @onclick="ShowSavedYear">Buchung im Jahr @savedOutsideYear anzeigen</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@if (account == null)
|
@if (account == null)
|
||||||
{
|
{
|
||||||
<div class="text-center py-5">
|
<div class="text-center py-5">
|
||||||
@@ -43,10 +51,11 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
|
|
||||||
<button class="btn btn-nav btn-primary" @onclick="OpenYearFilterDialog">
|
<button class="btn btn-nav btn-primary" @onclick="OpenYearFilterDialog">
|
||||||
<i class="bi bi-funnel"></i> Ansicht<br>@GetFilterLabel()
|
<i class="bi bi-funnel"></i> @GetFilterLabel()
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p class="text-muted">Zeitraum für Liste und Summen: @Period.Label</p>
|
||||||
@if (balance != null)
|
@if (balance != null)
|
||||||
{
|
{
|
||||||
<div class="summary-section">
|
<div class="summary-section">
|
||||||
@@ -96,7 +105,7 @@
|
|||||||
@if (entries != null)
|
@if (entries != null)
|
||||||
{
|
{
|
||||||
<div class="entries-scroll-wrapper">
|
<div class="entries-scroll-wrapper">
|
||||||
<EntryTable Entries="entries" OnDeleteEntry="RequestDeleteEntry" OnRestoreEntry="RequestRestoreEntry" OnEditEntry="RequestEditEntry" />
|
<EntryTable Entries="entries" EmptyMessage="emptyEntriesMessage" OnDeleteEntry="RequestDeleteEntry" OnRestoreEntry="RequestRestoreEntry" OnEditEntry="RequestEditEntry" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,14 +113,14 @@
|
|||||||
|
|
||||||
@if (showAddEntry)
|
@if (showAddEntry)
|
||||||
{
|
{
|
||||||
<AddEntryDialog AccountId="AccountId"
|
<AddEntryDialog AccountId="AccountId" ContextYear="selectedYear"
|
||||||
OnSave="HandleEntryCreated"
|
OnSave="HandleEntryCreated"
|
||||||
OnCancel="() => showAddEntry = false" />
|
OnCancel="() => showAddEntry = false" />
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (showAddTransfer)
|
@if (showAddTransfer)
|
||||||
{
|
{
|
||||||
<AddTransferDialog SourceAccountId="AccountId"
|
<AddTransferDialog SourceAccountId="AccountId" ContextYear="selectedYear"
|
||||||
OnSave="HandleTransferCreated"
|
OnSave="HandleTransferCreated"
|
||||||
OnCancel="() => showAddTransfer = false" />
|
OnCancel="() => showAddTransfer = false" />
|
||||||
}
|
}
|
||||||
@@ -168,7 +177,7 @@
|
|||||||
|
|
||||||
@if (editingEntry != null)
|
@if (editingEntry != null)
|
||||||
{
|
{
|
||||||
<AddEntryDialog AccountId="AccountId"
|
<AddEntryDialog AccountId="AccountId" ContextYear="selectedYear"
|
||||||
EditEntry="editingEntry"
|
EditEntry="editingEntry"
|
||||||
OnSave="HandleEntryEdited"
|
OnSave="HandleEntryEdited"
|
||||||
OnCancel="() => editingEntry = null" />
|
OnCancel="() => editingEntry = null" />
|
||||||
@@ -176,7 +185,7 @@
|
|||||||
|
|
||||||
@if (editingTransferEntry != null)
|
@if (editingTransferEntry != null)
|
||||||
{
|
{
|
||||||
<AddTransferDialog SourceAccountId="AccountId"
|
<AddTransferDialog SourceAccountId="AccountId" ContextYear="selectedYear"
|
||||||
EditEntry="editingTransferEntry"
|
EditEntry="editingTransferEntry"
|
||||||
OnSave="HandleEntryEdited"
|
OnSave="HandleEntryEdited"
|
||||||
OnCancel="() => editingTransferEntry = null" />
|
OnCancel="() => editingTransferEntry = null" />
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public partial class AccountDetail
|
|||||||
private bool showEditName;
|
private bool showEditName;
|
||||||
private bool showEditCarryover;
|
private bool showEditCarryover;
|
||||||
private bool showYearFilterDialog;
|
private bool showYearFilterDialog;
|
||||||
private int? selectedYear = DateTime.Now.Year;
|
private int? selectedYear { get => Period.Year; set => Period.Year = value; }
|
||||||
private List<int> availableYears = [];
|
private List<int> availableYears = [];
|
||||||
|
|
||||||
private int? confirmDeleteEntryId;
|
private int? confirmDeleteEntryId;
|
||||||
@@ -35,6 +35,8 @@ public partial class AccountDetail
|
|||||||
private FileOperationResult? fileResult;
|
private FileOperationResult? fileResult;
|
||||||
private bool fileBusy;
|
private bool fileBusy;
|
||||||
|
|
||||||
|
private int? savedOutsideYear;
|
||||||
|
private string emptyEntriesMessage = "Noch keine Buchungen vorhanden.";
|
||||||
private EntryDto? editingEntry;
|
private EntryDto? editingEntry;
|
||||||
private EntryDto? editingTransferEntry;
|
private EntryDto? editingTransferEntry;
|
||||||
|
|
||||||
@@ -62,7 +64,9 @@ public partial class AccountDetail
|
|||||||
account = await AccountService.GetAccountAsync(AccountId);
|
account = await AccountService.GetAccountAsync(AccountId);
|
||||||
balance = await BalanceQueryService.GetAccountBalanceAsync(AccountId, selectedYear);
|
balance = await BalanceQueryService.GetAccountBalanceAsync(AccountId, selectedYear);
|
||||||
entries = await EntryService.GetEntriesAsync(AccountId, selectedYear);
|
entries = await EntryService.GetEntriesAsync(AccountId, selectedYear);
|
||||||
availableYears = await EntryService.GetEntryYearsAsync(AccountId);
|
var entryYears = await EntryService.GetEntryYearsAsync(AccountId);
|
||||||
|
availableYears = Period.Choices(entryYears);
|
||||||
|
emptyEntriesMessage = entryYears.Count == 0 ? "Noch keine Buchungen vorhanden." : "Keine Buchungen in diesem Zeitraum. Wähle ein anderes Jahr oder Alle Jahre.";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenYearFilterDialog()
|
private void OpenYearFilterDialog()
|
||||||
@@ -78,6 +82,7 @@ public partial class AccountDetail
|
|||||||
private async Task SelectFilterYear(int? year)
|
private async Task SelectFilterYear(int? year)
|
||||||
{
|
{
|
||||||
selectedYear = year;
|
selectedYear = year;
|
||||||
|
savedOutsideYear = null;
|
||||||
showYearFilterDialog = false;
|
showYearFilterDialog = false;
|
||||||
await LoadAll();
|
await LoadAll();
|
||||||
}
|
}
|
||||||
@@ -100,18 +105,22 @@ public partial class AccountDetail
|
|||||||
await LoadAll();
|
await LoadAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleEntryCreated()
|
private async Task HandleEntryCreated(DateTime savedDate)
|
||||||
{
|
{
|
||||||
|
savedOutsideYear = selectedYear.HasValue && selectedYear.Value != savedDate.Year ? savedDate.Year : null;
|
||||||
showAddEntry = false;
|
showAddEntry = false;
|
||||||
await LoadAll();
|
await LoadAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleTransferCreated()
|
private async Task HandleTransferCreated(DateTime savedDate)
|
||||||
{
|
{
|
||||||
|
savedOutsideYear = selectedYear.HasValue && selectedYear.Value != savedDate.Year ? savedDate.Year : null;
|
||||||
showAddTransfer = false;
|
showAddTransfer = false;
|
||||||
await LoadAll();
|
await LoadAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Task ShowSavedYear() => SelectFilterYear(savedOutsideYear);
|
||||||
|
|
||||||
private void RequestDeleteEntry(int entryId)
|
private void RequestDeleteEntry(int entryId)
|
||||||
{
|
{
|
||||||
confirmDeleteEntryId = entryId;
|
confirmDeleteEntryId = entryId;
|
||||||
@@ -171,8 +180,9 @@ public partial class AccountDetail
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleEntryEdited()
|
private async Task HandleEntryEdited(DateTime savedDate)
|
||||||
{
|
{
|
||||||
|
savedOutsideYear = selectedYear.HasValue && selectedYear.Value != savedDate.Year ? savedDate.Year : null;
|
||||||
editingEntry = null;
|
editingEntry = null;
|
||||||
editingTransferEntry = null;
|
editingTransferEntry = null;
|
||||||
await LoadAll();
|
await LoadAll();
|
||||||
@@ -226,7 +236,7 @@ public partial class AccountDetail
|
|||||||
|
|
||||||
private static string FormatAmount(decimal amount) => $"{amount:N2} €";
|
private static string FormatAmount(decimal amount) => $"{amount:N2} €";
|
||||||
|
|
||||||
private string GetFilterLabel() => selectedYear?.ToString() ?? "Alle";
|
private string GetFilterLabel() => Period.Label;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
@inject ReportingPeriod Period
|
||||||
@page "/"
|
@page "/"
|
||||||
@inject IAccountService AccountService
|
@inject IAccountService AccountService
|
||||||
@inject IBackupService BackupService
|
@inject IBackupService BackupService
|
||||||
@@ -40,7 +41,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
|
|
||||||
<button class="btn btn-nav btn-primary" @onclick="OpenYearFilterDialog">
|
<button class="btn btn-nav btn-primary" @onclick="OpenYearFilterDialog">
|
||||||
<i class="bi bi-funnel"></i> Ansicht<br>@GetFilterLabel()
|
<i class="bi bi-funnel"></i> @GetFilterLabel()
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-nav btn-secondary" @onclick="ToggleThemeAsync" title="Farbmodus wechseln">
|
<button class="btn btn-nav btn-secondary" @onclick="ToggleThemeAsync" title="Farbmodus wechseln">
|
||||||
<i class="@ThemeButtonIconClass"></i> Modus<br>@ThemeButtonLabel
|
<i class="@ThemeButtonIconClass"></i> Modus<br>@ThemeButtonLabel
|
||||||
@@ -50,7 +51,7 @@
|
|||||||
@if (accounts != null && accounts.Any())
|
@if (accounts != null && accounts.Any())
|
||||||
{
|
{
|
||||||
<div class="alert alert-secondary py-2 px-3 mb-3" role="status">
|
<div class="alert alert-secondary py-2 px-3 mb-3" role="status">
|
||||||
<strong>Summe aller Konten:</strong>
|
<strong>Summe aller Konten – @Period.Label:</strong>
|
||||||
<span class="ms-2 @(TotalClubBalance >= 0 ? "text-success" : "text-danger")">
|
<span class="ms-2 @(TotalClubBalance >= 0 ? "text-success" : "text-danger")">
|
||||||
@FormatCurrency(TotalClubBalance)
|
@FormatCurrency(TotalClubBalance)
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public partial class Dashboard
|
|||||||
private bool showEditClubName;
|
private bool showEditClubName;
|
||||||
private bool showRestoreConfirm;
|
private bool showRestoreConfirm;
|
||||||
private bool showYearFilterDialog;
|
private bool showYearFilterDialog;
|
||||||
private int? selectedYear = DateTime.Now.Year;
|
private int? selectedYear { get => Period.Year; set => Period.Year = value; }
|
||||||
private List<int> availableYears = [];
|
private List<int> availableYears = [];
|
||||||
private string clubName = string.Empty;
|
private string clubName = string.Empty;
|
||||||
private string? savedPdfPath;
|
private string? savedPdfPath;
|
||||||
@@ -67,7 +67,7 @@ public partial class Dashboard
|
|||||||
private async Task LoadAll()
|
private async Task LoadAll()
|
||||||
{
|
{
|
||||||
await LoadAccounts();
|
await LoadAccounts();
|
||||||
availableYears = await EntryService.GetAllEntryYearsAsync();
|
availableYears = Period.Choices(await EntryService.GetAllEntryYearsAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadAccounts()
|
private async Task LoadAccounts()
|
||||||
@@ -207,7 +207,7 @@ public partial class Dashboard
|
|||||||
return amount.ToString("N2", CultureInfo.GetCultureInfo("de-DE")) + " €";
|
return amount.ToString("N2", CultureInfo.GetCultureInfo("de-DE")) + " €";
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetFilterLabel() => selectedYear?.ToString() ?? "Alle";
|
private string GetFilterLabel() => Period.Label;
|
||||||
|
|
||||||
private string ThemeButtonLabel => currentTheme == "dark" ? "Hell" : "Dunkel";
|
private string ThemeButtonLabel => currentTheme == "dark" ? "Hell" : "Dunkel";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Duempelkas.App.Services;
|
||||||
|
|
||||||
|
/// <summary>The user's explicit selection survives navigation, including All years.</summary>
|
||||||
|
public sealed class ReportingPeriod
|
||||||
|
{
|
||||||
|
public int? Year { get; set; } = DateTime.Today.Year;
|
||||||
|
public string Label => Year.HasValue ? $"Jahr: {Year}" : "Alle Jahre";
|
||||||
|
public List<int> Choices(IEnumerable<int> entryYears) => entryYears
|
||||||
|
.Append(DateTime.Today.Year).Concat(Year.HasValue ? [Year.Value] : Array.Empty<int>())
|
||||||
|
.Distinct().OrderDescending().ToList();
|
||||||
|
}
|
||||||
@@ -11,8 +11,8 @@ public static class DependencyInjection
|
|||||||
public static IServiceCollection AddInfrastructure(this IServiceCollection services, string connectionString)
|
public static IServiceCollection AddInfrastructure(this IServiceCollection services, string connectionString)
|
||||||
{
|
{
|
||||||
services.AddSingleton(new DatabaseLocation(connectionString));
|
services.AddSingleton(new DatabaseLocation(connectionString));
|
||||||
services.AddDbContext<FinanceDbContext>(options =>
|
// The factory also registers FinanceDbContext as scoped. Registering
|
||||||
options.UseSqlite(connectionString));
|
// AddDbContext first would give its singleton factory scoped options.
|
||||||
services.AddDbContextFactory<FinanceDbContext>(options =>
|
services.AddDbContextFactory<FinanceDbContext>(options =>
|
||||||
options.UseSqlite(connectionString));
|
options.UseSqlite(connectionString));
|
||||||
|
|
||||||
@@ -24,6 +24,7 @@ public static class DependencyInjection
|
|||||||
services.AddScoped<IFileSaveService, FileSaveService>();
|
services.AddScoped<IFileSaveService, FileSaveService>();
|
||||||
services.AddSingleton<IBackupService, BackupService>();
|
services.AddSingleton<IBackupService, BackupService>();
|
||||||
services.AddSingleton<ISettingsService, SettingsService>();
|
services.AddSingleton<ISettingsService, SettingsService>();
|
||||||
|
services.AddSingleton<ReportingPeriod>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="bunit" Version="2.13.3" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.11.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.11.0" />
|
||||||
<PackageReference Include="PdfPig" Version="0.1.16" />
|
<PackageReference Include="PdfPig" Version="0.1.16" />
|
||||||
<PackageReference Include="xunit" Version="2.9.3" />
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using Duempelkas.App.Services;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Duempelkas.Tests;
|
||||||
|
|
||||||
|
public class ReportingPeriodTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void EmptyYearList_PreservesSelectedYear_AndOffersCurrentYear()
|
||||||
|
{
|
||||||
|
var period = new ReportingPeriod { Year = 2025 };
|
||||||
|
Assert.Contains(2025, period.Choices([]));
|
||||||
|
Assert.Contains(DateTime.Today.Year, period.Choices([]));
|
||||||
|
Assert.Equal("Jahr: 2025", period.Label);
|
||||||
|
period.Year = null;
|
||||||
|
Assert.Equal("Alle Jahre", period.Label);
|
||||||
|
Assert.Null(period.Year);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using Bunit;
|
||||||
|
using Duempelkas.App.Components;
|
||||||
|
using Duempelkas.App.Components.Dialogs;
|
||||||
|
using Duempelkas.App.Services;
|
||||||
|
using Duempelkas.Domain.Enums;
|
||||||
|
using Duempelkas.Infrastructure;
|
||||||
|
using Duempelkas.Infrastructure.Persistence;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Duempelkas.Tests;
|
||||||
|
|
||||||
|
public class YearNavigationTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Navigation_PreservesYear_AndOutsideBookingOffersReturn()
|
||||||
|
{
|
||||||
|
using var context = new BunitContext();
|
||||||
|
var connectionString = $"Data Source=year-ui-{Guid.NewGuid():N};Mode=Memory;Cache=Shared";
|
||||||
|
using var anchor = new SqliteConnection(connectionString);
|
||||||
|
anchor.Open();
|
||||||
|
context.Services.AddInfrastructure(connectionString);
|
||||||
|
context.JSInterop.Mode = JSRuntimeMode.Loose;
|
||||||
|
using (var db = context.Services.GetRequiredService<IDbContextFactory<FinanceDbContext>>().CreateDbContext())
|
||||||
|
{
|
||||||
|
db.Database.EnsureCreated();
|
||||||
|
db.Accounts.Add(new() { Name = "Testkasse" });
|
||||||
|
db.SaveChanges();
|
||||||
|
await context.Services.GetRequiredService<IEntryService>().CreateEntryAsync(db.Accounts.Single().Id,
|
||||||
|
EntryType.Income, new(2025, 1, 1), "Vorjahr", 10m);
|
||||||
|
}
|
||||||
|
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.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(".summary-section");
|
||||||
|
page.FindAll("button").Single(b => b.TextContent.Trim() == "Buchung").Click();
|
||||||
|
var dialog = page.FindComponent<AddEntryDialog>();
|
||||||
|
dialog.Find("input[type=date]").Change("2026-01-02");
|
||||||
|
dialog.Find("input[type=text]").Change("Außerhalb");
|
||||||
|
dialog.Find("input[type=number]").Change("12.50");
|
||||||
|
Assert.Contains("außerhalb", dialog.Markup);
|
||||||
|
Assert.True(dialog.Find("button.btn-success").HasAttribute("disabled"));
|
||||||
|
dialog.Find("input[type=checkbox]").Change(true);
|
||||||
|
dialog.Find("button.btn-success").Click();
|
||||||
|
page.WaitForAssertion(() => Assert.Contains("Buchung im Jahr 2026 anzeigen", page.Markup));
|
||||||
|
Assert.DoesNotContain("Außerhalb", page.Find("table").TextContent);
|
||||||
|
page.FindAll("button").Single(b => b.TextContent.Contains("Buchung im Jahr 2026 anzeigen")).Click();
|
||||||
|
page.WaitForAssertion(() => Assert.Contains("Außerhalb", page.Find("table").TextContent));
|
||||||
|
Assert.Equal(2026, context.Services.GetRequiredService<ReportingPeriod>().Year);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user