DashboardPage now reuses the Cartes report's presentational widgets
(KpiCard with MoM/YoY deltas, top movers, budget adherence) sourced from
getCartesSnapshot against a Dashboard-owned reference month (defaults to
the last complete month). The expense-only pie chart is replaced by a
ranked bar chart of top expense categories (resurrecting the previously
unused CategoryBarChart), and the account (import-source) filter
introduced on Trends/Compare/Budget now also applies to the Dashboard's
own transactional widgets via a Dashboard-local accountIds state (kept
separate from useReportsPeriod, since the Dashboard owns its own two
temporal axes).
A new net-worth tile surfaces the Balance sheet's latest total
(getSnapshotTotalsByDate) — a distinct metric from every transactional
card here, so it stays hidden (never a misleading "$0") until at least
one balance account has a recorded snapshot, reusing deriveLandingState
rather than re-inferring emptiness from nulls. It is not scoped by the
account filter (balance_accounts is a disjoint concept from
import_sources) and is fetched independently on mount.
The category-over-time trend chart now passes typeFilter "expense",
fixing a latent mismatch where a revenue category could silently show
up in a chart titled "expenses over time".
Resolves#279
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
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>
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>
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>
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>
Document Étape 1 of the balance audit (vehicle_type axis), already shipped
in #202/#203/#204:
- ADR 0014 (Accepted): fiscal envelope is an account attribute, the category
is a pure asset class; Étape 2 (per-security detail) explicitly out of scope.
- ADR 0012 marked Rejected (never accepted, not Superseded) + pointer to 0014.
- User guide (markdown + in-app docs.balance i18n FR/EN): optional fiscal
envelope, the two chart axes, type renaming, and the historical-reclass note.
- CHANGELOG.md + CHANGELOG.fr.md [Unreleased]: Added (envelope field, envelope
axis, collapsible returns) + Changed (asset-class category, CELI/REER reclass,
rename no longer alters translation, historical-reclass note).
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 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
- Add docs.balance.* keys (FR + EN) for the new Balance Sheet section
consumed by DocsPage (title / overview / features / steps / tips).
- Wire the new section into DocsPage.tsx SECTIONS array with a Wallet
icon, slotted between reports and settings.
- Add bilingual [Unreleased] CHANGELOG entry for #145 covering the
architecture.md updates, the 3 ADRs, the new guide section and the
i18n keys.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Issue #142 / Bilan #4 — translations and changelog entries.
i18n (FR + EN):
- `balance.returns.partialTooltip`, `balance.returns.noTransfersWarning`
- `balance.accountsTable.return3m/return1y/sinceCreation/unadjusted`
(label + tooltip variants)
- `balance.transfers.linkAction` + `balance.transfers.direction.{in,out}`
- `balance.transfers.modal.*` (every modal label, including the
partial-failure summary and the per-row direction toggle)
- `balance.transfers.errors.*` (5 new typed error codes)
- `balance.evolution.transferIn/transferOut` (chart label)
- `transactions.transferIcon.tooltip/ariaLabel`
CHANGELOG (English source + French translation):
- New entry under `[Unreleased]` summarising the Modified Dietz
formula, the per-account return columns (3M / 1A / since-inception
+ unadjusted), the link-transfers modal, the transactions-page
inline icon, the typed FK error on bulk-delete paths, and the
vertical reference markers on the evolution chart. References #142.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
i18n FR/EN under balance.snapshot.* — page (titles, date label and
immutability notice, total, prefill, save/create/delete buttons),
editor (empty state), line (placeholder + a11y label), delete
(double-confirm modal copy). Five new error codes added to
balance.errors.* (snapshot_date_required, snapshot_date_taken,
snapshot_not_found, snapshot_value_invalid, snapshot_priced_unsupported).
Adds common.back so the SnapshotEditPage back arrow has a localized title.
CHANGELOG entries for #146 under [Unreleased] in both EN and FR.
Refs #146
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the FR/EN translation namespace `balance.*` covering:
- balance.accountsPage.* (page chrome, tab labels, empty states)
- balance.account.* (table fields, status badges, action labels,
full account form copy with priced/simple-aware hints)
- balance.category.* (intro text, table fields, kind labels, origin
labels, action prompts, simple-only creation form, seeded labels
for the 7 standard categories)
- balance.errors.* (one entry per BalanceErrorCode union member)
CHANGELOG.md and CHANGELOG.fr.md both gain a single entry under
`[Unreleased] / Added` summarising the schema migration v9, the new
balance.service CRUD section and the AccountsPage tabs.
Refs #138
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>
Adds a non-destructive, dismissable banner on the Dashboard that invites
profiles still on the legacy v2 category seed to discover the new v1 IPC
taxonomy. The banner links to the standard categories guide page
(/settings/categories/standard, shipped in #117).
Visibility rules:
- Only rendered when `categories_schema_version='v2'` in `user_preferences`.
- Hidden once the user dismisses it — dismissal is persisted in the same
`user_preferences` table under the key `categories_v1_banner_dismissed`
(value '1'), so the banner never reappears across app restarts.
- New v1-seeded profiles never see it.
No DB schema change: reuses the existing key/value `user_preferences`
table via the existing `getPreference`/`setPreference` helpers. No
migration added.
i18n keys under `dashboard.categoriesBanner.*` (FR + EN).
Changelog entry added under [Unreleased] in both CHANGELOG.md and
CHANGELOG.fr.md.
Adds a read-only Settings subpage at /settings/categories/standard that
exposes the full v1 IPC category taxonomy:
- Recursive tree with per-root expand/collapse (chevron buttons), clickable
only via the disclosure caret — no destructive actions anywhere on the
page.
- Live counter banner: roots / subcategories / leaves / total, computed
from the bundled categoryTaxonomyV1 JSON.
- Accent- and case-insensitive full-text search over translated names;
matching nodes keep their ancestor chain visible, non-matching branches
are pruned from the visible tree.
- Hover tooltips (native `title`) showing i18n_key, type (income /
expense / transfer — translated) and numeric id of each node — useful
for power-users cross-referencing the consolidated schema.
- Export as PDF button that triggers window.print(); a dedicated
@media print rule in styles.css forces every branch to render fully
expanded during printing regardless of the on-screen collapse state,
and hides the toolbar / back-link.
- All labels resolve via t(node.i18n_key, { defaultValue: node.name })
to be forward-compatible with future user-created taxonomy rows that
have no i18n_key.
Also:
- New CategoriesCard in Settings that links to the page (FolderTree
icon, consistent with the userGuide / changelog card pattern).
- i18n keys added under categoriesSeed.guidePage.* and
settings.categoriesCard.* (FR + EN).
- CHANGELOG.md + CHANGELOG.fr.md updated under [Unreleased] / Added.
Route uses the English-style `/settings/categories/standard` to match
the rest of the app (/settings, /categories, /changelog, ...). The
original spec mentions a French-accented path but the existing router
is English-only; documenting here so reviewers can see the decision.
No SQL migration, no schema change, no write to the database — this
is strictly a read-only view on the TS-side taxonomy bundle.
Type-check clean (tsc --noEmit), 148/148 vitest tests pass, vite build
succeeds.
Livraison 1 du milestone spec-refonte-seed-categories-ipc. Applies the
new v1 IPC (Indice des prix à la consommation) taxonomy to freshly
created profiles while leaving existing v2 profiles untouched until the
migration wizard (upcoming issue #121) prompts them to move.
- Migration v8 (additive only):
- ALTER TABLE categories ADD COLUMN i18n_key TEXT
- INSERT OR IGNORE user_preferences.categories_schema_version=v2
(existing profiles tagged as v2 for later migration)
- consolidated_schema.sql rewritten with the full v1 seed and
categories_schema_version='v1' default for brand-new profiles
- src/data/categoryTaxonomyV1.json bundled as the TS-side source of
truth (consumed by #116 categoryTaxonomyService next)
- categoriesSeed.* i18n namespace (FR/EN) — 150 entries each
- CategoryTree and CategoryCombobox fall back to the raw `name` when
i18n_key is null (user-created categories stay literal)
- CategoryTreeNode and CategoryRow gain the i18n_key field end-to-end
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a segmented Monthly/YTD toggle next to the reference-month picker that
flips the four KPI cards (income, expenses, net, savings rate) between the
reference-month value (unchanged default) and a Year-to-Date cumulative view.
In YTD mode, the "current" value sums January to the reference month of the
reference year; MoM delta compares it to Jan to (refMonth - 1) of the same
year (null in January, since no prior YTD window exists); YoY delta compares
it to Jan to refMonth of the previous year; savings rate is recomputed from
YTD income and expenses, and stays null when YTD income is zero.
The 13-month sparkline, top movers, seasonality and budget adherence cards
remain monthly regardless of the toggle (by design). The savings-rate tooltip
is now dynamic and mirrors the active mode. The mode is persisted in
localStorage under `reports-cartes-period-mode`.
Also adds a dedicated Cartes section to `docs/guide-utilisateur.md` covering
the four KPI formulas, the Monthly/YTD toggle and its effect on deltas, the
sparkline, top movers, seasonality, budget adherence and the savings-rate
edge case. Mirrored in the in-app `docs.reports` i18n tree (features/steps/
tips extended) for both FR and EN.
No SQL migration: YTD sums are computed from the already-fetched
`flowByMonth` map, so no extra round trip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The legacy chart was a stacked BarChart, not a LineChart — the initial 'line'
naming was misleading. Rename internal type, i18n key (chartLine -> chartBar,
Lignes -> Barres, Lines -> Bars) and icon. Legacy 'line' in localStorage is
migrated to 'bar' on read.
Adds a segmented chart-type toggle to the /reports/trends "By category"
sub-view that switches between the existing stacked bars (default,
unchanged) and a new Recharts AreaChart with stackId="1" showing total
composition over time. Both modes share the same category palette and
SVG grayscale patterns so the visual signature is preserved.
- CategoryOverTimeChart gains a chartType: 'line' | 'area' prop
(default 'line' for backward compatibility with the dashboard usage).
- New TrendsChartTypeToggle component, persisted in localStorage under
"reports-trends-category-charttype".
- Toggle only renders in the "By category" sub-view with chart view
mode selected; hidden otherwise.
- i18n keys reports.trends.chartLine / chartArea / chartTypeAria in
FR and EN.
- CHANGELOG entries in both languages.
Mirror the BudgetVsActualTable structure in the Actual-vs-Actual compare
mode so MoM and YoY both surface a Monthly block (reference month vs
comparison month) and a Cumulative YTD block (progress through the
reference month vs progress through the previous window).
- CategoryDelta gains cumulative{Previous,Current}Amount and
cumulativeDelta{Abs,Pct}. Legacy previousAmount / currentAmount /
deltaAbs / deltaPct are kept as aliases of the monthly block so the
Highlights hub, Cartes dashboard and ComparePeriodChart keep working
unchanged.
- getCompareMonthOverMonth: cumulative-previous window ends at the end
of the previous month within the SAME year; when the reference month
is January, the previous window sits entirely in the prior calendar
year (Jan → Dec).
- getCompareYearOverYear: now takes an optional reference month
(defaults to December for backward compatibility). Monthly block
compares the single reference month across years; cumulative block
compares Jan → refMonth across years.
- ComparePeriodTable rebuilt with two colspan header groups, four
sub-columns each, a totals row and month/year boundary sub-labels.
- ComparePeriodChart unchanged: still reads the monthly primary fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swap the native <select> in CategoryZoomHeader for the reusable
CategoryCombobox. Enhances the combobox with ARIA compliance
(combobox, listbox, option roles + aria-expanded, aria-controls,
aria-activedescendant) and hierarchy indentation based on parent_id
depth. Adds reports.category.searchPlaceholder in FR/EN.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove the non-functional PeriodSelector from /reports/cartes — the Cartes
report is by design a "month X vs X-1 vs X-12" snapshot, so the
reference-month picker is the only control needed.
- Simplify useCartes to drop its useReportsPeriod dependency; the hook now
only exposes the reference year/month and its setter.
- Add a (?) help bubble (lucide HelpCircle) next to the savings-rate KPI
title, wired up via a new `tooltip?: string` prop on KpiCard.
- Propagate `number | null` through CartesKpi.current and buildKpi so
savings rate is now null (rendered as "—") when reference-month income
is 0 instead of a misleading "0 %". Use refExpenses directly for
seasonality.referenceAmount since it is always numeric.
- Update the cartes snapshot tests to expect null for the zero-income case.
- Add FR/EN strings reports.cartes.savingsRateTooltip + CHANGELOG entries
in both locales.
Closes#67
Add opt-in Feedback Hub widget integrated into the Settings Logs card. Routes through a Rust command to bypass CORS and centralize privacy audit. First submission triggers a one-time consent dialog; three opt-in checkboxes (context, logs, identify with Maximus account) all unchecked by default. Wording and payload follow the cross-app conventions in la-compagnie-maximus/docs/feedback-hub-ops.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New /reports/cartes page surfaces a dashboard-style snapshot of the
reference month:
- 4 KPI cards (income / expenses / net / savings rate) showing MoM and
YoY deltas simultaneously, each with a 13-month sparkline highlighting
the reference month
- 12-month income vs expenses overlay chart (bars + net balance line)
- Top 5 category increases + top 5 decreases MoM, clickable through to
the category zoom report
- Budget adherence card: on-target count + 3 worst overruns with
progress bars
- Seasonality card: reference month vs same calendar month averaged
over the two previous years, with deviation indicator
All data is fetched in a single getCartesSnapshot() service call that
runs four queries concurrently (25-month flow, MoM category deltas,
budget-vs-actual, seasonality). Missing months are filled with zeroes
in the sparklines but preserved as null in the MoM/YoY deltas so the UI
can distinguish "no data" from "zero spend".
- Exported pure helpers: shiftMonth, defaultCartesReferencePeriod
- 13 vitest cases covering zero data, MoM/YoY computation, January
wrap-around, missing-month handling, division by zero for the
savings rate, seasonality with and without history, top mover sign
splitting and 5-cap
Note: src/components/reports/CompareReferenceMonthPicker.tsx is a
temporary duplicate — the canonical copy lives on the issue-96 branch
(refactor: compare report). Once both branches merge the content is
identical and git will dedupe. Keeping the local copy here means the
Cartes branch builds cleanly on main without depending on #96.
Closes#97
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Collapse the three Compare tabs (MoM / YoY / Budget) into two modes. The
new "Actual vs actual" mode exposes an explicit reference-month dropdown
in the header (defaults to the previous month, wraps around January) and
a MoM/YoY sub-toggle. The chart is rewritten to a grouped side-by-side
BarChart with two bars per category (reference period vs comparison
period) so both values are visible at a glance instead of just the
delta. The URL PeriodSelector stays in sync with the reference month.
- useCompare: state splits into { mode: "actual"|"budget", subMode:
"mom"|"yoy", year, month }. Pure helpers previousMonth(),
defaultReferencePeriod(), comparisonMeta() extracted for tests
- CompareModeTabs: 2 modes instead of 3
- New CompareSubModeToggle and CompareReferenceMonthPicker components
- ComparePeriodChart: grouped bars via two <Bar dataKey="..."/> on a
vertical BarChart
- i18n: modeActual / subModeMoM / subModeYoY / referenceMonth (FR+EN),
retire modeMoM / modeYoY
- 9 new vitest cases covering the pure helpers (January wrap-around for
both MoM and YoY, default reference period, month/year arithmetic)
Closes#96
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Delete legacy src/hooks/useReports.ts (the monolithic hook is now fully
replaced by the per-domain hooks from #70)
- Delete src/components/reports/ReportFilterPanel.tsx (last caller was the
pre-refactor ReportsPage; no longer referenced anywhere)
- Update docs/architecture.md: reports hook list now lists the 5 per-domain
hooks, reports service entry lists every new endpoint, routing section
lists the 4 sub-routes, categorizationService entry mentions the new
keyword-editing helpers, components folder count + page count updated
- Update docs/guide-utilisateur.md section 9: rewrite around hub + 4
sub-reports, explain bookmarkable period via query string, walk through
the right-click keyword editing flow, remove stale pivot section
- Rewrite in-app docs.reports.* i18n in both FR and EN to match the new
UX (hub, sub-reports, contextual keywords)
- New ADR docs/adr/0007-reports-hub-refactor.md: context, decision (hub +
four routes, per-domain hooks, URL period, security guarantees on the
keyword dialog, bounded recursive CTE for category zoom), consequences,
alternatives considered
- CHANGELOG.md + CHANGELOG.fr.md: Unreleased entries describing the hub,
each sub-report, contextual keyword editing, bookmarkable period, view
mode persistence, useReports split, pivot removal, and the security
posture of AddKeywordDialog / getCategoryZoom
Fixes#76
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Service layer
- New reportService.getCategoryZoom(categoryId, from, to, includeChildren) —
bounded recursive CTE (WHERE ct.depth < 5) protects against parent_id cycles;
direct-only path skips the CTE; every binding is parameterised
- Export categorizationService helpers normalizeDescription / buildKeywordRegex /
compileKeywords so the dialog can reuse them
- New validateKeyword() enforces 2–64 char length (anti-ReDoS), whitespace-only
rejection, returns discriminated result
- New previewKeywordMatches(keyword, limit=50) uses parameterised LIKE + regex
filter in memory; caps candidate scan at 1000 rows to protect against
catastrophic backtracking
- New applyKeywordWithReassignment wraps INSERT (or UPDATE-reassign) +
per-transaction UPDATEs in an explicit BEGIN/COMMIT/ROLLBACK; rejects
existing keyword reassignment unless allowReplaceExisting is set; never
recategorises historical transactions beyond the ids the caller supplied
Hook
- Flesh out useCategoryZoom with reducer + fetch + refetch hook
Components (flat under src/components/reports/)
- CategoryZoomHeader — category combobox + include/direct toggle
- CategoryDonutChart — template'd from dashboard/CategoryPieChart with
innerRadius=55 and ChartPatternDefs for SVG patterns
- CategoryEvolutionChart — AreaChart with Intl-formatted axes
- CategoryTransactionsTable — sortable table with per-row onContextMenu
→ ContextMenu → "Add as keyword" action
AddKeywordDialog — src/components/categories/AddKeywordDialog.tsx
- Lives in categories/ (not reports/) because it is a keyword-editing widget
consumed from multiple sections
- Renders transaction descriptions as React children only (no
dangerouslySetInnerHTML); CSS truncation (CWE-79 safe)
- Per-row checkboxes for applying recategorisation; cap visible rows at 50;
explicit opt-in checkbox to extend to N-50 non-displayed matches
- Surfaces apply errors + "keyword already exists" replace prompt
- Re-runs category zoom fetch on success so the zoomed view updates
Page
- ReportsCategoryPage composes header + donut + evolution + transactions
+ AddKeywordDialog, fetches from useCategoryZoom, preserves query string
for back navigation
i18n
- New keys reports.category.* and reports.keyword.* in FR + EN
- Plural forms use i18next v25 _one / _other suffixes (nMatches)
Tests
- 3 reportService tests cover bounded CTE, cycle-guard depth check, direct-only fallthrough
- New categorizationService.test.ts: 13 tests covering validation boundaries,
parameterised LIKE preview, regex word-boundary filter, explicit BEGIN/COMMIT
wrapping, rollback on failure, existing keyword reassignment policy
- 62 total tests passing
Fixes#74
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Services: getCompareMonthOverMonth(year, month) and getCompareYearOverYear(year)
return CategoryDelta[] (expense-side, ABS aggregates, parameterised SQL only)
- Shared CategoryDelta type with HighlightMover now aliased to it
- Flesh out useCompare hook: reducer + fetch + automatic year/month inference
from the shared useReportsPeriod `to` date; budget mode skips fetch and
delegates to CompareBudgetView which wraps the existing BudgetVsActualTable
- Components: CompareModeTabs (MoM/YoY/Budget tabs), ComparePeriodTable (sortable
table with signed delta coloring), ComparePeriodChart (diverging horizontal
bar chart with ChartPatternDefs for SVG patterns), CompareBudgetView
(fetches budget rows for the current target year/month)
- ReportsComparePage wires everything with PeriodSelector + ViewModeToggle
(storage key reports-viewmode-compare); chart/table toggle is hidden in budget
mode since the budget table has its own presentation
- i18n keys: reports.compare.modeMoM / modeYoY / modeBudget in FR + EN
- 4 new vitest cases for the compare services: parameterised boundaries,
January wrap-around to December previous year, delta conversion with
previous=0 fallback to null pct, year-over-year spans
Fixes#73
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>