Align BudgetTable on the income-statement standard shipped for the
compare/trend reports: sections now read Income -> Expense -> Transfer
(useBudget's TYPE_ORDER and BudgetTable's typeOrder both flipped), and
the previously unlabeled grand-total row is replaced by two interleaved
result rows (Result before transfers / Net result), computed by a new
pure, tested module (budgetTableResults.ts) mirroring compareResults.ts's
shape. Roll-up covers the previous-year-actual, budgeted-annual, and
budgeted-monthly columns alike.
All categories remain displayed (the grid stays a full edit surface) --
no collapse, no empty-row toggle, matching this issue's frozen decisions.
Resolves#278
Mounts <FilterPanel> on ReportsComparePage (next to the existing
CompareReferenceMonthPicker) and BudgetPage (next to the existing
YearNavigator), keeping each page's temporal control unchanged. Threads
accountIds from useReportsPeriod through useCompare into
getCompareMonthOverMonth/getCompareYearOverYear, and through
CompareBudgetView into getBudgetVsActualData — closing the gap where
that sub-tab silently ignored the filter while the rest of the Compare
page respected it.
Budget: the issue named getBudgetVsActualData as the target for
useBudget, but that hook never calls it (it's exclusive to
CompareBudgetView/Dashboard/Cartes) — its only real actuals fetch is
getActualTotalsForYear(year - 1), the previous-year reference column.
Extended that function with the same optional accountIds pass-through
established by #273, rather than wiring in an unrelated call shape.
Both hooks fetch their account checkbox list via getAllImportSources,
mirroring the useTrends/#275 pattern. Empty selection = no filter,
byte-identical to pre-#276 output (regression-tested).
Resolves#276
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds src/components/reports/FilterPanel.tsx, the shared filter bar for the
report pages. It renders the page's own temporal control (PeriodSelector /
CompareReferenceMonthPicker / YearNavigator / …) as-is via a `temporalControl`
ReactNode prop, plus a checkbox multi-select over `accounts: ImportSource[]`.
No `temporalMode` enum — that design was rejected to avoid mixing state
sources; the panel owns no temporal state at all.
Empty `accountIds` means no filter (all sources). Selection toggling is a
pure, exported `toggleAccountId` helper. Copy says "sources"/"import
sources" everywhere, never "comptes"/"accounts", so the filter reads as
distinct from the Bilan module's own "Compte" vocabulary.
This issue creates the standalone component only; wiring it into report
pages is left to follow-up issues (#275/#276).
Resolves#274
Renders the trends-by-category table from the #264 id-keyed tree as an indented
parent/child hierarchy (parity with ComparePeriodTable): top-level parents carry
a chevron and fold their subtree down to a subtotal, with an "expand all /
collapse all" button. Collapsed by default; expansion state is persisted under a
trends-specific localStorage key (reports-trends-expanded), distinct from the
comparable tables. The "result before transfers" line is interleaved before the
transfers section instead of sitting at the bottom.
Collapse is purely visual: section subtotals, before/net results and totals come
from computeOverTimeResults over the raw tree, never the visible rows.
New pure module overTimeTableModel.ts (section grouping + collapse accessors)
keeps the component thin and unit-testable without a React render harness;
overTimeResults.ts is left untouched. 8 new tests; build + 728 vitest green.
Resolves#265
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an id-keyed hierarchical `tree: OverTimeRow[]` sidecar to
getCategoryOverTime, built via the generic buildLeafDrivenTree (#263) as
buildOverTimeTree — leaf-driven, income-first, no top-N / no "Other" bucket.
The name-keyed pivot (data/categories/colors/types/categoryIds) is left
BYTE-IDENTICAL, so the Trends chart and the dashboard render unchanged.
computeOverTimeResults now consumes the id-keyed tree LEAVES (never the
`is_parent` subtotals) grouped by each leaf's category_id-resolved type, so
the section subtotals and the Result-before/net lines are exact: two homonym
categories of different types no longer collide, and a non-top-N income or
transfer category is no longer lumped into "Other" as an expense.
CategoryOverTimeTable renders the tree leaves (every category, id-safe)
instead of the top-N pivot; the chart keeps reading the top-N-capped pivot so
its stacked series stay bounded. OverTimeRow mirrors CategoryDelta's snake_case
hierarchy block (parent_id/is_parent/depth/category_type) so it composes with
collapsibleRows / useCollapsibleGroups for the #265 hierarchy work.
Tests: rewrote overTimeResults.test.ts onto the tree (homonym regression +
leaves-only-not-parents); added buildOverTimeTree suite (nesting, income-first,
grand-total invariance, orphan, A->B->A cycle depth guard) and a
getCategoryOverTime tree-wiring test proving the pivot stays top-N+Other while
the tree carries every category. 720 vitest green, build + tsc green.
Resolves#264
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The two hierarchical comparable reports — real-vs-real Compare and
real-vs-budget — now let the user collapse or expand each top-level
category's sub-categories. Groups start collapsed by default (#260): only
the parent's subtotal row shows until the user expands it, and each expanded
group is remembered per report.
- New pure module utils/collapsibleRows.ts (visibility filter, group-key
extraction, expanded-set (de)serialization) + useCollapsibleGroups hook
wrapping the localStorage-persisted expanded set. Persisting the *expanded*
set (not the collapsed one) makes "all collapsed" the zero/default state.
- A chevron toggles each top-level parent; an "Expand all / Collapse all"
button toggles them together. Section subtotals and result lines keep
summing every leaf, so collapsing never changes any total.
- Accessors mirror each table's own depth/parent logic so hidden rows are
exactly a group's indented descendants.
- i18n keys reports.collapse.{expandAll,collapseAll} (FR/EN); CHANGELOG.
CategoryOverTimeTable (Trends -> by category) is intentionally left out: its
rows are a flat top-N category list with no parent/child hierarchy to fold.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Apply the compare report's "result analysis" presentation to Trends -> By
category (table view). Categories are grouped into Income -> Expenses ->
Transfers sections with per-month and total subtotals; the old mixed "Total"
row (which summed absolute magnitudes into a meaningless figure) is replaced by
a Net result row per month (income - expenses + transfers) plus a Result before
transfers row shown only when transfers exist, both coloured by sign. Category
cells stay neutral levels.
- reportService.getCategoryOverTime: project COALESCE(c.type,'expense') in both
SELECTs and return an additive `types` map (built from the top-N rows, like
`colors`). Dashboard widget and CategoryOverTimeChart are untouched.
- shared types: add `types` to CategoryOverTimeData.
- new pure module overTimeResults.ts (computeOverTimeResults) with unit tests -
the per-month result reducer, mirroring the compare's pure Totals helper.
- i18n: add reports.compare.resultNet / resultBeforeTransfers (FR+EN); reuse the
existing sections.* and total* keys.
- .gitignore: anchor the autopilot `reports/` rule to `/reports/` so it no
longer swallows new files under src/components/reports/.
Section order is income-first (Revenus -> Depenses -> Transferts), per the
issue; the existing COMPARE_TYPE_ORDER is expense-first, so a local
OVER_TIME_TYPE_ORDER is used rather than reusing it.
Resolves#256
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses /pr-review REQUEST_CHANGES on #255.
- useCompare: the "skip first sync" boolean was not StrictMode-safe — the dev
double-invoke of effects flipped the flag on setup #1, so setup #2 re-synced
the reference month to the civil-year December, re-introducing the very bug
Changement 2 fixes (dev only; prod has no double-invoke). Replace it with a
value-change guard: a ref seeded with the initial `to` plus a pure
syncReferenceOnPeriodChange() that only dispatches when `to` actually changes.
Idempotent across the double-invoke, and now unit-tested (5 cases) since the
decision is a pure function (the project has no renderHook harness).
- Remove the now-orphaned reports.compare.totalRow i18n key (both locales) — the
flat grand total it labelled was replaced by the result lines.
- ComparePeriodTable: gate the "before transfers" line on results.hasTransfers
(previously computed/tested but unused).
- ComparePeriodChart: show the no-data empty state when the expense filter
leaves nothing (a pure income/transfer period) instead of bare axes.
Build + 690 vitest green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "actual vs actual" compare was expenses-only (WHERE amount < 0), so revenue
categories were invisible — you could not tell a surplus from a deficit. Make it
an income statement (the product's namesake "résultat"):
- COMPARE_DELTA_SQL: broaden the WHERE and per-type CASE to include `income` as
a signed SUM (like `transfer`), so revenue credits survive the outflow filter.
Expenses stay ABS-of-outflows, byte-identical. Transfer netting (#243) intact.
- Section order is now income -> expense -> transfer (COMPARE_TYPE_ORDER).
- ComparePeriodTable: replace the now-meaningless flat grand total with two
result lines — "result before transfers" (revenues - expenses) and the net
"result" (after transfers) — extracted into a pure, tested compareResults
module. Delta colours are direction-aware (income/result up = green, spending
up = red); result amounts are coloured by sign (surplus/deficit).
- ComparePeriodChart stays a spending view: filter to expense leaves so revenue
bars don't mix into the same axis.
- useCompare: skip the initial period-sync so the compare opens on the previous
(last complete) month instead of the civil-year December that useReportsPeriod
yields by default.
Also anchor the `reports/` gitignore to `/reports/` — the unanchored rule was
silently ignoring new files under src/components/reports/.
Tests: new compareResults.test.ts (result roll-up, unbalanced transfer, deficit,
subtotal exclusion); reportService.test.ts updated for the broadened SQL, the
income bucket rule, and income-first section order. Build + 685 vitest green.
Resolves#253
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address the two non-blocking review notes on PR #252:
- The re-injected non-leaf parent (e.g. #1710) is technically clickable, not
'never offered'; correct the MappingRow comment to state selecting it is a
no-op (only leaves resolve).
- Extract the per-row option-building glue into a pure, unit-tested helper
comboboxCategoriesForTarget (4 cases: null/leaf pass-through by reference,
non-leaf append, unresolvable stale id).
The v1 target combobox in the migration preview now offers only leaf
categories (the inputable end-categories), not intermediate parents, so a
transaction is never mapped to a grouping bucket. Leaves render as a flat,
un-indented list in taxonomy order.
A low-confidence default can still point a v2 category at a non-leaf parent
(e.g. 'Divertissement' #1710); MappingRow re-injects that current target for
its single row via findTaxonomyCategory so the input stays populated, while
new picks remain leaves-only. Adds a findTaxonomyCategory pure helper + tests
(adapter now returns 112 leaves, not the 150-node full tree).
Make the v1 target editable on EVERY row of the categories-migration
preview (StepSimulate), not only the "needs review" ones. Each row now
renders a reused CategoryCombobox (accent-insensitive type-ahead,
keyboard, hierarchical) in place of the read-only text / flat <select>.
A pure adapter (taxonomyToComboboxCategories) flattens the full v1
taxonomy into Category-shaped rows once in StepSimulate and passes it as
a prop to the rows. The full tree (not just leaves) is fed so mid-tree
default targets such as "Divertissement" (id 1710, the default for
"Jeux, Films & Livres") both display and can be re-picked. The target
cell stops click/keydown propagation so picker interaction never toggles
the row preview. Reducer/guard behavior unchanged: resolving a "none"
row still bumps confidence to medium, and the unresolved-count guard
stays functional.
Resolves#246
A detailed (by-security) account can now import its positions from a CSV
instead of adding each security one by one. An "Import CSV" button next to
"Add a title" opens a native picker; the delimiter, encoding and columns
(symbol, quantity, optional price + book_cost) are auto-detected via new
csvAutoDetect helpers and adjustable in a small mapping editor. Rows sharing
a symbol are merged (SUM qty + book_cost, first price) and the batch is merged
into the basket by normalized symbol, so no UNIQUE(snapshot_line_id,
security_id) violation can occur at save. A CSV without a price column imports
quantities with an empty unit_price (fetch/type later) — the existing atomic
save path is unchanged (no SQL/Rust change).
- csvAutoDetect: autoDetectHoldingColumns + analyzeHoldingsCsv (+ tests)
- useSnapshotEditor: holdingsFromCsvRows + IMPORT_HOLDINGS action + importHoldings (+ tests)
- HoldingsCsvImportModal: file pick -> parse -> mapping editor + preview
- i18n FR/EN under balance.snapshot.detailed.importCsv.*
- CHANGELOG (Added / Ajouté)
Resolves#245
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Decouple the /balance empty-state guard into three explicit states via a
new pure helper `deriveLandingState` (empty / accounts-no-snapshot /
populated). Having accounts but no snapshot no longer strands the user on a
snapshot-only onboarding card: each state now renders navigation tiles
(HubReportNavCard, modeled on the Reports hub), and "Manage accounts"
(/balance/accounts) is reachable in every state, including the populated
dashboard.
- Remove BalanceOnboardingCard (component + test) and the dead
balance.onboarding.* i18n keys; add balanceLanding.ts + test.
- Add balance.hub.* and balance.landing.* keys in FR and EN.
- CHANGELOG (EN + FR) under [Unreleased].
Resolves#244
Render the real-vs-real comparable report as a parent/child category tree
with subtotal rows, mirroring the budget report — rows grouped into
expense/income/transfer sections with per-section and grand totals and a
subtotals-on-top/bottom toggle. The compare service now builds the tree on
top of the flat per-category deltas, so leaf-category figures are unchanged
and the #243 transfer netting is preserved (a balanced transfer group
subtotals to ~0, i.e. the group's net).
- reportService: buildCompareTree() synthesizes subtotal rows from the flat
leaves + category metadata; getCompareMonthOverMonth/YoY return the tree
(COMPARE_DELTA_SQL and rowsToDeltas untouched).
- CategoryDelta gains optional parent_id/is_parent/depth/category_type.
- ComparePeriodTable: sections, depth indentation, reorderRows toggle,
section/grand net totals.
- Cartes top-movers and ComparePeriodChart filter to leaf rows only.
- reorderRows constraint loosened to the two fields it reads.
- i18n (FR+EN) section labels; CHANGELOG entries.
Resolves#247
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Database lock (the reported 0.10.0 regression):
- tauri-plugin-sql loads SQLite through a default multi-connection sqlx pool
(Pool::connect => max_connections = 10) and exposes no JS transaction
primitive. BEGIN and COMMIT issued as separate db.execute calls could land on
different pooled connections, stranding an open write transaction on an idle
connection — a permanent write lock until app restart. Triggered when the
Bilan page's concurrent reads interleaved with an in-flight snapshot save.
- Fix: funnel every db operation through one FIFO lock in db.ts; withTransaction
holds it across the whole BEGIN..COMMIT so a transaction's statements never
span connections (and the saves become genuinely atomic). A reentrancy guard
lets nested getDb() calls run directly instead of deadlocking. Applied to all
5 transaction sites (saveSnapshotAtomic, upsertSnapshotLines,
proposeStarterAccounts, applyKeywordWithReassignment, applyMigration) via
in-place helper extraction. New db.test.ts covers serialization,
cross-transaction non-interleaving, reentrancy, and lock release on error.
Log console (Settings -> Systems -> Journaux):
- getLogs returned the same mutated array reference, so useSyncExternalStore
(identity comparison) never re-rendered on a new log -> the console never
updated live. getLogs now returns an immutable snapshot rebuilt on each
mutation (stable between mutations, new identity after each).
- Add logInfo/logWarn/logError app-logging API and instrument the snapshot save
(info on success, error on failure) so DB issues surface in the console.
tsc + 635 vitest + vite build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a light confirmation modal (DetailAccountWizard) that flips a simple
balance account to detailed entry mode: sets kind='detailed' and
detailed_since = today (local civil day, YYYY-MM-DD) via updateBalanceAccount.
Toggle-only — no title capture; per-security holdings are entered at the next
normal snapshot, where validateDetailedSnapshot requires them from the pivot on.
Entry point: a 'Détailler en titres' action in the per-row actions menu of
BalanceAccountsTable, shown only for kind==='simple' rows (replaces the disabled
'Détail / coming soon' placeholder). Past aggregated history stays frozen
read-only. The flip is one-way: the #212 service backstop rejects
detailed -> simple once holdings exist, and the UI exposes no inverse action.
Exports buildDetailToggleInput() as a pure helper for a focused unit test
(project has no jsdom harness). FR/EN i18n under balance.detailWizard.*; removed
the now-dead balance.overview.detailAction / detailComingSoon keys.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Surface unrealized (latent) gain on the existing balance surfaces, no new
visualization (decision 2026-06-04).
Service:
- AccountLatestSnapshot gains account `kind` + `latest_snapshot_line_id`, so
the table knows which rows are detailed and where their holdings live.
- getAccountLatentGainByLine(lineId) folds a line's holdings through the
existing computeUnrealizedGain (shares the book_cost=0 / NULL -> N/A guard).
- rollupLatentGain(accounts): pure aggregation by asset class, by envelope
(vehicle_type, 'none' bucket), and grand total; per-bucket % denominator is
the known book_cost only (null when none), mirroring the aggregate guard.
Hook (useBalanceOverview):
- Prefetches per-detailed-account latent gain in parallel (failures isolated),
exposes latentGainByAccount + latentGainRollup. Simple accounts skipped.
UI:
- BalanceAccountsTable: expandable detailed rows -> per-security value + latent
gain %; a latent-gain column (shown only when a detailed account has one);
a summary block aggregating latent gain by asset class / envelope.
- BalanceOverviewCard: total latent gain line (hidden without detailed accounts).
- N/A rendered (never divide-by-zero) when book_cost is NULL or 0; partial-%
flag when some positions lack a cost basis.
Native non-CAD currency display de-scoped this round (untestable while all
securities are CAD). Modified Dietz return columns (#204) unchanged.
i18n: balance.latentGain.* in FR + EN. Focused unit tests for
getAccountLatentGainByLine and rollupLatentGain (grouping, 'none' envelope,
null-% / unknown book_cost, empty).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Turn the detailed-account snapshot variant into the real per-title entry
surface (building on the minimal sub-rows from #213):
- New SecurityPicker (src/components/balance/SecurityPicker.tsx): an
autocomplete combobox over the existing balance_securities catalogue
(loaded via listSecurities()) with inline creation. Accepts any
normalized symbol (UPPER/TRIM) with NO live ticker validation — the
price fetch is best-effort and separate. On pick/create it emits a
SecurityPick {symbol, asset_type, name, isNew}; a stock/crypto toggle
lets the user set the asset class when creating a new symbol (default
'stock'). Built on the CategoryCombobox UI idiom (ARIA listbox,
keyboard nav, click-outside). Pure helpers filterSecurities /
decideCreateOption are exported and unit-tested (no jsdom harness).
- SnapshotLineRow detailed sub-rows: labeled columns
[title (SecurityPicker), quantity, price (+ existing PriceFetchControl),
value (qty x price, read-only), book_cost, live unrealized gain].
Account value = displayed SUM of positions. Simple accounts unchanged.
- useSnapshotEditor: new SET_HOLDING_SECURITY action + setHoldingSecurity
callback (atomically sets symbol + asset_type + name and drops the
stale fetched-price attribution since the symbol changed). The
securities catalogue is loaded in loadForDate and exposed as
state.securities, so it refreshes after a save that creates a security.
- i18n: extended balance.snapshot.detailed.* (col.*, picker.*, book cost,
unrealized gain) in FR + EN — no hardcoded UI text.
- CHANGELOG (EN + FR) under [Unreleased]: first user-visible surface of
the per-title detail chain (#210-#213 were schema/service/reducer).
Build (tsc + vite) green; npm test green (613 tests, +10 SecurityPicker).
Generated autonomously by /autopilot run of 2026-06-06
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Structural rewrite of useSnapshotEditor for N holdings per detailed account,
and switch ALL simple/detailed dispatch from category_kind to the account's
own kind. This is the state/plumbing layer; the full multi-security entry UI
(SecurityPicker, rich sub-rows) lands in #214. Simple accounts behave
identically.
Reducer state shape:
- values: Record<accountId, string> (simple accounts, scalar)
- holdings: Record<accountId, HoldingDraft[]> (detailed accounts, one per title)
HoldingDraft is a string-typed, editable mirror of SnapshotHoldingInput with a
stable client-side rowId for React keys. Actions: ADD_HOLDING / REMOVE_HOLDING /
SET_HOLDING_FIELD (plus existing SET_VALUE/PREFILL/RESET). The legacy priced
scalar path (SET_PRICED_FIELD / pricedValues) is removed: after migration v16
(#211) every former-priced account is kind='detailed' with one holding, so those
accounts flow through the holdings path.
Dispatch:
- LOADED hydrates detailed baskets via listHoldingsBySnapshotLine (edit) keeping
the saved price, or getHoldingsForLatestSnapshot (new) dropping the price
(qty-0 excluded server-side). Simple accounts keep the scalar value path.
- SnapshotLineRow / SnapshotEditor / AccountForm now gate on account.kind, not
category_kind. category.kind survives ONLY as the suggested seed default for a
NEW account in AccountForm.
Save: detailed accounts pass their holdings array into SnapshotLineInput.holdings
(presence marks the line detailed; value = rounded-cent SUM); simple accounts
pass a scalar value with no holdings. Blank holding rows are skipped; a partial
row throws a typed error before any DB mutation.
AccountForm: adds an entry-mode selector (defaulting to the category-mapped
kind). New accounts persist as 'simple' (CreateBalanceAccountInput carries no
kind, and the service is out of this issue's scope); converting a fresh account
to detailed + pivot date is #215. Editing locks the selector for an already-
detailed account (the detailed->simple downgrade is service-guarded).
Tests: 19 new reducer/helper unit tests (pure exports; the project has no
renderHook harness) covering ADD/REMOVE/SET_HOLDING_FIELD, LOADED-vs-PREFILL
hydration (price drop, book_cost), qty-0 already excluded upstream, the
build*Lines save builders, and the dispatch-on-account.kind regression
(detailed account under a 'simple' category).
Resolves#213.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Issue 3 of overnight-2026-06-01-bilan-axe-vehicule. Builds the tracking UI
on top of the merged data layer (#202) and input UI (#203).
- Service: getSnapshotTotalsByVehicleAndDate(range) mirrors the by-category
aggregation with GROUP BY COALESCE(a.vehicle_type, 'none') so null-envelope
accounts land in a single 'none' bucket (never a SQL NULL key). Add
vehicle_type to getAccountsLatestSnapshot SELECT + type.
- useBalanceOverview: new groupAxis ('class'|'vehicle') state ORTHOGONAL to
chartMode; loads byVehicle alongside byCategory. Default groupAxis='class'.
- BalanceEvolutionChart + BalancePage: stacked-mode sub-toggle 'Par classe
d'actif' (default) / 'Par enveloppe'. Vehicle legend reuses the #203
vehicleType.* labels; the 'none' bucket uses balance.vehicle.none.
- BalanceAccountsTable: 4 return columns collapsed by default with a toggle,
persisted across sessions via userPreferenceService key balance_show_returns.
- i18n FR/EN: balance.chart.axis.{byAssetClass,byVehicle}, balance.vehicle.none,
balance.accountsTable.toggleReturns.{show,hide} (+ axisLegend aria label).
Tests: npm run build green (0 type errors); vitest 3314 passed. Added 5
service tests for the 'none' bucket + mixed envelopes + date range.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The internal Step component received `t: TFunction` as a prop while every
other component in the codebase calls useTranslation() directly at the
top of the function. Aligns with the majority pattern.
Suggestion S5 from PR #184 review (#187).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds defense-in-depth: each iteration runs a SELECT COUNT(*) WHERE name=?
AND balance_category_id=? AND archived_at IS NULL inside the BEGIN/COMMIT
block, immediately before its INSERT. On a hit, the iteration skips the
INSERT silently and the returned ids array excludes the skipped starter.
Rationale: balance_accounts has no UNIQUE constraint on (name, category)
and the upstream pre-filter (getStarterCollisions) is best-effort. If a
race or a bypass slips a duplicate through, the in-txn check catches it
without surfacing a confusing error to the user.
Existing two tests in StarterAccountsModal.test.tsx updated to mock the
new SELECT call sequence; new test "skips silently when in-txn collision
check finds an existing account" added.
Suggestion S3 from PR #185 review (#187).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
getStarterCollisions now filters `archived_at IS NULL` so a starter
account the user voluntarily archived no longer blocks re-creation
through the StarterAccountsModal. Matches the rest-of-codebase
convention (active = is_active=1 AND archived_at IS NULL).
Suggestion S4 from PR #185 review (#187).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends PR #189's fix (one input on /balance/snapshot) to the 7 remaining
native <input type="date"> fields across 4 components:
- transactions/TransactionFilterBar.tsx (dateFrom + dateTo)
- adjustments/AdjustmentForm.tsx (form.date)
- balance/LinkTransfersModal.tsx (from + to)
- dashboard/PeriodSelector.tsx (localFrom + localTo)
Each onChange handler now calls e.currentTarget.blur() after the state
update to dismiss the native date popup on Linux Tauri WebView. The call
is a no-op on Windows WebView2 / macOS WKWebView, where the picker
already auto-closes.
No automated test added: this is a WebKitGTK-specific WebView quirk that
cannot be reproduced in jsdom/vitest. Manual smoke test on Linux Tauri
dev was the validation, mirroring PR #189's approach.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The single 12-card SettingsPage is replaced by a hub at /settings linking
to three thematic sub-pages mounted via a shared SettingsLayout (Outlet):
/settings SettingsHomePage (3 cards-cluster + PageHelp)
/settings/users UsersSettingsPage (Account, License, DocsContent)
/settings/data DataSettingsPage (Categories, DataManagement,
PriceFetchConsentToggle)
/settings/systems SystemsSettingsPage (Version, UpdateCard,
ChangelogContent, LogViewer)
DocsPage and ChangelogPage are extracted into reusable DocsContent /
ChangelogContent components and the standalone /docs and /changelog
routes become Navigate redirects to preserve external bookmarks and
release-note links. UpdateCard is extracted from the inline updater
block for symmetry and testability.
TokenStoreFallbackBanner is mounted once in SettingsLayout, surfacing
the OS-keychain-fallback warning across the four main routes only.
The two existing /settings/categories/{standard,migrate} sub-routes
stay flat (siblings of SettingsLayout) to keep their focused flows
free of the banner — their internal back-links now point to
/settings/data.
i18n FR/EN gain settings.{home, users, data, systems, backToHome};
docs/architecture.md and CHANGELOG{,.fr}.md updated. Pure refactor of
presentation: no new business logic, no Tauri commands, no SQL
migrations.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Part 1 — New profiles: seed 4 starter accounts in
consolidated_schema.sql (Compte chèque/CELI/REER/Compte
non-enregistré, currency CAD, is_active=1) right after the
balance_categories seeds. Categories resolved via SELECT subquery
on the seeded `key` values for robustness.
Part 2 — Existing profiles: StarterAccountsModal proposes the same
4 starters at first /balance visit. Default-checked checkboxes,
collision rule (case-insensitive trim name + matching category)
disables matches with a "Déjà présent" tooltip. The atomic helper
`proposeStarterAccounts` wraps the inserts in BEGIN/COMMIT (rolls
back on error). user_preferences.balance_starter_proposed records
{shown_at, accepted} so the modal never reappears, dismissed or
confirmed.
Part 3 — docs/adr/0012-balance-two-level-model.md (Proposed):
captures the future vehicles × compositions model for reflection,
no code change. Numbered 0012 because 0011 was already taken by
the providers-best-effort-yahoo ADR. Linked from architecture.md
ADR table and Bilan section.
Tests: StarterAccountsModal.test.tsx covers STARTER_ACCOUNTS shape,
getStarterCollisions (case-insensitive trim, category-scoped) and
proposeStarterAccounts (insert order, COMMIT, ROLLBACK on failure).
No render tests — mirrors the BalanceOnboardingCard pattern (no
jsdom configured).
Resolves#179
Replace empty BalanceOverviewCard with BalanceOnboardingCard showing
two steps:
1. Create an account
2. Enter a snapshot
Step 2 is grayed out until at least one account exists; the entire
card is replaced by BalanceOverviewCard once a snapshot is recorded.
Hide "+ New snapshot" button when 0 accounts (it lives inside the
overview card, which is now hidden in that state).
Improve SnapshotEditPage noAccounts copy to clarify account vs
snapshot semantics.
Resolves#178
Priced balance categories now carry an explicit `asset_type`
('stock' | 'crypto') so PriceFetchControl can route to the right
provider without symbol heuristics. ETH = Ethan Allen NYSE AND
Ethereum crypto are no longer ambiguous.
Migration v10 adds a nullable column and backfills the two seeded
priced categories (key='stock','crypto'). Legacy custom priced rows
stay NULL until the user edits the category — SnapshotLineRow hides
the price-fetch button when asset_type is NULL on a priced row, so
manual entry remains available.
Service-side validation rejects priced creation without asset_type
('asset_type_required') and rejects values outside ('stock','crypto')
('asset_type_invalid'). Simple kind coerces asset_type to NULL.
The CategoryVariant of AccountForm shows the selector only when
kind=priced, requires it on submit, and resets it on kind switch.
i18n keys added under balance.category.assetType.* (FR + EN).
Tests:
- 4 new Rust migration tests in lib.rs (column add, seed backfill,
legacy row stays NULL, CHECK rejects 'gold')
- 6 new vitest cases on createBalanceCategory + listBalanceAccounts
asserts c.asset_type AS category_asset_type in the join
- balance-flow integration test updated to pass asset_type='stock'
No new test for SnapshotLineRow render guard — project lacks
@testing-library/react + jsdom; the guard is one boolean expression
covered by manual QA per autopilot decisions in PR #167.
Fixes#169
- Adds PriceFetchConsentToggle to SettingsPage Privacy section
- Reads/writes user_preferences.price_fetching_consent for active profile
- Confirmation dialog before revoke (DELETE the key entirely so next click re-opens consent modal)
- Disabled (with notPremium tooltip) when license is not premium
- Adds deletePreference() to userPreferenceService
- Adds settings.privacy.title i18n key (FR + EN)
- 10 vitest tests covering all paths
Closes#159
- New component renders button + consent modal + spinner + attribution
- Best-effort warning shown once per session for stock categories
- Hidden if not premium or category kind != 'priced'
- Consent persisted per-profile in user_preferences.price_fetching_consent
- Manual unit_price input remains active in all paths
- 17 vitest tests (no RTL/jsdom — logged MEDIUM in decisions-log.md)
- Wired into SnapshotLineRow/SnapshotEditor/SnapshotEditPage
- asset_type hardcoded to 'stock' pending category schema extension (MEDIUM)
Closes#158
Issue #142 / Bilan #4 — vertical reference lines for tagged transfers.
`BalanceEvolutionChart.tsx` accepts a new optional prop
`transferMarkers?: BalanceAccountTransferWithTransaction[]`. For every
marker whose `transaction_date` matches a date already on the X axis,
the chart renders a `<ReferenceLine>` (Recharts) — green for `in`
(capital added), red for `out` (capital removed). The marker is drawn
in both `line` and `stacked` modes; in line mode an inline label
("In" / "Out") sits at the top-right of the marker so the user can
identify the direction without hovering.
Markers whose date is between two snapshot ticks are filtered out
(Recharts categorical axis silently drops unknown ticks; preferred
over an off-axis bug). A future improvement is to switch the X axis
to a numeric/time scale so markers can land anywhere — out of scope
here per the autopilot prompt's "least invasive" guideline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Issue #142 / Bilan #4 — non-regressive transfer awareness in the
transactions table + clean error mapping on bulk delete.
- `TransactionTable.tsx`: optional new prop
`linkedTransfersByTxId?: Map<txId, links[]>`. When supplied, a small
`<Link2>` icon appears next to the description for every linked
transaction; tooltip lists the account name(s) and direction(s).
Without the prop, the table renders byte-for-byte identical to
before — preserves the spec's non-regression invariant.
- `TransactionsPage.tsx`: loads the linked-transfers map once on mount
via `listAllLinkedTransfersForTooltip()` (one batch SELECT) and
threads it through to the table. Failure to load the map degrades
gracefully to an empty map (icon simply doesn't appear).
- `transactionService.ts`: new `deleteTransaction(id)` helper +
`TransactionLinkedToBalanceError` (typed FK guard). Pre-checks
`balance_account_transfers` before attempting the DELETE so the
error carries the offending account names; falls back to the FK
pattern matcher if a race linked the transaction between the
SELECT and the DELETE.
- `importedFileService.ts`: both bulk delete paths
(`deleteImportWithTransactions`, `deleteAllImportsWithTransactions`)
now pre-check for any linked transfer and surface the same typed
error before they would explode on FK RESTRICT. The pre-check has
a `LIMIT 50` safety cap on the global path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Issue #142 / Bilan #4 — UI for transfer linking + per-account returns.
- New `LinkTransfersModal.tsx`: portal modal with date-range / category /
free-text filters, multi-select with auto-proposed direction (`in` for
negative bank amounts, `out` for positive — flippable per row).
Submits via sequential `linkTransfer` calls; reports per-row failures
inline (most common case: `transfer_already_linked` on a re-submit).
- `BalanceAccountsTable.tsx`: 4 new columns rendered side-by-side —
3M / 1A / Since-inception (Modified Dietz via `compute_account_return`)
+ Unadjusted (`(V_end - V_start) / V_start`). Returns load lazily
after mount via `Promise.all` over (account × horizon); per-cell
failure leaves the slot at "—" without blocking the rest of the
table. The actions menu gains a *Link transfers* item that bubbles
the request up to the parent page. New props:
`sinceCreationDate` (anchors the since-inception horizon) and
`onLinkTransfers` (modal opener).
- `BalancePage.tsx`: hosts the new modal, loads the categories list
once on mount for the filter dropdown, fetches the union of
`listAccountTransfers` per account so the chart can render markers,
and threads the earliest snapshot date down to the table. Reload
is triggered after the modal reports at least one successful link.
- `balance.service.ts`: dropped the unused `BalanceAccountTransfer`
import to satisfy `tsc --noUnusedLocals`.
`npm run build` clean. `npm test` → 429 passed. Manual sanity check:
the table renders "…" placeholders during the per-row return load,
then resolves to either a percentage or a "—" with the partial
tooltip when the underlying snapshot endpoint is missing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Insert a new "Bilan" / "Balance sheet" entry in NAV_ITEMS pointing
at /balance with the Wallet lucide-react icon. Position: between
Reports and Settings, matching the autopilot prompt instruction
and the spec-plan-bilan v2 ordering.
Sidebar.tsx imports Wallet and registers it in iconMap.
Refs: #141
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three new components composed under a new BalancePage at /balance:
- BalanceOverviewCard — latest aggregate net worth, Δ% vs the
previous chronological snapshot (rendered as "—" when only
one snapshot exists), 60-day staleness warning, and a
"+ Nouveau snapshot" CTA pointing at /balance/snapshot.
- BalanceEvolutionChart — Recharts-based line / stacked-area
toggle. Line mode plots SUM(value) per snapshot_date with a
single primary-coloured stroke. Stacked mode transposes the
byCategory series into one Area per category_key with a
fixed 10-color palette indexed deterministically. Tooltip
formats CAD via Intl.NumberFormat.
- BalanceAccountsTable — one row per active account with name,
category label, latest value, and Δ% over the active period
(latest_value vs the period anchor). Returns columns
(3M / 1Y / since-creation / unadjusted) reserved for #142
with a TODO marker. Action menu includes a disabled "Detail"
placeholder + functional "Archive" wired through reload().
BalancePage composes the three with an inline period selector
(3M / 6M / 1A / 3A / Tout) and chart-mode toggle, both styled
as segmented controls. State flows through useBalanceOverview.
Route /balance registered before /balance/accounts in App.tsx.
Refs: #141
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- AccountForm now exposes a 'category' variant with a kind selector
(simple | priced); the legacy 'account' variant is unchanged
modulo the new symbol-required-for-priced UI guard.
- SnapshotLineRow dispatches on account.category_kind:
* simple variant unchanged from #146
* priced variant: quantity + unit_price inputs + read-only
computed value rendered live (qty × price, 2 decimals) +
[Manuel] attribution tag
- useSnapshotEditor extends state with pricedValues map, exposes
setLineQuantity / setLineUnitPrice handlers, prefill copies
quantity but leaves unit_price blank (per spec-decisions row),
save() builds mixed simple+priced batches.
- SnapshotEditor + SnapshotEditPage thread the new priced state.
- Total line at the top of SnapshotEditPage now sums simple + priced
contributions live as the user types.
Refs #140
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New scoped useReducer hook covering the full single-snapshot lifecycle —
LOAD_FOR_DATE / SET_LINE_VALUE / SAVE / DELETE / PREFILL_FROM_PREVIOUS /
RESET — with the following semantics:
- 'new' mode (?date= absent or no snapshot at that date) creates the row
at save time only, so abandoning the form does not leave an empty
snapshot behind;
- 'edit' mode loads existing lines + prefills the values map;
- prefillFromPrevious copies simple-kind values from the most recent
earlier snapshot (priced branch is a no-op + TODO Issue #140);
- save() flips 'new' -> 'edit' on success and updates the URL ?date=
so refresh keeps the user in edit mode;
- snapshotDate is immutable in edit mode (UI guard, matches spec).
New SnapshotEditPage at /balance/snapshot:
- date picker (native input type=date — matches the AdjustmentForm /
TransactionFilterBar / PeriodSelector pattern, no new dep)
- per-category groups of accounts with one value field each
- prefill button (disabled when no earlier snapshot exists, with
tooltip explaining why)
- delete button with double-confirmation modal that requires retyping
the snapshot date before the destructive action enables.
New SnapshotEditor (groups by category sort_order) and SnapshotLineRow
(simple variant — single value field per account) components.
Route /balance/snapshot wired in App.tsx.
Refs #146
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the AccountsPage end-to-end with a new scoped useReducer hook,
the page itself (accessible at /balance/accounts) and the account form.
useBalanceAccounts (src/hooks/useBalanceAccounts.ts):
- Loads accounts (excludes archived by default) + categories in parallel
- Surfaces typed errors from balance.service via state.errorCode so the
UI can localize them (e.g. seed protection, currency rejection)
- CRUD operations on both domains: addAccount/editAccount/archive/
unarchiveAccount + addCategory/editCategory/removeCategory
AccountsPage (src/pages/AccountsPage.tsx):
- Two tabs: Comptes + Catégories
- Accounts tab: archive toggle, table of (name, category, symbol,
currency, status), inline edit/archive/restore
- Categories tab: full list of seeded + user categories. Add new
simple-kind category (priced creation lands in #140). Rename via
inline prompt; delete disabled on seeded rows. Errors surfaced via
i18n keys keyed on BalanceErrorCode.
AccountForm (src/components/balance/AccountForm.tsx):
- Variant=account only (category variant lands in #140)
- Auto-detects priced category to hint the symbol field
- Full FR/EN coverage of labels and validation messages
Per spec-plan-bilan.md v2 the sidebar entry "Bilan" is intentionally
not added in this issue — it lands in #141 (Bilan #3) when the
/balance overview becomes navigable. Until then the route is reachable
directly via URL.
Refs #138
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The by-category report combobox (`/reports/category`) was showing its full
category list with scrambled indentation — parents from one sub-tree
interleaved with children from another. Root cause: `getAllCategoriesWithCounts`
returns rows via `ORDER BY sort_order, name`, which is a *global* sort; two
different roots with sort_order=1 would be followed by their respective
children in the same bucket, mixing depths together.
Add a pure `sortHierarchical(categories, resolveName)` helper in
`CategoryCombobox.tsx` that rebuilds the display order as a DFS walk of the
tree: each parent is emitted immediately followed by its descendants, with
siblings within a group sorted by `sort_order` then localized display name.
Orphans (parent filtered out or missing) are appended at the end so nothing
disappears. The helper runs client-side inside the combobox's `useMemo`, so
the fix is scoped to this component and doesn't affect other consumers of
`getAllCategoriesWithCounts`. Filtering on the input query remains unchanged.
Covered by 7 unit tests on the helper (empty list, single root, the exact
bug-reproducing scrambled case, sort_order + name tiebreak, 3-level
hierarchy, orphans, idempotence).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surfaces the pre-migration SREF backup to the user so they can roll back a
category migration without digging into the filesystem:
- 90-day dismissable banner at the top of Settings > Categories pointing to
the automatic backup (hidden once reverted, once dismissed, or past 90d).
- Permanent "Restore a backup" entry in Settings > Categories, available as
long as a migration journal exists (even past the 90-day window).
- Confirmation modal with two-step consent, red Restore button, fallback
file picker when the recorded path is missing, PIN prompt for encrypted
SREF files, full-page reload on success.
Internals:
- New `categoryRestoreService` wrapping `read_import_file` +
`importTransactionsWithCategories` with stable error codes
(file_missing, read_failed, parse_failed, wrong_envelope_type,
needs_password, wrong_password, import_failed).
- New `file_exists` Tauri command for the pre-flight presence check.
- On success: `categories_schema_version=v2` + merge `reverted_at` into
`last_categories_migration`.
- Pure `shouldShowBanner` / `isWithinBannerWindow` helpers with tests.
- FR/EN i18n keys under `settings.categoriesCard.restore*`.
- CHANGELOG entries in both locales.
Closes#122
New user-facing 3-step migration flow at /settings/categories/migrate that
allows legacy v2 profiles to opt in to the v1 IPC taxonomy.
Step 1 Discover — read-only taxonomy tree (reuses CategoryTaxonomyTree from
Livraison 1, #117).
Step 2 Simulate — 3-column dry-run table with confidence badges (high /
medium / low / needs-review), transaction preview side panel, inline target
picker for unresolved rows. The "next" button is blocked until every row is
resolved.
Step 3 Consent — checklist + optional PIN field for PIN-protected profiles +
4-step loader (backup created / verified / SQL applied / committed).
Success and error screens surface the SREF backup path and the counts of
rows migrated. Errors never leave the profile in a partial state — the new
categoryMigrationService wraps the entire SQL writeover in a
BEGIN/COMMIT/ROLLBACK atomic transaction and aborts up-front if the backup
is not present / verified.
New code:
- src/services/categoryMigrationService.ts — applyMigration(plan, backup)
atomic writer (INSERT v1 → UPDATE transactions/budgets/budget_templates/
keywords/suppliers → reparent preserved customs → deactivate v2 seed →
bump categories_schema_version=v1 → journal last_categories_migration).
- src/hooks/useCategoryMigration.ts — useReducer state machine
(discover → simulate → consent → running → success | error).
- src/hooks/useCategoryMigration.test.ts — 13 pure reducer tests.
- src/components/categories-migration/{StepDiscover,StepSimulate,StepConsent,
MappingRow,TransactionPreviewPanel}.tsx — UI per the mockup.
- src/pages/CategoriesMigrationPage.tsx — wrapper with internal router,
stepper, backup/migrate orchestration, success/error screens.
Tweaks:
- src/App.tsx — new /settings/categories/migrate route.
- src/components/settings/CategoriesCard.tsx — additional card surfacing
the migrate entry for v2 profiles only.
- src/i18n/locales/{fr,en}.json — categoriesSeed.migration.* namespace
(page / stepper / 3 steps / running / success / error / backup error codes).
- CHANGELOG.{md,fr.md} — [Unreleased] / Added entry.
Scope limits respected: no SQL migration modified, no new migration added,
no unit tests of the applyMigration writer (covered by #123 in wave 4),
no restore-backup button (#122 in wave 4), no post-migration banner (#122).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>