SplitBankTransactionAsync ended with NavigateTo(forceLoad: true), which
tore down the SignalR circuit and re-ran every repo constructor just
to refresh the pending-bookings list. Extract data loading into a
LoadDataAsync helper called from OnInitializedAsync, SplitBank
TransactionAsync, and OnCompletedAsync — the page refreshes in-circuit,
scoped caches survive, and the NavigationManager injection can go.
Also flags the plan: U-4 is split into U-4a (this) and U-4b (persist
settings + drop MyHeader's forceLoad, touching ~15 pages).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The single EditContext only wrapped the first split row; subsequent
rows added by AddTransactionAsync silently bypassed
DataAnnotationsValidator. Replace editContext.Validate() with an
explicit ValidateAllRows() loop that checks Text, TargetAccountId, and
Value != 0 for every row, and render the collected messages under the
existing FluentValidationSummary.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Guard SaveAsync against a null SelectedValue (previously
SelectedValue!.Value threw InvalidOperationException) and disable
the Save button while the date is empty so the user gets a visible
cue.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Move the SelectedAccount = Accounts.FirstOrDefault(...) lookup from
OnInitialized to OnParametersSet so the preselection tracks the
Accounts list arriving asynchronously (every caller now passes []
first and hydrates after an await). Idempotent — the OnSelectedChanged
callback path returns the same AccountId back and the parent re-render
lookup finds the same account.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Every repository, service, Blazor page/dialog, and test now uses
async/await. Single atomic diff; the codebase does not compile in
intermediate states.
- BaseRepository: LoadListAsync/LoadAsync/SaveAsync return Task<T>;
per-file locks use SemaphoreSlim so waiters can await; Save
serialises to a MemoryStream sync (XmlSerializer has no async
form), then File.WriteAllBytesAsync + sync File.Move.
- RepositoryCache.GetOrLoadAsync takes a Func<Task<List<T>>>.
- All 7 repository interfaces + implementations async.
- All service interfaces + implementations async (except vendor
IFxService and stateless IFxConverter / SettingsService).
- Every Blazor OnInitializedAsync switches to await base.
- Test suite fully async, 42 tests pass.
AccountRepository.EnsureAccountsFile keeps two .GetAwaiter().GetResult()
bridges because it runs from the constructor.
Null-render guard follow-up (folded in):
Blazor now renders the component once with fields at their initial
values while OnInitializedAsync awaits — so fields declared `= null!`
are actually null on that first render and things like
`accounts.GroupBy(...)` throw ArgumentNullException. Fixed across
Transactions, BalanceReport, BalanceSheetReport, ProfitLossReport,
DetailReport, Assets, Spendings, SpendingsOverTime, TransactionDialog,
TransactionSplitDialog, and BookingRuleDialog:
- Collection fields initialise to [] so first-render loops are empty.
- Single-object data fields become nullable; the razor wraps
consumption in `@if (field is null) { <p>Lädt…</p> return; }`.
- <PlotlyChart> guarded behind a null check on config/layout/data so
Plotly.Blazor's @bind doesn't see nulls.
- Header/footer strings initialise to "" instead of null!.
Architectural hygiene on a single-user local Blazor Server app: the
observed win is one File.ReadAllBytesAsync and one
File.WriteAllBytesAsync per Load/Save, and after PR D each file is
loaded at most once per SignalR circuit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three long-noted rough edges from the original analysis, each a
one-line touch:
- ClassIds becomes static class (was instantiable).
- Home.razor.cs drops the `loaded` bool guard (Blazor already runs
OnInitializedAsync exactly once per component instance).
- Move the "Claudio Schaad" mandator default out of SettingsService
into SettingsDataset.DefaultMandator, plumbed through
appsettings.Development.json.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add ILogger<ViewService> and log the match-count summary at the end
of MatchOpenBankTransactions: "Matched {Matched} of {Total} open bank
transactions". Completes item 18.
Also drop the ISettingsService [Inject] from Home.razor.cs that was
declared but never referenced.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New IFxConverter.ConvertToChf(amount, fromCurrency) and FxConverter
wrap the vendor IFxService and the FixerIo API key so callers stop
threading the key through every conversion. ViewService drops
IFxService and ISettingsService from its constructor and takes
IFxConverter instead; GetAccountViewList and GetBalanceSheetView no
longer read settingsService.GetSettings() per method.
FxConverterTestShould locks in the target-currency + API-key routing.
40 tests total, all passing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a Scoped RepositoryCache that memoizes deserialized XML lists,
keyed by absolute file path. Each XML file is loaded at most once per
SignalR circuit; Save invalidates the corresponding entry so
subsequent reads reload from disk.
BaseRepository gains a LoadList<T>(fileName) helper; each of the seven
repositories now defines Get*List() as a one-liner over that helper
and takes RepositoryCache via the constructor.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Consolidate decimal.ToFormattedString into Schaad.Accounting.Formatting
- Rename EnsureFileExisits -> EnsureFileExists and
SplitPredefiniton* -> SplitPredefinition* (class, interface, files, DI reg)
- AccountRepository: single-save year-rollover start-balance seed
- Drop legacy .NET Standard packages redundant on net9.0 and the stale
Interfaces\Extensions\ folder entry
- Set culture via RequestLocalizationOptions middleware
- Plan: item 9 (unit of work) split off into a dedicated PR D
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add IMPROVEMENT_PLAN.md with phased plan for follow-up work
- Consolidate DI registrations into a single AddAccounting() extension;
drop the duplicate service registrations and the PdfParsingService
self-registration
- TrySetYear: capture this.year before overwriting so rollback actually
restores the previous value
- DummyFxService: check toCurrency (was checking fromCurrency twice)
- TransactionRepository.GetTransaction: return a copy instead of mutating
the loaded entity, and guard against unknown ids
- ViewService.GetTransactionViewList(accountId): flip the sign on a copy
rather than mutating the entity returned by the repository
- FileService.GetTransactionListCsv: same treatment; use a local
signedValue instead of mutating trx.Value
- ProfitLossReport: use ClassIds.Income/Expenses instead of magic 3/4
- CLAUDE.md: correct the ClassIds documentation (1/2/3/4, not
1000/2000/3000/4000)
SettingsService lifetime is intentionally left as Singleton for now;
making it Scoped requires persisting year/mandator selection across
page reloads first (tracked in the plan).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Transaction amounts, totals, and sidebar badges in the Transactions page now show in the account's native currency (EUR, USD, etc.) instead of always CHF
- Fixed FX conversion in SaveTransaction/GetTransaction to fire when either the origin or target account is a foreign-currency account, not only the origin side
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>