Adds TransactionDataset.GetValue(bool, string accountId), which
negates the value when the given account is the transaction's origin
(paying out) so the account's own transaction list and charts show
a minus sign for outgoing money, regardless of account class. Removes
the old Activa-only sign flip baked into
ViewService.GetTransactionViewListAsync(accountId), which would have
double-negated Activa bookings against the new accountId-aware
GetValue. ChartService's booking-text totals now sum through the same
accountId-aware GetValue instead of filtering to inbound transactions
only, so a booking text nets its in/out transactions correctly.
Adds two charts to the per-account transactions view: a stacked bar
chart of monthly totals and a pie chart of overall totals, both
grouped by booking text, so recurring bookings (rent, subscriptions,
etc.) are visible at a glance. Chart components are keyed on the
account id so switching accounts remounts them instead of reusing a
stale Plotly instance.
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>
Both were constructor-initialised value carriers with read-only
consumers. Positional record syntax replaces the class + property +
ctor boilerplate; ViewService callers updated to PascalCase named
args to match the record-generated parameter names.
Rest of item 26 descoped: XmlSerializer needs mutable public
setters (value-equality on mutable data is a footgun) and has no
source generator without changing file format. See
IMPROVEMENT_PLAN.md for the full rationale.
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>
Replace the hand-rolled model.Copy(target) pattern in every SaveXxx
with FindIndex -> in-place replace (or Add on new). Move the two
hidden defaults out of the deleted Copy bodies: Currency = "CHF"
default now lives in AccountRepository.SaveAccount; BookingDate =
ValueDate default now lives in TransactionRepository.SaveTransaction.
Delete Copy from Account, BookingRule, BookingText, SubClass, and
SplitPredefinition. Keep Transaction.Clone() (renamed from Copy, and
now includes RelatedParty) for the defensive copy in GetTransaction
and ViewService.WithDisplaySign.
Fixes a pre-existing bug where Transaction.Copy silently dropped
RelatedParty on every update save.
42 tests total, all passing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
IViewService, IFileService, and IChartService move from
Schaad.Accounting.Common/Interfaces/ to
Schaad.Accounting.Services/Interfaces/. Namespaces are unchanged, so
no consumer needs a using update. Common's vendor <Reference
Include="Schaad.Finance.Api"> can be dropped, matching Common's
documented role as the shared models/DTOs/interfaces layer.
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>